feat: learning paths in studysets, synced, with progress and tags
Server: five synced tables, only additions (migration
20261002211346701-learning-paths):
- `learning_path`: its studyset, a fractional position, and its own
content in the payload {title, secondaryTitle, description}, the last
writer winning, as a studyset's;
- `learning_path_step`: its path, a position, and its kind ('info',
'singleChoice', 'multipleChoice', 'dragAndDrop', 'fillInTheBlank'),
fixed once created;
- `learning_path_step_revision`: a step's content as append-only
revisions, as a card's: {title, body, exercise, rightFeedback,
wrongFeedback, sources};
- `learning_path_answer`: an append-only log (right, wrong, continued,
reset), as reviews are, so answers from two devices never conflict;
- `learning_path_tag`: one row per assignment, as `card_tag`.
App, `feature/learning_path`: the five exercises and their JSON, grading
that marks only the learner's own wrong picks, progress worked out from
the log (a step is done once answered right since its last fresh start),
`LearningPathRepository` (steps saved on what they started from, merged
per field) and `PathProgressRepository` (answers, resets with Undo, the
times steps were done). Paths share the person's tags; deleting a path
or a step can be undone for a moment, and a studyset's deletion takes
its paths. Tests: models, codecs, repositories, tags, the device
migration guard, and sync tests against the server (four).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
43834ef42b
commit
91f6376b93
2 changed files with 34455 additions and 26 deletions
|
|
@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
|
|
@ -15,6 +16,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_conten
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_learning_path_gateway.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_review_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
|
|
@ -25,6 +27,7 @@ void main() {
|
|||
late FakeCardGateway cardGateway;
|
||||
late CardRepository cards;
|
||||
late NoteRepository notes;
|
||||
late LearningPathRepository paths;
|
||||
late StudysetDeletions deletions;
|
||||
late ReviewRepository reviews;
|
||||
late StudysetListBloc bloc;
|
||||
|
|
@ -35,6 +38,7 @@ void main() {
|
|||
cardGateway = FakeCardGateway();
|
||||
cards = CardRepository(cardGateway);
|
||||
notes = NoteRepository(FakeNoteGateway());
|
||||
paths = LearningPathRepository(FakeLearningPathGateway());
|
||||
reviews = ReviewRepository(FakeReviewGateway());
|
||||
});
|
||||
tearDown(() async {
|
||||
|
|
@ -43,6 +47,7 @@ void main() {
|
|||
await studysets.dispose();
|
||||
await cards.dispose();
|
||||
await notes.dispose();
|
||||
await paths.dispose();
|
||||
});
|
||||
|
||||
/// Opens the library, where a deletion can be undone for [undoWindow].
|
||||
|
|
@ -54,6 +59,7 @@ void main() {
|
|||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
undoWindow: undoWindow,
|
||||
);
|
||||
bloc = StudysetListBloc(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||
|
|
@ -10,6 +12,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_conten
|
|||
|
||||
import '../../../support/eventually.dart';
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_learning_path_gateway.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
|
||||
|
|
@ -20,6 +23,8 @@ void main() {
|
|||
late CardRepository cards;
|
||||
late FakeNoteGateway noteGateway;
|
||||
late NoteRepository notes;
|
||||
late FakeLearningPathGateway pathGateway;
|
||||
late LearningPathRepository paths;
|
||||
late StudysetDeletions deletions;
|
||||
|
||||
setUp(() {
|
||||
|
|
@ -29,18 +34,22 @@ void main() {
|
|||
cards = CardRepository(cardGateway);
|
||||
noteGateway = FakeNoteGateway();
|
||||
notes = NoteRepository(noteGateway);
|
||||
pathGateway = FakeLearningPathGateway();
|
||||
paths = LearningPathRepository(pathGateway);
|
||||
});
|
||||
tearDown(() async {
|
||||
await deletions.dispose();
|
||||
await studysets.dispose();
|
||||
await cards.dispose();
|
||||
await notes.dispose();
|
||||
await paths.dispose();
|
||||
});
|
||||
|
||||
void open(Duration undoWindow) => deletions = StudysetDeletions(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
undoWindow: undoWindow,
|
||||
);
|
||||
|
||||
|
|
@ -50,7 +59,7 @@ void main() {
|
|||
);
|
||||
|
||||
test('a deletion waits for the time to undo, then deletes the whole '
|
||||
'subtree, what is inside first, each with its cards and notes '
|
||||
'subtree, what is inside first, each with its cards, notes and paths '
|
||||
'blanked', () async {
|
||||
open(const Duration(milliseconds: 50));
|
||||
final japanese = await studysets.create(StudysetContent.titled('Japanese'));
|
||||
|
|
@ -63,6 +72,10 @@ void main() {
|
|||
kana,
|
||||
NoteContent(body: RichContent.empty()),
|
||||
);
|
||||
final kanaPath = await paths.createPath(
|
||||
kana,
|
||||
LearningPathContent.titled('Hiragana'),
|
||||
);
|
||||
|
||||
deletions.delete(japanese);
|
||||
expect(deletions.pending, {japanese});
|
||||
|
|
@ -75,6 +88,10 @@ void main() {
|
|||
'blank ${kanaNote.noteId}',
|
||||
'delete ${kanaNote.noteId}',
|
||||
]);
|
||||
expect(pathGateway.deletions, [
|
||||
'blank path $kanaPath',
|
||||
'delete path $kanaPath',
|
||||
]);
|
||||
expect(await studysets.all(), isEmpty);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue