Files
step3/api/QuestionsApi/Models/FlashcardModels.cs
T
hz 7f76ca0fdc 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.
2026-07-22 15:52:19 -05:00

397 lines
13 KiB
C#

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QuestionsApi.Models;
public static class FlashcardTypes
{
public const string Basic = "basic";
public const string Cloze = "cloze";
public static readonly HashSet<string> All = [Basic, Cloze];
}
// A reserved, never-authenticatable owner id. Decks/notes owned by this id are shared,
// admin-curated content visible to every user (studied via their own independent schedule
// rows) rather than a real user's private content.
public static class FlashcardOwners
{
public const string System = "system";
}
public static class FlashcardRatings
{
public const string Again = "again";
public const string Hard = "hard";
public const string Good = "good";
public const string Easy = "easy";
public static readonly string[] All = [Again, Hard, Good, Easy];
}
public static class FlashcardScheduleStates
{
public const string New = "new";
public const string Learning = "learning";
public const string Review = "review";
public const string Relearning = "relearning";
}
public sealed class FlashcardDeck
{
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
public string UserId { get; set; } = "";
public string StableId { get; set; } = Guid.NewGuid().ToString();
public string Name { get; set; } = "";
public string NormalizedName { get; set; } = "";
public string Description { get; set; } = "";
public bool IsDefault { get; set; }
public int? NewCardsPerDay { get; set; }
public int? ReviewsPerDay { get; set; }
public double? DesiredRetention { get; set; }
public long Revision { get; set; } = 1;
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
public DateTime? ArchivedUtc { get; set; }
[BsonIgnore] public bool IsSystem => UserId == FlashcardOwners.System;
}
public sealed class FlashcardImageReference
{
public string Kind { get; set; } = "owned";
public string AssetId { get; set; } = "";
}
public sealed class FlashcardNote
{
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
public string UserId { get; set; } = "";
public string StableId { get; set; } = Guid.NewGuid().ToString();
public string DeckId { get; set; } = "";
public string Type { get; set; } = FlashcardTypes.Basic;
public string Prompt { get; set; } = "";
public string Answer { get; set; } = "";
public string Extra { get; set; } = "";
public List<string> Tags { get; set; } = [];
public List<FlashcardImageReference> Images { get; set; } = [];
public string SourceQuestionExternalId { get; set; } = "";
public string ContentFingerprint { get; set; } = "";
public long Revision { get; set; } = 1;
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
public DateTime? ArchivedUtc { get; set; }
[BsonIgnore] public bool IsSystem => UserId == FlashcardOwners.System;
}
public sealed class FlashcardSchedule
{
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
public string UserId { get; set; } = "";
public string NoteId { get; set; } = "";
public int Variant { get; set; }
public string State { get; set; } = FlashcardScheduleStates.New;
public int? Step { get; set; }
public double? Stability { get; set; }
public double? Difficulty { get; set; }
public DateTime DueUtc { get; set; } = DateTime.UtcNow;
public DateTime? LastReviewUtc { get; set; }
public DateTime? BuriedUntilUtc { get; set; }
public bool IsSuspended { get; set; }
public long Revision { get; set; } = 1;
public string AlgorithmVersion { get; set; } = "fsrs-core-1.0.7";
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
}
public sealed class FlashcardReviewLog
{
[BsonId] public string Id { get; set; } = "";
public string UserId { get; set; } = "";
public string CardId { get; set; } = "";
public string Rating { get; set; } = "";
public DateTime ReviewedUtc { get; set; }
public int DurationSeconds { get; set; }
public FlashcardScheduleSnapshot Before { get; set; } = new();
public FlashcardScheduleSnapshot After { get; set; } = new();
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
public DateTime? UndoneUtc { get; set; }
}
public sealed class FlashcardScheduleSnapshot
{
public string State { get; set; } = FlashcardScheduleStates.New;
public int? Step { get; set; }
public double? Stability { get; set; }
public double? Difficulty { get; set; }
public DateTime DueUtc { get; set; }
public DateTime? LastReviewUtc { get; set; }
public long Revision { get; set; }
}
public sealed class FlashcardMediaAsset
{
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
public string UserId { get; set; } = "";
public string FileName { get; set; } = "";
public string ContentType { get; set; } = "";
public byte[] Bytes { get; set; } = [];
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
}
public sealed class FlashcardSettings
{
[BsonId, BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; }
public string UserId { get; set; } = "";
public int NewCardsPerDay { get; set; } = 20;
public int ReviewsPerDay { get; set; } = 200;
public double DesiredRetention { get; set; } = 0.90;
public long Revision { get; set; } = 1;
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
}
public sealed class UpsertFlashcardDeckRequest
{
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public int? NewCardsPerDay { get; set; }
public int? ReviewsPerDay { get; set; }
public double? DesiredRetention { get; set; }
public long? ExpectedRevision { get; set; }
}
public sealed class UpsertFlashcardNoteRequest
{
public string DeckId { get; set; } = "";
public string StableId { get; set; } = "";
public string Type { get; set; } = FlashcardTypes.Basic;
public string Prompt { get; set; } = "";
public string Answer { get; set; } = "";
public string Extra { get; set; } = "";
public List<string> Tags { get; set; } = [];
public List<FlashcardImageReference> Images { get; set; } = [];
public string SourceQuestionExternalId { get; set; } = "";
public long? ExpectedRevision { get; set; }
}
public sealed class UpdateFlashcardSettingsRequest
{
public int NewCardsPerDay { get; set; } = 20;
public int ReviewsPerDay { get; set; } = 200;
public double DesiredRetention { get; set; } = 0.90;
public long? ExpectedRevision { get; set; }
}
public sealed class FlashcardDeckSummary
{
public FlashcardDeck Deck { get; set; } = new();
public int Total { get; set; }
public int Due { get; set; }
public int New { get; set; }
public int Suspended { get; set; }
public int Learning { get; set; }
public int Review { get; set; }
public int Mastered { get; set; }
}
public sealed class FlashcardLibraryResponse
{
public List<FlashcardNote> Items { get; set; } = [];
public int Total { get; set; }
}
public sealed class FlashcardLibraryQuery
{
public string? DeckId { get; set; }
public string? Tag { get; set; }
public string? Type { get; set; }
public string? State { get; set; }
public string? Search { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 50;
}
public sealed class FlashcardStudyRequest
{
public string DeckId { get; set; } = "";
public List<string> ExcludeCardIds { get; set; } = [];
public DateTime StudyDayStartUtc { get; set; } = DateTime.UtcNow.Date;
public bool Shuffle { get; set; }
}
public sealed class FlashcardBulkNoteIdsRequest
{
public List<string> NoteIds { get; set; } = [];
}
public sealed class FlashcardMoveRequest
{
public List<string> NoteIds { get; set; } = [];
public string TargetDeckId { get; set; } = "";
}
public sealed class FlashcardBatchReviewRequest
{
public List<FlashcardReviewRequest> Reviews { get; set; } = [];
}
public sealed class FlashcardBatchReviewResponse
{
public List<FlashcardReviewResponse> Results { get; set; } = [];
}
public sealed class FlashcardUndoRequest
{
public string ReviewLogId { get; set; } = "";
}
public sealed class FlashcardRatingOption
{
public string Rating { get; set; } = "";
public string IntervalLabel { get; set; } = "";
public DateTime DueUtc { get; set; }
}
public sealed class FlashcardStudyCardResponse
{
public FlashcardNote? Note { get; set; }
public FlashcardSchedule? Card { get; set; }
public List<FlashcardRatingOption> RatingOptions { get; set; } = [];
public int DueRemaining { get; set; }
public int NewRemaining { get; set; }
}
public sealed class FlashcardReviewRequest
{
public string ClientReviewId { get; set; } = "";
public string CardId { get; set; } = "";
public string Rating { get; set; } = "";
public long ExpectedRevision { get; set; }
public DateTime ReviewedUtc { get; set; }
public int DurationSeconds { get; set; }
public DateTime StudyDayStartUtc { get; set; } = DateTime.UtcNow.Date;
}
public sealed class FlashcardReviewResponse
{
public FlashcardSchedule Card { get; set; } = new();
public List<FlashcardRatingOption> RatingOptions { get; set; } = [];
public bool Duplicate { get; set; }
}
public sealed class FlashcardDashboardCounts
{
public int Due { get; set; }
public int NewAvailable { get; set; }
public int ReviewedThisWeek { get; set; }
public int Learning { get; set; }
public int Review { get; set; }
public int CurrentStreakDays { get; set; }
public int ReviewsToday { get; set; }
public int DailyGoal { get; set; }
}
public static class FlashcardImportFormats
{
public const string Csv = "csv";
public const string Json = "json";
}
public static class FlashcardImportRowActions
{
public const string Create = "create";
public const string Update = "update";
public const string Skip = "skip";
public const string Error = "error";
}
public sealed class FlashcardImportPendingRecord
{
public int RowNumber { get; set; }
public string Action { get; set; } = FlashcardImportRowActions.Create;
public string? Error { get; set; }
public string StableId { get; set; } = "";
public string DeckName { get; set; } = "";
public string Type { get; set; } = FlashcardTypes.Basic;
public string Prompt { get; set; } = "";
public string Answer { get; set; } = "";
public string Extra { get; set; } = "";
public List<string> Tags { get; set; } = [];
public string SourceQuestionExternalId { get; set; } = "";
public List<FlashcardSchedule> Cards { get; set; } = [];
public List<FlashcardReviewLog> ReviewLogs { get; set; } = [];
}
public sealed class FlashcardImportPreview
{
[BsonId] public string Id { get; set; } = "";
public string UserId { get; set; } = "";
public string Format { get; set; } = FlashcardImportFormats.Csv;
public List<FlashcardImportPendingRecord> Records { get; set; } = [];
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
public DateTime ExpiresUtc { get; set; } = DateTime.UtcNow.AddHours(1);
}
public sealed class FlashcardImportPreviewRow
{
public int RowNumber { get; set; }
public string Action { get; set; } = FlashcardImportRowActions.Create;
public string? Error { get; set; }
public string StableId { get; set; } = "";
public string Deck { get; set; } = "";
public string Type { get; set; } = FlashcardTypes.Basic;
public string Prompt { get; set; } = "";
public string Answer { get; set; } = "";
}
public sealed class FlashcardImportPreviewResponse
{
public string ImportToken { get; set; } = "";
public int CreateCount { get; set; }
public int UpdateCount { get; set; }
public int SkipCount { get; set; }
public int ErrorCount { get; set; }
public List<FlashcardImportPreviewRow> Rows { get; set; } = [];
}
public sealed class FlashcardImportCommitRequest
{
public string ImportToken { get; set; } = "";
}
public sealed class FlashcardImportCommitResponse
{
public int Created { get; set; }
public int Updated { get; set; }
public int Skipped { get; set; }
public int Errored { get; set; }
public List<FlashcardImportPreviewRow> Errors { get; set; } = [];
}
public sealed class FlashcardExportOmission
{
public string Kind { get; set; } = "";
public string Reason { get; set; } = "";
}
public sealed class FlashcardJsonExport
{
public int SchemaVersion { get; set; } = 1;
public DateTime ExportedUtc { get; set; } = DateTime.UtcNow;
public FlashcardSettings Settings { get; set; } = new();
public List<FlashcardDeck> Decks { get; set; } = [];
public List<FlashcardJsonExportNote> Notes { get; set; } = [];
}
public sealed class FlashcardJsonExportNote
{
public FlashcardNote Note { get; set; } = new();
public List<FlashcardSchedule> Cards { get; set; } = [];
public List<FlashcardReviewLog> ReviewLogs { get; set; } = [];
public List<FlashcardJsonExportImage> Images { get; set; } = [];
}
public sealed class FlashcardJsonExportImage
{
public string AssetId { get; set; } = "";
public string FileName { get; set; } = "";
public string ContentType { get; set; } = "";
public string Base64Bytes { get; set; } = "";
}