From c62eff88468bd9b2e1784b85b730ea5670c8261c Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 23 Jul 2026 03:29:57 -0500 Subject: [PATCH] Auto-flag questions with media at write time, not just via one-time sweep The flag-media-questions CLI command only caught questions that existed in the DB when it ran once. Any question created, imported, or edited afterward could add media without ever being flagged for review, since nothing gated new writes going forward. CreateAsync, UpdateAsync, and UpsertManyByExternalIdAsync now all run new/updated questions through ApplyMediaReviewGate, which flags any question that has media and isn't already flagged. --- api/QuestionsApi.Tests/QuestionStoreTests.cs | 49 ++++++++++++++++++++ api/QuestionsApi/Services/QuestionStore.cs | 18 +++++++ 2 files changed, 67 insertions(+) create mode 100644 api/QuestionsApi.Tests/QuestionStoreTests.cs diff --git a/api/QuestionsApi.Tests/QuestionStoreTests.cs b/api/QuestionsApi.Tests/QuestionStoreTests.cs new file mode 100644 index 0000000..88146f8 --- /dev/null +++ b/api/QuestionsApi.Tests/QuestionStoreTests.cs @@ -0,0 +1,49 @@ +using QuestionsApi.Models; +using QuestionsApi.Services; +using Xunit; + +namespace QuestionsApi.Tests; + +public sealed class QuestionStoreTests +{ + [Fact] + public void ApplyMediaReviewGate_FlagsUnflaggedQuestionWithMedia() + { + var question = new Question { Media = ["asset-1"] }; + + QuestionStore.ApplyMediaReviewGate(question); + + Assert.NotNull(question.FlaggedForReviewUtc); + Assert.Equal("system", question.FlaggedByUserId); + Assert.False(string.IsNullOrWhiteSpace(question.FlagReason)); + } + + [Fact] + public void ApplyMediaReviewGate_LeavesMediaFreeQuestionsAlone() + { + var question = new Question { Media = [] }; + + QuestionStore.ApplyMediaReviewGate(question); + + Assert.Null(question.FlaggedForReviewUtc); + } + + [Fact] + public void ApplyMediaReviewGate_DoesNotOverwriteAnExistingFlag() + { + var flaggedAt = DateTime.UtcNow.AddDays(-1); + var question = new Question + { + Media = ["asset-1"], + FlaggedForReviewUtc = flaggedAt, + FlagReason = "Reported by student", + FlaggedByUserId = "user-42" + }; + + QuestionStore.ApplyMediaReviewGate(question); + + Assert.Equal(flaggedAt, question.FlaggedForReviewUtc); + Assert.Equal("Reported by student", question.FlagReason); + Assert.Equal("user-42", question.FlaggedByUserId); + } +} diff --git a/api/QuestionsApi/Services/QuestionStore.cs b/api/QuestionsApi/Services/QuestionStore.cs index 3e6b74e..9ade674 100644 --- a/api/QuestionsApi/Services/QuestionStore.cs +++ b/api/QuestionsApi/Services/QuestionStore.cs @@ -6,6 +6,9 @@ namespace QuestionsApi.Services; public class QuestionStore { + private const string MediaReviewReason = "Contains media - pending review"; + private const string MediaReviewFlaggedBy = "system"; + private readonly IMongoCollection _col; public QuestionStore(MongoDatabaseProvider mongo) @@ -105,6 +108,7 @@ public class QuestionStore var question = FromRequest(req); question.CreatedUtc = DateTime.UtcNow; question.UpdatedUtc = DateTime.UtcNow; + ApplyMediaReviewGate(question); try { @@ -131,6 +135,7 @@ public class QuestionStore question.FlaggedForReviewUtc = existing.FlaggedForReviewUtc; question.FlagReason = existing.FlagReason; question.FlaggedByUserId = existing.FlaggedByUserId; + ApplyMediaReviewGate(question); var result = await _col.ReplaceOneAsync(q => q.Id == id, question); return result.MatchedCount == 0 ? null : question; } @@ -158,6 +163,7 @@ public class QuestionStore question.Id = ObjectId.GenerateNewId().ToString(); question.CreatedUtc = DateTime.UtcNow; } + ApplyMediaReviewGate(question); await _col.ReplaceOneAsync(filter, question, new ReplaceOptions { IsUpsert = true }); count++; @@ -246,6 +252,18 @@ public class QuestionStore return (int)result.MatchedCount; } + // Questions with media must be reviewed before reaching students - gate any question that + // gains media and isn't already flagged, so newly created/imported/edited questions can't + // slip past the media-review requirement the way a one-time sweep alone would miss. + internal static void ApplyMediaReviewGate(Question question) + { + if (question.Media.Count == 0 || question.FlaggedForReviewUtc is not null) + return; + question.FlaggedForReviewUtc = DateTime.UtcNow; + question.FlagReason = MediaReviewReason; + question.FlaggedByUserId = MediaReviewFlaggedBy; + } + private static Question FromRequest(UpsertQuestionRequest req) => new() { ExternalId = req.ExternalId.Trim(),