Add a review-flag state for questions, independent of publish status
Flagged questions disappear from every student-facing surface (search, exam simulation sampling, the missed-question review queue, bookmarks, direct fetch) while staying published - flagging and unpublishing are now separate, reversible actions with their own audit trail (who flagged it, when, why). Backend-only for now: SUPER-only flag/unflag/ list endpoints, no client UI. Includes a one-time CLI sweep (flag-media-questions) to flag every existing question with an image so they can be reviewed.
This commit is contained in:
@@ -39,7 +39,7 @@ public class QuestionReportsController : ControllerBase
|
||||
return BadRequest($"details must be between {MinDetailsLength} and {MaxDetailsLength} characters");
|
||||
|
||||
var question = await _questions.GetByExternalIdAsync(questionExternalId);
|
||||
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
||||
if (question is null || (!QuestionVisibility.IsVisibleToStudent(question.IsPublished, question.FlaggedForReviewUtc != null) && !User.IsInRole("SUPER")))
|
||||
return NotFound();
|
||||
|
||||
var userId = CurrentUserId();
|
||||
|
||||
@@ -94,7 +94,7 @@ public class QuestionsController : ControllerBase
|
||||
public async Task<IActionResult> Get(string id)
|
||||
{
|
||||
var question = await _questions.GetByIdAsync(id);
|
||||
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
||||
if (question is null || (!QuestionVisibility.IsVisibleToStudent(question.IsPublished, question.FlaggedForReviewUtc != null) && !User.IsInRole("SUPER")))
|
||||
return NotFound();
|
||||
|
||||
var percentages = await _statistics.GetPercentagesForQuestionAsync(question.ExternalId);
|
||||
@@ -119,7 +119,7 @@ public class QuestionsController : ControllerBase
|
||||
return BadRequest("choiceId required");
|
||||
|
||||
var question = await _questions.GetByExternalIdAsync(req.QuestionExternalId.Trim());
|
||||
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
||||
if (question is null || (!QuestionVisibility.IsVisibleToStudent(question.IsPublished, question.FlaggedForReviewUtc != null) && !User.IsInRole("SUPER")))
|
||||
return NotFound();
|
||||
if (!question.Choices.Any(c => c.Id == req.ChoiceId.Trim()))
|
||||
return BadRequest("choiceId must match a choice id");
|
||||
@@ -258,7 +258,7 @@ public class QuestionsController : ControllerBase
|
||||
return BadRequest("externalId required");
|
||||
|
||||
var question = await _questions.GetByExternalIdAsync(externalId.Trim());
|
||||
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
|
||||
if (question is null || (!QuestionVisibility.IsVisibleToStudent(question.IsPublished, question.FlaggedForReviewUtc != null) && !User.IsInRole("SUPER")))
|
||||
return NotFound();
|
||||
|
||||
var userId = CurrentUserId();
|
||||
@@ -404,6 +404,37 @@ public class QuestionsController : ControllerBase
|
||||
return Ok("Deleted");
|
||||
}
|
||||
|
||||
[HttpPost("{id}/flag")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> Flag(string id, [FromBody] FlagQuestionRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.Reason))
|
||||
return BadRequest("reason required");
|
||||
|
||||
var flagged = await _questions.FlagAsync(id, req.Reason.Trim(), CurrentUserId());
|
||||
if (flagged is null)
|
||||
return NotFound();
|
||||
return Ok(QuestionFlagResponse.FromModel(flagged));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unflag")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> Unflag(string id)
|
||||
{
|
||||
var unflagged = await _questions.UnflagAsync(id);
|
||||
if (unflagged is null)
|
||||
return NotFound();
|
||||
return Ok(QuestionFlagResponse.FromModel(unflagged));
|
||||
}
|
||||
|
||||
[HttpGet("admin/flagged")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> Flagged()
|
||||
{
|
||||
var flagged = await _questions.GetFlaggedAsync();
|
||||
return Ok(flagged.Select(QuestionFlagResponse.FromModel));
|
||||
}
|
||||
|
||||
private string CurrentUserId() =>
|
||||
User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? User.FindFirstValue(ClaimTypes.Name)
|
||||
|
||||
Reference in New Issue
Block a user