Add system (curated) vs. user-uploaded flashcards
System decks/notes are owned by a reserved sentinel user id, reusing all existing per-user store/index plumbing unchanged. Regular users see them under a separate "Official Decks" section, can study them with fully independent per-user FSRS progress (lazily materialized on first visit), but can never edit or archive them - enforced both in the client (no Edit/Archive controls) and server-side (ownership guard on every mutating endpoint). Includes a small idempotent seed command (`dotnet run -- seed-flashcards`) for testing, since there's no in-app authoring UI yet.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using QuestionsApi.Models;
|
||||
using QuestionsApi.Services.Flashcards;
|
||||
|
||||
namespace QuestionsApi.Services;
|
||||
|
||||
// Idempotent, single-target seed for testing "system" (shared, admin-curated) flashcards.
|
||||
// Unlike ContentPromotionCommand (cross-environment copy, source+target connections), this only
|
||||
// ever writes to the one database the app is already configured against, using the exact same
|
||||
// store methods a real user's create/edit path uses - just under the reserved FlashcardOwners.System
|
||||
// identity - so seeded content is created/updated through the same validation and revision
|
||||
// handling as any other deck/note.
|
||||
public static class SeedFlashcardsCommand
|
||||
{
|
||||
public static async Task<int> RunAsync(IConfiguration configuration, TextWriter output)
|
||||
{
|
||||
var mongo = new MongoDatabaseProvider(configuration);
|
||||
var scheduleStore = new FlashcardScheduleStore(mongo, new FsrsFlashcardScheduler());
|
||||
var deckStore = new FlashcardDeckStore(mongo);
|
||||
var noteStore = new FlashcardNoteStore(mongo, scheduleStore);
|
||||
|
||||
var deck = await UpsertSystemDeckAsync(deckStore, "Step 3 Essentials", "Official curated review deck");
|
||||
await UpsertSystemNoteAsync(noteStore, deck.Id!, FlashcardTypes.Basic,
|
||||
"A 62-year-old woman presents with sudden, painless, monocular vision loss. What is the diagnosis?",
|
||||
"Central retinal artery occlusion", "\"Cherry-red spot\" on fundoscopy is the classic finding.");
|
||||
await UpsertSystemNoteAsync(noteStore, deck.Id!, FlashcardTypes.Basic,
|
||||
"Thyroid storm: correct order of pharmacologic treatment?",
|
||||
"Beta-blocker (propranolol) first -> thionamide (PTU/methimazole) -> iodine solution (started >=1hr after thionamide) -> glucocorticoids",
|
||||
"Giving iodine before the thionamide can fuel new hormone synthesis (Wolff-Chaikoff escape risk).");
|
||||
await UpsertSystemNoteAsync(noteStore, deck.Id!, FlashcardTypes.Cloze,
|
||||
"First-line treatment for uncomplicated hypertension in a Black patient is {{c1::a thiazide diuretic or calcium channel blocker}}.",
|
||||
"", "");
|
||||
|
||||
await output.WriteLineAsync($"Seeded system flashcards into deck '{deck.Name}' ({deck.Id}).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task<FlashcardDeck> UpsertSystemDeckAsync(FlashcardDeckStore deckStore, string name, string description)
|
||||
{
|
||||
var existing = await deckStore.GetByNameAsync(FlashcardOwners.System, FlashcardDeckStore.Normalize(name));
|
||||
var request = new UpsertFlashcardDeckRequest { Name = name, Description = description };
|
||||
var result = await deckStore.UpsertAsync(FlashcardOwners.System, request, existing?.Id);
|
||||
return result.Deck ?? existing ?? throw new InvalidOperationException($"failed to seed deck '{name}'");
|
||||
}
|
||||
|
||||
private static async Task UpsertSystemNoteAsync(
|
||||
FlashcardNoteStore noteStore, string deckId, string type, string prompt, string answer, string extra)
|
||||
{
|
||||
var fingerprint = FlashcardNoteStore.Fingerprint(type, prompt, answer);
|
||||
var existingNotes = await noteStore.ListByDeckAsync(FlashcardOwners.System, deckId);
|
||||
var existing = existingNotes.FirstOrDefault(v => v.ContentFingerprint == fingerprint);
|
||||
var request = new UpsertFlashcardNoteRequest
|
||||
{
|
||||
DeckId = deckId,
|
||||
Type = type,
|
||||
Prompt = prompt,
|
||||
Answer = answer,
|
||||
Extra = extra,
|
||||
ExpectedRevision = existing?.Revision
|
||||
};
|
||||
await noteStore.UpsertAsync(FlashcardOwners.System, request, existing?.Id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user