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:
@@ -26,7 +26,10 @@ public class QuestionStore
|
||||
var filters = new List<FilterDefinition<Question>>();
|
||||
|
||||
if (!includeUnpublished)
|
||||
{
|
||||
filters.Add(builder.Eq(q => q.IsPublished, true));
|
||||
filters.Add(builder.Eq(q => q.FlaggedForReviewUtc, null));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(req.System) && req.System != "All Systems")
|
||||
filters.Add(builder.Eq(q => q.System, req.System.Trim()));
|
||||
if (!string.IsNullOrWhiteSpace(req.Discipline))
|
||||
@@ -65,7 +68,7 @@ public class QuestionStore
|
||||
await _col.Find(Builders<Question>.Filter.Empty).ToListAsync();
|
||||
|
||||
public async Task<List<Question>> GetPublishedAsync() =>
|
||||
await _col.Find(question => question.IsPublished).ToListAsync();
|
||||
await _col.Find(question => question.IsPublished && question.FlaggedForReviewUtc == null).ToListAsync();
|
||||
|
||||
public async Task<Dictionary<string, Question>> GetByExternalIdsAsync(IEnumerable<string> externalIds)
|
||||
{
|
||||
@@ -125,6 +128,9 @@ public class QuestionStore
|
||||
return null;
|
||||
|
||||
question.CreatedUtc = existing.CreatedUtc;
|
||||
question.FlaggedForReviewUtc = existing.FlaggedForReviewUtc;
|
||||
question.FlagReason = existing.FlagReason;
|
||||
question.FlaggedByUserId = existing.FlaggedByUserId;
|
||||
var result = await _col.ReplaceOneAsync(q => q.Id == id, question);
|
||||
return result.MatchedCount == 0 ? null : question;
|
||||
}
|
||||
@@ -143,6 +149,9 @@ public class QuestionStore
|
||||
{
|
||||
question.Id = existing.Id;
|
||||
question.CreatedUtc = existing.CreatedUtc;
|
||||
question.FlaggedForReviewUtc = existing.FlaggedForReviewUtc;
|
||||
question.FlagReason = existing.FlagReason;
|
||||
question.FlaggedByUserId = existing.FlaggedByUserId;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -167,7 +176,7 @@ public class QuestionStore
|
||||
{
|
||||
var filter = includeUnpublished
|
||||
? Builders<Question>.Filter.Empty
|
||||
: Builders<Question>.Filter.Eq(q => q.IsPublished, true);
|
||||
: Builders<Question>.Filter.Eq(q => q.IsPublished, true) & Builders<Question>.Filter.Eq(q => q.FlaggedForReviewUtc, null);
|
||||
var systems = await _col.Distinct(q => q.System, filter).ToListAsync();
|
||||
return systems.Where(s => !string.IsNullOrWhiteSpace(s)).OrderBy(s => s).ToList();
|
||||
}
|
||||
@@ -181,6 +190,45 @@ public class QuestionStore
|
||||
return requested.Except(existing, StringComparer.Ordinal).OrderBy(id => id).ToList();
|
||||
}
|
||||
|
||||
// One-time bulk sweep: flags every question that has at least one image and isn't already
|
||||
// flagged, in a single update rather than fetching everything into memory.
|
||||
public async Task<long> FlagAllWithMediaAsync(string flaggedByUserId, string reason)
|
||||
{
|
||||
var filter = Builders<Question>.Filter.Exists("Media.0", true)
|
||||
& Builders<Question>.Filter.Eq(q => q.FlaggedForReviewUtc, null);
|
||||
var update = Builders<Question>.Update
|
||||
.Set(q => q.FlaggedForReviewUtc, DateTime.UtcNow)
|
||||
.Set(q => q.FlagReason, reason)
|
||||
.Set(q => q.FlaggedByUserId, flaggedByUserId);
|
||||
var result = await _col.UpdateManyAsync(filter, update);
|
||||
return result.ModifiedCount;
|
||||
}
|
||||
|
||||
public async Task<Question?> FlagAsync(string id, string reason, string flaggedByUserId)
|
||||
{
|
||||
var update = Builders<Question>.Update
|
||||
.Set(q => q.FlaggedForReviewUtc, DateTime.UtcNow)
|
||||
.Set(q => q.FlagReason, reason)
|
||||
.Set(q => q.FlaggedByUserId, flaggedByUserId);
|
||||
return await _col.FindOneAndUpdateAsync(q => q.Id == id, update,
|
||||
new FindOneAndUpdateOptions<Question> { ReturnDocument = ReturnDocument.After });
|
||||
}
|
||||
|
||||
public async Task<Question?> UnflagAsync(string id)
|
||||
{
|
||||
var update = Builders<Question>.Update
|
||||
.Set(q => q.FlaggedForReviewUtc, null)
|
||||
.Set(q => q.FlagReason, "")
|
||||
.Set(q => q.FlaggedByUserId, "");
|
||||
return await _col.FindOneAndUpdateAsync(q => q.Id == id, update,
|
||||
new FindOneAndUpdateOptions<Question> { ReturnDocument = ReturnDocument.After });
|
||||
}
|
||||
|
||||
public async Task<List<Question>> GetFlaggedAsync() =>
|
||||
await _col.Find(q => q.FlaggedForReviewUtc != null)
|
||||
.SortByDescending(q => q.FlaggedForReviewUtc)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> UpdateReferencesByExternalIdAsync(IEnumerable<QuestionReferencesUpdate> updates)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user