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.
397 lines
13 KiB
C#
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; } = "";
|
|
}
|