Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ae4ece10f | ||
|
|
bc513810ec | ||
|
|
5934c396c2 | ||
|
|
d37f165c19 | ||
|
|
768ca344c9 | ||
|
|
d5873748cf | ||
|
|
6745f98c27 | ||
|
|
f45d6036e4 | ||
|
|
ee9c531ea6 | ||
|
|
f4d8267da1 | ||
|
|
0b983b7fda | ||
|
|
13673b2c48 |
@@ -75,3 +75,6 @@ dmypy.json
|
|||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# PyInstaller spec files
|
||||||
|
*.spec
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ A flash card program for learning the most frequent Malay words with their Danis
|
|||||||
|
|
||||||
## 🧭 Overview
|
## 🧭 Overview
|
||||||
|
|
||||||
The program shows a Malay word on a card. After three seconds the card turns over and shows the Danish translation. You press the green tick when you know the word and the red cross when you do not. A known word is removed from the words to learn, and the words still to learn are saved, so the next start continues where you stopped.
|
The program first asks for a level: the first 100 words, the first 1000 words or all words of the word list, which is ordered from the most to the least frequent word. Then it shows a Malay word on a card. After three seconds the card turns over and shows the Danish translation. You press the green tick when you know the word and the red cross when you do not. A known word is removed from the words to learn, and the words still to learn are saved, so the next start continues where you stopped.
|
||||||
|
|
||||||
| Part | What it is for |
|
| Part | What it is for |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -23,16 +23,16 @@ The program shows a Malay word on a card. After three seconds the card turns ove
|
|||||||
| `tests/` | `pytest` tests: `unit`, `integration`, `e2e` |
|
| `tests/` | `pytest` tests: `unit`, `integration`, `e2e` |
|
||||||
| `data/` | The word list `ms-da.csv` and, after the first known word, your own `words_to_learn.csv` |
|
| `data/` | The word list `ms-da.csv` and, after the first known word, your own `words_to_learn.csv` |
|
||||||
| `docs/` | Project documents (business case, plan, design, review records), `docs/wiki/` and `docs/images/` |
|
| `docs/` | Project documents (business case, plan, design, review records), `docs/wiki/` and `docs/images/` |
|
||||||
| `tools/` | Helper scripts: `generate_images.py` and `capture_screenshots.py` |
|
| `tools/` | Helper scripts: `generate_images.py`, `capture_screenshots.py` and `build_executable.py` |
|
||||||
| `Doxyfile` | Doxygen configuration for the source code documentation |
|
| `Doxyfile` | Doxygen configuration for the source code documentation |
|
||||||
|
|
||||||
The core program uses only the Python standard library. Spoken Malay words are an optional extra.
|
The core program uses only the Python standard library. Spoken Malay words are an optional extra.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
| Front: the Malay word | Back: the Danish translation after 3 seconds |
|
| Choose your level | Front: the Malay word | Back: the Danish translation after 3 seconds |
|
||||||
| --- | --- |
|
| --- | --- | --- |
|
||||||
| ![The front of a card][screenshot-front] | ![The back of a card][screenshot-back] |
|
| ![The level screen][screenshot-level] | ![The front of a card][screenshot-front] | ![The back of a card][screenshot-back] |
|
||||||
|
|
||||||
### Where is each assignment step?
|
### Where is each assignment step?
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ The core program uses only the Python standard library. Spoken Malay words are a
|
|||||||
| 2. New flash cards | `CsvVocabularyRepository` (the standard library `csv` instead of `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
| 2. New flash cards | `CsvVocabularyRepository` (the standard library `csv` instead of `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
||||||
| 3. Flip the cards | `FlashCardView.flip_card` and `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
| 3. Flip the cards | `FlashCardView.flip_card` and `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
||||||
| 4. Save your progress | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository` |
|
| 4. Save your progress | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository` |
|
||||||
|
| Extra: level choice | `LevelView`, `LevelViewModel`, `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session` |
|
||||||
| Extra: spoken word | `GttsSpeech`, `NullSpeech`, `build_speech` in `presentation/app.py` |
|
| Extra: spoken word | `GttsSpeech`, `NullSpeech`, `build_speech` in `presentation/app.py` |
|
||||||
|
|
||||||
The wiki ([English][wiki-en], [Dansk][wiki-da], [Bahasa Malaysia][wiki-ms]) explains the same in three languages.
|
The wiki ([English][wiki-en], [Dansk][wiki-da], [Bahasa Malaysia][wiki-ms]) explains the same in three languages.
|
||||||
@@ -57,6 +58,7 @@ The wiki ([English][wiki-en], [Dansk][wiki-da], [Bahasa Malaysia][wiki-ms]) expl
|
|||||||
| Python 3.13 or later, with Tkinter | The program and its window (Tkinter is part of the standard Python installers for Windows and macOS; on Debian or Ubuntu install `python3-tk`) |
|
| Python 3.13 or later, with Tkinter | The program and its window (Tkinter is part of the standard Python installers for Windows and macOS; on Debian or Ubuntu install `python3-tk`) |
|
||||||
| `git` | To clone the repository |
|
| `git` | To clone the repository |
|
||||||
| Doxygen (optional) | To build the source code documentation |
|
| Doxygen (optional) | To build the source code documentation |
|
||||||
|
| `pyinstaller` (optional, extra `build`) | To build a single executable file (see Run) |
|
||||||
| `gTTS` and `playsound==1.2.2` (optional, extra `speech`) | To hear the Malay word; needs a network connection |
|
| `gTTS` and `playsound==1.2.2` (optional, extra `speech`) | To hear the Malay word; needs a network connection |
|
||||||
|
|
||||||
## 🛠️ Setup
|
## 🛠️ Setup
|
||||||
@@ -92,14 +94,24 @@ python -m pip install -e ".[dev]"
|
|||||||
python -m flash_card
|
python -m flash_card
|
||||||
```
|
```
|
||||||
|
|
||||||
The program is run from the activated `.venv`. The window opens with a card. Press the green tick for a word you know and the red cross for a word you do not. Options:
|
The program is run from the activated `.venv`. The window opens with the level screen: choose 100 words, 1000 words or All words. Then a card appears. Press the green tick for a word you know and the red cross for a word you do not. Options:
|
||||||
|
|
||||||
| Option | Meaning |
|
| Option | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--data-dir <folder>` | Folder with `ms-da.csv` and `words_to_learn.csv` |
|
| `--data-dir <folder>` | Folder with `ms-da.csv` and `words_to_learn.csv` |
|
||||||
|
| `--level {100,1000,all}` | Start at that level and skip the level screen |
|
||||||
| `--no-speech` | Do not speak the Malay word |
|
| `--no-speech` | Do not speak the Malay word |
|
||||||
|
|
||||||
To start again, delete `data/words_to_learn.csv`.
|
Known words stay known at every level: the level only limits which of the words you do not know yet come up. To start again, delete `data/words_to_learn.csv`.
|
||||||
|
|
||||||
|
### Executable (Windows, no Python needed to run it)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -e ".[build,speech]"
|
||||||
|
python tools/build_executable.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script builds `dist/flash-card.exe`: one file, no console window, with the images, the word list and the spoken word inside. Open it by double-clicking. It keeps your progress in a `data/` folder next to the executable (`data/words_to_learn.csv`); a `data/ms-da.csv` next to it replaces the word list inside. Add `--no-speech` to the build script to leave the speech packages out. The executable is a build output and is not committed; build it on the system you want to run it on (Windows, Linux or macOS).
|
||||||
|
|
||||||
## 🧪 Tests
|
## 🧪 Tests
|
||||||
|
|
||||||
@@ -149,6 +161,7 @@ The documents a newcomer needs, in reading order.
|
|||||||
---
|
---
|
||||||
|
|
||||||
[license]: ./LICENSE
|
[license]: ./LICENSE
|
||||||
|
[screenshot-level]: ./docs/images/screenshot-level.png
|
||||||
[screenshot-front]: ./docs/images/screenshot-front.png
|
[screenshot-front]: ./docs/images/screenshot-front.png
|
||||||
[screenshot-back]: ./docs/images/screenshot-back.png
|
[screenshot-back]: ./docs/images/screenshot-back.png
|
||||||
[wiki-en]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home
|
[wiki-en]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# ADR-0004: Windows executable built with PyInstaller
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | ADR-0004 |
|
||||||
|
| CrossReference | [BC-001], [US-001], [DCD-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Product Owner (S01) wants an executable file, so that the program can be started without a Python installation (US-001.07). The program has images, a word list, a progress file that must survive restarts, and an optional speech extra. Options:
|
||||||
|
|
||||||
|
1. A one-directory build (a folder with the executable and its libraries): starts faster and the data can sit beside the files, but it is a folder of hundreds of files to hand over.
|
||||||
|
2. A one-file build (a single `flash-card.exe` that unpacks itself to a temporary folder): one file to hand over, slower start; resources are in a temporary folder, so progress needs a place beside the executable.
|
||||||
|
3. Another packager (Nuitka, cx_Freeze) or an installer: more tooling and a larger change for the same result.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Build a one-file, windowed (no console) Windows executable with PyInstaller by `tools/build_executable.py`. The images and the word list are inside the executable. The progress file and an optional word list are in a `data/` folder beside the executable (found with `sys.executable` when `sys.frozen` is set), and `--data-dir` still overrides it. PyInstaller is only in the optional extra `build`, so `dependencies` stays empty (ADR-0001). The executable is a build output and is not committed.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
|
||||||
|
- One file to hand over, and no Python, `.venv` or pip needed to run it.
|
||||||
|
- The source run is unchanged; only the path rule differs when frozen, and it is tested without PyInstaller.
|
||||||
|
- The word list stays editable beside the executable.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
|
||||||
|
- PyInstaller cannot cross-compile: the executable must be built on Windows (and on Linux or macOS for those systems).
|
||||||
|
- A one-file executable unpacks to a temporary folder at each start and is larger with the speech packages; unsigned executables can trigger Windows SmartScreen or antivirus warnings.
|
||||||
|
- The Windows Store Python can cause Tcl/Tk path problems with PyInstaller; a build with it must be checked by a real start.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- [BC-001] — scope: the executable is in scope; installer, signing and release upload are out
|
||||||
|
- [US-001] — US-001.07 Start without Python
|
||||||
|
- [DCD-001] — packaging support and the choice of the word list file in the composition root
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../business-case.md
|
||||||
|
[US-001]: ../user-stories.md
|
||||||
|
[DCD-001]: ../dcd.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -14,7 +14,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
|||||||
| BC | Business Case | docs/business-case.md | 002 |
|
| BC | Business Case | docs/business-case.md | 002 |
|
||||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 008 |
|
| MIL | Milestone / Gateway | docs/milestones/*.md | 010 |
|
||||||
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
|
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
|
||||||
| US | User Story | docs/user-stories.md | 002 |
|
| US | User Story | docs/user-stories.md | 002 |
|
||||||
| UC | Use Case | docs/uc-*/uc.md | 002 |
|
| UC | Use Case | docs/uc-*/uc.md | 002 |
|
||||||
@@ -24,8 +24,8 @@ document of a type. `Primary File` may contain a glob (e.g.
|
|||||||
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
|
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
|
||||||
| DCD | Design Class Diagram | docs/dcd.md | 002 |
|
| DCD | Design Class Diagram | docs/dcd.md | 002 |
|
||||||
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
||||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
|
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0005 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 030 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 049 |
|
||||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|||||||
+155
-149
@@ -1,149 +1,155 @@
|
|||||||
# Business Case: Flash Card
|
# Business Case: Flash Card
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | BC-001 |
|
| ID | BC-001 |
|
||||||
| CrossReference | [SA-001] |
|
| CrossReference | [SA-001] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | In Scope: choice of level (new request of S01) | [0b983b7] |
|
||||||
---
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope: executable built with PyInstaller (new request of S01) | [768ca34] |
|
||||||
|
|
||||||
## Executive Summary
|
---
|
||||||
|
|
||||||
The Day 31 assignment of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp* asks the course participant to build a flash card program that helps learn the most frequent words of a foreign language. The program shows a word in the target language, flips the card after three seconds to show its translation, and keeps only the words the learner does not yet know. This project delivers that program for a Malay to Danish word list, built from the four assignment steps (user interface, new flash cards, flipping the cards, saving progress) and extended with spoken Malay words. It is delivered as readable, tested, documented code that other course participants can run and compare with their own solution.
|
## Executive Summary
|
||||||
|
|
||||||
## Methodological and Standards Foundation
|
The Day 31 assignment of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp* asks the course participant to build a flash card program that helps learn the most frequent words of a foreign language. The program shows a word in the target language, flips the card after three seconds to show its translation, and keeps only the words the learner does not yet know. This project delivers that program for a Malay to Danish word list, built from the four assignment steps (user interface, new flash cards, flipping the cards, saving progress) and extended with spoken Malay words. It is delivered as readable, tested, documented code that other course participants can run and compare with their own solution.
|
||||||
|
|
||||||
- Method: the SQA and QC framework of this repository (plan first, review every artifact against its QC checklist, one pull request per milestone); Larman, *Applying UML and Patterns*, for the use of layers and the presentation pattern.
|
## Methodological and Standards Foundation
|
||||||
- Architecture: clean architecture (domain, application, infrastructure, presentation) with MVVM for the user interface, as the `clean-architecture` skill sets out.
|
|
||||||
- Quality standard: ISO/IEC 25010:2023 product quality characteristics, as every QC criterion is tagged.
|
- Method: the SQA and QC framework of this repository (plan first, review every artifact against its QC checklist, one pull request per milestone); Larman, *Applying UML and Patterns*, for the use of layers and the presentation pattern.
|
||||||
- Code: PEP 8, PEP 257 and type hints; Python 3.13 or later; Doxygen comments; `pytest` for tests.
|
- Architecture: clean architecture (domain, application, infrastructure, presentation) with MVVM for the user interface, as the `clean-architecture` skill sets out.
|
||||||
|
- Quality standard: ISO/IEC 25010:2023 product quality characteristics, as every QC criterion is tagged.
|
||||||
## Problem Statement
|
- Code: PEP 8, PEP 257 and type hints; Python 3.13 or later; Doxygen comments; `pytest` for tests.
|
||||||
|
|
||||||
- A learner who memorises vocabulary from a plain list has no way to see which words are already known, so time is spent on words that are mastered.
|
## Problem Statement
|
||||||
- The course assignment gives a step-by-step description, not a finished and tested reference that a participant can read, run and compare against.
|
|
||||||
- The supplied starting data is French to English. The Product Owner learns Malay and reads Danish, so the program must work with a Malay to Danish list and pronounce the Malay word.
|
- A learner who memorises vocabulary from a plain list has no way to see which words are already known, so time is spent on words that are mastered.
|
||||||
|
- The course assignment gives a step-by-step description, not a finished and tested reference that a participant can read, run and compare against.
|
||||||
## Business Opportunity
|
- The supplied starting data is French to English. The Product Owner learns Malay and reads Danish, so the program must work with a Malay to Danish list and pronounce the Malay word.
|
||||||
|
|
||||||
A small, well-structured reference solution gives the Product Owner a language learning tool for daily use, shows how clean architecture applies to a Tkinter program, and gives other course participants and repository visitors an example to learn from.
|
## Business Opportunity
|
||||||
|
|
||||||
## Objectives
|
A small, well-structured reference solution gives the Product Owner a language learning tool for daily use, shows how clean architecture applies to a Tkinter program, and gives other course participants and repository visitors an example to learn from.
|
||||||
|
|
||||||
1. Deliver the four assignment steps as working behaviour: the window with a card and two buttons; a random card per button press; a flip to the translation after 3000 ms; and persistence of the unknown words in `words_to_learn.csv`.
|
## Objectives
|
||||||
2. Use the Malay to Danish frequency word list (`data/ms-da.csv`, 1000 words) instead of the French to English list, and speak the Malay word through text to speech without making that a requirement of the core program.
|
|
||||||
3. Keep the core program free of third-party runtime dependencies.
|
1. Deliver the four assignment steps as working behaviour: the window with a card and two buttons; a random card per button press; a flip to the translation after 3000 ms; and persistence of the unknown words in `words_to_learn.csv`.
|
||||||
4. Provide a repository others can use: README, wiki in English, Danish and Bahasa Malaysia, screenshots, a repository description and topics.
|
2. Use the Malay to Danish frequency word list (`data/ms-da.csv`, 1000 words) instead of the French to English list, and speak the Malay word through text to speech without making that a requirement of the core program.
|
||||||
5. Keep track of the work step by step, with one reviewed milestone and one pull request per step.
|
3. Keep the core program free of third-party runtime dependencies.
|
||||||
|
4. Provide a repository others can use: README, wiki in English, Danish and Bahasa Malaysia, screenshots, a repository description and topics.
|
||||||
## Scope
|
5. Keep track of the work step by step, with one reviewed milestone and one pull request per step.
|
||||||
|
|
||||||
### In Scope
|
## Scope
|
||||||
|
|
||||||
- Python 3.13 or later Tkinter program with the assignment's function names (`next_card`, `flip_card`, `is_known`).
|
### In Scope
|
||||||
- Reading the Malay to Danish list from `data/ms-da.csv` (semicolon separated, header `ms;da`).
|
|
||||||
- Saving the words still to learn to `data/words_to_learn.csv` and using that file on the next start.
|
- Python 3.13 or later Tkinter program with the assignment's function names (`next_card`, `flip_card`, `is_known`).
|
||||||
- Optional spoken Malay word (text to speech) as an extra that can be installed.
|
- Reading the Malay to Danish list from `data/ms-da.csv` (semicolon separated, header `ms;da`).
|
||||||
- `pyproject.toml`, `.gitignore`, `Doxyfile`, virtual environment instructions, `pytest` tests and `constants.py`.
|
- Saving the words still to learn to `data/words_to_learn.csv` and using that file on the next start.
|
||||||
- README from the framework template, screenshots, and a wiki in English, Danish and Bahasa Malaysia.
|
- Optional spoken Malay word (text to speech) as an extra that can be installed.
|
||||||
- Repository description and topics on the git host.
|
- A choice of level (the first 100 words, the first 1000 words or all words of the frequency ordered word list) before learning starts.
|
||||||
- The framework documents of the baseline: Business Case, Stakeholder Analysis, Project Plan, milestones, review records and traceability matrix.
|
- A single Windows executable file, built with PyInstaller by a script of this repository, that runs without an installed Python.
|
||||||
|
- `pyproject.toml`, `.gitignore`, `Doxyfile`, virtual environment instructions, `pytest` tests and `constants.py`.
|
||||||
### Out of Scope
|
- README from the framework template, screenshots, and a wiki in English, Danish and Bahasa Malaysia.
|
||||||
|
- Repository description and topics on the git host.
|
||||||
- Other languages: the user interface labels are Malay and Danish only; another word list works by changing the data file but is not delivered.
|
- The framework documents of the baseline: Business Case, Stakeholder Analysis, Project Plan, milestones, review records and traceability matrix.
|
||||||
- Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps.
|
|
||||||
- Packaging for distribution on PyPI or as an installer.
|
### Out of Scope
|
||||||
- Redistribution of the course's own image files; the card and button images are generated for this project.
|
|
||||||
- A translation quality review of the word list.
|
- Other languages: the user interface labels are Malay and Danish only; another word list works by changing the data file but is not delivered.
|
||||||
|
- Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps.
|
||||||
## Expected Benefits
|
- Packaging for distribution on PyPI, an installer, a signed executable and an upload of the executable to a release (the executable built with PyInstaller is in scope; the file itself is not committed).
|
||||||
|
- Redistribution of the course's own image files; the card and button images are generated for this project.
|
||||||
### Tangible Benefits
|
- A translation quality review of the word list.
|
||||||
|
|
||||||
- A working flash card program for 1000 Malay words with progress that survives a restart.
|
## Expected Benefits
|
||||||
- A test suite that protects the behaviour of every assignment step.
|
|
||||||
- A repository other participants can run in five commands or fewer.
|
### Tangible Benefits
|
||||||
|
|
||||||
### Intangible Benefits
|
- A working flash card program for 1000 Malay words with progress that survives a restart.
|
||||||
|
- A test suite that protects the behaviour of every assignment step.
|
||||||
- A worked example of clean architecture and MVVM in a small Tkinter program.
|
- A repository other participants can run in five commands or fewer.
|
||||||
- Practice of the plan-first workflow with reviews and pull requests per step.
|
|
||||||
- Learning material in three languages for readers of the wiki.
|
### Intangible Benefits
|
||||||
|
|
||||||
## Strategic Alignment
|
- A worked example of clean architecture and MVVM in a small Tkinter program.
|
||||||
|
- Practice of the plan-first workflow with reviews and pull requests per step.
|
||||||
The project supports the Product Owner's goal of completing the 100 Days of Code course with understood, well-documented solutions, and the goal of a public repository that shows disciplined engineering practice to its viewers.
|
- Learning material in three languages for readers of the wiki.
|
||||||
|
|
||||||
## Success Criteria
|
## Strategic Alignment
|
||||||
|
|
||||||
| # | Criterion | Target | Measure |
|
The project supports the Product Owner's goal of completing the 100 Days of Code course with understood, well-documented solutions, and the goal of a public repository that shows disciplined engineering practice to its viewers.
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 1 | Assignment steps delivered | 4 of 4 steps work as described in the assignment | Acceptance checks listed in each step's milestone are all passed |
|
## Success Criteria
|
||||||
| 2 | Tests | `pytest` exits with 0 and every behaviour of steps 1 to 4 has at least one test | `pytest` run on a clean `.venv`, and the milestone's task list |
|
|
||||||
| 3 | Runtime dependencies of the core program | 0 third-party packages | `dependencies` in `pyproject.toml` is empty; speech is an optional extra |
|
| # | Criterion | Target | Measure |
|
||||||
| 4 | Time to run for a newcomer | 5 commands or fewer from clone to running window | Command count in the README's Setup and Run sections |
|
| --- | --- | --- | --- |
|
||||||
| 5 | Wiki coverage | 3 languages (en, da, ms) with the same page set | File list of `docs/wiki/` |
|
| 1 | Assignment steps delivered | 4 of 4 steps work as described in the assignment | Acceptance checks listed in each step's milestone are all passed |
|
||||||
| 6 | Repository presentation | A description and at least 5 topics are set on the repository | Read back from the git host API |
|
| 2 | Tests | `pytest` exits with 0 and every behaviour of steps 1 to 4 has at least one test | `pytest` run on a clean `.venv`, and the milestone's task list |
|
||||||
| 7 | Review coverage | Every artifact and every milestone's code has an `RC-*` record with verdict Go | `docs/sqa/reviews/` and the Traceability Matrix |
|
| 3 | Runtime dependencies of the core program | 0 third-party packages | `dependencies` in `pyproject.toml` is empty; speech is an optional extra |
|
||||||
| 8 | Delivery window | Everything merged by 2026-10-21 | Date of the last merged pull request |
|
| 4 | Time to run for a newcomer | 5 commands or fewer from clone to running window | Command count in the README's Setup and Run sections |
|
||||||
|
| 5 | Wiki coverage | 3 languages (en, da, ms) with the same page set | File list of `docs/wiki/` |
|
||||||
## Risks
|
| 6 | Repository presentation | A description and at least 5 topics are set on the repository | Read back from the git host API |
|
||||||
|
| 7 | Review coverage | Every artifact and every milestone's code has an `RC-*` record with verdict Go | `docs/sqa/reviews/` and the Traceability Matrix |
|
||||||
| Risk | Impact | Mitigation |
|
| 8 | Delivery window | Everything merged by 2026-10-21 | Date of the last merged pull request |
|
||||||
| --- | --- | --- |
|
|
||||||
| Text to speech needs network access and an audio player that may not work on every machine | Spoken word missing for some users | Speech is an optional extra behind a port; the program starts and works without it, and a failure is logged, not raised |
|
## Risks
|
||||||
| The `playsound` package is unmaintained and fails on some Python versions | Install of the extra fails | Pin the version known to work (1.2.2), keep the player behind a port so it can be replaced |
|
|
||||||
| Tkinter cannot be tested without a display on a build machine | Presentation code untested in CI | The view model holds the behaviour and is tested without Tkinter; the view test skips when no display exists |
|
| Risk | Impact | Mitigation |
|
||||||
| The word list has translation errors (machine translated) | Learner learns a wrong meaning | Out of scope for this project; the README says the list is machine translated and may be edited |
|
| --- | --- | --- |
|
||||||
| Course images are copyrighted | Redistribution not allowed in a public repository | Images are generated by a script in this repository |
|
| Text to speech needs network access and an audio player that may not work on every machine | Spoken word missing for some users | Speech is an optional extra behind a port; the program starts and works without it, and a failure is logged, not raised |
|
||||||
|
| The `playsound` package is unmaintained and fails on some Python versions | Install of the extra fails | Pin the version known to work (1.2.2), keep the player behind a port so it can be replaced |
|
||||||
## Assumptions
|
| Tkinter cannot be tested without a display on a build machine | Presentation code untested in CI | The view model holds the behaviour and is tested without Tkinter; the view test skips when no display exists |
|
||||||
|
| The word list has translation errors (machine translated) | Learner learns a wrong meaning | Out of scope for this project; the README says the list is machine translated and may be edited |
|
||||||
- S01 is the Product Owner, the developer and the reviewer; no other person reviews, so the AI acts as stand-in reviewer in autonomous mode and the review records say so.
|
| Course images are copyrighted | Redistribution not allowed in a public repository | Images are generated by a script in this repository |
|
||||||
- S01 can reach S02 and S03 only through the public repository; no interviews are held.
|
|
||||||
- A display and Tkinter are available on the machine where the program runs.
|
## Assumptions
|
||||||
- Network access is available when the optional speech extra is used.
|
|
||||||
- The git host `git.tirsystem.com` is used for the repository, the wiki and the pull requests.
|
- S01 is the Product Owner, the developer and the reviewer; no other person reviews, so the AI acts as stand-in reviewer in autonomous mode and the review records say so.
|
||||||
|
- S01 can reach S02 and S03 only through the public repository; no interviews are held.
|
||||||
## Constraints
|
- A display and Tkinter are available on the machine where the program runs.
|
||||||
|
- Network access is available when the optional speech extra is used.
|
||||||
- Python 3.13 or later, with a virtual environment in `.venv`.
|
- The git host `git.tirsystem.com` is used for the repository, the wiki and the pull requests.
|
||||||
- The folder structure is `src/`, `tests/` and `docs/`.
|
|
||||||
- Constants are defined in `constants.py`.
|
## Constraints
|
||||||
- The assignment's function names are kept so that participants can compare solutions.
|
|
||||||
- The Product Owner's language is English and the domain is IT; the wiki is in English, Danish and Bahasa Malaysia.
|
- Python 3.13 or later, with a virtual environment in `.venv`.
|
||||||
- Delivery by 2026-10-21 (self-set, one week).
|
- The folder structure is `src/`, `tests/` and `docs/`.
|
||||||
|
- Constants are defined in `constants.py`.
|
||||||
## Cost–Benefit Assessment
|
- The assignment's function names are kept so that participants can compare solutions.
|
||||||
|
- The Product Owner's language is English and the domain is IT; the wiki is in English, Danish and Bahasa Malaysia.
|
||||||
| Costs | Benefits |
|
- Delivery by 2026-10-21 (self-set, one week).
|
||||||
| --- | --- |
|
|
||||||
| About one working week of the Product Owner's time to review the plan and the result | A reusable learning tool and a reference solution. Qualitative on purpose: no revenue is expected, so a quantitative return does not apply; the benefit is learning and portfolio value |
|
## Cost–Benefit Assessment
|
||||||
| No licence or hosting cost; the git host and Python are already available | Documentation and tests reusable for the next course day |
|
|
||||||
| Maintenance of the optional speech extra | An example of clean architecture for S02 and S03 |
|
| Costs | Benefits |
|
||||||
|
| --- | --- |
|
||||||
## Stakeholders
|
| About one working week of the Product Owner's time to review the plan and the result | A reusable learning tool and a reference solution. Qualitative on purpose: no revenue is expected, so a quantitative return does not apply; the benefit is learning and portfolio value |
|
||||||
|
| No licence or hosting cost; the git host and Python are already available | Documentation and tests reusable for the next course day |
|
||||||
| Stakeholder ID (SA) | Interest in this project |
|
| Maintenance of the optional speech extra | An example of clean architecture for S02 and S03 |
|
||||||
| --- | --- |
|
|
||||||
| S01 | Owns the product, builds it and reviews it; wants a working tool and a learning record |
|
## Stakeholders
|
||||||
| S02 | Wants readable, runnable code with the assignment's function names and run instructions |
|
|
||||||
| S03 | Wants a clear repository description, topics and README, and no runtime dependencies |
|
| Stakeholder ID (SA) | Interest in this project |
|
||||||
|
| --- | --- |
|
||||||
## Recommendation
|
| S01 | Owns the product, builds it and reviews it; wants a working tool and a learning record |
|
||||||
|
| S02 | Wants readable, runnable code with the assignment's function names and run instructions |
|
||||||
Proceed — the scope is small, the risks are mitigated, and the result serves the Product Owner and two groups of readers at no cost beyond the Product Owner's time.
|
| S03 | Wants a clear repository description, topics and README, and no runtime dependencies |
|
||||||
|
|
||||||
---
|
## Recommendation
|
||||||
|
|
||||||
[SA-001]: ./stakeholder-analysis.md
|
Proceed — the scope is small, the risks are mitigated, and the result serves the Product Owner and two groups of readers at no cost beyond the Product Owner's time.
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[SA-001]: ./stakeholder-analysis.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
|
|||||||
+230
-194
@@ -1,194 +1,230 @@
|
|||||||
# Design Class Diagram: Flash Card
|
# Design Class Diagram: Flash Card
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | DCD-001 |
|
| ID | DCD-001 |
|
||||||
| CrossReference | [DM-001], [SD-001], [DICT-001], [ADR-0002] |
|
| CrossReference | [DM-001], [SD-001], [DICT-001], [ADR-0002], [ADR-0004] |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added Level, Deck.within, the level argument and the level screen | [0b983b7] |
|
||||||
---
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope note: runtime path rules and the executable build are packaging support, not design classes | [768ca34] |
|
||||||
|
|
||||||
## Purpose and Scope
|
---
|
||||||
|
|
||||||
The design classes of the program in `src/flash_card/`, in four layers (domain, application, infrastructure, presentation) with the presentation pattern MVVM ([ADR-0002]). The classes refine the concepts of [DM-001]; names are the IT terms of [DICT-001]. Methods trace to the operation contracts of [OC-001] and the sequence diagrams of [SD-001] (the SSD messages are in [SSD-001]).
|
## Purpose and Scope
|
||||||
|
|
||||||
## Diagram
|
The design classes of the program in `src/flash_card/`, in four layers (domain, application, infrastructure, presentation) with the presentation pattern MVVM ([ADR-0002]). The classes refine the concepts of [DM-001]; names are the IT terms of [DICT-001]. Methods trace to the operation contracts of [OC-001] and the sequence diagrams of [SD-001] (the SSD messages are in [SSD-001]). The module `flash_card/runtime.py` (where the resources and the data folder are when the program is frozen by PyInstaller) and the scripts in `tools/` are packaging support ([ADR-0004]): they have no system operation and are not design classes; `app.main` is still the only place that names the adapters, and it chooses the word list file.
|
||||||
|
|
||||||
```plantuml
|
## Diagram
|
||||||
@startuml
|
|
||||||
package domain {
|
```plantuml
|
||||||
class Card <<frozen>> {
|
@startuml
|
||||||
+front : str
|
package domain {
|
||||||
+back : str
|
class Card <<frozen>> {
|
||||||
}
|
+front : str
|
||||||
class LanguagePair <<frozen>> {
|
+back : str
|
||||||
+front_code : str
|
}
|
||||||
+back_code : str
|
class LanguagePair <<frozen>> {
|
||||||
}
|
+front_code : str
|
||||||
class Vocabulary <<frozen>> {
|
+back_code : str
|
||||||
+language_pair : LanguagePair
|
}
|
||||||
+cards : tuple[Card, ...]
|
class Level <<frozen>> {
|
||||||
}
|
+limit : int | None
|
||||||
class Deck {
|
+first(cards : Sequence[Card]) : tuple[Card, ...]
|
||||||
+language_pair : LanguagePair
|
}
|
||||||
-_cards : list[Card]
|
class Vocabulary <<frozen>> {
|
||||||
+from_vocabulary(vocabulary : Vocabulary) : Deck
|
+language_pair : LanguagePair
|
||||||
+cards : tuple[Card, ...]
|
+cards : tuple[Card, ...]
|
||||||
+is_empty : bool
|
}
|
||||||
+draw(choose, exclude : Card | None) : Card
|
class Deck {
|
||||||
+remove(card : Card) : None
|
+language_pair : LanguagePair
|
||||||
}
|
-_cards : list[Card]
|
||||||
interface VocabularyRepository {
|
+from_vocabulary(vocabulary : Vocabulary) : Deck
|
||||||
+load() : Vocabulary
|
+cards : tuple[Card, ...]
|
||||||
}
|
+is_empty : bool
|
||||||
interface ProgressRepository {
|
+within(cards : Sequence[Card]) : tuple[Card, ...]
|
||||||
+load() : Deck | None
|
+draw(choose, exclude : Card | None, among : Sequence[Card] | None) : Card
|
||||||
+save(deck : Deck) : None
|
+remove(card : Card) : None
|
||||||
}
|
}
|
||||||
class VocabularyError
|
interface VocabularyRepository {
|
||||||
class VocabularyFormatError
|
+load() : Vocabulary
|
||||||
class VocabularyNotFoundError
|
}
|
||||||
class EmptyDeckError
|
interface ProgressRepository {
|
||||||
}
|
+load() : Deck | None
|
||||||
package application {
|
+save(deck : Deck) : None
|
||||||
class CardDto <<frozen>> {
|
}
|
||||||
+front_text : str
|
class VocabularyError
|
||||||
+back_text : str
|
class VocabularyFormatError
|
||||||
+front_language : str
|
class VocabularyNotFoundError
|
||||||
+back_language : str
|
class EmptyDeckError
|
||||||
}
|
}
|
||||||
interface SpeechPort {
|
package application {
|
||||||
+speak(text : str, language_code : str) : None
|
class CardDto <<frozen>> {
|
||||||
}
|
+front_text : str
|
||||||
class StudySession {
|
+back_text : str
|
||||||
-_deck : Deck
|
+front_language : str
|
||||||
-_progress : ProgressRepository
|
+back_language : str
|
||||||
-_speech : SpeechPort
|
}
|
||||||
+current_card : CardDto | None
|
interface SpeechPort {
|
||||||
+remaining : int
|
+speak(text : str, language_code : str) : None
|
||||||
+next_card() : CardDto | None
|
}
|
||||||
+mark_known() : None
|
class StudySession {
|
||||||
}
|
-_deck : Deck
|
||||||
class open_study_session <<function>>
|
-_progress : ProgressRepository
|
||||||
}
|
-_speech : SpeechPort
|
||||||
package infrastructure {
|
-_level_cards : tuple[Card, ...]
|
||||||
class CsvVocabularyRepository {
|
+current_card : CardDto | None
|
||||||
-_path : Path
|
+remaining : int
|
||||||
+load() : Vocabulary
|
+next_card() : CardDto | None
|
||||||
}
|
+mark_known() : None
|
||||||
class CsvProgressRepository {
|
}
|
||||||
-_path : Path
|
class open_study_session <<function>>
|
||||||
+load() : Deck | None
|
}
|
||||||
+save(deck : Deck) : None
|
package infrastructure {
|
||||||
}
|
class CsvVocabularyRepository {
|
||||||
class GttsSpeech {
|
-_path : Path
|
||||||
+speak(text : str, language_code : str) : None
|
+load() : Vocabulary
|
||||||
}
|
}
|
||||||
class NullSpeech {
|
class CsvProgressRepository {
|
||||||
+speak(text : str, language_code : str) : None
|
-_path : Path
|
||||||
}
|
+load() : Deck | None
|
||||||
}
|
+save(deck : Deck) : None
|
||||||
package presentation {
|
}
|
||||||
class FlashCardViewModel {
|
class GttsSpeech {
|
||||||
-_session : StudySession
|
+speak(text : str, language_code : str) : None
|
||||||
+side : Side
|
}
|
||||||
+language_name : str
|
class NullSpeech {
|
||||||
+text : str
|
+speak(text : str, language_code : str) : None
|
||||||
+next_card() : None
|
}
|
||||||
+flip_card() : None
|
}
|
||||||
+is_known() : None
|
package presentation {
|
||||||
}
|
class LevelChoice <<frozen>> {
|
||||||
class FlashCardView {
|
+label : str
|
||||||
+flip_timer : str | None
|
+level : Level
|
||||||
+next_card() : None
|
}
|
||||||
+flip_card() : None
|
class LevelViewModel {
|
||||||
+is_known() : None
|
+title : str
|
||||||
}
|
+choices : tuple[LevelChoice, ...]
|
||||||
class "app.main" as Main <<function>>
|
}
|
||||||
}
|
class LevelView {
|
||||||
Vocabulary "1" *-- "1..*" Card
|
+choose(level : Level) : None
|
||||||
Vocabulary "1" --> "1" LanguagePair
|
}
|
||||||
Deck "1" o-- "0..*" Card
|
class FlashCardViewModel {
|
||||||
Deck ..> Vocabulary : built from
|
-_session : StudySession
|
||||||
Deck ..> EmptyDeckError : raises
|
+side : Side
|
||||||
VocabularyFormatError --|> VocabularyError
|
+language_name : str
|
||||||
VocabularyNotFoundError --|> VocabularyError
|
+text : str
|
||||||
EmptyDeckError --|> VocabularyError
|
+next_card() : None
|
||||||
StudySession "1" --> "1" Deck
|
+flip_card() : None
|
||||||
StudySession "1" --> "1" ProgressRepository
|
+is_known() : None
|
||||||
StudySession "1" --> "1" SpeechPort
|
}
|
||||||
StudySession ..> CardDto : returns
|
class FlashCardView {
|
||||||
open_study_session ..> VocabularyRepository
|
+flip_timer : str | None
|
||||||
open_study_session ..> ProgressRepository
|
+next_card() : None
|
||||||
open_study_session ..> StudySession : creates
|
+flip_card() : None
|
||||||
CsvVocabularyRepository ..|> VocabularyRepository
|
+is_known() : None
|
||||||
CsvProgressRepository ..|> ProgressRepository
|
}
|
||||||
GttsSpeech ..|> SpeechPort
|
class "app.main" as Main <<function>>
|
||||||
NullSpeech ..|> SpeechPort
|
}
|
||||||
FlashCardViewModel "1" --> "1" StudySession
|
Vocabulary "1" *-- "1..*" Card
|
||||||
FlashCardView "1" --> "1" FlashCardViewModel
|
Vocabulary "1" --> "1" LanguagePair
|
||||||
Main ..> CsvVocabularyRepository : composes
|
Level ..> Card : selects the first
|
||||||
Main ..> CsvProgressRepository : composes
|
Deck ..> Level : filtered by the cards of
|
||||||
Main ..> GttsSpeech : composes
|
Deck "1" o-- "0..*" Card
|
||||||
Main ..> FlashCardView : composes
|
Deck ..> Vocabulary : built from
|
||||||
@enduml
|
Deck ..> EmptyDeckError : raises
|
||||||
```
|
VocabularyFormatError --|> VocabularyError
|
||||||
|
VocabularyNotFoundError --|> VocabularyError
|
||||||
## Class Table
|
EmptyDeckError --|> VocabularyError
|
||||||
|
StudySession "1" --> "1" Deck
|
||||||
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
|
StudySession "1" --> "1" ProgressRepository
|
||||||
| --- | --- | --- | --- | --- |
|
StudySession "1" --> "1" SpeechPort
|
||||||
| `Card` | Flash Card | Holds a word and its translation | `front`, `back` | none |
|
StudySession ..> CardDto : returns
|
||||||
| `LanguagePair` | Word List (first and second language) | Names the two languages of a word list by code | `front_code`, `back_code` | none |
|
open_study_session ..> VocabularyRepository
|
||||||
| `Vocabulary` | Word List | Holds all cards of a language pair as delivered | `language_pair`, `cards` | none |
|
open_study_session ..> ProgressRepository
|
||||||
| `Deck` | Words To Learn | Holds the cards still to learn and picks and removes cards | `language_pair`, `_cards` | `from_vocabulary`, `draw`, `remove` |
|
open_study_session ..> Level
|
||||||
| `VocabularyRepository` | Word List | Port: load the delivered word list | none | `load` |
|
open_study_session ..> StudySession : creates
|
||||||
| `ProgressRepository` | Words To Learn | Port: load and save the words to learn | none | `load`, `save` |
|
CsvVocabularyRepository ..|> VocabularyRepository
|
||||||
| `StudySession` | Study Session | Runs one session: draws cards, marks cards known, saves progress, asks for speech | `_deck`, `_progress`, `_speech`, `_current` | `next_card`, `mark_known` |
|
CsvProgressRepository ..|> ProgressRepository
|
||||||
| `CardDto` | Flash Card | Plain data that crosses to the presentation layer | four text fields | none |
|
GttsSpeech ..|> SpeechPort
|
||||||
| `SpeechPort` | (none: external service) | Port: speak a text in a language | none | `speak` |
|
NullSpeech ..|> SpeechPort
|
||||||
| `CsvVocabularyRepository`, `CsvProgressRepository` | Word List, Words To Learn | Adapters that read and write the semicolon separated CSV file | `_path` | `load`, `save` |
|
FlashCardViewModel "1" --> "1" StudySession
|
||||||
| `GttsSpeech`, `NullSpeech` | (none) | Adapters for speech: the gTTS service, and silence | none | `speak` |
|
FlashCardView "1" --> "1" FlashCardViewModel
|
||||||
| `FlashCardViewModel` | Study Session | Exposes the side, language name and text of the card shown, and the three commands | `_session`, `side`, `language_name`, `text` | `next_card`, `flip_card`, `is_known` |
|
LevelViewModel "1" *-- "3" LevelChoice
|
||||||
| `FlashCardView` | (none: user interface) | Draws the window, forwards the buttons, owns the 3000 ms timer | `flip_timer`, widgets | `next_card`, `flip_card`, `is_known` |
|
LevelChoice "1" --> "1" Level
|
||||||
|
LevelView "1" --> "1" LevelViewModel
|
||||||
## Method Traceability
|
Main ..> LevelView : composes
|
||||||
|
Main ..> CsvVocabularyRepository : composes
|
||||||
| Method signature | Operation Contract / SD message |
|
Main ..> CsvProgressRepository : composes
|
||||||
| --- | --- |
|
Main ..> GttsSpeech : composes
|
||||||
| `open_study_session(vocabulary_repository, progress_repository, speech, choose) -> StudySession` | [OC-001] `startStudySession`, [SD-001] sequence startStudySession (loads the words to learn) |
|
Main ..> FlashCardView : composes
|
||||||
| `StudySession.next_card() -> CardDto \| None` | [OC-001] `startStudySession` (first card), `markUnknown`; [SD-001] |
|
@enduml
|
||||||
| `StudySession.mark_known() -> None` | [OC-001] `markKnown` (remove and save); [SD-001] |
|
```
|
||||||
| `FlashCardViewModel.next_card() -> None` | [OC-001] `markUnknown`; [SD-001] |
|
|
||||||
| `FlashCardViewModel.is_known() -> None` | [OC-001] `markKnown`; [SD-001] |
|
## Class Table
|
||||||
| `FlashCardViewModel.flip_card() -> None` | [SSD-001] timer turn-over (no contract: started by the system's timer) |
|
|
||||||
| `FlashCardView.next_card()`, `.is_known()`, `.flip_card()` | the same three operations, forwarded from buttons and the timer; names are the assignment's |
|
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
## Pattern Annotations
|
| `Card` | Flash Card | Holds a word and its translation | `front`, `back` | none |
|
||||||
|
| `LanguagePair` | Word List (first and second language) | Names the two languages of a word list by code | `front_code`, `back_code` | none |
|
||||||
| Pattern | Classes | Rationale |
|
| `Level` | Level | Names how many of the first words of the word list a session uses and selects them | `limit` | `first` |
|
||||||
| --- | --- | --- |
|
| `Vocabulary` | Word List | Holds all cards of a language pair as delivered | `language_pair`, `cards` | none |
|
||||||
| Repository | `VocabularyRepository`, `ProgressRepository`, CSV adapters | The domain does not know the CSV file |
|
| `Deck` | Words To Learn | Holds the cards still to learn and picks and removes cards | `language_pair`, `_cards` | `from_vocabulary`, `within`, `draw`, `remove` |
|
||||||
| Ports and adapters | `SpeechPort`, `GttsSpeech`, `NullSpeech` | Speech is an optional extra; the core runs without it |
|
| `VocabularyRepository` | Word List | Port: load the delivered word list | none | `load` |
|
||||||
| Null Object | `NullSpeech` | No `if speech:` checks in the use case |
|
| `ProgressRepository` | Words To Learn | Port: load and save the words to learn | none | `load`, `save` |
|
||||||
| MVVM | `FlashCardView`, `FlashCardViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) |
|
| `StudySession` | Study Session | Runs one session: draws cards of the level, marks cards known, saves progress, asks for speech | `_deck`, `_progress`, `_speech`, `_level_cards`, `_current` | `next_card`, `mark_known` |
|
||||||
| Composition root | `app.main` | The one place that names the adapters |
|
| `CardDto` | Flash Card | Plain data that crosses to the presentation layer | four text fields | none |
|
||||||
|
| `SpeechPort` | (none: external service) | Port: speak a text in a language | none | `speak` |
|
||||||
## Dependency Check
|
| `CsvVocabularyRepository`, `CsvProgressRepository` | Word List, Words To Learn | Adapters that read and write the semicolon separated CSV file | `_path` | `load`, `save` |
|
||||||
|
| `GttsSpeech`, `NullSpeech` | (none) | Adapters for speech: the gTTS service, and silence | none | `speak` |
|
||||||
Imports point inward: `domain` imports nothing from the other packages; `application` imports `domain`; `infrastructure` imports `application` and `domain`; `presentation` imports `application` and `domain`, and `app.main` additionally imports `infrastructure`. There is no cycle. `constants.py` imports nothing from the packages.
|
| `FlashCardViewModel` | Study Session | Exposes the side, language name and text of the card shown, and the three commands | `_session`, `side`, `language_name`, `text` | `next_card`, `flip_card`, `is_known` |
|
||||||
|
| `LevelChoice`, `LevelViewModel` | Level | Plain data and view model of the level screen: the title and the three choices with their labels | `title`, `choices` | none |
|
||||||
---
|
| `LevelView` | (none: user interface) | Draws the level screen and forwards the chosen level | buttons | `choose` |
|
||||||
|
| `FlashCardView` | (none: user interface) | Draws the window, forwards the buttons, owns the 3000 ms timer | `flip_timer`, widgets | `next_card`, `flip_card`, `is_known` |
|
||||||
[DM-001]: ./domain-model.md
|
|
||||||
[DICT-001]: ./dictionary.md
|
## Method Traceability
|
||||||
[SSD-001]: ./uc-001/ssd.md
|
|
||||||
[OC-001]: ./uc-001/oc.md
|
| Method signature | Operation Contract / SD message |
|
||||||
[SD-001]: ./uc-001/sd.md
|
| --- | --- |
|
||||||
[ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.md
|
| `open_study_session(vocabulary_repository, progress_repository, speech, choose, level) -> StudySession` | [OC-001] `chooseLevel`, [SD-001] sequence chooseLevel (loads the words to learn and the level's cards) |
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
| `Level.first(cards) -> tuple[Card, ...]`, `Deck.within(cards) -> tuple[Card, ...]` | [OC-001] `chooseLevel` postcondition (the level's cards); [SD-001] |
|
||||||
|
| `LevelViewModel.choices -> tuple[LevelChoice, ...]` | [OC-001] `startStudySession`; [SD-001] sequence startStudySession |
|
||||||
|
| `LevelView.choose(level) -> None` | [OC-001] `chooseLevel` (forwards the level to the composition root) |
|
||||||
|
| `StudySession.next_card() -> CardDto \| None` | [OC-001] `chooseLevel` (first card), `markUnknown`; [SD-001] |
|
||||||
|
| `StudySession.mark_known() -> None` | [OC-001] `markKnown` (remove and save); [SD-001] |
|
||||||
|
| `FlashCardViewModel.next_card() -> None` | [OC-001] `markUnknown`; [SD-001] |
|
||||||
|
| `FlashCardViewModel.is_known() -> None` | [OC-001] `markKnown`; [SD-001] |
|
||||||
|
| `FlashCardViewModel.flip_card() -> None` | [SSD-001] timer turn-over (no contract: started by the system's timer) |
|
||||||
|
| `FlashCardView.next_card()`, `.is_known()`, `.flip_card()` | the same three operations, forwarded from buttons and the timer; names are the assignment's |
|
||||||
|
|
||||||
|
## Pattern Annotations
|
||||||
|
|
||||||
|
| Pattern | Classes | Rationale |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Repository | `VocabularyRepository`, `ProgressRepository`, CSV adapters | The domain does not know the CSV file |
|
||||||
|
| Ports and adapters | `SpeechPort`, `GttsSpeech`, `NullSpeech` | Speech is an optional extra; the core runs without it |
|
||||||
|
| Null Object | `NullSpeech` | No `if speech:` checks in the use case |
|
||||||
|
| MVVM | `FlashCardView`, `FlashCardViewModel`, `LevelView`, `LevelViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) |
|
||||||
|
| Composition root | `app.main` | The one place that names the adapters, shows the level screen and chooses the word list file (beside the executable, else inside it) |
|
||||||
|
| Value object | `Level`, `Card`, `LanguagePair` | Immutable data with its own rule (`Level.first`) |
|
||||||
|
|
||||||
|
## Dependency Check
|
||||||
|
|
||||||
|
Imports point inward: `domain` imports nothing from the other packages; `application` imports `domain`; `infrastructure` imports `application` and `domain`; `presentation` imports `application` and `domain`, and `app.main` additionally imports `infrastructure`. There is no cycle. `constants.py` imports nothing from the packages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[DM-001]: ./domain-model.md
|
||||||
|
[DICT-001]: ./dictionary.md
|
||||||
|
[SSD-001]: ./uc-001/ssd.md
|
||||||
|
[OC-001]: ./uc-001/oc.md
|
||||||
|
[SD-001]: ./uc-001/sd.md
|
||||||
|
[ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.md
|
||||||
|
[ADR-0004]: ./adr/adr-0004-pyinstaller-executable.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
|
|||||||
+48
-45
@@ -1,45 +1,48 @@
|
|||||||
# Domain Dictionary: Flash Card
|
# Domain Dictionary: Flash Card
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | DICT-001 |
|
| ID | DICT-001 |
|
||||||
| CrossReference | [BC-001], [SA-001], [DM-001] |
|
| CrossReference | [BC-001], [SA-001], [DM-001] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the term Level | [0b983b7] |
|
||||||
---
|
|
||||||
|
---
|
||||||
## Purpose and Scope
|
|
||||||
|
## Purpose and Scope
|
||||||
Maps each Product Owner (PO) term to its professional IT term. PO language: English (`en`), domain: Software and IT (`it`), from the registry's `Languages` section. The PO terms are the words of a language learner; the IT terms are the class and function names of the code.
|
|
||||||
|
Maps each Product Owner (PO) term to its professional IT term. PO language: English (`en`), domain: Software and IT (`it`), from the registry's `Languages` section. The PO terms are the words of a language learner; the IT terms are the class and function names of the code.
|
||||||
## Dictionary
|
|
||||||
|
## Dictionary
|
||||||
| PO term | Language | IT term | Definition | Used as PO term in | Used as IT term in |
|
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| PO term | Language | IT term | Definition | Used as PO term in | Used as IT term in |
|
||||||
| Flash card | en | `Card` | A word in one language together with its translation in another language. | DM, UC, US | DCD |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| Word list | en | `Vocabulary` | The complete set of flash cards of one language pair, as delivered with the program. | DM, UC, US | DCD |
|
| Flash card | en | `Card` | A word in one language together with its translation in another language. | DM, UC, US | DCD |
|
||||||
| Words to learn | en | `Deck` | The flash cards the learner does not yet know; it starts as the word list and only gets smaller. | DM, UC, US | DCD |
|
| Word list | en | `Vocabulary` | The complete set of flash cards of one language pair, as delivered with the program. | DM, UC, US | DCD |
|
||||||
| Study session | en | `StudySession` | One run of the program in which the learner is shown cards one by one. | DM, UC, US | DCD |
|
| Words to learn | en | `Deck` | The flash cards the learner does not yet know; it starts as the word list and only gets smaller. | DM, UC, US | DCD |
|
||||||
| Language pair | en | `LanguagePair` | The two languages of a word list, the language learned and the language of the translation. | DM, UC | DCD |
|
| Study session | en | `StudySession` | One run of the program in which the learner is shown cards one by one. | DM, UC, US | DCD |
|
||||||
| Progress file | en | `ProgressRepository` | The file `words_to_learn.csv` that keeps the words to learn between runs. | UC, US | DCD |
|
| Level | en | `Level` | The number of the most frequent words of the word list that a study session uses: 100, 1000 or all. | DM, UC, US | DCD |
|
||||||
| Spoken word | en | `SpeechPort` | The Malay word read aloud when its card is shown. | UC, US | DCD |
|
| Language pair | en | `LanguagePair` | The two languages of a word list, the language learned and the language of the translation. | DM, UC | DCD |
|
||||||
|
| Progress file | en | `ProgressRepository` | The file `words_to_learn.csv` that keeps the words to learn between runs. | UC, US | DCD |
|
||||||
## Rules
|
| Spoken word | en | `SpeechPort` | The Malay word read aloud when its card is shown. | UC, US | DCD |
|
||||||
|
|
||||||
- The Domain Model, use cases and user stories use the PO term; the Operation Contract, Sequence Diagram, Design Class Diagram and ERD use the IT term.
|
## Rules
|
||||||
- One IT term per PO term and one PO term per IT term; no synonyms.
|
|
||||||
- "Card" alone means a flash card; "front" is its word in the language learned and "back" is its translation, and the card on screen shows one of them at a time.
|
- The Domain Model, use cases and user stories use the PO term; the Operation Contract, Sequence Diagram, Design Class Diagram and ERD use the IT term.
|
||||||
|
- One IT term per PO term and one PO term per IT term; no synonyms.
|
||||||
---
|
- "Card" alone means a flash card; "front" is its word in the language learned and "back" is its translation, and the card on screen shows one of them at a time.
|
||||||
|
|
||||||
[BC-001]: ./business-case.md
|
---
|
||||||
[SA-001]: ./stakeholder-analysis.md
|
|
||||||
[DM-001]: ./domain-model.md
|
[BC-001]: ./business-case.md
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
[SA-001]: ./stakeholder-analysis.md
|
||||||
|
[DM-001]: ./domain-model.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
|||||||
+90
-80
@@ -1,80 +1,90 @@
|
|||||||
# Domain Model: Flash Card
|
# Domain Model: Flash Card
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | DM-001 |
|
| ID | DM-001 |
|
||||||
| CrossReference | [UC-001], [UCD-001], [SSD-001], [DICT-001] |
|
| CrossReference | [UC-001], [UCD-001], [SSD-001], [DICT-001] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the concept Level | [0b983b7] |
|
||||||
---
|
|
||||||
|
---
|
||||||
## Purpose and Scope
|
|
||||||
|
## Purpose and Scope
|
||||||
Covers: [UC-001] Study Flash Cards. The model holds the concepts a Learner talks about: the flash card, the word list, the words to learn and the study session. The project-level model is this document; no other use case exists.
|
|
||||||
|
Covers: [UC-001] Study Flash Cards. The model holds the concepts a Learner talks about: the flash card, the word list, the words to learn, the level and the study session. The project-level model is this document; no other use case exists.
|
||||||
## Diagram
|
|
||||||
|
## Diagram
|
||||||
Concepts, attributes and associations only — no operations.
|
|
||||||
|
Concepts, attributes and associations only — no operations.
|
||||||
```plantuml
|
|
||||||
@startuml
|
```plantuml
|
||||||
class "Flash Card" as FlashCard {
|
@startuml
|
||||||
word in the first language
|
class "Flash Card" as FlashCard {
|
||||||
translation in the second language
|
word in the first language
|
||||||
}
|
translation in the second language
|
||||||
class "Word List" as WordList {
|
}
|
||||||
first language
|
class "Word List" as WordList {
|
||||||
second language
|
first language
|
||||||
}
|
second language
|
||||||
class "Words To Learn" as WordsToLearn
|
}
|
||||||
class "Study Session" as StudySession {
|
class "Words To Learn" as WordsToLearn
|
||||||
card shown
|
class "Level" as Level {
|
||||||
side shown
|
number of words, or all words
|
||||||
}
|
}
|
||||||
WordList "1" o-- "1..*" FlashCard : holds
|
class "Study Session" as StudySession {
|
||||||
WordsToLearn "1" o-- "0..*" FlashCard : holds
|
card shown
|
||||||
WordsToLearn "1" --> "1" WordList : starts as a copy of
|
side shown
|
||||||
StudySession "1" --> "1" WordsToLearn : works through
|
}
|
||||||
StudySession "1" --> "0..1" FlashCard : shows
|
WordList "1" o-- "1..*" FlashCard : holds
|
||||||
@enduml
|
WordsToLearn "1" o-- "0..*" FlashCard : holds
|
||||||
```
|
WordsToLearn "1" --> "1" WordList : starts as a copy of
|
||||||
|
StudySession "1" --> "1" WordsToLearn : works through
|
||||||
## Concept Table
|
StudySession "1" --> "0..1" FlashCard : shows
|
||||||
|
StudySession "1" --> "1" Level : is limited to
|
||||||
| Concept | Definition | Attributes | Source (use case / glossary) |
|
Level "1" --> "1" WordList : takes the first words of
|
||||||
| --- | --- | --- | --- |
|
@enduml
|
||||||
| Flash Card | A word in one language with its translation in another | word, translation | UC-001 steps 3 and 4 ("card", "translation") |
|
```
|
||||||
| Word List | The complete set of flash cards of a language pair, as delivered with the program | first language, second language | UC-001 step 2 ("word list") |
|
|
||||||
| Words To Learn | The flash cards the Learner does not yet know; starts as the word list and only gets smaller | none | UC-001 steps 2 and 6 ("words to learn") |
|
## Concept Table
|
||||||
| Study Session | One run of the program in which the Learner is shown cards | card shown, side shown | UC-001 main scenario |
|
|
||||||
|
| Concept | Definition | Attributes | Source (use case / glossary) |
|
||||||
## Association Table
|
| --- | --- | --- | --- |
|
||||||
|
| Flash Card | A word in one language with its translation in another | word, translation | UC-001 steps 3 and 4 ("card", "translation") |
|
||||||
| From | Association (reading direction) | To | Multiplicity |
|
| Word List | The complete set of flash cards of a language pair, as delivered with the program | first language, second language | UC-001 step 2 ("word list") |
|
||||||
| --- | --- | --- | --- |
|
| Words To Learn | The flash cards the Learner does not yet know; starts as the word list and only gets smaller | none | UC-001 steps 2 and 6 ("words to learn") |
|
||||||
| Word List | holds | Flash Card | 1 to 1..* |
|
| Level | The part of the word list a study session uses: its first 100 or 1000 words, or all words | number of words, or all words | UC-001 steps 2, 3 and 5 ("level") |
|
||||||
| Words To Learn | holds | Flash Card | 1 to 0..* |
|
| Study Session | One run of the program in which the Learner is shown cards | card shown, side shown | UC-001 main scenario |
|
||||||
| Words To Learn | starts as a copy of | Word List | 1 to 1 |
|
|
||||||
| Study Session | works through | Words To Learn | 1 to 1 |
|
## Association Table
|
||||||
| Study Session | shows | Flash Card | 1 to 0..1 |
|
|
||||||
|
| From | Association (reading direction) | To | Multiplicity |
|
||||||
## Generalizations
|
| --- | --- | --- | --- |
|
||||||
|
| Word List | holds | Flash Card | 1 to 1..* |
|
||||||
| General | Specializations | Is-a justification |
|
| Words To Learn | holds | Flash Card | 1 to 0..* |
|
||||||
| --- | --- | --- |
|
| Words To Learn | starts as a copy of | Word List | 1 to 1 |
|
||||||
| none | none | no generalization exists in this domain |
|
| Study Session | works through | Words To Learn | 1 to 1 |
|
||||||
|
| Study Session | shows | Flash Card | 1 to 0..1 |
|
||||||
---
|
| Study Session | is limited to | Level | 1 to 1 |
|
||||||
|
| Level | takes the first words of | Word List | 1 to 1 |
|
||||||
[UC-001]: ./uc-001/uc.md
|
|
||||||
[UCD-001]: ./use-case-diagram.md
|
## Generalizations
|
||||||
[SSD-001]: ./uc-001/ssd.md
|
|
||||||
[DICT-001]: ./dictionary.md
|
| General | Specializations | Is-a justification |
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
| --- | --- | --- |
|
||||||
|
| none | none | no generalization exists in this domain |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[UC-001]: ./uc-001/uc.md
|
||||||
|
[UCD-001]: ./use-case-diagram.md
|
||||||
|
[SSD-001]: ./uc-001/ssd.md
|
||||||
|
[DICT-001]: ./dictionary.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,82 @@
|
|||||||
|
# MIL-008: Choose level
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | MIL-008 |
|
||||||
|
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
|
||||||
|
| Language | en |
|
||||||
|
| Domain | it |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Decide whether the Learner can choose a level before learning: the program asks for 100 words, 1000 words or all words, and then uses the first N words of the word list, which is ordered from the most to the least frequent word.
|
||||||
|
|
||||||
|
## Deliverable
|
||||||
|
|
||||||
|
At start the window shows the choice "100 words", "1000 words" and "All words". After a choice the card window of the earlier milestones opens and every card comes from the first N words of the word list that the Learner does not yet know. Known words stay known across levels because there is still one progress file. The design documents, README, wiki (English, Danish, Bahasa Malaysia) and screenshots describe the choice.
|
||||||
|
|
||||||
|
## Go / No-Go Criteria
|
||||||
|
|
||||||
|
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Choosing 100 draws only cards that are among the first 100 rows of `data/ms-da.csv`, for every card of a full run through the deck | Integration test passes on the real word list | A card from row 101 or later appears |
|
||||||
|
| 2 | Choosing 1000 words or All words uses every word of the list; with a list of fewer than N words all are used | Unit and integration tests pass | Any word is missing |
|
||||||
|
| 3 | Marking words known at level 100 and then choosing All words leaves those words known, and `words_to_learn.csv` still holds every word that is not known (also those outside level 100) | Integration test passes | A word outside the level is lost from the file |
|
||||||
|
| 4 | When every word of the chosen level is known the card shows the message that nothing is left to learn, and the program does not fail | Unit and e2e tests pass | Exception or empty window |
|
||||||
|
| 5 | The level screen holds no rule: the labels and the choice come from the level view model, which imports no Tkinter | View model unit tests pass | A rule is in the view |
|
||||||
|
| 6 | `python -m flash_card --level 100` (also `1000`, `all`) skips the screen and starts at that level; an unknown value is refused by the argument parser | Unit tests pass | The screen appears or the program starts |
|
||||||
|
| 7 | The updated UC-001, DM-001, DICT-001, SSD-001, OC-001, SD-001, DCD-001, BC-001, wireframe, README and the wiki in three languages mention the level choice, and each changed document has a new Version History row and a `Go` review | Review records `Go`; `check-readme.sh` exit 0 | A document is not updated |
|
||||||
|
| 8 | Code review against QC-PY-001 has no failed Mandatory criterion; `pytest` exits with 0 and `ruff`, `mypy --strict`, `lint-imports` and `doxygen` are clean | `RC-*` verdict Go and exit codes 0 | Otherwise |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Depends on | Reason |
|
||||||
|
| --- | --- |
|
||||||
|
| MIL-007 | The finished program, README, wiki and screenshots that this milestone extends |
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
| Business Case objective / KPI / user story | Reference |
|
||||||
|
| --- | --- |
|
||||||
|
| Objective 1 (learning with the word list) and success criterion 1, extended by the new request of S01 | [BC-001] |
|
||||||
|
| US-001.06 Choose my level | [US-001] |
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
| Role | Stakeholder ID (SA) |
|
||||||
|
| --- | --- |
|
||||||
|
| Owner | S01 |
|
||||||
|
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
|
||||||
|
|
||||||
|
## Target Date
|
||||||
|
|
||||||
|
2026-10-20 — within the delivery window that ends 2026-10-21 ([BC-001] constraint).
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | Update the design documents for the level choice | Add the level to UC-001 (a first step before the cards, and the finished-level extension), to DM-001 and DICT-001 (concept Level), to SSD-001, OC-001 and SD-001 (operation chooseLevel and the level in startStudySession) and to DCD-001; draw the wireframe of the level screen in wireframe.md; add the level choice to the In Scope list of BC-001. Every changed document gets a Version History row and a new review. | No | UC-001, DCD-001 |
|
||||||
|
| 2 | Add the Level value object and the level choices | `Level` (limit as a number of words, or none for all words) in the domain and the three choices 100, 1000 and all words in constants.py with their labels, so that the choices are defined once. | No | DCD-001 |
|
||||||
|
| 3 | Draw cards from the chosen level | `StudySession` draws only from the first N words of the word list that are still to learn, and removes a known card from the whole words to learn and saves the whole deck, so that words of other levels are never lost. | Yes | US-001.06 |
|
||||||
|
| 4 | Open the session with a level | `open_study_session` gets the level, loads the word list for its order and the progress file for what is left, and builds the session; a level with nothing left gives a finished session. | Yes | US-001.06 |
|
||||||
|
| 5 | Add the level screen | `LevelViewModel` (the choices and their labels, no Tkinter) and `LevelView` in `presentation/features/level/` with the title "Choose your level" and three buttons on the window background. | Yes | US-001.06 |
|
||||||
|
| 6 | Show the level screen before the cards | The composition root shows the level screen first and builds the card view on the same window after the choice; the option `--level 100`, `--level 1000` or `--level all` skips the screen. | Yes | US-001.06 |
|
||||||
|
| 7 | Test the level choice | Unit tests of the level, the filter, the session and the view model; integration tests on the real word list and the progress file across levels; e2e test of the level screen and the flow to the first card. | No | |
|
||||||
|
| 8 | Update the README, the wiki and the screenshots | README, Usage and Home pages of the wiki in English, Danish and Bahasa Malaysia, and a new screenshot of the level screen made by tools/capture_screenshots.py; republish the wiki after the merge. | No | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../business-case.md
|
||||||
|
[US-001]: ../user-stories.md
|
||||||
|
[UC-001]: ../uc-001/uc.md
|
||||||
|
[DCD-001]: ../dcd.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# MIL-009: Executable with PyInstaller
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | MIL-009 |
|
||||||
|
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
|
||||||
|
| Language | en |
|
||||||
|
| Domain | it |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Decide whether the Learner can start the program from one executable file without a Python installation: the program is packaged with PyInstaller by a script of this repository, and behaves in the executable as it does from the source.
|
||||||
|
|
||||||
|
## Deliverable
|
||||||
|
|
||||||
|
`python tools/build_executable.py` builds `dist/flash-card.exe` (one file, no console window, an icon, the images and the word list inside, the speech extra included). The executable shows the level screen, keeps the progress in `data/words_to_learn.csv` next to it and prefers a `data/ms-da.csv` next to it over the word list inside. The executable itself is not committed to git; README and wiki (English, Danish, Bahasa Malaysia) explain how to build and use it.
|
||||||
|
|
||||||
|
## Go / No-Go Criteria
|
||||||
|
|
||||||
|
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | `python tools/build_executable.py` ends with exit code 0 and produces `dist/flash-card.exe` | File exists and is larger than 1 MB | No file or a build error |
|
||||||
|
| 2 | The executable starts on this machine with the virtual environment not activated and shows the level screen without a console window | A window titled "Flashy" exists after 5 seconds and the process is a Windows GUI program | The process ends or shows a console |
|
||||||
|
| 3 | With a `data/ms-da.csv` copied next to the executable it uses that file; without it the word list inside the executable is used | Smoke test with a marked list of two words, then without | The wrong list is used |
|
||||||
|
| 4 | Marking a word known in the executable writes `data/words_to_learn.csv` next to it and the next start continues from it | Smoke test reads the file after the run | No file or progress lost |
|
||||||
|
| 5 | The four images and the icon are inside the executable and are found when frozen | Card and button images are visible in a captured screenshot of the executable | A missing image or an exception |
|
||||||
|
| 6 | Path resolution when frozen (`sys.frozen`, `sys._MEIPASS`) is tested without PyInstaller | Unit tests pass | Not covered |
|
||||||
|
| 7 | `dependencies` in `pyproject.toml` is still empty; PyInstaller is only in the extra `build` | File check | A runtime dependency was added |
|
||||||
|
| 8 | BC-001, DCD-001 and ADR-0004 are updated and reviewed `Go`; README and wiki in three languages describe the build | Review records `Go`; `check-readme.sh` exit 0 | A document is missing |
|
||||||
|
| 9 | Code review against QC-PY-001 has no failed Mandatory criterion; `pytest`, `ruff`, `mypy --strict`, `lint-imports` and `doxygen` are clean | `RC-*` verdict Go and exit codes 0 | Otherwise |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Depends on | Reason |
|
||||||
|
| --- | --- |
|
||||||
|
| MIL-008 | The finished program, with the level screen, that is packaged |
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
| Business Case objective / KPI / user story | Reference |
|
||||||
|
| --- | --- |
|
||||||
|
| Objective 4 (a repository others can use), extended by the new request of S01; success criterion 4 (time to run) | [BC-001] |
|
||||||
|
| US-001.07 Start without Python | [US-001] |
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
| Role | Stakeholder ID (SA) |
|
||||||
|
| --- | --- |
|
||||||
|
| Owner | S01 |
|
||||||
|
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
|
||||||
|
|
||||||
|
## Target Date
|
||||||
|
|
||||||
|
2026-10-16 — within the delivery window that ends 2026-10-21 ([BC-001] constraint).
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | Update the documents for the executable | Amend BC-001 (In Scope: an executable built with PyInstaller; Out of Scope: an installer and a signed release stay out), add ADR-0004 (PyInstaller one-file windowed build, word list inside and a `data` folder beside the executable for progress, Windows only), and add the runtime path resolution to DCD-001. Every changed document gets a Version History row and a review. | No | BC-001, DCD-001 |
|
||||||
|
| 2 | Resolve resource and data paths when frozen | The images are found under `sys._MEIPASS` and the default data folder is `data/` next to the executable when `sys.frozen` is set; the source run keeps its paths. The rule is one function used by `constants.py`, tested by simulating the frozen state. | Yes | US-001.07 |
|
||||||
|
| 3 | Prefer the word list next to the executable | When the data folder has no `ms-da.csv` the program uses the copy inside the executable; the progress file is always written to the data folder; `--data-dir` still overrides both. | Yes | US-001.07 |
|
||||||
|
| 4 | Add the build script and the build extra | `tools/build_executable.py` calls PyInstaller (one file, windowed, name `flash-card`, the assets and the word list as data, the icon, the speech packages as hidden imports); `pyproject.toml` gets the extra `build`; `build/`, `dist/` and `*.spec` are ignored. | No | ADR-0004 |
|
||||||
|
| 5 | Generate the application icon | `tools/generate_images.py` also writes `flash_card.ico` (the card with the tick) from the same drawing code, so no external asset is used. | No | |
|
||||||
|
| 6 | Build and smoke-test the executable | Build it, start it with the virtual environment not active, check the window, the images, the level choice, the progress file next to it, the word list override and the speech import; record the result in the review record. | Yes | US-001.07 |
|
||||||
|
| 7 | Test the path rules and the build arguments | Unit tests for the frozen and source path rules and for the argument list the build script gives PyInstaller (without running PyInstaller). | No | |
|
||||||
|
| 8 | Update README and wiki | README and the Install page of the wiki in English, Danish and Bahasa Malaysia: how to build, where the executable and the data folder are, that Python is not needed to run it. The wiki is republished after the merge. | No | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../business-case.md
|
||||||
|
[US-001]: ../user-stories.md
|
||||||
|
[UC-001]: ../uc-001/uc.md
|
||||||
|
[DCD-001]: ../dcd.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
+18
-4
@@ -4,14 +4,16 @@
|
|||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | PP-001 |
|
| ID | PP-001 |
|
||||||
| CrossReference | [BC-001], [SA-001], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007] |
|
| CrossReference | [BC-001], [SA-001], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-008 Choose level (new request of S01) | [0b983b7] |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-009 Executable with PyInstaller (new request of S01) | [768ca34] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,6 +40,8 @@ This plan schedules the seven phases (gateways) that deliver the flash card prog
|
|||||||
| Step 4 - Save progress | [MIL-005] | 2026-10-18 | 2026-10-18 | S01 | US-001.04 | words_to_learn.csv | [Milestone MIL-005][MS-005] |
|
| Step 4 - Save progress | [MIL-005] | 2026-10-18 | 2026-10-18 | S01 | US-001.04 | words_to_learn.csv | [Milestone MIL-005][MS-005] |
|
||||||
| Spoken Malay word | [MIL-006] | 2026-10-19 | 2026-10-19 | S01 | US-001.05 | Text to speech as optional extra | [Milestone MIL-006][MS-006] |
|
| Spoken Malay word | [MIL-006] | 2026-10-19 | 2026-10-19 | S01 | US-001.05 | Text to speech as optional extra | [Milestone MIL-006][MS-006] |
|
||||||
| Documentation and publication | [MIL-007] | 2026-10-19 to 2026-10-21 | 2026-10-21 | S01 | none | README, screenshots, wiki in 3 languages, description and topics | [Milestone MIL-007][MS-007] |
|
| Documentation and publication | [MIL-007] | 2026-10-19 to 2026-10-21 | 2026-10-21 | S01 | none | README, screenshots, wiki in 3 languages, description and topics | [Milestone MIL-007][MS-007] |
|
||||||
|
| Choose level | [MIL-008] | 2026-10-20 | 2026-10-20 | S01 | US-001.06 | A level choice (100, 1000, all words) before the cards; documents, wiki and screenshots updated | |
|
||||||
|
| Executable with PyInstaller | [MIL-009] | 2026-10-16 | 2026-10-16 | S01 | US-001.07 | One Windows executable that runs without Python, built by a script; documents, wiki and README updated | |
|
||||||
|
|
||||||
```plantuml
|
```plantuml
|
||||||
@startgantt
|
@startgantt
|
||||||
@@ -56,6 +60,10 @@ Project starts 2026-10-14
|
|||||||
[Step 4 Go/No-Go] happens 2026-10-18
|
[Step 4 Go/No-Go] happens 2026-10-18
|
||||||
[Speech Go/No-Go] happens 2026-10-19
|
[Speech Go/No-Go] happens 2026-10-19
|
||||||
[Documentation Go/No-Go] happens 2026-10-21
|
[Documentation Go/No-Go] happens 2026-10-21
|
||||||
|
[Choose level] starts 2026-10-20 and ends 2026-10-20
|
||||||
|
[Choose level Go/No-Go] happens 2026-10-20
|
||||||
|
[Executable] starts 2026-10-16 and ends 2026-10-16
|
||||||
|
[Executable Go/No-Go] happens 2026-10-16
|
||||||
@endgantt
|
@endgantt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,15 +78,17 @@ Project starts 2026-10-14
|
|||||||
| `pyproject.toml`, `.gitignore`, `Doxyfile`, `.venv` instructions, `pytest`, `constants.py` | MIL-001 |
|
| `pyproject.toml`, `.gitignore`, `Doxyfile`, `.venv` instructions, `pytest`, `constants.py` | MIL-001 |
|
||||||
| README, screenshots, wiki in three languages | MIL-007 |
|
| README, screenshots, wiki in three languages | MIL-007 |
|
||||||
| Repository description and topics | MIL-007 |
|
| Repository description and topics | MIL-007 |
|
||||||
|
| Choice of level (new request after MIL-007) | MIL-008 |
|
||||||
|
| Executable built with PyInstaller (new request after MIL-008) | MIL-009 |
|
||||||
| Framework documents of the baseline | Baseline pull request before MIL-001 |
|
| Framework documents of the baseline | Baseline pull request before MIL-001 |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
```
|
```
|
||||||
Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007
|
Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008 → MIL-009
|
||||||
```
|
```
|
||||||
|
|
||||||
A No-Go on a phase moves every later date by the days the rework takes; the buffer is the 2026-10-20 working day, which has no phase of its own.
|
A No-Go on a phase moves every later date by the days the rework takes; MIL-008 was added after MIL-007 and takes the unplanned day 2026-10-20, so no buffer is left inside the window; a No-Go moves the end date past 2026-10-21 and S01 decides whether to accept that.
|
||||||
|
|
||||||
## Plan Risks
|
## Plan Risks
|
||||||
|
|
||||||
@@ -105,6 +115,8 @@ A No-Go on a phase moves every later date by the days the rework takes; the buff
|
|||||||
[MIL-005]: ./milestones/mil-005-step4-save-progress.md
|
[MIL-005]: ./milestones/mil-005-step4-save-progress.md
|
||||||
[MIL-006]: ./milestones/mil-006-malay-speech.md
|
[MIL-006]: ./milestones/mil-006-malay-speech.md
|
||||||
[MIL-007]: ./milestones/mil-007-documentation-and-publication.md
|
[MIL-007]: ./milestones/mil-007-documentation-and-publication.md
|
||||||
|
[MIL-008]: ./milestones/mil-008-choose-level.md
|
||||||
|
[MIL-009]: ./milestones/mil-009-executable.md
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
[MS-001]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/165
|
[MS-001]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/165
|
||||||
[MS-002]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/166
|
[MS-002]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/166
|
||||||
@@ -113,3 +125,5 @@ A No-Go on a phase moves every later date by the days the rework takes; the buff
|
|||||||
[MS-005]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/169
|
[MS-005]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/169
|
||||||
[MS-006]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/170
|
[MS-006]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/170
|
||||||
[MS-007]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/171
|
[MS-007]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/171
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# RC-030: Review of BC-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-030 |
|
||||||
|
| CrossReference | [BC-001], [QC-BC-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [BC-001]
|
||||||
|
- Checklist used: [QC-BC-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | The Cost-Benefit table is qualitative and says why: no revenue is expected, so a quantitative return does not apply. |
|
||||||
|
| 2 | Risks are identified with documented impact and mitigation | Pass | The Risks table has 5 risks, each with an impact and a mitigation. |
|
||||||
|
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | The Success Criteria table has 8 rows with numeric targets and a measure each (for example 0 third-party packages; 5 commands or fewer). |
|
||||||
|
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | Scope has `### In Scope` and `### Out of Scope` subsections. |
|
||||||
|
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | The Stakeholders table cites S01, S02 and S03 and does not re-describe their roles. |
|
||||||
|
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Methodological and Standards Foundation names the framework, Larman, ISO/IEC 25010:2023 and PEP 8. |
|
||||||
|
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | Assumptions and Constraints are separate sections. |
|
||||||
|
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Recommendation reads `Proceed` with a one-sentence rationale. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of BC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../../../docs/business-case.md
|
||||||
|
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# RC-031: Review of US-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-031 |
|
||||||
|
| CrossReference | [US-001], [QC-US-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [US-001]
|
||||||
|
- Checklist used: [QC-US-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | INVEST Check section confirms the six properties for all six stories and states one dependency exception. |
|
||||||
|
| 2 | Written in "As a / I want / So that" form | Pass | All six stories use `As a / I want / so that`. |
|
||||||
|
| 3 | Clear, testable acceptance criteria are included | Pass | Every story, including US-001.06, has Given/When/Then acceptance criteria. |
|
||||||
|
| 4 | Traceable to a use case or epic | Pass | Every story traces to a milestone and to UC-001. |
|
||||||
|
| 5 | Story is sized to fit within a single iteration | Pass | Each story is one milestone, which fits one iteration. |
|
||||||
|
| 6 | Story statement avoids technical implementation detail | Fail | Acceptance criteria name `words_to_learn.csv`, `ms-da.csv` and 3000 ms, which are assignment-mandated names and values rather than pure behaviour. |
|
||||||
|
| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | The role `Learner` is the actor of UCD-001. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of US-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion 6 (Optional) failed: Story statement avoids technical implementation detail; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[US-001]: ../../../docs/user-stories.md
|
||||||
|
[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# RC-032: Review of PP-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-032 |
|
||||||
|
| CrossReference | [PP-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [PP-001]
|
||||||
|
- Checklist used: none exists for this type, and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of PP-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — no checklist exists for the Project Plan type; the plan now schedules MIL-008 on 2026-10-20 inside the Business Case constraint (delivery by 2026-10-21) and the language checklist passes. Accepted as the Product Owner's stand-in, after the Product Owner accepted the MIL-008 plan in chat. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[PP-001]: ../../../docs/project-plan.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# RC-033: Review of UC-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-033 |
|
||||||
|
| CrossReference | [UC-001], [QC-UC-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [UC-001]
|
||||||
|
- Checklist used: [QC-UC-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | N-A | The format is Fully Dressed; the Brief format criterion does not apply. |
|
||||||
|
| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | The format is Fully Dressed; the Casual format criterion does not apply. |
|
||||||
|
| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Scope, level, actor, stakeholders, pre- and postconditions, main success scenario and extensions are present. |
|
||||||
|
| 4 | Preconditions and postconditions are explicitly defined | Pass | Preconditions and postconditions are listed. |
|
||||||
|
| 5 | Primary actor is explicitly stated | Pass | Primary actor is `Learner`. |
|
||||||
|
| 6 | Stakeholders and their interests are stated | Pass | Stakeholders S01 and S02 and their interests are stated. |
|
||||||
|
| 7 | Main success scenario is written as clear, numbered steps | Pass | Main success scenario has 9 numbered steps, including the level choice (steps 2 and 3). |
|
||||||
|
| 8 | Alternative/exception flows correctly reference `<<include>>`/`<<extend>>` use cases where relevant, per UML 2.5.1 | Pass | UCD-001 has no include or extend relationship, so no extension needs to reference one; the extensions (3a, 4a, 5a, 7a, 7b, 9a) are plain alternative flows. |
|
||||||
|
| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | Pass | The Business Rules table gives a rule per step. |
|
||||||
|
| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Title and actor match UCD-001 and US-001; the level step matches US-001.06. |
|
||||||
|
| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | `Level: user-goal` and `Scope: Flash Card program`. |
|
||||||
|
| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Steps are written as goals and answers; buttons, files and milliseconds are in the wireframe and milestones. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of UC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[UC-001]: ../../../docs/uc-001/uc.md
|
||||||
|
[QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# RC-034: Review of DM-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-034 |
|
||||||
|
| CrossReference | [DM-001], [QC-DM-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [DM-001]
|
||||||
|
- Checklist used: [QC-DM-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Uses ubiquitous/business language throughout; no technical or implementation jargon (e.g. no "table", "class", "pointer") | Pass | Concepts are Flash Card, Word List, Words To Learn, Level and Study Session; no table or class names. |
|
||||||
|
| 2 | Multiplicities on associations are correct and complete (e.g. `1..*`, `0..1`) | Pass | Association table gives both multiplicities for all 7 associations. |
|
||||||
|
| 3 | No operation/method signatures shown — attributes and associations only | Pass | The diagram has attributes and associations only. |
|
||||||
|
| 4 | Associations are named with an unambiguous reading direction | Pass | Every association is named with a reading direction (`holds`, `works through`, `is limited to`, `takes the first words of`). |
|
||||||
|
| 5 | Generalization/specialization used correctly, reflecting true "is-a" relationships, not misused for code reuse | Pass | Generalization table says none exists, and why. |
|
||||||
|
| 6 | Every concept traces to a noun phrase found in the use cases or glossary | Pass | Concept table gives a source noun phrase in UC-001 for every concept. |
|
||||||
|
| 7 | Attributes are simple domain data (no foreign-key-like references or object pointers modeled as attributes) | Pass | Attributes are plain data (word, translation, first language, card shown). |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of DM-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[DM-001]: ../../../docs/domain-model.md
|
||||||
|
[QC-DM-001]: ../../../framework/qc/artifacts/qc-domain-model.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# RC-035: Review of DICT-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-035 |
|
||||||
|
| CrossReference | [DICT-001], [QC-DICT-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [DICT-001]
|
||||||
|
- Checklist used: [QC-DICT-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Every row has a PO term, its language, an IT term and a definition | Pass | All 8 rows have PO term, language, IT term and definition. |
|
||||||
|
| 2 | Each PO term maps to exactly one IT term and the reverse (no synonyms) | Pass | One IT term per PO term; no synonym appears. |
|
||||||
|
| 3 | Every Domain Model concept has a row, and the Domain Model uses its PO term | Pass | The 5 concepts of DM-001 have a row (`Level` added) and DM-001 uses the PO term. |
|
||||||
|
| 4 | The Operation Contracts, Sequence Diagrams, Design Class Diagrams and ERD use the IT term, not the PO term | Pass | DCD-001, OC-001 and SD-001 use `Card`, `Vocabulary`, `Deck`, `StudySession`. |
|
||||||
|
| 5 | Definitions are written in the PO language and are one sentence | Pass | Definitions are one English sentence each. |
|
||||||
|
| 6 | "Used as PO term in" and "Used as IT term in" name artifact types that exist in the project | Pass | Artifact types named in both columns exist in the project. |
|
||||||
|
| 7 | The dictionary's `Language` and `Domain` rows, and the language of every row, match the PO language and domain in the project registry | Pass | `Language | en` and `Domain | it` match the registry; every row says `en`. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of DICT-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[DICT-001]: ../../../docs/dictionary.md
|
||||||
|
[QC-DICT-001]: ../../../framework/qc/artifacts/qc-dictionary.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# RC-036: Review of SSD-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-036 |
|
||||||
|
| CrossReference | [SSD-001], [QC-SSD-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [SSD-001]
|
||||||
|
- Checklist used: [QC-SSD-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Input/output messages match the corresponding Use Case's main success scenario step-for-step | Pass | System Operations table maps each message to steps of the main success scenario (1 to 9); the timer step is the one stated deviation. |
|
||||||
|
| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | The system is `:System`; no internal object appears. |
|
||||||
|
| 3 | Object creation/destruction of the System instance handled explicitly where relevant | Pass | Lifecycle Notes state that the system lives for one run. |
|
||||||
|
| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed return arrows are drawn for `startStudySession`, `chooseLevel`, the turn-over, `markKnown` and `markUnknown`. |
|
||||||
|
| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | The Alternate Flows section names extensions 3a, 4a, 5a, 7b and 9a, states that they add no operation, and says they are out of scope. |
|
||||||
|
| 6 | Message names are verb phrases consistent with the use case's system responsibilities | Pass | `startStudySession`, `markKnown` and `markUnknown` are verb phrases. |
|
||||||
|
| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | Source Use Case section names UC-001 and the scenario. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of SSD-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[SSD-001]: ../../../docs/uc-001/ssd.md
|
||||||
|
[QC-SSD-001]: ../../../framework/qc/artifacts/qc-ssd.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# RC-037: Review of OC-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-037 |
|
||||||
|
| CrossReference | [OC-001], [QC-OC-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [OC-001]
|
||||||
|
- Checklist used: [QC-OC-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Method signature is complete: operation name, parameter types, and return type | Pass | Each contract gives name, parameter list and return type (`startStudySession(): CardDto | None`). |
|
||||||
|
| 2 | Preconditions explicitly list required state before execution | Pass | Preconditions are listed per contract. |
|
||||||
|
| 3 | Postconditions explicitly describe resulting state using Larman's "instance created/associated/attribute modified" style | Pass | Postconditions use created / associated / destroyed / attribute set wording. |
|
||||||
|
| 4 | Exceptions and error conditions are documented, including the triggering precondition failure | Pass | Each contract has an Exceptions table with the failing precondition. |
|
||||||
|
| 5 | Operation is explicitly traceable to a single SSD message | Pass | Each of the four contracts (`startStudySession`, `chooseLevel`, `markKnown`, `markUnknown`) traces to exactly one SSD message; the timer step has none and says so. |
|
||||||
|
| 6 | Contract avoids specifying implementation/algorithmic details (declarative, not procedural) | Pass | Postconditions are declarative. |
|
||||||
|
| 7 | Cross-references the Domain Model classes/associations affected by pre/postconditions | Pass | Domain Model concepts and IT terms are cited per contract. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[OC-001]: ../../../docs/uc-001/oc.md
|
||||||
|
[QC-OC-001]: ../../../framework/qc/artifacts/qc-operation-contract.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# RC-038: Review of SD-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-038 |
|
||||||
|
| CrossReference | [SD-001], [QC-SD-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [SD-001]
|
||||||
|
- Checklist used: [QC-SD-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Message passing strictly follows UML sync/async/return arrow syntax | Pass | Calls are solid arrows with dashed returns. |
|
||||||
|
| 2 | GRASP/GoF patterns applied and explicitly annotated where used (e.g. Controller, Observer, Mediator, Factory) | Pass | Each sequence has a Pattern Annotations table (Creator, Information Expert, Null Object, MVVM, Repository). |
|
||||||
|
| 3 | Lifelines show activation bars matching actual processing time/call nesting | Pass | Activations are drawn for the session and the view model. |
|
||||||
|
| 4 | Object creation and destruction shown with correct UML notation (`create`/`destroy` messages, X on lifeline) | Pass | `create` is used for `Deck` and `StudySession`; no object is destroyed during the program's life and that is stated by omission, the process ends. |
|
||||||
|
| 5 | Diagram realizes the postconditions of a specific Operation Contract | Pass | Each of the four sequences has a Postcondition Coverage table for its contract in OC-001. |
|
||||||
|
| 6 | Responsibility assignment favors low coupling/high cohesion (no god-object receiving all messages) | Pass | Responsibility Check notes state that no object receives all messages. |
|
||||||
|
| 7 | Loop, alt, and opt combined fragments used correctly for conditional/repeated behavior | Pass | `alt` and `opt` fragments are used for the progress file and the empty deck. |
|
||||||
|
| 8 | Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, or its absence is justified | Fail | The exceptions of OC-001 (missing file, malformed file) are raised before the sequence starts and are not drawn as `alt` fragments; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion 8 (Optional) failed: Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, ; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[SD-001]: ../../../docs/uc-001/sd.md
|
||||||
|
[QC-SD-001]: ../../../framework/qc/artifacts/qc-sequence-diagram.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# RC-039: Review of DCD-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-039 |
|
||||||
|
| CrossReference | [DCD-001], [QC-DCD-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [DCD-001]
|
||||||
|
- Checklist used: [QC-DCD-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | SOLID principles applied; no god classes with excessive responsibilities | Pass | Each class has one responsibility (Class Table); view, view model, session, deck and adapters are separate. |
|
||||||
|
| 2 | Visibility markers correct and consistent (`+` public, `-` private, `#` protected) | Pass | Visibility markers are on all members. |
|
||||||
|
| 3 | Relationships correctly distinguished: Association vs Aggregation vs Composition vs Dependency | Pass | Composition (Vocabulary and Card), aggregation (Deck), composition (LevelViewModel and LevelChoice), association, dependency and realization are distinguished. |
|
||||||
|
| 4 | Multiplicities and navigability specified on all associations | Pass | Multiplicities are on the associations; dependencies are directed. |
|
||||||
|
| 5 | Applied design patterns are annotated explicitly (e.g. Singleton, Factory, Strategy) | Pass | Pattern Annotations table lists Repository, Ports and adapters, Null Object, MVVM, Composition root. |
|
||||||
|
| 6 | Method signatures are traceable to Operation Contracts and/or design Sequence Diagrams | Pass | Method Traceability table maps every operation, including `Level.first`, `Deck.within`, `LevelViewModel.choices` and `LevelView.choose`, to OC-001 and SD-001. |
|
||||||
|
| 7 | Class names and structure remain consistent with the Domain Model concepts they refine | Pass | The Class Table has a `Refines` column; `Level` refines the concept Level of DM-001; names match the dictionary's IT terms. |
|
||||||
|
| 8 | No circular dependencies between classes/packages unless explicitly justified | Pass | Dependency Check states there are no cycles. |
|
||||||
|
| 9 | Dependencies between classes and packages point inward: domain classes do not depend on infrastructure, delivery or framework classes | Pass | Domain imports nothing; infrastructure and presentation point inward (Dependency Check). |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the level choice was added for MIL-008.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[DCD-001]: ../../../docs/dcd.md
|
||||||
|
[QC-DCD-001]: ../../../framework/qc/artifacts/qc-dcd.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# RC-040: Review of MIL-008
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-040 |
|
||||||
|
| CrossReference | [MIL-008], [QC-MIL-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-008]
|
||||||
|
- Checklist used: [QC-MIL-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A concrete deliverable is defined for every gate | Pass | A concrete Deliverable section names the output evaluated at the gate. |
|
||||||
|
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | The Go / No-Go table has numbered criteria, each with an observable Go and No-Go. |
|
||||||
|
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the milestone or documents it needs. |
|
||||||
|
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability table maps to Business Case objectives and the user story. |
|
||||||
|
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership table names S01 as owner and approving reviewer. |
|
||||||
|
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies inside the 2026-10-14 to 2026-10-21 window of the Business Case. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of MIL-008 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-008]: ../../../docs/milestones/mil-008-choose-level.md
|
||||||
|
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# RC-041: Review of PY-MIL-008
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-041 |
|
||||||
|
| CrossReference | [PY-MIL-008], [QC-PY-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f45d603] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [PY-MIL-008]
|
||||||
|
- Checklist used: [QC-PY-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | PEP 8 casing holds in all new modules (`level.py`, `level_view.py`, `window.py`); ruff rule set N passes. |
|
||||||
|
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose and are the dictionary's IT terms (`Level`, `Deck.within`, `LevelViewModel`, `LevelChoice`, `Application.start`). |
|
||||||
|
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass on 67 files with no suppressions. |
|
||||||
|
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 44 source files and on `tools/capture_screenshots.py`. |
|
||||||
|
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `Level` raises `ValueError` for a limit below 1; a failed session start after a click is logged, closes the window with exit code 1 and never swallows silently; no bare `except`. |
|
||||||
|
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
|
||||||
|
| 7 | Files, locks and connections are managed with context managers | Pass | No new file handling beyond the existing context managers. |
|
||||||
|
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | New and changed members have Doxygen docstrings; `doxygen Doxyfile` ends with 0 warnings. |
|
||||||
|
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`; failures are logged with `logging`; no personal data is logged. |
|
||||||
|
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `Level`, `Deck.within`, `Deck.draw(among)`, `StudySession(level_cards)`, `open_study_session(..., level)`, `LevelChoice`, `LevelViewModel`, `LevelView` and `Application` match the revised DCD-001, OC-001 `chooseLevel` and SD-001. Deviations: `Application` (root, repositories, speech) with `start`, `show_level_screen` and `exit_code` is the composition root detail that DCD-001 shows as `app.main`; `StudySession.remaining_in_level` and `LevelView.frame` are additions. |
|
||||||
|
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 191 tests (3 consecutive full runs green) cover `Level`, `Deck.within`, the level filter, the cross-level progress rules on the real word list, the view model, `--level` and the real level window flow; none uses the network. |
|
||||||
|
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
|
||||||
|
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; no new package. |
|
||||||
|
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | The level rule exists only in `Level.first` and `Deck.within`; the choices, labels and sizes exist once in `constants.py`; both views use `configure_window`. |
|
||||||
|
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; a grep finds no `tkinter`, `csv` or `random` import in `domain` or `application`; the level view model imports no Tkinter. |
|
||||||
|
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | The view model owns the choices and the view draws them; the composition root only wires; the session depends on protocols and receives its collaborators. |
|
||||||
|
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | `presentation/features/level/` and `presentation/shared/` follow the clean-architecture folder structure; tests stay in `unit`, `integration`, `e2e` (with a session-scoped `tk_root` in `tests/e2e/conftest.py`). |
|
||||||
|
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names follow the rule. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Milestone MIL-008 Go criteria: 1 level 100 draws exactly the first 100 rows over a full run of the real list (integration test); 2 levels 1000 and All use all 1000 words, and a shorter list is used whole (unit and integration tests); 3 words known at level 100 stay known at All and the progress file keeps every unknown word, also outside the level (integration test); 4 a level with nothing left gives the finished message and no failure (unit and e2e tests); 5 the level view model has no Tkinter import and its tests pass without a display; 6 `--level 100|1000|all` skips the screen and an unknown value exits with 2 (unit tests); 7 UC-001, DM-001, DICT-001, SSD-001, OC-001, SD-001, DCD-001, BC-001, US-001, PP-001 and MIL-008 were re-reviewed `Go` (RC-030 to RC-040), the wireframe has Screen 1, and the README and the wiki in en, da and ms describe the level; 8 this record, `pytest` 191 passed, ruff, mypy, lint-imports and doxygen clean. A real screenshot of the level screen was captured and inspected after fixing a too-small window. The Danish and Malay wiki additions were written by the AI and are not read by a native speaker. All Go.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[PY-MIL-008]: ../../../src/flash_card
|
||||||
|
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||||
|
[f45d603]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f45d6036e4210e1fbe52f6a582c540a6e1877056
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# RC-042: Review of BC-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-042 |
|
||||||
|
| CrossReference | [BC-001], [QC-BC-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [BC-001]
|
||||||
|
- Checklist used: [QC-BC-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | The Cost-Benefit table is qualitative and says why: no revenue is expected, so a quantitative return does not apply. |
|
||||||
|
| 2 | Risks are identified with documented impact and mitigation | Pass | The Risks table has 5 risks, each with an impact and a mitigation. |
|
||||||
|
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | The Success Criteria table has 8 rows with numeric targets and a measure each (for example 0 third-party packages; 5 commands or fewer). |
|
||||||
|
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | Scope has `### In Scope` and `### Out of Scope`; the executable is added to In Scope and the installer, signing and release upload are named in Out of Scope. |
|
||||||
|
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | The Stakeholders table cites S01, S02 and S03 and does not re-describe their roles. |
|
||||||
|
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Methodological and Standards Foundation names the framework, Larman, ISO/IEC 25010:2023 and PEP 8. |
|
||||||
|
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | Assumptions and Constraints are separate sections. |
|
||||||
|
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Recommendation reads `Proceed` with a one-sentence rationale. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of BC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the change for MIL-009 (executable).
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../../../docs/business-case.md
|
||||||
|
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# RC-043: Review of US-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-043 |
|
||||||
|
| CrossReference | [US-001], [QC-US-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [US-001]
|
||||||
|
- Checklist used: [QC-US-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | INVEST Check section confirms the six properties for all seven stories and states one dependency exception. |
|
||||||
|
| 2 | Written in "As a / I want / So that" form | Pass | All seven stories use `As a / I want / so that`. |
|
||||||
|
| 3 | Clear, testable acceptance criteria are included | Pass | Every story, including US-001.07, has Given/When/Then acceptance criteria. |
|
||||||
|
| 4 | Traceable to a use case or epic | Pass | Every story traces to a milestone and to UC-001. |
|
||||||
|
| 5 | Story is sized to fit within a single iteration | Pass | Each story is one milestone, which fits one iteration. |
|
||||||
|
| 6 | Story statement avoids technical implementation detail | Fail | Acceptance criteria name `words_to_learn.csv`, `ms-da.csv` and 3000 ms, which are assignment-mandated names and values rather than pure behaviour. |
|
||||||
|
| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | The role `Learner` is the actor of UCD-001. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of US-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the change for MIL-009 (executable).
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion 6 (Optional) failed: Story statement avoids technical implementation detail; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[US-001]: ../../../docs/user-stories.md
|
||||||
|
[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# RC-044: Review of PP-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-044 |
|
||||||
|
| CrossReference | [PP-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [PP-001]
|
||||||
|
- Checklist used: none exists for this type, and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of PP-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — no checklist exists for the Project Plan type; the plan now schedules MIL-009 on 2026-10-16 inside the Business Case constraint (delivery by 2026-10-21) and the language checklist passes. Accepted as the Product Owner's stand-in, after the Product Owner accepted the MIL-009 plan in chat. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[PP-001]: ../../../docs/project-plan.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# RC-045: Review of DCD-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-045 |
|
||||||
|
| CrossReference | [DCD-001], [QC-DCD-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [DCD-001]
|
||||||
|
- Checklist used: [QC-DCD-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | SOLID principles applied; no god classes with excessive responsibilities | Pass | Each class has one responsibility (Class Table); view, view model, session, deck and adapters are separate. |
|
||||||
|
| 2 | Visibility markers correct and consistent (`+` public, `-` private, `#` protected) | Pass | Visibility markers are on all members. |
|
||||||
|
| 3 | Relationships correctly distinguished: Association vs Aggregation vs Composition vs Dependency | Pass | Composition (Vocabulary and Card), aggregation (Deck), association, dependency and realization are distinguished. |
|
||||||
|
| 4 | Multiplicities and navigability specified on all associations | Pass | Multiplicities are on the associations; dependencies are directed. |
|
||||||
|
| 5 | Applied design patterns are annotated explicitly (e.g. Singleton, Factory, Strategy) | Pass | Pattern Annotations table lists Repository, Ports and adapters, Null Object, MVVM, Composition root. |
|
||||||
|
| 6 | Method signatures are traceable to Operation Contracts and/or design Sequence Diagrams | Pass | Method Traceability table is unchanged; the packaging support (`runtime.py`, `tools/`) is stated in Purpose and Scope as having no system operation and not being a design class, so it needs no row. |
|
||||||
|
| 7 | Class names and structure remain consistent with the Domain Model concepts they refine | Pass | The Class Table has a `Refines` column; names match the dictionary's IT terms; no class was added. |
|
||||||
|
| 8 | No circular dependencies between classes/packages unless explicitly justified | Pass | Dependency Check states there are no cycles. |
|
||||||
|
| 9 | Dependencies between classes and packages point inward: domain classes do not depend on infrastructure, delivery or framework classes | Pass | Domain imports nothing; infrastructure and presentation point inward (Dependency Check). |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Full review (not a delta) after the change for MIL-009 (executable).
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[DCD-001]: ../../../docs/dcd.md
|
||||||
|
[QC-DCD-001]: ../../../framework/qc/artifacts/qc-dcd.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# RC-046: Review of ADR-0004
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-046 |
|
||||||
|
| CrossReference | [ADR-0004], [QC-ADR-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [ADR-0004]
|
||||||
|
- Checklist used: [QC-ADR-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The ID (`ADR-NNNN`, 4-digit) matches the filename number and is unique and sequential | Pass | ID `ADR-0004` matches the file name number. |
|
||||||
|
| 2 | Context states the problem, the forces (cost, risk, constraints) and the options evaluated | Pass | Context states the need (US-001.07), the forces and three options. |
|
||||||
|
| 3 | At least two options were considered, or the absence of alternatives is justified | Pass | Three options were considered. |
|
||||||
|
| 4 | Decision is stated in one or two unhedged sentences and matches one of the evaluated options | Pass | Decision is stated plainly and is option 2. |
|
||||||
|
| 5 | Consequences list both Positive and Negative outcomes | Pass | Consequences have Positive and Negative lists. |
|
||||||
|
| 6 | Quality-attribute impacts (performance, security, portability, etc.) are named where the decision affects them | Pass | Portability (Windows only, cross-compile limit), start-up time and size are named. |
|
||||||
|
| 7 | Affected Artifacts are listed as links, and each is also in CrossReference | Pass | Affected Artifacts BC-001, US-001 and DCD-001 are listed and in CrossReference. |
|
||||||
|
| 8 | Status changes are recorded as new Version History rows with a Change summary and Commit link; retained rows are never rewritten | Pass | First version; no status change yet. |
|
||||||
|
| 9 | A superseded or deprecated ADR names its successor, and the successor links back | N-A | Not superseded or deprecated. |
|
||||||
|
| 10 | Written in business or technical language of the domain, with no unexplained abbreviations | Pass | Written in plain technical English; `sys.frozen`, `sys.executable` are code names. |
|
||||||
|
| 11 | Status is one of `Proposed`, `Accepted`, `Rejected`, `Deprecated` or `Superseded by ADR-NNNN`, and every Version History row uses only these | Pass | Status `Proposed` before this review; uses an allowed value. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[ADR-0004]: ../../../docs/adr/adr-0004-pyinstaller-executable.md
|
||||||
|
[QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# RC-047: Review of MIL-009
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-047 |
|
||||||
|
| CrossReference | [MIL-009], [QC-MIL-001], [QC-LANG-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-009]
|
||||||
|
- Checklist used: [QC-MIL-001], and [QC-LANG-001] (language and domain)
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: en / it
|
||||||
|
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A concrete deliverable is defined for every gate | Pass | A concrete Deliverable section names the output evaluated at the gate. |
|
||||||
|
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | The Go / No-Go table has numbered criteria, each with an observable Go and No-Go. |
|
||||||
|
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the milestone or documents it needs. |
|
||||||
|
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability table maps to Business Case objectives and the user story. |
|
||||||
|
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership table names S01 as owner and approving reviewer. |
|
||||||
|
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies inside the 2026-10-14 to 2026-10-21 window of the Business Case. |
|
||||||
|
|
||||||
|
## Language and Domain Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of MIL-009 has `Language | en` and `Domain | it`; neither is a placeholder. |
|
||||||
|
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
|
||||||
|
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
|
||||||
|
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
|
||||||
|
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
|
||||||
|
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
|
||||||
|
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
|
||||||
|
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
|
||||||
|
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
|
||||||
|
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-009]: ../../../docs/milestones/mil-009-executable.md
|
||||||
|
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||||
|
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# RC-048: Review of PY-MIL-009
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-048 |
|
||||||
|
| CrossReference | [PY-MIL-009], [QC-PY-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [bc51381] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [PY-MIL-009]
|
||||||
|
- Checklist used: [QC-PY-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | PEP 8 casing holds in `runtime.py`, `build_executable.py` and the changed modules; ruff rule set N passes. |
|
||||||
|
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`resource_root`, `executable_dir`, `build_arguments`, `resolve_data_files`). |
|
||||||
|
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass on 71 files with no suppressions. |
|
||||||
|
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 45 source files and on the three tool scripts. |
|
||||||
|
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `main` of the build tool returns 2 with a message when PyInstaller is not installed; no bare `except`; no swallowed exception. |
|
||||||
|
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
|
||||||
|
| 7 | Files, locks and connections are managed with context managers | Pass | No new file handling beyond the existing context managers; the ICO file is written with `Path.write_bytes`. |
|
||||||
|
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | New and changed members have Doxygen docstrings; `doxygen Doxyfile` ends with 0 warnings. |
|
||||||
|
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` in `src`; the tools write results with `sys.stdout.write`; no secret. |
|
||||||
|
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | The program classes are unchanged (DCD-001); `runtime.py` and `tools/` are packaging support outside the design classes, as ADR-0004 and the Scope note of DCD-001 state; `resolve_data_files` is a function of the composition root. |
|
||||||
|
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 216 tests (3 consecutive runs green): pure runtime helpers for the frozen and source cases, the argument list of the build, the ICO file, the bundled word list fallback, and the level screen window title; none starts PyInstaller or uses the network. The real executable was built and smoke-tested separately (note below). |
|
||||||
|
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; `sys._MEIPASS` is read with `getattr` and no `Any` is used. |
|
||||||
|
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` is still empty; PyInstaller is only in the extra `build` and Pillow only in `tools`. |
|
||||||
|
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | The frozen rule is one definition (`runtime.py`) used by `constants.py`; file names and sizes stay in `constants.py`. |
|
||||||
|
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; `runtime.py` imports only the standard library. |
|
||||||
|
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | The build tool has one purpose and a pure `build_arguments` that is tested; the composition root chooses the word list file. |
|
||||||
|
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | `tools/build_executable.py` and `runtime.py` sit outside the four layers as packaging support; layers and test folders are unchanged. |
|
||||||
|
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names follow the rule. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Milestone MIL-009 Go criteria: 1 `python tools/build_executable.py` ended with exit code 0 and wrote `dist/flash-card.exe` (25.8 MB) with PyInstaller 6.22.3 on the Windows Store Python 3.13.14; 2 started from a temporary folder without the virtual environment, the executable showed a window titled `Flashy` after about 2 seconds and has no console (windowed bootloader); 3 with `data/ms-da.csv` beside it (3 words) one tick left 2 cards in the progress file, without it the embedded list was used (1000 words); 4 one tick wrote `data/words_to_learn.csv` next to the executable with 999 cards at level 100; 5 a captured screenshot of the executable shows the card and both button images; 6 unit tests of the frozen rule pass; 7 `dependencies` empty, extra `build`; 8 BC-001, DCD-001, ADR-0004, PP-001, US-001 and MIL-009 were reviewed Go (RC-042 to RC-047), README and the Install page of the wiki in three languages describe the build; 9 this record, `pytest` 216 passed, ruff, mypy, lint-imports and doxygen clean. Defects found by the smoke test and fixed in this milestone: the level screen window had the default title `tk` and no padding because `LevelView` did not call `configure_window`. Notes: the one-file build starts a second process (the bootloader), so a tool that watches a window must not match the first process id; the first start after a build took over 20 seconds on this machine (unpacking and a virus scan) and later starts about 2 seconds; the spoken word was not tested in the executable (the speech packages are collected into it, the network call was not exercised). All Go.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[PY-MIL-009]: ../../../src/flash_card
|
||||||
|
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||||
|
[bc51381]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/bc513810ece2506d5ab63c1d4c603803c2ad415e
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added MIL-008 and the review records of the level choice (no checklist exists for this type) | [0b983b7] |
|
||||||
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [768ca34] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,18 +25,18 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
|
|
||||||
| Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
|
| Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| BC-001 | Business Case | en | it | - | SA-001, PP-001 | [RC-001] |
|
| BC-001 | Business Case | en | it | - | SA-001, PP-001 | [RC-042] |
|
||||||
| SA-001 | Stakeholder Analysis | en | it | BC-001 | PP-001, DICT-001 | [RC-002] |
|
| SA-001 | Stakeholder Analysis | en | it | BC-001 | PP-001, DICT-001 | [RC-002] |
|
||||||
| PP-001 | Project Plan | en | it | BC-001, SA-001 | MIL-001 to MIL-007 | [RC-003] |
|
| PP-001 | Project Plan | en | it | BC-001, SA-001 | MIL-001 to MIL-008 | [RC-044] |
|
||||||
| UCD-001 | Use Case Diagram | en | it | SA-001, BC-001 | US-001, UC-001 | [RC-004] |
|
| UCD-001 | Use Case Diagram | en | it | SA-001, BC-001 | US-001, UC-001 | [RC-004] |
|
||||||
| US-001 | User Story | en | it | UCD-001, BC-001 | MIL-002 to MIL-006 | [RC-005] |
|
| US-001 | User Story | en | it | UCD-001, BC-001 | MIL-002 to MIL-006, MIL-008 | [RC-043] |
|
||||||
| UC-001 | Use Case | en | it | UCD-001, US-001 | DM-001, SSD-001 | [RC-006] |
|
| UC-001 | Use Case | en | it | UCD-001, US-001 | DM-001, SSD-001 | [RC-033] |
|
||||||
| DM-001 | Domain Model | en | it | UC-001 | SSD-001, DICT-001, DCD-001 | [RC-007] |
|
| DM-001 | Domain Model | en | it | UC-001 | SSD-001, DICT-001, DCD-001 | [RC-034] |
|
||||||
| SSD-001 | System Sequence Diagram | en | it | UC-001, DM-001 | DCD-001 | [RC-008] |
|
| SSD-001 | System Sequence Diagram | en | it | UC-001, DM-001 | DCD-001 | [RC-036] |
|
||||||
| OC-001 | Operation Contract | - | - | SSD-001, DM-001 | SD-001 | [RC-009] |
|
| OC-001 | Operation Contract | - | - | SSD-001, DM-001 | SD-001 | [RC-037] |
|
||||||
| SD-001 | Sequence Diagram | - | - | OC-001 | DCD-001 | [RC-010] |
|
| SD-001 | Sequence Diagram | - | - | OC-001 | DCD-001 | [RC-038] |
|
||||||
| DCD-001 | Design Class Diagram | - | - | DM-001, SD-001, DICT-001 | src/flash_card | [RC-011] |
|
| DCD-001 | Design Class Diagram | - | - | DM-001, SD-001, DICT-001 | src/flash_card | [RC-045] |
|
||||||
| DICT-001 | Domain Dictionary | en | it | BC-001, SA-001, DM-001 | DCD-001 | [RC-012] |
|
| DICT-001 | Domain Dictionary | en | it | BC-001, SA-001, DM-001 | DCD-001 | [RC-035] |
|
||||||
| ADR-0001 | Architecture Decision Record | - | - | BC-001 | MIL-001, MIL-003, MIL-006 | [RC-013] |
|
| ADR-0001 | Architecture Decision Record | - | - | BC-001 | MIL-001, MIL-003, MIL-006 | [RC-013] |
|
||||||
| ADR-0002 | Architecture Decision Record | - | - | BC-001 | DCD-001 | [RC-014] |
|
| ADR-0002 | Architecture Decision Record | - | - | BC-001 | DCD-001 | [RC-014] |
|
||||||
| ADR-0003 | Architecture Decision Record | - | - | BC-001 | - | [RC-015] |
|
| ADR-0003 | Architecture Decision Record | - | - | BC-001 | - | [RC-015] |
|
||||||
@@ -45,6 +47,9 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
| MIL-005 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-005 | [RC-020] |
|
| MIL-005 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-005 | [RC-020] |
|
||||||
| MIL-006 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-006 | [RC-021] |
|
| MIL-006 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-006 | [RC-021] |
|
||||||
| MIL-007 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-007 | [RC-022] |
|
| MIL-007 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-007 | [RC-022] |
|
||||||
|
| MIL-008 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-008 | [RC-040] |
|
||||||
|
| MIL-009 | Milestone / Gateway | en | it | BC-001 | - | [RC-047] |
|
||||||
|
| ADR-0004 | Architecture Decision Record | - | - | BC-001 | - | [RC-046] |
|
||||||
| PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] |
|
| PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] |
|
||||||
| PY-MIL-002 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-024] |
|
| PY-MIL-002 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-024] |
|
||||||
| PY-MIL-003 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-025] |
|
| PY-MIL-003 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-025] |
|
||||||
@@ -52,6 +57,8 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
| PY-MIL-005 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-027] |
|
| PY-MIL-005 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-027] |
|
||||||
| PY-MIL-006 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-028] |
|
| PY-MIL-006 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-028] |
|
||||||
| PY-MIL-007 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-029] |
|
| PY-MIL-007 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-029] |
|
||||||
|
| PY-MIL-008 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-041] |
|
||||||
|
| PY-MIL-009 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-048] |
|
||||||
|
|
||||||
## Coverage Notes
|
## Coverage Notes
|
||||||
|
|
||||||
@@ -120,3 +127,29 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
[PY-MIL-006]: ../../src/flash_card
|
[PY-MIL-006]: ../../src/flash_card
|
||||||
[RC-029]: ./reviews/rc-029-py-mil-007.md
|
[RC-029]: ./reviews/rc-029-py-mil-007.md
|
||||||
[PY-MIL-007]: ../../src/flash_card
|
[PY-MIL-007]: ../../src/flash_card
|
||||||
|
[RC-030]: ./reviews/rc-030-business-case-level.md
|
||||||
|
[RC-031]: ./reviews/rc-031-user-stories-level.md
|
||||||
|
[RC-032]: ./reviews/rc-032-project-plan-level.md
|
||||||
|
[RC-033]: ./reviews/rc-033-use-case-level.md
|
||||||
|
[RC-034]: ./reviews/rc-034-domain-model-level.md
|
||||||
|
[RC-035]: ./reviews/rc-035-domain-dictionary-level.md
|
||||||
|
[RC-036]: ./reviews/rc-036-system-sequence-diagram-level.md
|
||||||
|
[RC-037]: ./reviews/rc-037-operation-contracts-level.md
|
||||||
|
[RC-038]: ./reviews/rc-038-sequence-diagrams-level.md
|
||||||
|
[RC-039]: ./reviews/rc-039-design-class-diagram-level.md
|
||||||
|
[RC-040]: ./reviews/rc-040-mil-008.md
|
||||||
|
[MIL-008]: ../milestones/mil-008-choose-level.md
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
[RC-041]: ./reviews/rc-041-py-mil-008.md
|
||||||
|
[PY-MIL-008]: ../../src/flash_card
|
||||||
|
[RC-042]: ./reviews/rc-042-business-case-exe.md
|
||||||
|
[RC-043]: ./reviews/rc-043-user-stories-exe.md
|
||||||
|
[RC-044]: ./reviews/rc-044-project-plan-exe.md
|
||||||
|
[RC-045]: ./reviews/rc-045-design-class-diagram-exe.md
|
||||||
|
[RC-046]: ./reviews/rc-046-adr-0004.md
|
||||||
|
[RC-047]: ./reviews/rc-047-mil-009.md
|
||||||
|
[MIL-009]: ../milestones/mil-009-executable.md
|
||||||
|
[ADR-0004]: ../adr/adr-0004-pyinstaller-executable.md
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
|
[RC-048]: ./reviews/rc-048-py-mil-009.md
|
||||||
|
[PY-MIL-009]: ../../src/flash_card
|
||||||
|
|||||||
+128
-99
@@ -1,99 +1,128 @@
|
|||||||
# Operation Contracts: Study Flash Cards
|
# Operation Contracts: Study Flash Cards
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | OC-001 |
|
| ID | OC-001 |
|
||||||
| CrossReference | [SSD-001], [DM-001], [DICT-001] |
|
| CrossReference | [SSD-001], [DM-001], [DICT-001] |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] |
|
||||||
---
|
|
||||||
|
---
|
||||||
The turn-over of the card after 3000 ms is started by the system's own timer, not by a message from the Learner, so it has no contract; it is realized in [SD-001].
|
|
||||||
|
The turn-over of the card after 3000 ms is started by the system's own timer, not by a message from the Learner, so it has no contract; it is realized in [SD-001].
|
||||||
## Contract: startStudySession
|
|
||||||
|
## Contract: startStudySession
|
||||||
| Item | Value |
|
|
||||||
| --- | --- |
|
| Item | Value |
|
||||||
| Operation | `startStudySession(): CardDto \| None` |
|
| --- | --- |
|
||||||
| Traces to | `startStudySession` in [SSD-001] |
|
| Operation | `startStudySession(): tuple[Level, ...]` |
|
||||||
| Domain Model concepts | Study Session, Words To Learn, Word List, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Vocabulary`, `Card` ([DICT-001]) |
|
| Traces to | `startStudySession` in [SSD-001] |
|
||||||
|
| Domain Model concepts | Level ([DM-001]); IT term `Level` ([DICT-001]) |
|
||||||
**Preconditions**
|
|
||||||
|
**Preconditions**
|
||||||
- A `Vocabulary` exists in the word list file.
|
|
||||||
|
- The program was started without a level.
|
||||||
**Postconditions**
|
|
||||||
|
**Postconditions**
|
||||||
- A `StudySession` instance was created.
|
|
||||||
- A `Deck` instance was created and associated with the `StudySession`.
|
- The levels 100 words, 1000 words and all words are returned, in that order.
|
||||||
- The `Deck` holds the cards of the progress file when it exists, otherwise a copy of the cards of the `Vocabulary`.
|
- No `StudySession` or `Deck` was created and no file was read.
|
||||||
- When the `Deck` is not empty, `StudySession.current_card` was set to a card of the `Deck` and a `CardDto` of it is returned; otherwise `current_card` is `None` and `None` is returned.
|
|
||||||
|
**Exceptions**
|
||||||
**Exceptions**
|
|
||||||
|
| Condition (failing precondition) | Outcome |
|
||||||
| Condition (failing precondition) | Outcome |
|
| --- | --- |
|
||||||
| --- | --- |
|
| The program was started with a level | The operation is not sent; `chooseLevel` is the first operation |
|
||||||
| The word list file does not exist | `VocabularyNotFoundError` is raised; no session is created |
|
|
||||||
| The word list or progress file has no header of two cells, or a row with other than two cells | `VocabularyFormatError` is raised; no session is created and no file is changed |
|
## Contract: chooseLevel
|
||||||
|
|
||||||
## Contract: markKnown
|
| Item | Value |
|
||||||
|
| --- | --- |
|
||||||
| Item | Value |
|
| Operation | `chooseLevel(level: Level): CardDto \| None` |
|
||||||
| --- | --- |
|
| Traces to | `chooseLevel` in [SSD-001] |
|
||||||
| Operation | `markKnown(): None` |
|
| Domain Model concepts | Study Session, Words To Learn, Word List, Level, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Vocabulary`, `Level`, `Card` ([DICT-001]) |
|
||||||
| Traces to | `markKnown` in [SSD-001] |
|
|
||||||
| Domain Model concepts | Study Session, Words To Learn, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Card` ([DICT-001]) |
|
**Preconditions**
|
||||||
|
|
||||||
**Preconditions**
|
- A `Vocabulary` exists in the word list file.
|
||||||
|
- `level` is one of the levels returned by `startStudySession`, or was given at start.
|
||||||
- A `StudySession` exists and its `current_card` is not `None`.
|
|
||||||
|
**Postconditions**
|
||||||
**Postconditions**
|
|
||||||
|
- A `StudySession` instance was created and associated with `level`.
|
||||||
- The `Card` of `current_card` was removed from the `Deck` (the association was destroyed).
|
- A `Deck` instance was created and associated with the `StudySession`.
|
||||||
- The `Deck` was stored in the progress file.
|
- The `Deck` holds the cards of the progress file when it exists, otherwise a copy of the cards of the `Vocabulary`; cards outside the level are kept in it.
|
||||||
- `StudySession.current_card` was set to `None`.
|
- The level's cards are the first `level.limit` cards of the `Vocabulary` (all of them when the limit is none or larger than the list).
|
||||||
|
- When the `Deck` holds a card of the level, `StudySession.current_card` was set to such a card and a `CardDto` of it is returned; otherwise `current_card` is `None` and `None` is returned.
|
||||||
**Exceptions**
|
- The Malay word of the card was passed to the `SpeechPort`.
|
||||||
|
|
||||||
| Condition (failing precondition) | Outcome |
|
**Exceptions**
|
||||||
| --- | --- |
|
|
||||||
| `current_card` is `None` (no card is shown) | Nothing is changed and nothing is stored |
|
| Condition (failing precondition) | Outcome |
|
||||||
|
| --- | --- |
|
||||||
## Contract: markUnknown
|
| The word list file does not exist | `VocabularyNotFoundError` is raised; no session is created |
|
||||||
|
| The word list or progress file has no header of two cells, or a row with other than two cells | `VocabularyFormatError` is raised; no session is created and no file is changed |
|
||||||
| Item | Value |
|
| No card of the level is left in the `Deck` | `None` is returned; nothing is spoken |
|
||||||
| --- | --- |
|
|
||||||
| Operation | `markUnknown(): CardDto \| None` |
|
## Contract: markKnown
|
||||||
| Traces to | `markUnknown` in [SSD-001] |
|
|
||||||
| Domain Model concepts | Study Session, Words To Learn, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Card` ([DICT-001]) |
|
| Item | Value |
|
||||||
|
| --- | --- |
|
||||||
**Preconditions**
|
| Operation | `markKnown(): None` |
|
||||||
|
| Traces to | `markKnown` in [SSD-001] |
|
||||||
- A `StudySession` exists.
|
| Domain Model concepts | Study Session, Words To Learn, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Card` ([DICT-001]) |
|
||||||
|
|
||||||
**Postconditions**
|
**Preconditions**
|
||||||
|
|
||||||
- `StudySession.current_card` was set to a card of the `Deck` that differs from the previous `current_card` when the `Deck` holds two or more cards, and a `CardDto` of it is returned.
|
- A `StudySession` exists and its `current_card` is not `None`.
|
||||||
- When the `Deck` is empty, `current_card` is `None` and `None` is returned.
|
|
||||||
- The `Deck` and the progress file are unchanged.
|
**Postconditions**
|
||||||
- The Malay word of the new `current_card` was passed to the `SpeechPort`.
|
|
||||||
|
- The `Card` of `current_card` was removed from the `Deck` (the association was destroyed); cards outside the level are not touched.
|
||||||
**Exceptions**
|
- The whole `Deck` was stored in the progress file.
|
||||||
|
- `StudySession.current_card` was set to `None`.
|
||||||
| Condition (failing precondition) | Outcome |
|
|
||||||
| --- | --- |
|
**Exceptions**
|
||||||
| The `Deck` is empty | `None` is returned; nothing is spoken |
|
|
||||||
|
| Condition (failing precondition) | Outcome |
|
||||||
---
|
| --- | --- |
|
||||||
|
| `current_card` is `None` (no card is shown) | Nothing is changed and nothing is stored |
|
||||||
[SSD-001]: ./ssd.md
|
|
||||||
[DM-001]: ../domain-model.md
|
## Contract: markUnknown
|
||||||
[DICT-001]: ../dictionary.md
|
|
||||||
[SD-001]: ./sd.md
|
| Item | Value |
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
| --- | --- |
|
||||||
|
| Operation | `markUnknown(): CardDto \| None` |
|
||||||
|
| Traces to | `markUnknown` in [SSD-001] |
|
||||||
|
| Domain Model concepts | Study Session, Words To Learn, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Card` ([DICT-001]) |
|
||||||
|
|
||||||
|
**Preconditions**
|
||||||
|
|
||||||
|
- A `StudySession` exists.
|
||||||
|
|
||||||
|
**Postconditions**
|
||||||
|
|
||||||
|
- `StudySession.current_card` was set to a card of the level that differs from the previous `current_card` when the level holds two or more cards still to learn, and a `CardDto` of it is returned.
|
||||||
|
- When no card of the level is left, `current_card` is `None` and `None` is returned.
|
||||||
|
- The `Deck` and the progress file are unchanged.
|
||||||
|
- The Malay word of the new `current_card` was passed to the `SpeechPort`.
|
||||||
|
|
||||||
|
**Exceptions**
|
||||||
|
|
||||||
|
| Condition (failing precondition) | Outcome |
|
||||||
|
| --- | --- |
|
||||||
|
| No card of the level is left | `None` is returned; nothing is spoken |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[SSD-001]: ./ssd.md
|
||||||
|
[DM-001]: ../domain-model.md
|
||||||
|
[DICT-001]: ../dictionary.md
|
||||||
|
[SD-001]: ./sd.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
|||||||
+213
-171
@@ -1,171 +1,213 @@
|
|||||||
# Sequence Diagrams: Study Flash Cards
|
# Sequence Diagrams: Study Flash Cards
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | SD-001 |
|
| ID | SD-001 |
|
||||||
| CrossReference | [OC-001], [DCD-001], [DICT-001] |
|
| CrossReference | [OC-001], [DCD-001], [DICT-001] |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the sequences startStudySession (levels) and chooseLevel (level filter) | [0b983b7] |
|
||||||
---
|
|
||||||
|
---
|
||||||
## Sequence: startStudySession
|
|
||||||
|
## Sequence: startStudySession
|
||||||
**Realizes:** `startStudySession` in [OC-001]
|
|
||||||
|
**Realizes:** `startStudySession` in [OC-001]
|
||||||
### Diagram
|
|
||||||
|
### Diagram
|
||||||
```plantuml
|
|
||||||
@startuml
|
```plantuml
|
||||||
participant "main : app" as M
|
@startuml
|
||||||
participant ":CsvVocabularyRepository" as VR
|
participant ":LevelView" as V
|
||||||
participant ":CsvProgressRepository" as PR
|
participant ":LevelViewModel" as VM
|
||||||
participant ":StudySession" as S
|
V -> VM : choices
|
||||||
participant ":Deck" as D
|
VM --> V : tuple[Level, ...]
|
||||||
participant ":SpeechPort" as SP
|
@enduml
|
||||||
M -> PR : load()
|
```
|
||||||
PR --> M : deck | None
|
|
||||||
alt deck is None
|
### Pattern Annotations
|
||||||
M -> VR : load()
|
|
||||||
VR --> M : vocabulary
|
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
||||||
create D
|
| --- | --- | --- |
|
||||||
M -> D : from_vocabulary(vocabulary)
|
| MVVM | `LevelView`, `LevelViewModel` | The view shows the choices; the labels and the order are in the view model |
|
||||||
D --> M : deck
|
|
||||||
end
|
### Postcondition Coverage
|
||||||
create S
|
|
||||||
M -> S : create(deck, progress, speech, choose)
|
| Postcondition (from contract) | Satisfied by message |
|
||||||
M -> S : next_card()
|
| --- | --- |
|
||||||
activate S
|
| The three levels are returned in order | `choices` |
|
||||||
opt deck is not empty
|
| No session or deck is created and no file is read | no other message is sent |
|
||||||
S -> D : draw(choose, exclude)
|
|
||||||
D --> S : card
|
### Responsibility Check
|
||||||
S -> SP : speak(card.front, front_code)
|
|
||||||
end
|
The view model owns the choices; the view only draws buttons and forwards the click.
|
||||||
S --> M : CardDto | None
|
|
||||||
deactivate S
|
## Sequence: chooseLevel
|
||||||
@enduml
|
|
||||||
```
|
**Realizes:** `chooseLevel` in [OC-001]
|
||||||
|
|
||||||
### Pattern Annotations
|
### Diagram
|
||||||
|
|
||||||
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
```plantuml
|
||||||
| --- | --- | --- |
|
@startuml
|
||||||
| Creator | `app.main` creates `StudySession` | It holds the adapters the session needs |
|
participant "main : app" as M
|
||||||
| Information Expert | `Deck` draws and removes its own cards | It owns the list of cards |
|
participant ":CsvVocabularyRepository" as VR
|
||||||
| Null Object | `NullSpeech` as `SpeechPort` | The session never tests for speech |
|
participant ":CsvProgressRepository" as PR
|
||||||
|
participant "level : Level" as L
|
||||||
### Postcondition Coverage
|
participant ":StudySession" as S
|
||||||
|
participant ":Deck" as D
|
||||||
| Postcondition (from contract) | Satisfied by message |
|
participant ":SpeechPort" as SP
|
||||||
| --- | --- |
|
M -> VR : load()
|
||||||
| A `StudySession` instance was created | `create(deck, progress, speech, choose)` |
|
VR --> M : vocabulary
|
||||||
| A `Deck` was created from the progress file or a copy of the `Vocabulary` | `PR.load()`, `alt` fragment, `from_vocabulary` |
|
M -> PR : load()
|
||||||
| `current_card` set and a `CardDto` returned, or `None` | `next_card()` with the `opt` fragment |
|
PR --> M : deck | None
|
||||||
|
alt deck is None
|
||||||
### Responsibility Check
|
create D
|
||||||
|
M -> D : from_vocabulary(vocabulary)
|
||||||
The session coordinates, the deck holds the cards, the repositories hold the file format; no object receives all messages.
|
D --> M : deck
|
||||||
|
end
|
||||||
## Sequence: markKnown
|
M -> L : first(vocabulary.cards)
|
||||||
|
L --> M : level_cards
|
||||||
**Realizes:** `markKnown` in [OC-001]
|
create S
|
||||||
|
M -> S : create(deck, progress, speech, choose, level_cards)
|
||||||
### Diagram
|
M -> S : next_card()
|
||||||
|
activate S
|
||||||
```plantuml
|
S -> D : within(level_cards)
|
||||||
@startuml
|
D --> S : candidates
|
||||||
participant ":FlashCardView" as V
|
opt candidates is not empty
|
||||||
participant ":FlashCardViewModel" as VM
|
S -> D : draw(choose, exclude, among=candidates)
|
||||||
participant ":StudySession" as S
|
D --> S : card
|
||||||
participant ":Deck" as D
|
S -> SP : speak(card.front, front_code)
|
||||||
participant ":CsvProgressRepository" as PR
|
end
|
||||||
V -> VM : is_known()
|
S --> M : CardDto | None
|
||||||
activate VM
|
deactivate S
|
||||||
VM -> S : mark_known()
|
@enduml
|
||||||
activate S
|
```
|
||||||
opt current_card is not None
|
|
||||||
S -> D : remove(card)
|
### Pattern Annotations
|
||||||
S -> PR : save(deck)
|
|
||||||
end
|
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
||||||
deactivate S
|
| --- | --- | --- |
|
||||||
VM -> S : next_card()
|
| Creator | `app.main` creates `StudySession` | It holds the adapters the session needs |
|
||||||
S --> VM : CardDto | None
|
| Information Expert | `Level.first`, `Deck.within`, `Deck.draw` | The level knows its limit, the deck knows its cards |
|
||||||
VM --> V : side, language name, text
|
| Null Object | `NullSpeech` as `SpeechPort` | The session never tests for speech |
|
||||||
deactivate VM
|
|
||||||
@enduml
|
### Postcondition Coverage
|
||||||
```
|
|
||||||
|
| Postcondition (from contract) | Satisfied by message |
|
||||||
### Pattern Annotations
|
| --- | --- |
|
||||||
|
| A `StudySession` was created and associated with the level | `create(deck, progress, speech, choose, level_cards)` |
|
||||||
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
| A `Deck` was created from the progress file or a copy of the `Vocabulary`, cards outside the level kept | `PR.load()`, `alt` fragment, `from_vocabulary` |
|
||||||
| --- | --- | --- |
|
| The level's cards are the first N cards of the `Vocabulary` | `first(vocabulary.cards)` |
|
||||||
| MVVM | `FlashCardView`, `FlashCardViewModel` | The view raises the event and shows the result; the rule is behind the view model |
|
| `current_card` set and a `CardDto` returned, or `None`; the word was passed to the `SpeechPort` | `next_card()` with `within`, the `opt` fragment and `speak` |
|
||||||
| Repository | `CsvProgressRepository` | The session stores the deck through a port |
|
|
||||||
|
### Responsibility Check
|
||||||
### Postcondition Coverage
|
|
||||||
|
The composition root only wires; the level selects, the deck holds, the session coordinates; no object receives all messages.
|
||||||
| Postcondition (from contract) | Satisfied by message |
|
|
||||||
| --- | --- |
|
## Sequence: markKnown
|
||||||
| The card was removed from the `Deck` | `remove(card)` |
|
|
||||||
| The `Deck` was stored | `save(deck)` |
|
**Realizes:** `markKnown` in [OC-001]
|
||||||
| `current_card` was set to `None` | `mark_known()` ends with no current card; `next_card()` then draws the next |
|
|
||||||
|
### Diagram
|
||||||
### Responsibility Check
|
|
||||||
|
```plantuml
|
||||||
The view model only sequences two session calls and maps the result for the view.
|
@startuml
|
||||||
|
participant ":FlashCardView" as V
|
||||||
## Sequence: markUnknown
|
participant ":FlashCardViewModel" as VM
|
||||||
|
participant ":StudySession" as S
|
||||||
**Realizes:** `markUnknown` in [OC-001]
|
participant ":Deck" as D
|
||||||
|
participant ":CsvProgressRepository" as PR
|
||||||
### Diagram
|
V -> VM : is_known()
|
||||||
|
activate VM
|
||||||
```plantuml
|
VM -> S : mark_known()
|
||||||
@startuml
|
activate S
|
||||||
participant ":FlashCardView" as V
|
opt current_card is not None
|
||||||
participant ":FlashCardViewModel" as VM
|
S -> D : remove(card)
|
||||||
participant ":StudySession" as S
|
S -> PR : save(deck)
|
||||||
participant ":Deck" as D
|
end
|
||||||
participant ":SpeechPort" as SP
|
deactivate S
|
||||||
V -> VM : next_card()
|
VM -> S : next_card()
|
||||||
VM -> S : next_card()
|
S --> VM : CardDto | None
|
||||||
activate S
|
VM --> V : side, language name, text
|
||||||
opt deck is not empty
|
deactivate VM
|
||||||
S -> D : draw(choose, exclude=current)
|
@enduml
|
||||||
D --> S : card
|
```
|
||||||
S -> SP : speak(card.front, front_code)
|
|
||||||
end
|
### Pattern Annotations
|
||||||
S --> VM : CardDto | None
|
|
||||||
deactivate S
|
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
||||||
VM --> V : side, language name, text
|
| --- | --- | --- |
|
||||||
@enduml
|
| MVVM | `FlashCardView`, `FlashCardViewModel` | The view raises the event and shows the result; the rule is behind the view model |
|
||||||
```
|
| Repository | `CsvProgressRepository` | The session stores the deck through a port |
|
||||||
|
|
||||||
### Pattern Annotations
|
### Postcondition Coverage
|
||||||
|
|
||||||
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
| Postcondition (from contract) | Satisfied by message |
|
||||||
| --- | --- | --- |
|
| --- | --- |
|
||||||
| Information Expert | `Deck.draw` | The deck knows its cards |
|
| The card was removed from the `Deck` | `remove(card)` |
|
||||||
|
| The `Deck` was stored | `save(deck)` |
|
||||||
### Postcondition Coverage
|
| `current_card` was set to `None` | `mark_known()` ends with no current card; `next_card()` then draws the next |
|
||||||
|
|
||||||
| Postcondition (from contract) | Satisfied by message |
|
### Responsibility Check
|
||||||
| --- | --- |
|
|
||||||
| A different card was chosen when two or more exist | `draw(choose, exclude=current)` |
|
The view model only sequences two session calls and maps the result for the view.
|
||||||
| Deck and file unchanged | no `remove` or `save` message |
|
|
||||||
| The Malay word was passed to the `SpeechPort` | `speak(card.front, front_code)` |
|
## Sequence: markUnknown
|
||||||
|
|
||||||
### Responsibility Check
|
**Realizes:** `markUnknown` in [OC-001]
|
||||||
|
|
||||||
Same as `markKnown`; the session is the only object that speaks and draws.
|
### Diagram
|
||||||
|
|
||||||
---
|
```plantuml
|
||||||
|
@startuml
|
||||||
[OC-001]: ./oc.md
|
participant ":FlashCardView" as V
|
||||||
[DCD-001]: ../dcd.md
|
participant ":FlashCardViewModel" as VM
|
||||||
[DICT-001]: ../dictionary.md
|
participant ":StudySession" as S
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
participant ":Deck" as D
|
||||||
|
participant ":SpeechPort" as SP
|
||||||
|
V -> VM : next_card()
|
||||||
|
VM -> S : next_card()
|
||||||
|
activate S
|
||||||
|
S -> D : within(level_cards)
|
||||||
|
D --> S : candidates
|
||||||
|
opt candidates is not empty
|
||||||
|
S -> D : draw(choose, exclude=current, among=candidates)
|
||||||
|
D --> S : card
|
||||||
|
S -> SP : speak(card.front, front_code)
|
||||||
|
end
|
||||||
|
S --> VM : CardDto | None
|
||||||
|
deactivate S
|
||||||
|
VM --> V : side, language name, text
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern Annotations
|
||||||
|
|
||||||
|
| Pattern (GRASP / GoF) | Applied to | Rationale |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Information Expert | `Deck.draw` | The deck knows its cards |
|
||||||
|
|
||||||
|
### Postcondition Coverage
|
||||||
|
|
||||||
|
| Postcondition (from contract) | Satisfied by message |
|
||||||
|
| --- | --- |
|
||||||
|
| A different card of the level was chosen when two or more exist | `within(level_cards)`, `draw(choose, exclude=current, among=candidates)` |
|
||||||
|
| Deck and file unchanged | no `remove` or `save` message |
|
||||||
|
| The Malay word was passed to the `SpeechPort` | `speak(card.front, front_code)` |
|
||||||
|
|
||||||
|
### Responsibility Check
|
||||||
|
|
||||||
|
Same as `markKnown`; the session is the only object that speaks and draws.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[OC-001]: ./oc.md
|
||||||
|
[DCD-001]: ../dcd.md
|
||||||
|
[DICT-001]: ../dictionary.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
|||||||
+68
-63
@@ -1,63 +1,68 @@
|
|||||||
# System Sequence Diagram: Study Flash Cards
|
# System Sequence Diagram: Study Flash Cards
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | SSD-001 |
|
| ID | SSD-001 |
|
||||||
| CrossReference | [UC-001], [DM-001], [OC-001] |
|
| CrossReference | [UC-001], [DM-001], [OC-001] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] |
|
||||||
---
|
|
||||||
|
---
|
||||||
## Source Use Case
|
|
||||||
|
## Source Use Case
|
||||||
Study Flash Cards ([UC-001]) — scenario: main success scenario, with the unknown button as one repetition.
|
|
||||||
|
Study Flash Cards ([UC-001]) — scenario: main success scenario, with the unknown button as one repetition.
|
||||||
## Diagram
|
|
||||||
|
## Diagram
|
||||||
```plantuml
|
|
||||||
@startuml
|
```plantuml
|
||||||
actor Learner as L
|
@startuml
|
||||||
participant ":System" as S
|
actor Learner as L
|
||||||
L -> S : startStudySession()
|
participant ":System" as S
|
||||||
S --> L : card (word, language name)
|
L -> S : startStudySession()
|
||||||
... 3000 ms pass, the system turns the card over ...
|
S --> L : levels (100 words, 1000 words, all words)
|
||||||
S --> L : card (translation, language name)
|
L -> S : chooseLevel(level)
|
||||||
L -> S : markKnown()
|
S --> L : card (word, language name)
|
||||||
S --> L : next card (word, language name)
|
... 3000 ms pass, the system turns the card over ...
|
||||||
L -> S : markUnknown()
|
S --> L : card (translation, language name)
|
||||||
S --> L : next card (word, language name)
|
L -> S : markKnown()
|
||||||
@enduml
|
S --> L : next card (word, language name)
|
||||||
```
|
L -> S : markUnknown()
|
||||||
|
S --> L : next card (word, language name)
|
||||||
## System Operations
|
@enduml
|
||||||
|
```
|
||||||
| Step | Message | Parameters | Return | Use case step |
|
|
||||||
| --- | --- | --- | --- | --- |
|
## System Operations
|
||||||
| 1 | startStudySession | none | card to show, or "nothing left to learn" | 1, 2, 3 |
|
|
||||||
| 2 | (timer) turn the card over | none | the same card, translation side | 4 |
|
| Step | Message | Parameters | Return | Use case step |
|
||||||
| 3 | markKnown | none | next card, or "nothing left to learn" | 5, 6, 7 |
|
| --- | --- | --- | --- | --- |
|
||||||
| 4 | markUnknown | none | next card | 5a, 7 |
|
| 1 | startStudySession | none | the levels to choose from | 1, 2 |
|
||||||
|
| 2 | chooseLevel | level | card to show, or "nothing left to learn" | 3, 4, 5 |
|
||||||
The turn-over in step 2 is started by the system's own timer, not by a message from the Learner; it is shown as a return without a call, which is the one deviation from "every return answers a message".
|
| 3 | (timer) turn the card over | none | the same card, translation side | 6 |
|
||||||
|
| 4 | markKnown | none | next card, or "nothing left to learn" | 7, 8, 9 |
|
||||||
## Alternate Flows
|
| 5 | markUnknown | none | next card, or "nothing left to learn" | 7a, 9 |
|
||||||
|
|
||||||
The extensions 2a, 3a, 5b and 7a of [UC-001] add no system operation of their own (they are errors, speech and timer handling inside the three operations above); their behaviour is in [OC-001] as exceptions. They are out of scope of this diagram, which shows the main success scenario with `markUnknown` as the one alternate answer.
|
The turn-over in step 3 is started by the system's own timer, not by a message from the Learner; it is shown as a return without a call, which is the one deviation from "every return answers a message". When the level is given at start (extension 3a of [UC-001]) the Learner's first message is `chooseLevel` and `startStudySession` is not sent.
|
||||||
|
|
||||||
## Lifecycle Notes
|
## Alternate Flows
|
||||||
|
|
||||||
The system instance lives for one run of the program. The words to learn are created when the session starts and saved after each `markKnown`.
|
The extensions 3a, 4a, 5a, 7b and 9a of [UC-001] add no system operation of their own (they are errors, speech, timer and level handling inside the operations above); their behaviour is in [OC-001] as exceptions. They are out of scope of this diagram, which shows the main success scenario with `markUnknown` as the one alternate answer.
|
||||||
|
|
||||||
---
|
## Lifecycle Notes
|
||||||
|
|
||||||
[UC-001]: ./uc.md
|
The system instance lives for one run of the program. The words to learn are created when the level is chosen and saved after each `markKnown`.
|
||||||
[DM-001]: ../domain-model.md
|
|
||||||
[OC-001]: ./oc.md
|
---
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
|
||||||
|
[UC-001]: ./uc.md
|
||||||
|
[DM-001]: ../domain-model.md
|
||||||
|
[OC-001]: ./oc.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
|||||||
+87
-78
@@ -1,78 +1,87 @@
|
|||||||
# UC-001: Study Flash Cards
|
# UC-001: Study Flash Cards
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | UC-001 |
|
| ID | UC-001 |
|
||||||
| CrossReference | [UCD-001], [US-001], [SA-001], [DM-001], [SSD-001] |
|
| CrossReference | [UCD-001], [US-001], [SA-001], [DM-001], [SSD-001] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the choice of level (US-001.06) | [0b983b7] |
|
||||||
---
|
|
||||||
|
---
|
||||||
**Format:** Fully Dressed
|
|
||||||
|
**Format:** Fully Dressed
|
||||||
## Fully Dressed
|
|
||||||
|
## Fully Dressed
|
||||||
- **Scope:** Flash Card program
|
|
||||||
- **Level:** user-goal
|
- **Scope:** Flash Card program
|
||||||
- **Primary Actor:** Learner
|
- **Level:** user-goal
|
||||||
- **Stakeholders and Interests:**
|
- **Primary Actor:** Learner
|
||||||
- S01 — learns the Malay words and wants only unknown words to come up
|
- **Stakeholders and Interests:**
|
||||||
- S02 — wants to see the assignment's behaviour step by step
|
- S01 — learns the Malay words and wants only unknown words to come up
|
||||||
- **Preconditions:**
|
- S02 — wants to see the assignment's behaviour step by step
|
||||||
- A word list exists.
|
- **Preconditions:**
|
||||||
- A display is available.
|
- A word list exists.
|
||||||
- **Postconditions (success guarantee):**
|
- A display is available.
|
||||||
- The words the Learner marked as known are no longer in the words to learn.
|
- **Postconditions (success guarantee):**
|
||||||
- The words to learn are kept, so that the next session starts from them.
|
- The words the Learner marked as known are no longer in the words to learn.
|
||||||
|
- The words to learn are kept, so that the next session starts from them.
|
||||||
### Main Success Scenario
|
|
||||||
|
### Main Success Scenario
|
||||||
1. The Learner starts the program.
|
|
||||||
2. The system loads the words to learn: from the kept progress when it exists, otherwise from the word list.
|
1. The Learner starts the program.
|
||||||
3. The system shows a random card with the word in Malay.
|
2. The system offers the levels: 100 words, 1000 words and all words.
|
||||||
4. After three seconds the system turns the card over and shows the translation in Danish.
|
3. The Learner chooses a level.
|
||||||
5. The Learner states that the word is known.
|
4. The system loads the words to learn: from the kept progress when it exists, otherwise from the word list.
|
||||||
6. The system removes the word from the words to learn and keeps the words to learn.
|
5. The system shows a random card, with the word in Malay, from the first words of the word list that fit the level and are still to learn.
|
||||||
7. The system shows a new random card, and the Learner repeats from step 4 until no word is left.
|
6. After three seconds the system turns the card over and shows the translation in Danish.
|
||||||
|
7. The Learner states that the word is known.
|
||||||
### Extensions (Alternative / Exception Flows)
|
8. The system removes the word from the words to learn and keeps the words to learn.
|
||||||
|
9. The system shows a new random card of the level, and the Learner repeats from step 6 until no word is left at the level.
|
||||||
- 3a. Spoken words are available on the Learner's machine:
|
|
||||||
1. The system speaks the Malay word. If speaking fails, the system logs it and goes on.
|
### Extensions (Alternative / Exception Flows)
|
||||||
- 5a. The Learner states that the word is not known:
|
|
||||||
1. The system keeps the word in the words to learn and shows a new random card; the Learner goes on at step 4.
|
- 3a. The Learner has given the level when starting the program:
|
||||||
- 5b. The Learner answers before three seconds have passed:
|
1. The system skips steps 2 and 3 and goes on at step 4.
|
||||||
1. The system cancels the turn-over of the card being shown and starts a new one for the next card.
|
- 5a. Spoken words are available on the Learner's machine:
|
||||||
- 7a. No word is left:
|
1. The system speaks the Malay word. If speaking fails, the system logs it and goes on.
|
||||||
1. The system tells the Learner that there is nothing left to learn, and further answers change nothing.
|
- 7a. The Learner states that the word is not known:
|
||||||
- 2a. The kept progress or the word list is unreadable:
|
1. The system keeps the word in the words to learn and shows a new random card of the level; the Learner goes on at step 6.
|
||||||
1. The system reports the problem to the Learner and ends; it does not overwrite the kept progress.
|
- 7b. The Learner answers before three seconds have passed:
|
||||||
|
1. The system cancels the turn-over of the card being shown and starts a new one for the next card.
|
||||||
### Special Requirements / Business Rules
|
- 9a. No word is left at the level:
|
||||||
|
1. The system tells the Learner that there is nothing left to learn at this level, and further answers change nothing.
|
||||||
| Step | Rule |
|
- 4a. The kept progress or the word list is unreadable:
|
||||||
| --- | --- |
|
1. The system reports the problem to the Learner and ends; it does not overwrite the kept progress.
|
||||||
| 3, 7 | The next card is chosen at random from the words to learn |
|
|
||||||
| 4 | The turn-over comes three seconds after the card appears |
|
### Special Requirements / Business Rules
|
||||||
| 6 | A word is identified by its Malay and Danish text; the first equal word is removed |
|
|
||||||
| 2 | The word list names its two languages and holds one word and one translation per card |
|
| Step | Rule |
|
||||||
|
| --- | --- |
|
||||||
### Open Issues
|
| 2, 3 | A level is a number of words (100 or 1000) or all words |
|
||||||
|
| 5 | The word list is ordered from the most to the least frequent word; a level of N words is its first N words; a list shorter than N is used whole |
|
||||||
- Starting again (resetting the kept progress) is not offered in the program; deleting the kept progress does it.
|
| 5, 9 | The next card is chosen at random from the words of the level that are still to learn |
|
||||||
|
| 6 | The turn-over comes three seconds after the card appears |
|
||||||
---
|
| 8 | A word is identified by its Malay and Danish text; the first equal word is removed; known words stay known at every level and words outside the level are kept |
|
||||||
|
| 4 | The word list names its two languages and holds one word and one translation per card |
|
||||||
[UCD-001]: ../use-case-diagram.md
|
|
||||||
[US-001]: ../user-stories.md
|
### Open Issues
|
||||||
[SA-001]: ../stakeholder-analysis.md
|
|
||||||
[DM-001]: ../domain-model.md
|
- Starting again (resetting the kept progress) is not offered in the program; deleting the kept progress does it.
|
||||||
[SSD-001]: ./ssd.md
|
- Going back to the level choice without restarting the program is not offered.
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[UCD-001]: ../use-case-diagram.md
|
||||||
|
[US-001]: ../user-stories.md
|
||||||
|
[SA-001]: ../stakeholder-analysis.md
|
||||||
|
[DM-001]: ../domain-model.md
|
||||||
|
[SSD-001]: ./ssd.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
# Wireframe: Study Flash Cards
|
# Wireframe: Study Flash Cards
|
||||||
|
|
||||||
|
_Revision 2026-10-14: the level screen (screen 1) was added for US-001.06. It was drawn by the AI from the use case because the Product Owner gave no wireframe for it; S01 may change it._
|
||||||
|
|
||||||
The Product Owner gave this screen in the first prompt (the course's target screenshot). The presentation layer implements it and does not redesign it. Source use case: [UC-001](./uc.md).
|
The Product Owner gave this screen in the first prompt (the course's target screenshot). The presentation layer implements it and does not redesign it. Source use case: [UC-001](./uc.md).
|
||||||
|
|
||||||
## Screen
|
## Screen 1: Choose your level
|
||||||
|
|
||||||
|
```text
|
||||||
|
+----------------------------------------------------------+ background #B1DDC6
|
||||||
|
| padding 50 px |
|
||||||
|
| |
|
||||||
|
| Choose your level (Arial 40 italic) |
|
||||||
|
| |
|
||||||
|
| [ 100 words ] |
|
||||||
|
| [ 1000 words ] |
|
||||||
|
| [ All words ] |
|
||||||
|
| |
|
||||||
|
+----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
| Element | Position | Details |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Window | whole | Same title, padding and background as the card screen |
|
||||||
|
| Title | row 0 | "Choose your level", Arial 40 italic |
|
||||||
|
| Level buttons | rows 1 to 3, one column | Labels "100 words", "1000 words", "All words"; Arial 24 bold; white background, equal width |
|
||||||
|
| After a choice | whole | The screen is replaced by screen 2 on the same window |
|
||||||
|
|
||||||
|
## Screen 2: The card
|
||||||
|
|
||||||
```text
|
```text
|
||||||
+----------------------------------------------------------+ background #B1DDC6
|
+----------------------------------------------------------+ background #B1DDC6
|
||||||
|
|||||||
+142
-105
@@ -1,105 +1,142 @@
|
|||||||
# User Stories: Flash Card
|
# User Stories: Flash Card
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | US-001 |
|
| ID | US-001 |
|
||||||
| CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006] |
|
| CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-008], [MIL-009] |
|
||||||
| Language | en |
|
| Language | en |
|
||||||
| Domain | it |
|
| Domain | it |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
|
||||||
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.06 Choose my level | [0b983b7] |
|
||||||
---
|
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.07 Start without Python | [768ca34] |
|
||||||
|
|
||||||
## Purpose and Scope
|
---
|
||||||
|
|
||||||
These stories cover the epic "Study Flash Cards": the four steps of the course assignment (the window, new cards, flipping the cards, saving progress) and the spoken Malay word. Each story is delivered by one milestone. The wireframe the stories follow is in `docs/uc-001/wireframe.md`.
|
## Purpose and Scope
|
||||||
|
|
||||||
## Story List
|
These stories cover the epic "Study Flash Cards": the four steps of the course assignment (the window, new cards, flipping the cards, saving progress), the spoken Malay word the choice of level and the start without an installed Python. Each story is delivered by one milestone. The wireframe the stories follow is in `docs/uc-001/wireframe.md`.
|
||||||
|
|
||||||
### US-001.01 — See a flash card
|
## Story List
|
||||||
|
|
||||||
**As a** Learner, **I want** a window that shows one flash card with the language name, a word and two buttons (known and unknown), **so that** I can see what I am asked to learn.
|
### US-001.01 — See a flash card
|
||||||
|
|
||||||
**Acceptance Criteria**
|
**As a** Learner, **I want** a window that shows one flash card with the language name, a word and two buttons (known and unknown), **so that** I can see what I am asked to learn.
|
||||||
|
|
||||||
- Given the program is started, when the window opens, then it shows a card with the language name above a word, a button for "I do not know it" under the left side and a button for "I know it" under the right side.
|
**Acceptance Criteria**
|
||||||
- Given the window is open, when I look at the card, then the language name is in italics and the word is in bold, both centred on the card.
|
|
||||||
|
- Given the program is started, when the window opens, then it shows a card with the language name above a word, a button for "I do not know it" under the left side and a button for "I know it" under the right side.
|
||||||
| Traces to | Size | INVEST exceptions |
|
- Given the window is open, when I look at the card, then the language name is in italics and the word is in bold, both centred on the card.
|
||||||
| --- | --- | --- |
|
|
||||||
| [MIL-002], [UC-001] | fits one iteration | none |
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
### US-001.02 — Get a new card
|
| [MIL-002], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
**As a** Learner, **I want** a new random word every time I press a button, **so that** I do not meet the words in a fixed order.
|
### US-001.02 — Get a new card
|
||||||
|
|
||||||
**Acceptance Criteria**
|
**As a** Learner, **I want** a new random word every time I press a button, **so that** I do not meet the words in a fixed order.
|
||||||
|
|
||||||
- Given a card is shown, when I press the known button or the unknown button, then a different random word from the word list is shown.
|
**Acceptance Criteria**
|
||||||
- Given the word list has one word, when I press a button, then that word is shown again and the program does not fail.
|
|
||||||
|
- Given a card is shown, when I press the known button or the unknown button, then a different random word from the word list is shown.
|
||||||
| Traces to | Size | INVEST exceptions |
|
- Given the word list has one word, when I press a button, then that word is shown again and the program does not fail.
|
||||||
| --- | --- | --- |
|
|
||||||
| [MIL-003], [UC-001] | fits one iteration | none |
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
### US-001.03 — See the translation
|
| [MIL-003], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
**As a** Learner, **I want** the card to turn over after three seconds and show the translation, **so that** I can check my answer without asking for it.
|
### US-001.03 — See the translation
|
||||||
|
|
||||||
**Acceptance Criteria**
|
**As a** Learner, **I want** the card to turn over after three seconds and show the translation, **so that** I can check my answer without asking for it.
|
||||||
|
|
||||||
- Given a new card is shown, when 3000 ms have passed, then the card shows the back image, the language name of the translation, the translation, and the text is white.
|
**Acceptance Criteria**
|
||||||
- Given a card was shown less than 3000 ms ago, when I press a button, then the earlier timer is cancelled and only the new card is flipped after its own 3000 ms.
|
|
||||||
|
- Given a new card is shown, when 3000 ms have passed, then the card shows the back image, the language name of the translation, the translation, and the text is white.
|
||||||
| Traces to | Size | INVEST exceptions |
|
- Given a card was shown less than 3000 ms ago, when I press a button, then the earlier timer is cancelled and only the new card is flipped after its own 3000 ms.
|
||||||
| --- | --- | --- |
|
|
||||||
| [MIL-004], [UC-001] | fits one iteration | none |
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
### US-001.04 — Keep my progress
|
| [MIL-004], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
**As a** Learner, **I want** the words I know to stop coming up, and the words still to learn to be kept when I close the program, **so that** I am only tested on what I do not know.
|
### US-001.04 — Keep my progress
|
||||||
|
|
||||||
**Acceptance Criteria**
|
**As a** Learner, **I want** the words I know to stop coming up, and the words still to learn to be kept when I close the program, **so that** I am only tested on what I do not know.
|
||||||
|
|
||||||
- Given a card is shown, when I press the known button, then that word is removed from the words to learn and the remaining words are saved in `words_to_learn.csv`.
|
**Acceptance Criteria**
|
||||||
- Given `words_to_learn.csv` exists, when the program starts, then the cards come from that file.
|
|
||||||
- Given `words_to_learn.csv` does not exist, when the program starts, then the cards come from the word list `ms-da.csv`.
|
- Given a card is shown, when I press the known button, then that word is removed from the words to learn and the remaining words are saved in `words_to_learn.csv`.
|
||||||
- Given all words are known, when the last known button is pressed, then the program shows a message that there is nothing left to learn and does not fail.
|
- Given `words_to_learn.csv` exists, when the program starts, then the cards come from that file.
|
||||||
|
- Given `words_to_learn.csv` does not exist, when the program starts, then the cards come from the word list `ms-da.csv`.
|
||||||
| Traces to | Size | INVEST exceptions |
|
- Given all words are known, when the last known button is pressed, then the program shows a message that there is nothing left to learn and does not fail.
|
||||||
| --- | --- | --- |
|
|
||||||
| [MIL-005], [UC-001] | fits one iteration | none |
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
### US-001.05 — Hear the Malay word
|
| [MIL-005], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
**As a** Learner, **I want** to hear the Malay word when its card is shown, **so that** I learn how it is pronounced.
|
### US-001.05 — Hear the Malay word
|
||||||
|
|
||||||
**Acceptance Criteria**
|
**As a** Learner, **I want** to hear the Malay word when its card is shown, **so that** I learn how it is pronounced.
|
||||||
|
|
||||||
- Given the speech extra is installed and a network is available, when a card shows a Malay word, then the word is spoken in Malay.
|
**Acceptance Criteria**
|
||||||
- Given the speech extra is not installed or no network is available, when a card is shown, then the program works as without speech and does not show an error to the Learner.
|
|
||||||
|
- Given the speech extra is installed and a network is available, when a card shows a Malay word, then the word is spoken in Malay.
|
||||||
| Traces to | Size | INVEST exceptions |
|
- Given the speech extra is not installed or no network is available, when a card is shown, then the program works as without speech and does not show an error to the Learner.
|
||||||
| --- | --- | --- |
|
|
||||||
| [MIL-006], [UC-001] | fits one iteration | none |
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
## INVEST Check
|
| [MIL-006], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
All five stories are Independent in delivery order (each builds on the one before but can be accepted alone), Negotiable in wording, Valuable to the Learner, Estimable, Small (one milestone each) and Testable (every criterion is Given/When/Then). Exception: US-001.02 to US-001.05 depend on the window of US-001.01, which is stated in their milestones' Dependencies.
|
### US-001.06 — Choose my level
|
||||||
|
|
||||||
---
|
**As a** Learner, **I want** to choose a level of 100 words, 1000 words or all words before I start, **so that** I begin with the most frequent words and widen the list when I am ready.
|
||||||
|
|
||||||
[UCD-001]: ./use-case-diagram.md
|
**Acceptance Criteria**
|
||||||
[BC-001]: ./business-case.md
|
|
||||||
[UC-001]: ./uc-001/uc.md
|
- Given the program is started, when the window opens, then I see the choices 100 words, 1000 words and All words, and no card yet.
|
||||||
[MIL-002]: ./milestones/mil-002-step1-user-interface.md
|
- Given I choose a level of N words, when the first card is shown, then every card comes from the first N words of the word list, which is ordered from the most to the least frequent word.
|
||||||
[MIL-003]: ./milestones/mil-003-step2-new-flash-cards.md
|
- Given I choose 1000 words or All words and the word list has fewer words than that, when the cards are drawn, then all words of the word list are used.
|
||||||
[MIL-004]: ./milestones/mil-004-step3-flip-cards.md
|
- Given I have marked words as known at one level, when I choose another level, then the known words stay known and only the words of the chosen level that I do not know yet come up.
|
||||||
[MIL-005]: ./milestones/mil-005-step4-save-progress.md
|
- Given every word of the chosen level is known, when the level is chosen, then the program tells me that there is nothing left to learn at this level.
|
||||||
[MIL-006]: ./milestones/mil-006-malay-speech.md
|
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [MIL-008], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
|
### US-001.07 — Start without Python
|
||||||
|
|
||||||
|
**As a** Learner, **I want** to start the program by opening one executable file, **so that** I can learn without installing Python or a virtual environment.
|
||||||
|
|
||||||
|
**Acceptance Criteria**
|
||||||
|
|
||||||
|
- Given the executable was built, when I open it on a Windows computer without Python, then the level screen appears and no console window opens.
|
||||||
|
- Given I mark a word as known in the executable, when I open it again, then the known word does not come up (my progress is kept in `data/words_to_learn.csv` next to the executable).
|
||||||
|
- Given a `data/ms-da.csv` exists next to the executable, when the program starts, then that word list is used; otherwise the word list inside the executable is used.
|
||||||
|
- Given the speech extra was included in the build and a network is available, when a card is shown, then the Malay word is spoken; without a network the program works silently.
|
||||||
|
|
||||||
|
| Traces to | Size | INVEST exceptions |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [MIL-009], [UC-001] | fits one iteration | none |
|
||||||
|
|
||||||
|
## INVEST Check
|
||||||
|
|
||||||
|
All seven stories are Independent in delivery order (each builds on the one before but can be accepted alone), Negotiable in wording, Valuable to the Learner, Estimable, Small (one milestone each) and Testable (every criterion is Given/When/Then). Exception: US-001.02 to US-001.07 depend on the window of US-001.01, which is stated in their milestones' Dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[UCD-001]: ./use-case-diagram.md
|
||||||
|
[BC-001]: ./business-case.md
|
||||||
|
[UC-001]: ./uc-001/uc.md
|
||||||
|
[MIL-002]: ./milestones/mil-002-step1-user-interface.md
|
||||||
|
[MIL-003]: ./milestones/mil-003-step2-new-flash-cards.md
|
||||||
|
[MIL-004]: ./milestones/mil-004-step3-flip-cards.md
|
||||||
|
[MIL-005]: ./milestones/mil-005-step4-save-progress.md
|
||||||
|
[MIL-006]: ./milestones/mil-006-malay-speech.md
|
||||||
|
[MIL-008]: ./milestones/mil-008-choose-level.md
|
||||||
|
[MIL-009]: ./milestones/mil-009-executable.md
|
||||||
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||||
|
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Programmet er bygget i fire lag under `src/flash_card/`. Importer peger indad; `
|
|||||||
|
|
||||||
| Lag | Indeholder | Må importere |
|
| Lag | Indeholder | Må importere |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `domain` | `Card`, `LanguagePair`, `Vocabulary`, `Deck`, fejlene og repository-grænsefladerne | ingenting |
|
| `domain` | `Card`, `LanguagePair`, `Level`, `Vocabulary`, `Deck`, fejlene og repository-grænsefladerne | ingenting |
|
||||||
| `application` | `StudySession`, `open_study_session`, `CardDto` og `SpeechPort` | `domain` |
|
| `application` | `StudySession`, `open_study_session`, `CardDto` og `SpeechPort` | `domain` |
|
||||||
| `infrastructure` | CSV-repositories og tale-adaptere | `application`, `domain` |
|
| `infrastructure` | CSV-repositories og tale-adaptere | `application`, `domain` |
|
||||||
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) og kompositionsroden `app.py` | alle lag |
|
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) og kompositionsroden `app.py` | alle lag |
|
||||||
@@ -21,6 +21,7 @@ Alle konstanter står i `src/flash_card/constants.py`.
|
|||||||
| Trin 2: læs CSV, tilfældigt ord ved hvert tryk | `CsvVocabularyRepository` og `read_word_list` (erstatter `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
| Trin 2: læs CSV, tilfældigt ord ved hvert tryk | `CsvVocabularyRepository` og `read_word_list` (erstatter `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
||||||
| Trin 3: vend efter 3 sekunder | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
| Trin 3: vend efter 3 sekunder | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
||||||
| Trin 4: kendte ord, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
|
| Trin 4: kendte ord, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
|
||||||
|
| Ekstra: niveauvalg | `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session`, `LevelView`, `LevelViewModel`, `Application.start` i `presentation/app.py` |
|
||||||
| Ekstra: tale | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` i `presentation/app.py` |
|
| Ekstra: tale | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` i `presentation/app.py` |
|
||||||
|
|
||||||
## Hvorfor en lagdelt løsning til en lille opgave?
|
## Hvorfor en lagdelt løsning til en lille opgave?
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Program dibina dalam empat lapisan di bawah `src/flash_card/`. Import menuju ke
|
|||||||
|
|
||||||
| Lapisan | Mengandungi | Boleh mengimport |
|
| Lapisan | Mengandungi | Boleh mengimport |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `domain` | `Card`, `LanguagePair`, `Vocabulary`, `Deck`, ralat dan antara muka repositori | tiada |
|
| `domain` | `Card`, `LanguagePair`, `Level`, `Vocabulary`, `Deck`, ralat dan antara muka repositori | tiada |
|
||||||
| `application` | `StudySession`, `open_study_session`, `CardDto` dan `SpeechPort` | `domain` |
|
| `application` | `StudySession`, `open_study_session`, `CardDto` dan `SpeechPort` | `domain` |
|
||||||
| `infrastructure` | Repositori CSV dan penyesuai suara | `application`, `domain` |
|
| `infrastructure` | Repositori CSV dan penyesuai suara | `application`, `domain` |
|
||||||
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) dan akar komposisi `app.py` | semua lapisan |
|
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) dan akar komposisi `app.py` | semua lapisan |
|
||||||
@@ -21,6 +21,7 @@ Semua pemalar terdapat dalam `src/flash_card/constants.py`.
|
|||||||
| Langkah 2: baca CSV, perkataan rawak setiap tekanan | `CsvVocabularyRepository` dan `read_word_list` (menggantikan `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
| Langkah 2: baca CSV, perkataan rawak setiap tekanan | `CsvVocabularyRepository` dan `read_word_list` (menggantikan `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
||||||
| Langkah 3: terbalik selepas 3 saat | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
| Langkah 3: terbalik selepas 3 saat | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
||||||
| Langkah 4: perkataan diketahui, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
|
| Langkah 4: perkataan diketahui, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
|
||||||
|
| Tambahan: pilihan tahap | `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session`, `LevelView`, `LevelViewModel`, `Application.start` dalam `presentation/app.py` |
|
||||||
| Tambahan: suara | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` dalam `presentation/app.py` |
|
| Tambahan: suara | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` dalam `presentation/app.py` |
|
||||||
|
|
||||||
## Mengapa program berlapis untuk tugasan kecil?
|
## Mengapa program berlapis untuk tugasan kecil?
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ The program is built in four layers under `src/flash_card/`. Imports point inwar
|
|||||||
|
|
||||||
| Layer | Holds | May import |
|
| Layer | Holds | May import |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `domain` | `Card`, `LanguagePair`, `Vocabulary`, `Deck`, the errors and the repository interfaces | nothing |
|
| `domain` | `Card`, `LanguagePair`, `Level`, `Vocabulary`, `Deck`, the errors and the repository interfaces | nothing |
|
||||||
| `application` | `StudySession`, `open_study_session`, `CardDto` and `SpeechPort` | `domain` |
|
| `application` | `StudySession`, `open_study_session`, `CardDto` and `SpeechPort` | `domain` |
|
||||||
| `infrastructure` | The CSV repositories and the speech adapters | `application`, `domain` |
|
| `infrastructure` | The CSV repositories and the speech adapters | `application`, `domain` |
|
||||||
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) and the composition root `app.py` | all layers |
|
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) and the composition root `app.py` | all layers |
|
||||||
@@ -21,6 +21,7 @@ All constants are in `src/flash_card/constants.py`.
|
|||||||
| Step 2: read the CSV, random word on each press | `CsvVocabularyRepository` and `read_word_list` (replace `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
| Step 2: read the CSV, random word on each press | `CsvVocabularyRepository` and `read_word_list` (replace `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
|
||||||
| Step 3: flip after 3 seconds | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
| Step 3: flip after 3 seconds | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
|
||||||
| Step 4: known words, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
|
| Step 4: known words, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
|
||||||
|
| Extra: level choice | `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session`, `LevelView`, `LevelViewModel`, `Application.start` in `presentation/app.py` |
|
||||||
| Extra: speech | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` in `presentation/app.py` |
|
| Extra: speech | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` in `presentation/app.py` |
|
||||||
|
|
||||||
## Why a layered program for a small assignment?
|
## Why a layered program for a small assignment?
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
Et flashkortprogram til at lære de mest almindelige malajiske ord med danske oversættelser. Det er løsningen på dag 31 i Udemys kursus *100 Days of Code: The Complete Python Pro Bootcamp*.
|
Et flashkortprogram til at lære de mest almindelige malajiske ord med danske oversættelser. Det er løsningen på dag 31 i Udemys kursus *100 Days of Code: The Complete Python Pro Bootcamp*.
|
||||||
|
|
||||||
|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ Et flashkortprogram til at lære de mest almindelige malajiske ord med danske ov
|
|||||||
| 3. Vend kortene | Efter 3 sekunder viser kortet den danske oversættelse | MIL-004 |
|
| 3. Vend kortene | Efter 3 sekunder viser kortet den danske oversættelse | MIL-004 |
|
||||||
| 4. Gem din fremgang | Kendte ord fjernes, og resten gemmes i `words_to_learn.csv` | MIL-005 |
|
| 4. Gem din fremgang | Kendte ord fjernes, og resten gemmes i `words_to_learn.csv` | MIL-005 |
|
||||||
| Ekstra | Det malajiske ord bliver udtalt (valgfrit) | MIL-006 |
|
| Ekstra | Det malajiske ord bliver udtalt (valgfrit) | MIL-006 |
|
||||||
|
| Ekstra | Du vælger først et niveau: de første 100 ord, de første 1000 ord eller alle ord | MIL-008 |
|
||||||
|
|
||||||
## Til kursister
|
## Til kursister
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
Program kad imbas untuk mempelajari perkataan Melayu yang paling kerap digunakan beserta terjemahan Denmark. Ia ialah penyelesaian hari ke-31 kursus Udemy *100 Days of Code: The Complete Python Pro Bootcamp*.
|
Program kad imbas untuk mempelajari perkataan Melayu yang paling kerap digunakan beserta terjemahan Denmark. Ia ialah penyelesaian hari ke-31 kursus Udemy *100 Days of Code: The Complete Python Pro Bootcamp*.
|
||||||
|
|
||||||
|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ Program kad imbas untuk mempelajari perkataan Melayu yang paling kerap digunakan
|
|||||||
| 3. Terbalikkan kad | Selepas 3 saat kad menunjukkan terjemahan Denmark | MIL-004 |
|
| 3. Terbalikkan kad | Selepas 3 saat kad menunjukkan terjemahan Denmark | MIL-004 |
|
||||||
| 4. Simpan kemajuan anda | Perkataan yang diketahui dibuang dan selebihnya disimpan dalam `words_to_learn.csv` | MIL-005 |
|
| 4. Simpan kemajuan anda | Perkataan yang diketahui dibuang dan selebihnya disimpan dalam `words_to_learn.csv` | MIL-005 |
|
||||||
| Tambahan | Perkataan Melayu disebut (pilihan) | MIL-006 |
|
| Tambahan | Perkataan Melayu disebut (pilihan) | MIL-006 |
|
||||||
|
| Tambahan | Anda memilih tahap dahulu: 100 perkataan pertama, 1000 perkataan pertama atau semua perkataan | MIL-008 |
|
||||||
|
|
||||||
## Untuk peserta kursus
|
## Untuk peserta kursus
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
A flash card program for learning the most frequent Malay words with their Danish translations. It is the solution of day 31 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*.
|
A flash card program for learning the most frequent Malay words with their Danish translations. It is the solution of day 31 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*.
|
||||||
|
|
||||||
|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ A flash card program for learning the most frequent Malay words with their Danis
|
|||||||
| 3. Flip the cards | After 3 seconds the card shows the Danish translation | MIL-004 |
|
| 3. Flip the cards | After 3 seconds the card shows the Danish translation | MIL-004 |
|
||||||
| 4. Save your progress | Known words are removed and the rest is saved in `words_to_learn.csv` | MIL-005 |
|
| 4. Save your progress | Known words are removed and the rest is saved in `words_to_learn.csv` | MIL-005 |
|
||||||
| Extra | The Malay word is spoken (optional) | MIL-006 |
|
| Extra | The Malay word is spoken (optional) | MIL-006 |
|
||||||
|
| Extra | You choose a level first: the first 100 words, the first 1000 words or all words | MIL-008 |
|
||||||
|
|
||||||
## For course participants
|
## For course participants
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,20 @@ python -m pytest
|
|||||||
|
|
||||||
Vinduestesten i `tests/e2e/` springes over, når der ikke er en skærm.
|
Vinduestesten i `tests/e2e/` springes over, når der ikke er en skærm.
|
||||||
|
|
||||||
|
## Byg en programfil (Windows)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -e ".[build,speech]"
|
||||||
|
python tools/build_executable.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Scriptet bruger PyInstaller og skriver `dist/flash-card.exe`: én fil uden konsolvindue, med billeder, ordliste og udtalen indeni. Dobbeltklik for at starte; Python er ikke nødvendigt for at køre den.
|
||||||
|
|
||||||
|
- Din fremgang gemmes i en `data/`-mappe ved siden af programfilen (`data/words_to_learn.csv`).
|
||||||
|
- En `data/ms-da.csv` ved siden af programfilen erstatter ordlisten indeni.
|
||||||
|
- `python tools/build_executable.py --no-speech` udelader talepakkerne; `--clean` bygger forfra.
|
||||||
|
- Programfilen er ikke i repositoryet. Byg den på det system, hvor den skal køre; Windows kan vise en SmartScreen-advarsel, fordi filen ikke er signeret.
|
||||||
|
|
||||||
## Dokumentation af kildekoden
|
## Dokumentation af kildekoden
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -56,6 +56,20 @@ python -m pytest
|
|||||||
|
|
||||||
Ujian tetingkap dalam `tests/e2e/` dilangkau apabila tiada paparan.
|
Ujian tetingkap dalam `tests/e2e/` dilangkau apabila tiada paparan.
|
||||||
|
|
||||||
|
## Bina fail boleh laku (Windows)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -e ".[build,speech]"
|
||||||
|
python tools/build_executable.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Skrip menggunakan PyInstaller dan menulis `dist/flash-card.exe`: satu fail tanpa tetingkap konsol, dengan imej, senarai perkataan dan suara di dalamnya. Klik dua kali untuk memulakan; Python tidak diperlukan untuk menjalankannya.
|
||||||
|
|
||||||
|
- Kemajuan anda disimpan dalam folder `data/` di sebelah fail boleh laku (`data/words_to_learn.csv`).
|
||||||
|
- `data/ms-da.csv` di sebelah fail boleh laku menggantikan senarai perkataan di dalamnya.
|
||||||
|
- `python tools/build_executable.py --no-speech` meninggalkan pakej suara; `--clean` membina semula dari awal.
|
||||||
|
- Fail boleh laku tidak disimpan dalam repositori. Bina di sistem tempat ia akan dijalankan; Windows mungkin menunjukkan amaran SmartScreen kerana fail tidak ditandatangani.
|
||||||
|
|
||||||
## Dokumentasi kod sumber
|
## Dokumentasi kod sumber
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -56,6 +56,20 @@ python -m pytest
|
|||||||
|
|
||||||
The window test in `tests/e2e/` is skipped when no display is available.
|
The window test in `tests/e2e/` is skipped when no display is available.
|
||||||
|
|
||||||
|
## Build an executable (Windows)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -e ".[build,speech]"
|
||||||
|
python tools/build_executable.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script uses PyInstaller and writes `dist/flash-card.exe`: one file, no console window, with the images, the word list and the spoken word inside. Double-click it to start; Python is not needed to run it.
|
||||||
|
|
||||||
|
- Your progress is kept in a `data/` folder next to the executable (`data/words_to_learn.csv`).
|
||||||
|
- A `data/ms-da.csv` next to the executable replaces the word list inside it.
|
||||||
|
- `python tools/build_executable.py --no-speech` leaves the speech packages out; `--clean` rebuilds from scratch.
|
||||||
|
- The executable is not committed to the repository. Build it on the system you want to run it on; Windows may show a SmartScreen warning because the file is not signed.
|
||||||
|
|
||||||
## Documentation of the source code
|
## Documentation of the source code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
## Øv kortene
|
## Øv kortene
|
||||||
|
|
||||||
1. Start programmet med `python -m flash_card`.
|
1. Start programmet med `python -m flash_card`.
|
||||||
2. Et kort viser et malajisk ord. Prøv at huske den danske oversættelse.
|
2. Vælg et niveau: **100 words**, **1000 words** eller **All words**. Ordlisten er sorteret fra det hyppigste til det sjældneste ord, så niveau 100 er de 100 hyppigste ord.
|
||||||
3. Efter tre sekunder vender kortet og viser oversættelsen.
|
3. Et kort viser et malajisk ord. Prøv at huske den danske oversættelse.
|
||||||
4. Tryk på det grønne flueben, hvis du kendte ordet, eller på det røde kryds, hvis du ikke gjorde.
|
4. Efter tre sekunder vender kortet og viser oversættelsen.
|
||||||
5. Et nyt tilfældigt ord vises. Gentag.
|
5. Tryk på det grønne flueben, hvis du kendte ordet, eller på det røde kryds, hvis du ikke gjorde.
|
||||||
|
6. Et nyt tilfældigt ord fra niveauet vises. Gentag.
|
||||||
|
|
||||||
Et ord, du har markeret som kendt, fjernes og kommer ikke igen. Når der ikke er flere ord, står der "Nothing left to learn" på kortet.
|
Et ord, du har markeret som kendt, fjernes og kommer ikke igen. Når der ikke er flere ord på niveauet, står der "Nothing left to learn" på kortet; start programmet igen og vælg et højere niveau.
|
||||||
|
|
||||||
## Din fremgang
|
## Din fremgang
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ Et ord, du har markeret som kendt, fjernes og kommer ikke igen. Når der ikke er
|
|||||||
- Næste start bruger den fil. Findes den ikke, bruger programmet hele ordlisten `data/ms-da.csv`.
|
- Næste start bruger den fil. Findes den ikke, bruger programmet hele ordlisten `data/ms-da.csv`.
|
||||||
- For at starte forfra skal du slette `data/words_to_learn.csv`.
|
- For at starte forfra skal du slette `data/words_to_learn.csv`.
|
||||||
- Det røde kryds ændrer aldrig filen.
|
- Det røde kryds ændrer aldrig filen.
|
||||||
|
- Kendte ord forbliver kendte på alle niveauer. Niveauet begrænser kun, hvilke af de ord, du endnu ikke kender, der kommer frem, og filen beholder altid alle ord, du ikke kender, også dem uden for niveauet.
|
||||||
|
- Denne liste har 1000 ord, så "1000 words" og "All words" giver de samme kort.
|
||||||
|
|
||||||
## Filformat
|
## Filformat
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ yang;hvilke
|
|||||||
| Indstilling | Betydning |
|
| Indstilling | Betydning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--data-dir <mappe>` | Mappen med `ms-da.csv` og `words_to_learn.csv` (standard: `data/` i repositoryet) |
|
| `--data-dir <mappe>` | Mappen med `ms-da.csv` og `words_to_learn.csv` (standard: `data/` i repositoryet) |
|
||||||
|
| `--level {100,1000,all}` | Start på det niveau og spring niveauskærmen over |
|
||||||
| `--no-speech` | Udtal ikke det malajiske ord, selv om tilvalget til tale er installeret |
|
| `--no-speech` | Udtal ikke det malajiske ord, selv om tilvalget til tale er installeret |
|
||||||
|
|
||||||
## Brug et andet sprogpar
|
## Brug et andet sprogpar
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
## Berlatih dengan kad
|
## Berlatih dengan kad
|
||||||
|
|
||||||
1. Mulakan program dengan `python -m flash_card`.
|
1. Mulakan program dengan `python -m flash_card`.
|
||||||
2. Kad menunjukkan satu perkataan Melayu. Cuba ingat terjemahan Denmarknya.
|
2. Pilih tahap: **100 words**, **1000 words** atau **All words**. Senarai perkataan disusun daripada perkataan paling kerap kepada paling jarang, jadi tahap 100 ialah 100 perkataan paling kerap.
|
||||||
3. Selepas tiga saat kad terbalik dan menunjukkan terjemahan.
|
3. Kad menunjukkan satu perkataan Melayu. Cuba ingat terjemahan Denmarknya.
|
||||||
4. Tekan tanda semak hijau jika anda tahu perkataan itu, atau pangkah merah jika tidak.
|
4. Selepas tiga saat kad terbalik dan menunjukkan terjemahan.
|
||||||
5. Satu perkataan rawak baharu muncul. Ulang.
|
5. Tekan tanda semak hijau jika anda tahu perkataan itu, atau pangkah merah jika tidak.
|
||||||
|
6. Satu perkataan rawak baharu daripada tahap itu muncul. Ulang.
|
||||||
|
|
||||||
Perkataan yang anda tandakan sebagai diketahui dibuang dan tidak muncul lagi. Apabila tiada perkataan lagi, kad menunjukkan "Nothing left to learn".
|
Perkataan yang anda tandakan sebagai diketahui dibuang dan tidak muncul lagi. Apabila tiada perkataan lagi pada tahap itu, kad menunjukkan "Nothing left to learn"; mulakan program semula dan pilih tahap yang lebih tinggi.
|
||||||
|
|
||||||
## Kemajuan anda
|
## Kemajuan anda
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ Perkataan yang anda tandakan sebagai diketahui dibuang dan tidak muncul lagi. Ap
|
|||||||
- Permulaan seterusnya menggunakan fail itu. Jika ia tiada, program menggunakan senarai penuh `data/ms-da.csv`.
|
- Permulaan seterusnya menggunakan fail itu. Jika ia tiada, program menggunakan senarai penuh `data/ms-da.csv`.
|
||||||
- Untuk bermula semula, padam `data/words_to_learn.csv`.
|
- Untuk bermula semula, padam `data/words_to_learn.csv`.
|
||||||
- Pangkah merah tidak pernah mengubah fail.
|
- Pangkah merah tidak pernah mengubah fail.
|
||||||
|
- Perkataan yang diketahui kekal diketahui pada semua tahap. Tahap hanya mengehadkan perkataan yang belum anda ketahui yang muncul, dan fail sentiasa menyimpan semua perkataan yang anda belum tahu, termasuk yang di luar tahap.
|
||||||
|
- Senarai ini mempunyai 1000 perkataan, jadi "1000 words" dan "All words" memberikan kad yang sama.
|
||||||
|
|
||||||
## Format fail
|
## Format fail
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ yang;hvilke
|
|||||||
| Pilihan | Maksud |
|
| Pilihan | Maksud |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--data-dir <folder>` | Folder yang mengandungi `ms-da.csv` dan `words_to_learn.csv` (lalai: `data/` dalam repositori) |
|
| `--data-dir <folder>` | Folder yang mengandungi `ms-da.csv` dan `words_to_learn.csv` (lalai: `data/` dalam repositori) |
|
||||||
|
| `--level {100,1000,all}` | Mulakan pada tahap itu dan langkau skrin tahap |
|
||||||
| `--no-speech` | Jangan sebut perkataan Melayu, walaupun tambahan suara dipasang |
|
| `--no-speech` | Jangan sebut perkataan Melayu, walaupun tambahan suara dipasang |
|
||||||
|
|
||||||
## Gunakan pasangan bahasa lain
|
## Gunakan pasangan bahasa lain
|
||||||
|
|||||||
+9
-5
@@ -5,12 +5,13 @@
|
|||||||
## Study the cards
|
## Study the cards
|
||||||
|
|
||||||
1. Start the program with `python -m flash_card`.
|
1. Start the program with `python -m flash_card`.
|
||||||
2. A card shows a Malay word. Try to remember its Danish translation.
|
2. Choose a level: **100 words**, **1000 words** or **All words**. The word list is ordered from the most to the least frequent word, so level 100 is the 100 most frequent words.
|
||||||
3. After three seconds the card turns over and shows the translation.
|
3. A card shows a Malay word. Try to remember its Danish translation.
|
||||||
4. Press the green tick when you knew the word, or the red cross when you did not.
|
4. After three seconds the card turns over and shows the translation.
|
||||||
5. A new random word appears. Repeat.
|
5. Press the green tick when you knew the word, or the red cross when you did not.
|
||||||
|
6. A new random word of the level appears. Repeat.
|
||||||
|
|
||||||
A word you marked as known is removed and does not come up again. When no word is left, the card says "Nothing left to learn".
|
A word you marked as known is removed and does not come up again. When no word of the level is left, the card says "Nothing left to learn"; restart the program and choose a higher level.
|
||||||
|
|
||||||
## Your progress
|
## Your progress
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ A word you marked as known is removed and does not come up again. When no word i
|
|||||||
- The next start uses that file. If it does not exist, the program uses the full word list `data/ms-da.csv`.
|
- The next start uses that file. If it does not exist, the program uses the full word list `data/ms-da.csv`.
|
||||||
- To start again, delete `data/words_to_learn.csv`.
|
- To start again, delete `data/words_to_learn.csv`.
|
||||||
- The red cross never changes the file.
|
- The red cross never changes the file.
|
||||||
|
- Known words stay known at every level. The level only limits which of the words you do not know yet come up, and the file always keeps every word you do not know, also those outside the level.
|
||||||
|
- This list has 1000 words, so "1000 words" and "All words" give the same cards.
|
||||||
|
|
||||||
## File format
|
## File format
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ yang;hvilke
|
|||||||
| Option | Meaning |
|
| Option | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--data-dir <folder>` | Folder that holds `ms-da.csv` and `words_to_learn.csv` (default: `data/` of the repository) |
|
| `--data-dir <folder>` | Folder that holds `ms-da.csv` and `words_to_learn.csv` (default: `data/` of the repository) |
|
||||||
|
| `--level {100,1000,all}` | Start at that level and skip the level screen |
|
||||||
| `--no-speech` | Do not speak the Malay word, even when the speech extra is installed |
|
| `--no-speech` | Do not speak the Malay word, even when the speech extra is installed |
|
||||||
|
|
||||||
## Use another language pair
|
## Use another language pair
|
||||||
|
|||||||
+3
-1
@@ -26,6 +26,8 @@ dependencies = []
|
|||||||
speech = ["gTTS>=2.5", "playsound==1.2.2"]
|
speech = ["gTTS>=2.5", "playsound==1.2.2"]
|
||||||
# Only for tools/capture_screenshots.py; not needed to run the program.
|
# Only for tools/capture_screenshots.py; not needed to run the program.
|
||||||
tools = ["pillow>=10"]
|
tools = ["pillow>=10"]
|
||||||
|
# Only for tools/build_executable.py; not needed to run the program.
|
||||||
|
build = ["pyinstaller>=6"]
|
||||||
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "import-linter>=2.0"]
|
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "import-linter>=2.0"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -35,7 +37,7 @@ flash-card = "flash_card.presentation.app:run"
|
|||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
flash_card = ["presentation/assets/*.png"]
|
flash_card = ["presentation/assets/*.png", "presentation/assets/*.ico"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from flash_card.domain.entities.card import Card
|
|||||||
from flash_card.domain.entities.deck import Deck
|
from flash_card.domain.entities.deck import Deck
|
||||||
from flash_card.domain.repositories.progress_repository import ProgressRepository
|
from flash_card.domain.repositories.progress_repository import ProgressRepository
|
||||||
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
|
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
|
||||||
|
|
||||||
def open_study_session(
|
def open_study_session(
|
||||||
@@ -19,18 +20,24 @@ def open_study_session(
|
|||||||
progress_repository: ProgressRepository,
|
progress_repository: ProgressRepository,
|
||||||
speech: SpeechPort,
|
speech: SpeechPort,
|
||||||
choose: Callable[[Sequence[Card]], Card],
|
choose: Callable[[Sequence[Card]], Card],
|
||||||
|
level: Level,
|
||||||
) -> StudySession:
|
) -> StudySession:
|
||||||
"""Build a session over the saved words to learn, else the whole word list.
|
"""Build a session over the saved words to learn, else the whole word list.
|
||||||
|
|
||||||
|
The word list is always loaded: its order defines which words a level holds.
|
||||||
|
|
||||||
@param vocabulary_repository Source of the delivered word list.
|
@param vocabulary_repository Source of the delivered word list.
|
||||||
@param progress_repository Store of the words still to learn.
|
@param progress_repository Store of the words still to learn.
|
||||||
@param speech Speaks the front text of each drawn card.
|
@param speech Speaks the front text of each drawn card.
|
||||||
@param choose Picks one card from a non-empty sequence.
|
@param choose Picks one card from a non-empty sequence.
|
||||||
|
@param level The level: the first words of the list that may be drawn.
|
||||||
@return The new session.
|
@return The new session.
|
||||||
@throws VocabularyError When the saved progress, or the word list when no
|
@throws VocabularyError When the word list or the saved progress cannot be
|
||||||
progress exists, cannot be loaded.
|
loaded.
|
||||||
"""
|
"""
|
||||||
|
vocabulary = vocabulary_repository.load()
|
||||||
deck = progress_repository.load()
|
deck = progress_repository.load()
|
||||||
if deck is None:
|
if deck is None:
|
||||||
deck = Deck.from_vocabulary(vocabulary_repository.load())
|
deck = Deck.from_vocabulary(vocabulary)
|
||||||
return StudySession(deck, choose, progress_repository, speech)
|
level_cards = level.first(vocabulary.cards)
|
||||||
|
return StudySession(deck, choose, progress_repository, speech, level_cards)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class StudySession:
|
|||||||
choose: Callable[[Sequence[Card]], Card],
|
choose: Callable[[Sequence[Card]], Card],
|
||||||
progress: ProgressRepository,
|
progress: ProgressRepository,
|
||||||
speech: SpeechPort,
|
speech: SpeechPort,
|
||||||
|
level_cards: Sequence[Card] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a session.
|
"""Create a session.
|
||||||
|
|
||||||
@@ -29,7 +30,10 @@ class StudySession:
|
|||||||
@param choose Picks one card from a non-empty sequence.
|
@param choose Picks one card from a non-empty sequence.
|
||||||
@param progress Store of the words still to learn.
|
@param progress Store of the words still to learn.
|
||||||
@param speech Speaks the front text of each drawn card.
|
@param speech Speaks the front text of each drawn card.
|
||||||
|
@param level_cards The cards of the chosen level, or None for no limit.
|
||||||
"""
|
"""
|
||||||
|
## @brief The cards of the chosen level, or None for no limit.
|
||||||
|
self._level_cards = None if level_cards is None else tuple(level_cards)
|
||||||
## @brief Speaks the front text of each drawn card.
|
## @brief Speaks the front text of each drawn card.
|
||||||
self._speech = speech
|
self._speech = speech
|
||||||
## @brief Store of the words still to learn.
|
## @brief Store of the words still to learn.
|
||||||
@@ -51,27 +55,36 @@ class StudySession:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def remaining(self) -> int:
|
def remaining(self) -> int:
|
||||||
"""Return the number of cards still to learn.
|
"""Return the number of cards still to learn, of every level.
|
||||||
|
|
||||||
@return The card count.
|
@return The card count.
|
||||||
"""
|
"""
|
||||||
return len(self._deck)
|
return len(self._deck)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def remaining_in_level(self) -> int:
|
||||||
|
"""Return the number of cards still to learn in the chosen level.
|
||||||
|
|
||||||
|
@return The card count.
|
||||||
|
"""
|
||||||
|
return len(self._candidates())
|
||||||
|
|
||||||
def next_card(self) -> CardDto | None:
|
def next_card(self) -> CardDto | None:
|
||||||
"""Draw the next card, different from the current one when possible.
|
"""Draw the next card, different from the current one when possible.
|
||||||
|
|
||||||
@return The new card, or None when the deck is empty.
|
@return The new card, or None when no card of the level is left.
|
||||||
"""
|
"""
|
||||||
if self._deck.is_empty:
|
candidates = self._candidates()
|
||||||
|
if not candidates:
|
||||||
self._current = None
|
self._current = None
|
||||||
return None
|
return None
|
||||||
card = self._deck.draw(self._choose, exclude=self._current)
|
card = self._deck.draw(self._choose, self._current, candidates)
|
||||||
self._current = card
|
self._current = card
|
||||||
self._speech.speak(card.front, self._deck.language_pair.front_code)
|
self._speech.speak(card.front, self._deck.language_pair.front_code)
|
||||||
return self._to_dto(card)
|
return self._to_dto(card)
|
||||||
|
|
||||||
def mark_known(self) -> None:
|
def mark_known(self) -> None:
|
||||||
"""Remove the current card from the deck and save the remaining cards.
|
"""Remove the current card from the whole deck and save the whole deck.
|
||||||
|
|
||||||
Does nothing and saves nothing when no card is shown.
|
Does nothing and saves nothing when no card is shown.
|
||||||
|
|
||||||
@@ -83,6 +96,15 @@ class StudySession:
|
|||||||
self._progress.save(self._deck)
|
self._progress.save(self._deck)
|
||||||
self._current = None
|
self._current = None
|
||||||
|
|
||||||
|
def _candidates(self) -> tuple[Card, ...]:
|
||||||
|
"""Return the cards that may be drawn.
|
||||||
|
|
||||||
|
@return The deck cards of the level, or all deck cards without a level.
|
||||||
|
"""
|
||||||
|
if self._level_cards is None:
|
||||||
|
return self._deck.cards
|
||||||
|
return self._deck.within(self._level_cards)
|
||||||
|
|
||||||
def _to_dto(self, card: Card | None) -> CardDto | None:
|
def _to_dto(self, card: Card | None) -> CardDto | None:
|
||||||
"""Map a card to its DTO.
|
"""Map a card to its DTO.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from collections.abc import Mapping
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
from flash_card.runtime import executable_dir, is_frozen, resource_root
|
||||||
|
|
||||||
## @brief Window background colour.
|
## @brief Window background colour.
|
||||||
BACKGROUND_COLOR: Final[str] = "#B1DDC6"
|
BACKGROUND_COLOR: Final[str] = "#B1DDC6"
|
||||||
## @brief Window title.
|
## @brief Window title.
|
||||||
@@ -20,6 +22,8 @@ WINDOW_PADDING: Final[int] = 50
|
|||||||
CARD_WIDTH: Final[int] = 800
|
CARD_WIDTH: Final[int] = 800
|
||||||
## @brief Card height in pixels.
|
## @brief Card height in pixels.
|
||||||
CARD_HEIGHT: Final[int] = 526
|
CARD_HEIGHT: Final[int] = 526
|
||||||
|
## @brief Level screen height in pixels: card plus the card window button row.
|
||||||
|
LEVEL_SCREEN_HEIGHT: Final[int] = 526 + 100
|
||||||
## @brief Horizontal centre of the card in pixels.
|
## @brief Horizontal centre of the card in pixels.
|
||||||
CARD_CENTER_X: Final[int] = 400
|
CARD_CENTER_X: Final[int] = 400
|
||||||
## @brief Vertical position of the language text in pixels.
|
## @brief Vertical position of the language text in pixels.
|
||||||
@@ -43,6 +47,29 @@ LANGUAGE_NAMES: Final[Mapping[str, str]] = {"ms": "Malay", "da": "Danish"}
|
|||||||
## @brief Message shown when no word is left to learn.
|
## @brief Message shown when no word is left to learn.
|
||||||
FINISHED_MESSAGE: Final[str] = "Nothing left to learn"
|
FINISHED_MESSAGE: Final[str] = "Nothing left to learn"
|
||||||
|
|
||||||
|
## @brief Title of the level screen.
|
||||||
|
LEVEL_TITLE: Final[str] = "Choose your level"
|
||||||
|
## @brief Level choices in display order: label and word limit (None = all words).
|
||||||
|
LEVEL_CHOICES: Final[tuple[tuple[str, int | None], ...]] = (
|
||||||
|
("100 words", 100),
|
||||||
|
("1000 words", 1000),
|
||||||
|
("All words", None),
|
||||||
|
)
|
||||||
|
## @brief Values of the --level option and the word limit each stands for.
|
||||||
|
LEVEL_ARGUMENT_VALUES: Final[Mapping[str, int | None]] = {
|
||||||
|
"100": 100,
|
||||||
|
"1000": 1000,
|
||||||
|
"all": None,
|
||||||
|
}
|
||||||
|
## @brief Font of the level screen title: family, size, style.
|
||||||
|
LEVEL_TITLE_FONT: Final[tuple[str, int, str]] = ("Arial", 40, "italic")
|
||||||
|
## @brief Font of the level buttons: family, size, style.
|
||||||
|
LEVEL_BUTTON_FONT: Final[tuple[str, int, str]] = ("Arial", 24, "bold")
|
||||||
|
## @brief Width of every level button in characters.
|
||||||
|
LEVEL_BUTTON_WIDTH: Final[int] = 14
|
||||||
|
## @brief Background colour of the level buttons.
|
||||||
|
LEVEL_BUTTON_COLOR: Final[str] = "white"
|
||||||
|
|
||||||
## @brief Delimiter of the CSV files.
|
## @brief Delimiter of the CSV files.
|
||||||
CSV_DELIMITER: Final[str] = ";"
|
CSV_DELIMITER: Final[str] = ";"
|
||||||
## @brief Encoding of the CSV files; drops a leading byte order mark.
|
## @brief Encoding of the CSV files; drops a leading byte order mark.
|
||||||
@@ -52,12 +79,24 @@ VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv"
|
|||||||
## @brief File name of the words still to learn.
|
## @brief File name of the words still to learn.
|
||||||
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
|
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
|
||||||
|
|
||||||
## @brief Root folder of the project.
|
## @brief Root folder of the project, for a source run.
|
||||||
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
|
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
|
||||||
## @brief Default folder of the data files.
|
## @brief Folder of the image assets, inside the bundle when frozen.
|
||||||
DEFAULT_DATA_DIR: Final[Path] = PROJECT_ROOT / "data"
|
ASSETS_DIR: Final[Path] = (
|
||||||
## @brief Folder of the image assets.
|
resource_root() / "flash_card" / "presentation" / "assets"
|
||||||
ASSETS_DIR: Final[Path] = Path(__file__).resolve().parent / "presentation" / "assets"
|
if is_frozen()
|
||||||
|
else Path(__file__).resolve().parent / "presentation" / "assets"
|
||||||
|
)
|
||||||
|
## @brief Folder of the word list delivered with the program.
|
||||||
|
BUNDLED_DATA_DIR: Final[Path] = resource_root() / "data"
|
||||||
|
## @brief The word list delivered with the program.
|
||||||
|
BUNDLED_VOCABULARY_PATH: Final[Path] = BUNDLED_DATA_DIR / VOCABULARY_FILE_NAME
|
||||||
|
## @brief Default folder of the data files, next to the executable when frozen.
|
||||||
|
DEFAULT_DATA_DIR: Final[Path] = executable_dir() / "data"
|
||||||
|
## @brief File name of the program icon.
|
||||||
|
ICON_FILE_NAME: Final[str] = "flash_card.ico"
|
||||||
|
## @brief Name of the executable without extension.
|
||||||
|
EXECUTABLE_NAME: Final[str] = "flash-card"
|
||||||
## @brief File name of the card front image.
|
## @brief File name of the card front image.
|
||||||
CARD_FRONT_IMAGE_NAME: Final[str] = "card_front.png"
|
CARD_FRONT_IMAGE_NAME: Final[str] = "card_front.png"
|
||||||
## @brief File name of the card back image.
|
## @brief File name of the card back image.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@brief The Deck entity: holds cards, draws one and removes one.
|
@brief The Deck entity: holds cards, draws one and removes one.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
from collections.abc import Callable, Iterable, Sequence
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
|
|
||||||
from flash_card.domain.entities.card import Card
|
from flash_card.domain.entities.card import Card
|
||||||
@@ -66,24 +67,43 @@ class Deck:
|
|||||||
"""
|
"""
|
||||||
return len(self._cards)
|
return len(self._cards)
|
||||||
|
|
||||||
|
def within(self, cards: Sequence[Card]) -> tuple[Card, ...]:
|
||||||
|
"""Select the cards of the deck that also appear in the given cards.
|
||||||
|
|
||||||
|
A card counts as often as it appears in both.
|
||||||
|
|
||||||
|
@param cards The cards to match against, for example a level.
|
||||||
|
@return The matching cards in deck order.
|
||||||
|
"""
|
||||||
|
allowed = Counter(cards)
|
||||||
|
selected: list[Card] = []
|
||||||
|
for card in self._cards:
|
||||||
|
if allowed[card] > 0:
|
||||||
|
allowed[card] -= 1
|
||||||
|
selected.append(card)
|
||||||
|
return tuple(selected)
|
||||||
|
|
||||||
def draw(
|
def draw(
|
||||||
self,
|
self,
|
||||||
choose: Callable[[Sequence[Card]], Card],
|
choose: Callable[[Sequence[Card]], Card],
|
||||||
exclude: Card | None = None,
|
exclude: Card | None = None,
|
||||||
|
among: Sequence[Card] | None = None,
|
||||||
) -> Card:
|
) -> Card:
|
||||||
"""Pick a card without removing it.
|
"""Pick a card without removing it.
|
||||||
|
|
||||||
The pick avoids the excluded card when another card exists.
|
The pick avoids the excluded card when another candidate exists.
|
||||||
|
|
||||||
@param choose Picks one card from a non-empty sequence.
|
@param choose Picks one card from a non-empty sequence.
|
||||||
@param exclude A card to avoid, usually the one shown now.
|
@param exclude A card to avoid, usually the one shown now.
|
||||||
|
@param among The candidates, or None for every card of the deck.
|
||||||
@return The picked card.
|
@return The picked card.
|
||||||
@throws EmptyDeckError When the deck has no card.
|
@throws EmptyDeckError When there is no candidate.
|
||||||
"""
|
"""
|
||||||
if not self._cards:
|
candidates = self._cards if among is None else list(among)
|
||||||
|
if not candidates:
|
||||||
raise EmptyDeckError("cannot draw from an empty deck")
|
raise EmptyDeckError("cannot draw from an empty deck")
|
||||||
others = [card for card in self._cards if card != exclude]
|
others = [card for card in candidates if card != exclude]
|
||||||
return choose(others or self._cards)
|
return choose(others or candidates)
|
||||||
|
|
||||||
def remove(self, card: Card) -> None:
|
def remove(self, card: Card) -> None:
|
||||||
"""Remove the first card equal to the given one.
|
"""Remove the first card equal to the given one.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""The level of a study session.
|
||||||
|
|
||||||
|
@file level.py
|
||||||
|
@brief The Level value object: how many of the first words a session uses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from flash_card.domain.entities.card import Card
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Level:
|
||||||
|
"""A limit on the words of the list, counted from the most frequent."""
|
||||||
|
|
||||||
|
## @brief Number of first words used, or None for all words.
|
||||||
|
limit: int | None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""Check the limit.
|
||||||
|
|
||||||
|
@return Nothing.
|
||||||
|
@throws ValueError When the limit is set and not above zero.
|
||||||
|
"""
|
||||||
|
if self.limit is not None and self.limit <= 0:
|
||||||
|
raise ValueError(f"level limit must be above zero, got {self.limit}")
|
||||||
|
|
||||||
|
def first(self, cards: Sequence[Card]) -> tuple[Card, ...]:
|
||||||
|
"""Select the cards of this level.
|
||||||
|
|
||||||
|
@param cards The word list in order from the most frequent word.
|
||||||
|
@return The first limit cards, or all cards when there is no limit or
|
||||||
|
the list is shorter.
|
||||||
|
"""
|
||||||
|
if self.limit is None:
|
||||||
|
return tuple(cards)
|
||||||
|
return tuple(cards[: self.limit])
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Entry point and composition root of the flash card program.
|
"""Entry point and composition root of the flash card program.
|
||||||
|
|
||||||
@file app.py
|
@file app.py
|
||||||
@brief Creates the window, its view model and view, and runs the main loop.
|
@brief Creates the window, shows the level screen and then the card screen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -9,18 +9,24 @@ import importlib.util
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from collections.abc import Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
from flash_card.application.ports.speech_port import SpeechPort
|
from flash_card.application.ports.speech_port import SpeechPort
|
||||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||||
from flash_card.constants import (
|
from flash_card.constants import (
|
||||||
|
BUNDLED_VOCABULARY_PATH,
|
||||||
DEFAULT_DATA_DIR,
|
DEFAULT_DATA_DIR,
|
||||||
|
LEVEL_ARGUMENT_VALUES,
|
||||||
PROGRESS_FILE_NAME,
|
PROGRESS_FILE_NAME,
|
||||||
VOCABULARY_FILE_NAME,
|
VOCABULARY_FILE_NAME,
|
||||||
)
|
)
|
||||||
|
from flash_card.domain.entities.card import Card
|
||||||
from flash_card.domain.errors.vocabulary_errors import VocabularyError
|
from flash_card.domain.errors.vocabulary_errors import VocabularyError
|
||||||
|
from flash_card.domain.repositories.progress_repository import ProgressRepository
|
||||||
|
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||||
CsvProgressRepository,
|
CsvProgressRepository,
|
||||||
)
|
)
|
||||||
@@ -33,6 +39,8 @@ from flash_card.presentation.features.flash_card.flash_card_view import FlashCar
|
|||||||
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
||||||
FlashCardViewModel,
|
FlashCardViewModel,
|
||||||
)
|
)
|
||||||
|
from flash_card.presentation.features.level.level_view import LevelView
|
||||||
|
from flash_card.presentation.features.level.level_view_model import LevelViewModel
|
||||||
|
|
||||||
## @brief Logger of the entry point.
|
## @brief Logger of the entry point.
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -55,26 +63,156 @@ def build_speech(enabled: bool) -> SpeechPort:
|
|||||||
return NullSpeech()
|
return NullSpeech()
|
||||||
|
|
||||||
|
|
||||||
|
class Application:
|
||||||
|
"""The running program: one window that shows the level screen, then the card."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
root: tk.Tk,
|
||||||
|
vocabulary_repository: VocabularyRepository,
|
||||||
|
progress_repository: ProgressRepository,
|
||||||
|
speech: SpeechPort,
|
||||||
|
choose: Callable[[Sequence[Card]], Card] = random.choice,
|
||||||
|
) -> None:
|
||||||
|
"""Create the application on a window; no screen is shown yet.
|
||||||
|
|
||||||
|
@param root The Tk root window.
|
||||||
|
@param vocabulary_repository Source of the delivered word list.
|
||||||
|
@param progress_repository Store of the words still to learn.
|
||||||
|
@param speech Speaks the front text of each drawn card.
|
||||||
|
@param choose Picks one card from a non-empty sequence.
|
||||||
|
"""
|
||||||
|
## @brief The Tk root window.
|
||||||
|
self.root = root
|
||||||
|
## @brief Source of the delivered word list.
|
||||||
|
self._vocabulary_repository = vocabulary_repository
|
||||||
|
## @brief Store of the words still to learn.
|
||||||
|
self._progress_repository = progress_repository
|
||||||
|
## @brief Speaks the front text of each drawn card.
|
||||||
|
self._speech = speech
|
||||||
|
## @brief Picks one card from a non-empty sequence.
|
||||||
|
self._choose = choose
|
||||||
|
## @brief The screen shown now, or None before the first one.
|
||||||
|
self._view: LevelView | FlashCardView | None = None
|
||||||
|
## @brief The exit code of the program.
|
||||||
|
self._exit_code = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view(self) -> LevelView | FlashCardView | None:
|
||||||
|
"""Return the screen shown now.
|
||||||
|
|
||||||
|
@return The level screen, the card screen, or None before the first.
|
||||||
|
"""
|
||||||
|
return self._view
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit_code(self) -> int:
|
||||||
|
"""Return the exit code the program ends with.
|
||||||
|
|
||||||
|
@return 0, or 1 when a session could not be started after a choice.
|
||||||
|
"""
|
||||||
|
return self._exit_code
|
||||||
|
|
||||||
|
def show_level_screen(self) -> None:
|
||||||
|
"""Show the level screen; a choice starts the session.
|
||||||
|
|
||||||
|
@return Nothing.
|
||||||
|
"""
|
||||||
|
self._view = LevelView(self.root, LevelViewModel(), self._choose_level)
|
||||||
|
|
||||||
|
def start(self, level: Level) -> None:
|
||||||
|
"""Open the session of a level and show the card screen on the window.
|
||||||
|
|
||||||
|
@param level The level the user chose.
|
||||||
|
@return Nothing.
|
||||||
|
@throws VocabularyError When the word list or saved progress is unusable.
|
||||||
|
"""
|
||||||
|
session = open_study_session(
|
||||||
|
self._vocabulary_repository,
|
||||||
|
self._progress_repository,
|
||||||
|
self._speech,
|
||||||
|
self._choose,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
if isinstance(self._view, LevelView):
|
||||||
|
self._view.destroy()
|
||||||
|
self._view = FlashCardView(self.root, FlashCardViewModel(session))
|
||||||
|
|
||||||
|
def _choose_level(self, level: Level) -> None:
|
||||||
|
"""Start the session after a click; close the window when it fails.
|
||||||
|
|
||||||
|
@param level The level the user chose.
|
||||||
|
@return Nothing.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.start(level)
|
||||||
|
except VocabularyError as error:
|
||||||
|
_logger.error("%s", error)
|
||||||
|
self._exit_code = 1
|
||||||
|
self.root.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_data_files(data_dir: Path | None) -> tuple[Path, Path]:
|
||||||
|
"""Find the word list and the progress file.
|
||||||
|
|
||||||
|
@param data_dir Folder given by the user, or None for the default folder.
|
||||||
|
@return The vocabulary path and the progress path. Without a given folder,
|
||||||
|
the delivered word list is used when the default folder has none.
|
||||||
|
"""
|
||||||
|
folder = DEFAULT_DATA_DIR if data_dir is None else data_dir
|
||||||
|
vocabulary = folder / VOCABULARY_FILE_NAME
|
||||||
|
if data_dir is None and not vocabulary.exists():
|
||||||
|
vocabulary = BUNDLED_VOCABULARY_PATH
|
||||||
|
return vocabulary, folder / PROGRESS_FILE_NAME
|
||||||
|
|
||||||
|
|
||||||
def create_application(
|
def create_application(
|
||||||
data_dir: Path | None = None,
|
data_dir: Path | None = None,
|
||||||
speech_enabled: bool = True,
|
speech_enabled: bool = True,
|
||||||
) -> tuple[tk.Tk, FlashCardView]:
|
level: Level | None = None,
|
||||||
"""Create the root window and the flash card view.
|
) -> Application:
|
||||||
|
"""Create the window and show the level screen, or start at the given level.
|
||||||
|
|
||||||
@param data_dir Folder of the data files, or None for the default folder.
|
@param data_dir Folder of the data files, or None for the default folder
|
||||||
|
with the delivered word list as fallback.
|
||||||
@param speech_enabled False to run without spoken words.
|
@param speech_enabled False to run without spoken words.
|
||||||
@return The root window and the view built on it.
|
@param level The level to start at, or None to show the level screen.
|
||||||
@throws VocabularyError When the word list or saved progress is unusable.
|
@return The application, its window in the root attribute.
|
||||||
|
@throws VocabularyError When a level is given and the word list or saved
|
||||||
|
progress is unusable.
|
||||||
"""
|
"""
|
||||||
folder = DEFAULT_DATA_DIR if data_dir is None else data_dir
|
vocabulary_path, progress_path = resolve_data_files(data_dir)
|
||||||
repository = CsvVocabularyRepository(folder / VOCABULARY_FILE_NAME)
|
|
||||||
progress = CsvProgressRepository(folder / PROGRESS_FILE_NAME)
|
|
||||||
speech = build_speech(speech_enabled)
|
|
||||||
session = open_study_session(repository, progress, speech, random.choice)
|
|
||||||
view_model = FlashCardViewModel(session)
|
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
view = FlashCardView(root, view_model)
|
application = Application(
|
||||||
return root, view
|
root,
|
||||||
|
CsvVocabularyRepository(vocabulary_path),
|
||||||
|
CsvProgressRepository(progress_path),
|
||||||
|
build_speech(speech_enabled),
|
||||||
|
)
|
||||||
|
if level is None:
|
||||||
|
application.show_level_screen()
|
||||||
|
return application
|
||||||
|
try:
|
||||||
|
application.start(level)
|
||||||
|
except VocabularyError:
|
||||||
|
root.destroy()
|
||||||
|
raise
|
||||||
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
def _level_argument(value: str) -> Level:
|
||||||
|
"""Convert a --level value to a level.
|
||||||
|
|
||||||
|
@param value One of the keys of LEVEL_ARGUMENT_VALUES.
|
||||||
|
@return The level.
|
||||||
|
@throws argparse.ArgumentTypeError When the value is unknown.
|
||||||
|
"""
|
||||||
|
if value not in LEVEL_ARGUMENT_VALUES:
|
||||||
|
choices = ", ".join(LEVEL_ARGUMENT_VALUES)
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"invalid level {value!r} (choose from {choices})"
|
||||||
|
)
|
||||||
|
return Level(LEVEL_ARGUMENT_VALUES[value])
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Sequence[str] | None = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
@@ -87,22 +225,29 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--data-dir",
|
"--data-dir",
|
||||||
type=Path,
|
type=Path,
|
||||||
default=DEFAULT_DATA_DIR,
|
default=None,
|
||||||
help="folder of the data files",
|
help="folder of the data files (default: data next to the program)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-speech",
|
"--no-speech",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="do not speak the words",
|
help="do not speak the words",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--level",
|
||||||
|
type=_level_argument,
|
||||||
|
default=None,
|
||||||
|
metavar="{" + ",".join(LEVEL_ARGUMENT_VALUES) + "}",
|
||||||
|
help="start at a level (100, 1000 or all words) and skip the level screen",
|
||||||
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
try:
|
try:
|
||||||
root, _view = create_application(args.data_dir, not args.no_speech)
|
application = create_application(args.data_dir, not args.no_speech, args.level)
|
||||||
except VocabularyError as error:
|
except VocabularyError as error:
|
||||||
_logger.error("%s", error)
|
_logger.error("%s", error)
|
||||||
return 1
|
return 1
|
||||||
root.mainloop()
|
application.root.mainloop()
|
||||||
return 0
|
return application.exit_code
|
||||||
|
|
||||||
|
|
||||||
def run() -> NoReturn:
|
def run() -> NoReturn:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -20,8 +20,6 @@ from flash_card.constants import (
|
|||||||
LANGUAGE_FONT,
|
LANGUAGE_FONT,
|
||||||
LANGUAGE_TEXT_Y,
|
LANGUAGE_TEXT_Y,
|
||||||
RIGHT_IMAGE_NAME,
|
RIGHT_IMAGE_NAME,
|
||||||
WINDOW_PADDING,
|
|
||||||
WINDOW_TITLE,
|
|
||||||
WORD_FONT,
|
WORD_FONT,
|
||||||
WORD_TEXT_Y,
|
WORD_TEXT_Y,
|
||||||
WRONG_IMAGE_NAME,
|
WRONG_IMAGE_NAME,
|
||||||
@@ -30,6 +28,7 @@ from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
|||||||
FlashCardViewModel,
|
FlashCardViewModel,
|
||||||
Side,
|
Side,
|
||||||
)
|
)
|
||||||
|
from flash_card.presentation.shared.window import configure_window
|
||||||
|
|
||||||
|
|
||||||
def _load_image(root: tk.Tk, name: str) -> tk.PhotoImage:
|
def _load_image(root: tk.Tk, name: str) -> tk.PhotoImage:
|
||||||
@@ -60,8 +59,7 @@ class FlashCardView:
|
|||||||
self._root = root
|
self._root = root
|
||||||
## @brief The data source of the texts.
|
## @brief The data source of the texts.
|
||||||
self._view_model = view_model
|
self._view_model = view_model
|
||||||
root.title(WINDOW_TITLE)
|
configure_window(root)
|
||||||
root.config(padx=WINDOW_PADDING, pady=WINDOW_PADDING, bg=BACKGROUND_COLOR)
|
|
||||||
|
|
||||||
# Kept as attributes: Tkinter drops an image nothing refers to.
|
# Kept as attributes: Tkinter drops an image nothing refers to.
|
||||||
## @brief Card front image, kept so Tk does not drop it.
|
## @brief Card front image, kept so Tk does not drop it.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""The level feature: its view and view model."""
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""View of the level screen.
|
||||||
|
|
||||||
|
@file level_view.py
|
||||||
|
@brief Builds the level screen of the wireframe: a title and one button per level.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from flash_card.constants import (
|
||||||
|
BACKGROUND_COLOR,
|
||||||
|
CARD_WIDTH,
|
||||||
|
LEVEL_BUTTON_COLOR,
|
||||||
|
LEVEL_BUTTON_FONT,
|
||||||
|
LEVEL_BUTTON_WIDTH,
|
||||||
|
LEVEL_SCREEN_HEIGHT,
|
||||||
|
LEVEL_TITLE_FONT,
|
||||||
|
)
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
from flash_card.presentation.features.level.level_view_model import (
|
||||||
|
LevelChoice,
|
||||||
|
LevelViewModel,
|
||||||
|
)
|
||||||
|
from flash_card.presentation.shared.window import configure_window
|
||||||
|
|
||||||
|
|
||||||
|
class LevelView:
|
||||||
|
"""The level screen: a title and a button for each level."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
root: tk.Tk,
|
||||||
|
view_model: LevelViewModel,
|
||||||
|
on_choose: Callable[[Level], None],
|
||||||
|
) -> None:
|
||||||
|
"""Build the screen on a root.
|
||||||
|
|
||||||
|
@param root The Tk root window.
|
||||||
|
@param view_model The data to show.
|
||||||
|
@param on_choose Called with the level the user picked.
|
||||||
|
"""
|
||||||
|
## @brief Called with the level the user picked.
|
||||||
|
self._on_choose = on_choose
|
||||||
|
configure_window(root)
|
||||||
|
## @brief Frame that holds the whole screen.
|
||||||
|
self.frame = tk.Frame(
|
||||||
|
root, bg=BACKGROUND_COLOR, width=CARD_WIDTH, height=LEVEL_SCREEN_HEIGHT
|
||||||
|
)
|
||||||
|
self.frame.grid(row=0, column=0)
|
||||||
|
self.frame.grid_propagate(False)
|
||||||
|
self.frame.grid_columnconfigure(0, weight=1)
|
||||||
|
for row in range(len(view_model.choices) + 1):
|
||||||
|
self.frame.grid_rowconfigure(row, weight=1)
|
||||||
|
## @brief The title text.
|
||||||
|
self.title_label = tk.Label(
|
||||||
|
self.frame,
|
||||||
|
text=view_model.title,
|
||||||
|
font=LEVEL_TITLE_FONT,
|
||||||
|
bg=BACKGROUND_COLOR,
|
||||||
|
)
|
||||||
|
self.title_label.grid(row=0, column=0)
|
||||||
|
## @brief One button per choice, in display order.
|
||||||
|
self.buttons = tuple(
|
||||||
|
self._build_button(row, choice)
|
||||||
|
for row, choice in enumerate(view_model.choices, start=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
def choose(self, level: Level) -> None:
|
||||||
|
"""Forward the chosen level.
|
||||||
|
|
||||||
|
@param level The level the user picked.
|
||||||
|
@return Nothing.
|
||||||
|
"""
|
||||||
|
self._on_choose(level)
|
||||||
|
|
||||||
|
def destroy(self) -> None:
|
||||||
|
"""Remove the screen from the window.
|
||||||
|
|
||||||
|
@return Nothing.
|
||||||
|
"""
|
||||||
|
self.frame.destroy()
|
||||||
|
|
||||||
|
def _build_button(self, row: int, choice: LevelChoice) -> tk.Button:
|
||||||
|
"""Create the button of one choice.
|
||||||
|
|
||||||
|
@param row Grid row of the button.
|
||||||
|
@param choice The choice the button stands for.
|
||||||
|
@return The button, placed in the frame.
|
||||||
|
"""
|
||||||
|
button = tk.Button(
|
||||||
|
self.frame,
|
||||||
|
text=choice.label,
|
||||||
|
font=LEVEL_BUTTON_FONT,
|
||||||
|
width=LEVEL_BUTTON_WIDTH,
|
||||||
|
bg=LEVEL_BUTTON_COLOR,
|
||||||
|
command=lambda: self.choose(choice.level),
|
||||||
|
)
|
||||||
|
button.grid(row=row, column=0)
|
||||||
|
return button
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""View model of the level screen.
|
||||||
|
|
||||||
|
@file level_view_model.py
|
||||||
|
@brief Exposes the title and the level choices; free of any Tkinter import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from flash_card.constants import LEVEL_CHOICES, LEVEL_TITLE
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class LevelChoice:
|
||||||
|
"""One choice on the level screen."""
|
||||||
|
|
||||||
|
## @brief Text of the choice.
|
||||||
|
label: str
|
||||||
|
## @brief The level the choice stands for.
|
||||||
|
level: Level
|
||||||
|
|
||||||
|
|
||||||
|
class LevelViewModel:
|
||||||
|
"""Data the level view shows."""
|
||||||
|
|
||||||
|
def __init__(self, choices: Sequence[tuple[str, int | None]] | None = None) -> None:
|
||||||
|
"""Create the view model.
|
||||||
|
|
||||||
|
@param choices Label and word limit of each choice, or None for the
|
||||||
|
choices of the program.
|
||||||
|
"""
|
||||||
|
source = LEVEL_CHOICES if choices is None else choices
|
||||||
|
## @brief The choices in display order.
|
||||||
|
self._choices = tuple(
|
||||||
|
LevelChoice(label, Level(limit)) for label, limit in source
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self) -> str:
|
||||||
|
"""Return the title of the screen.
|
||||||
|
|
||||||
|
@return The title text.
|
||||||
|
"""
|
||||||
|
return LEVEL_TITLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def choices(self) -> tuple[LevelChoice, ...]:
|
||||||
|
"""Return the choices.
|
||||||
|
|
||||||
|
@return The choices in display order.
|
||||||
|
"""
|
||||||
|
return self._choices
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Code shared by the screens of the presentation layer."""
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"""Window set-up shared by the screens.
|
||||||
|
|
||||||
|
@file window.py
|
||||||
|
@brief Gives the root window its title, padding and background.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from flash_card.constants import BACKGROUND_COLOR, WINDOW_PADDING, WINDOW_TITLE
|
||||||
|
|
||||||
|
|
||||||
|
def configure_window(root: tk.Tk) -> None:
|
||||||
|
"""Set the title, padding and background of the window.
|
||||||
|
|
||||||
|
@param root The Tk root window.
|
||||||
|
@return Nothing.
|
||||||
|
"""
|
||||||
|
root.title(WINDOW_TITLE)
|
||||||
|
root.config(padx=WINDOW_PADDING, pady=WINDOW_PADDING, bg=BACKGROUND_COLOR)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Locate resources and the executable folder, frozen or from source.
|
||||||
|
|
||||||
|
@file runtime.py
|
||||||
|
@brief Packaging support: tells where bundled files and the executable are.
|
||||||
|
Imports only the standard library; it is not one of the layers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
## @brief Number of folders from this file up to the project root.
|
||||||
|
_ROOT_LEVELS = 2
|
||||||
|
|
||||||
|
|
||||||
|
def is_frozen() -> bool:
|
||||||
|
"""Tell whether the program runs as a PyInstaller executable.
|
||||||
|
|
||||||
|
@return True when frozen.
|
||||||
|
"""
|
||||||
|
return bool(getattr(sys, "frozen", False))
|
||||||
|
|
||||||
|
|
||||||
|
def source_root() -> Path:
|
||||||
|
"""Return the project root of a source run.
|
||||||
|
|
||||||
|
@return The folder that holds src.
|
||||||
|
"""
|
||||||
|
return Path(__file__).resolve().parents[_ROOT_LEVELS]
|
||||||
|
|
||||||
|
|
||||||
|
def pick_resource_root(frozen: bool, bundle_dir: Path | None, root: Path) -> Path:
|
||||||
|
"""Choose the folder that holds the bundled resources.
|
||||||
|
|
||||||
|
@param frozen True when running as an executable.
|
||||||
|
@param bundle_dir The unpack folder of the executable, when frozen.
|
||||||
|
@param root The project root of a source run.
|
||||||
|
@return The bundle folder when frozen and known, else the project root.
|
||||||
|
"""
|
||||||
|
if frozen and bundle_dir is not None:
|
||||||
|
return bundle_dir
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def pick_executable_dir(frozen: bool, executable: Path, root: Path) -> Path:
|
||||||
|
"""Choose the folder next to the executable.
|
||||||
|
|
||||||
|
@param frozen True when running as an executable.
|
||||||
|
@param executable Path of the running executable.
|
||||||
|
@param root The project root of a source run.
|
||||||
|
@return The folder of the executable when frozen, else the project root.
|
||||||
|
"""
|
||||||
|
if frozen:
|
||||||
|
return executable.parent
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def resource_root() -> Path:
|
||||||
|
"""Return the folder that holds the bundled resources.
|
||||||
|
|
||||||
|
@return The PyInstaller unpack folder when frozen, else the project root.
|
||||||
|
"""
|
||||||
|
bundle = getattr(sys, "_MEIPASS", None)
|
||||||
|
bundle_dir = Path(bundle) if bundle else None
|
||||||
|
return pick_resource_root(is_frozen(), bundle_dir, source_root())
|
||||||
|
|
||||||
|
|
||||||
|
def executable_dir() -> Path:
|
||||||
|
"""Return the folder of the running executable.
|
||||||
|
|
||||||
|
@return The folder of the executable when frozen, else the project root.
|
||||||
|
"""
|
||||||
|
return pick_executable_dir(is_frozen(), Path(sys.executable), source_root())
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Fixtures shared by the end-to-end tests."""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def tk_root() -> Iterator[tk.Tk]:
|
||||||
|
"""The one Tk root of the process, shared by every e2e module.
|
||||||
|
|
||||||
|
A second interpreter right after destroying the first fails to find
|
||||||
|
init.tcl on some Windows Python installs, so there is only one. Each test
|
||||||
|
builds its views on it and destroys their widgets afterwards.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
root = tk.Tk()
|
||||||
|
except tk.TclError:
|
||||||
|
pytest.skip("no display available for Tk")
|
||||||
|
yield root
|
||||||
|
root.destroy()
|
||||||
@@ -29,6 +29,7 @@ from flash_card.constants import (
|
|||||||
from flash_card.domain.entities.card import Card
|
from flash_card.domain.entities.card import Card
|
||||||
from flash_card.domain.entities.deck import Deck
|
from flash_card.domain.entities.deck import Deck
|
||||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||||
CsvProgressRepository,
|
CsvProgressRepository,
|
||||||
)
|
)
|
||||||
@@ -85,18 +86,20 @@ def _shown(view: FlashCardView) -> tuple[str, str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _destroy_view(root: tk.Tk, view: FlashCardView) -> None:
|
||||||
|
if view.flip_timer is not None:
|
||||||
|
root.after_cancel(view.flip_timer)
|
||||||
|
view.flip_timer = None
|
||||||
|
for widget in (view.canvas, view.wrong_button, view.right_button):
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def window() -> Iterator[tuple[tk.Tk, FlashCardView]]:
|
def window(tk_root: tk.Tk) -> Iterator[tuple[tk.Tk, FlashCardView]]:
|
||||||
# One Tk per module: a second interpreter right after destroying the
|
view = _build_view(tk_root)
|
||||||
# first fails to find init.tcl on some Windows Python installs.
|
tk_root.update_idletasks()
|
||||||
try:
|
yield tk_root, view
|
||||||
root = tk.Tk()
|
_destroy_view(tk_root, view)
|
||||||
except tk.TclError:
|
|
||||||
pytest.skip("no display available for Tk")
|
|
||||||
view = _build_view(root)
|
|
||||||
root.update_idletasks()
|
|
||||||
yield root, view
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
def test_window_has_title_background_and_padding(
|
def test_window_has_title_background_and_padding(
|
||||||
@@ -306,6 +309,7 @@ def files_view(
|
|||||||
CsvProgressRepository(progress),
|
CsvProgressRepository(progress),
|
||||||
NullSpeech(),
|
NullSpeech(),
|
||||||
_first,
|
_first,
|
||||||
|
Level(None),
|
||||||
)
|
)
|
||||||
view = FlashCardView(root, FlashCardViewModel(session))
|
view = FlashCardView(root, FlashCardViewModel(session))
|
||||||
yield view, progress
|
yield view, progress
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
"""End-to-end tests of the level screen and the flow to the first card.
|
||||||
|
|
||||||
|
@file test_level_window.py
|
||||||
|
@brief Builds the real level screen with Tk and checks it against the wireframe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import tkinter as tk
|
||||||
|
from collections.abc import Iterator, Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flash_card.constants import (
|
||||||
|
BACKGROUND_COLOR,
|
||||||
|
CARD_WIDTH,
|
||||||
|
DEFAULT_DATA_DIR,
|
||||||
|
FINISHED_MESSAGE,
|
||||||
|
LEVEL_BUTTON_COLOR,
|
||||||
|
LEVEL_BUTTON_FONT,
|
||||||
|
LEVEL_SCREEN_HEIGHT,
|
||||||
|
LEVEL_TITLE,
|
||||||
|
LEVEL_TITLE_FONT,
|
||||||
|
PROGRESS_FILE_NAME,
|
||||||
|
VOCABULARY_FILE_NAME,
|
||||||
|
WINDOW_PADDING,
|
||||||
|
WINDOW_TITLE,
|
||||||
|
)
|
||||||
|
from flash_card.domain.entities.card import Card
|
||||||
|
from flash_card.domain.entities.deck import Deck
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||||
|
CsvProgressRepository,
|
||||||
|
)
|
||||||
|
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
|
||||||
|
CsvVocabularyRepository,
|
||||||
|
)
|
||||||
|
from flash_card.infrastructure.speech.null_speech import NullSpeech
|
||||||
|
from flash_card.presentation import app
|
||||||
|
from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView
|
||||||
|
from flash_card.presentation.features.level.level_view import LevelView
|
||||||
|
from flash_card.presentation.features.level.level_view_model import LevelViewModel
|
||||||
|
|
||||||
|
|
||||||
|
def _last(cards: Sequence[Card]) -> Card:
|
||||||
|
return cards[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _label(button: tk.Button) -> str:
|
||||||
|
return str(button.cget("text"))
|
||||||
|
|
||||||
|
|
||||||
|
def _word(view: FlashCardView) -> str:
|
||||||
|
return str(view.canvas.itemcget(view.word_text_id, "text"))
|
||||||
|
|
||||||
|
|
||||||
|
def _clear(root: tk.Tk, view: object) -> None:
|
||||||
|
"""Destroy the widgets of whichever screen is shown, and pending timers."""
|
||||||
|
if isinstance(view, FlashCardView):
|
||||||
|
if view.flip_timer is not None:
|
||||||
|
root.after_cancel(view.flip_timer)
|
||||||
|
view.flip_timer = None
|
||||||
|
for widget in (view.canvas, view.wrong_button, view.right_button):
|
||||||
|
widget.destroy()
|
||||||
|
elif isinstance(view, LevelView):
|
||||||
|
view.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data_dir(tmp_path: Path) -> Path:
|
||||||
|
shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, tmp_path)
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def application(tk_root: tk.Tk, data_dir: Path) -> Iterator[app.Application]:
|
||||||
|
instance = app.Application(
|
||||||
|
tk_root,
|
||||||
|
CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME),
|
||||||
|
CsvProgressRepository(data_dir / PROGRESS_FILE_NAME),
|
||||||
|
NullSpeech(),
|
||||||
|
_last,
|
||||||
|
)
|
||||||
|
yield instance
|
||||||
|
_clear(tk_root, instance.view)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def level_view(tk_root: tk.Tk) -> Iterator[LevelView]:
|
||||||
|
chosen: list[Level] = []
|
||||||
|
view = LevelView(tk_root, LevelViewModel(), chosen.append)
|
||||||
|
tk_root.update_idletasks()
|
||||||
|
yield view
|
||||||
|
view.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_screen_shows_title_with_font_on_window_background(
|
||||||
|
level_view: LevelView,
|
||||||
|
) -> None:
|
||||||
|
label = level_view.title_label
|
||||||
|
assert label.cget("text") == LEVEL_TITLE == "Choose your level"
|
||||||
|
assert label.cget("bg") == BACKGROUND_COLOR
|
||||||
|
family, size, style = LEVEL_TITLE_FONT
|
||||||
|
assert str(label.cget("font")).split() == [family, str(size), style]
|
||||||
|
assert level_view.frame.cget("bg") == BACKGROUND_COLOR
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_screen_sets_window_title_background_and_padding(
|
||||||
|
tk_root: tk.Tk,
|
||||||
|
) -> None:
|
||||||
|
tk_root.title("tk")
|
||||||
|
tk_root.config(padx=0, pady=0, bg="white")
|
||||||
|
|
||||||
|
view = LevelView(tk_root, LevelViewModel(), lambda level: None)
|
||||||
|
try:
|
||||||
|
assert tk_root.title() == WINDOW_TITLE
|
||||||
|
assert tk_root.cget("bg") == BACKGROUND_COLOR
|
||||||
|
assert int(str(tk_root.cget("padx"))) == WINDOW_PADDING
|
||||||
|
assert int(str(tk_root.cget("pady"))) == WINDOW_PADDING
|
||||||
|
finally:
|
||||||
|
view.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_screen_frame_has_the_card_window_size(level_view: LevelView) -> None:
|
||||||
|
assert level_view.frame.winfo_width() == CARD_WIDTH
|
||||||
|
assert level_view.frame.winfo_height() == LEVEL_SCREEN_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_screen_shows_three_buttons_in_order(level_view: LevelView) -> None:
|
||||||
|
labels = [_label(button) for button in level_view.buttons]
|
||||||
|
assert labels == ["100 words", "1000 words", "All words"]
|
||||||
|
rows = [int(button.grid_info()["row"]) for button in level_view.buttons]
|
||||||
|
assert rows == [1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_buttons_have_equal_width_font_and_colour(
|
||||||
|
level_view: LevelView,
|
||||||
|
) -> None:
|
||||||
|
widths = {int(str(button.cget("width"))) for button in level_view.buttons}
|
||||||
|
assert len(widths) == 1
|
||||||
|
family, size, style = LEVEL_BUTTON_FONT
|
||||||
|
for button in level_view.buttons:
|
||||||
|
assert str(button.cget("font")).split() == [family, str(size), style]
|
||||||
|
assert button.cget("bg") == LEVEL_BUTTON_COLOR
|
||||||
|
assert len({button.winfo_reqwidth() for button in level_view.buttons}) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_clicking_a_button_forwards_its_level(tk_root: tk.Tk) -> None:
|
||||||
|
chosen: list[Level] = []
|
||||||
|
view = LevelView(tk_root, LevelViewModel(), chosen.append)
|
||||||
|
for button in view.buttons:
|
||||||
|
button.invoke()
|
||||||
|
view.destroy()
|
||||||
|
assert chosen == [Level(100), Level(1000), Level(None)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_destroy_removes_the_level_frame(tk_root: tk.Tk) -> None:
|
||||||
|
view = LevelView(tk_root, LevelViewModel(), lambda level: None)
|
||||||
|
view.destroy()
|
||||||
|
assert not view.frame.winfo_exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_clicking_100_words_replaces_screen_with_first_card_of_first_hundred(
|
||||||
|
tk_root: tk.Tk, application: app.Application, data_dir: Path
|
||||||
|
) -> None:
|
||||||
|
first_hundred = (
|
||||||
|
CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().cards[:100]
|
||||||
|
)
|
||||||
|
application.show_level_screen()
|
||||||
|
level_view = application.view
|
||||||
|
assert isinstance(level_view, LevelView)
|
||||||
|
|
||||||
|
level_view.buttons[0].invoke()
|
||||||
|
|
||||||
|
card_view = application.view
|
||||||
|
assert isinstance(card_view, FlashCardView)
|
||||||
|
assert not level_view.frame.winfo_exists()
|
||||||
|
assert _word(card_view) == first_hundred[-1].front
|
||||||
|
assert tk_root.title() == WINDOW_TITLE
|
||||||
|
assert int(str(tk_root.cget("padx"))) == WINDOW_PADDING
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_whose_words_are_all_known_shows_the_finished_message(
|
||||||
|
application: app.Application, data_dir: Path
|
||||||
|
) -> None:
|
||||||
|
cards = CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().cards
|
||||||
|
pair = CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().language_pair
|
||||||
|
CsvProgressRepository(data_dir / PROGRESS_FILE_NAME).save(Deck(pair, cards[100:]))
|
||||||
|
application.show_level_screen()
|
||||||
|
level_view = application.view
|
||||||
|
assert isinstance(level_view, LevelView)
|
||||||
|
|
||||||
|
level_view.buttons[0].invoke()
|
||||||
|
|
||||||
|
card_view = application.view
|
||||||
|
assert isinstance(card_view, FlashCardView)
|
||||||
|
assert _word(card_view) == FINISHED_MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_application_with_level_skips_the_level_screen(
|
||||||
|
tk_root: tk.Tk, data_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(tk, "Tk", lambda: tk_root)
|
||||||
|
|
||||||
|
application = app.create_application(data_dir, False, Level(100))
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert isinstance(application.view, FlashCardView)
|
||||||
|
assert not [c for c in tk_root.winfo_children() if isinstance(c, tk.Frame)]
|
||||||
|
finally:
|
||||||
|
_clear(tk_root, application.view)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_application_without_level_shows_the_level_screen(
|
||||||
|
tk_root: tk.Tk, data_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(tk, "Tk", lambda: tk_root)
|
||||||
|
|
||||||
|
application = app.create_application(data_dir, False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert isinstance(application.view, LevelView)
|
||||||
|
assert application.root is tk_root
|
||||||
|
finally:
|
||||||
|
_clear(tk_root, application.view)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Integration tests of the levels over the real word list and a progress file."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||||
|
from flash_card.application.use_cases.study_session import StudySession
|
||||||
|
from flash_card.constants import (
|
||||||
|
DEFAULT_DATA_DIR,
|
||||||
|
PROGRESS_FILE_NAME,
|
||||||
|
VOCABULARY_FILE_NAME,
|
||||||
|
)
|
||||||
|
from flash_card.domain.entities.card import Card
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||||
|
CsvProgressRepository,
|
||||||
|
)
|
||||||
|
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
|
||||||
|
CsvVocabularyRepository,
|
||||||
|
)
|
||||||
|
from flash_card.infrastructure.speech.null_speech import NullSpeech
|
||||||
|
|
||||||
|
VOCABULARY = CsvVocabularyRepository(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME)
|
||||||
|
ALL_CARDS = VOCABULARY.load().cards
|
||||||
|
FIRST_HUNDRED = ALL_CARDS[:100]
|
||||||
|
|
||||||
|
Choose = Callable[[Sequence[Card]], Card]
|
||||||
|
|
||||||
|
|
||||||
|
def _first(cards: Sequence[Card]) -> Card:
|
||||||
|
return cards[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _open(folder: Path, level: Level, choose: Choose = _first) -> StudySession:
|
||||||
|
return open_study_session(
|
||||||
|
VOCABULARY,
|
||||||
|
CsvProgressRepository(folder / PROGRESS_FILE_NAME),
|
||||||
|
NullSpeech(),
|
||||||
|
choose,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _learn_to_the_end(session: StudySession) -> list[Card]:
|
||||||
|
"""Mark every drawn card known until the session is finished."""
|
||||||
|
drawn: list[Card] = []
|
||||||
|
while (card := session.next_card()) is not None:
|
||||||
|
drawn.append(Card(card.front_text, card.back_text))
|
||||||
|
session.mark_known()
|
||||||
|
return drawn
|
||||||
|
|
||||||
|
|
||||||
|
def _saved(folder: Path) -> tuple[Card, ...]:
|
||||||
|
loaded = CsvProgressRepository(folder / PROGRESS_FILE_NAME).load()
|
||||||
|
assert loaded is not None
|
||||||
|
return loaded.cards
|
||||||
|
|
||||||
|
|
||||||
|
def test_word_list_has_one_thousand_distinct_words() -> None:
|
||||||
|
assert len(ALL_CARDS) == 1000
|
||||||
|
assert len(set(ALL_CARDS)) == 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_100_draws_exactly_the_first_hundred_words(tmp_path: Path) -> None:
|
||||||
|
drawn = _learn_to_the_end(_open(tmp_path, Level(100)))
|
||||||
|
assert len(drawn) == 100
|
||||||
|
assert len(set(drawn)) == 100
|
||||||
|
assert set(drawn) == set(FIRST_HUNDRED)
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_100_keeps_the_other_words_in_order_in_the_file(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
_learn_to_the_end(_open(tmp_path, Level(100)))
|
||||||
|
assert _saved(tmp_path) == ALL_CARDS[100:]
|
||||||
|
assert len(_saved(tmp_path)) == 900
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_all_after_level_100_draws_only_the_other_words(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
_learn_to_the_end(_open(tmp_path, Level(100)))
|
||||||
|
drawn = _learn_to_the_end(_open(tmp_path, Level(None)))
|
||||||
|
assert len(drawn) == 900
|
||||||
|
assert set(drawn) == set(ALL_CARDS[100:])
|
||||||
|
assert not set(drawn) & set(FIRST_HUNDRED)
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_1000_equals_all_words_for_this_list(tmp_path: Path) -> None:
|
||||||
|
session_1000 = _open(tmp_path, Level(1000))
|
||||||
|
session_all = _open(tmp_path, Level(None))
|
||||||
|
assert session_1000.remaining_in_level == 1000
|
||||||
|
assert session_all.remaining_in_level == 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_words_known_at_level_100_stay_known_at_level_1000(tmp_path: Path) -> None:
|
||||||
|
first = _open(tmp_path, Level(100))
|
||||||
|
shown = first.next_card()
|
||||||
|
assert shown is not None
|
||||||
|
first.mark_known()
|
||||||
|
session = _open(tmp_path, Level(1000))
|
||||||
|
assert session.remaining == 999
|
||||||
|
assert session.remaining_in_level == 999
|
||||||
|
|
||||||
|
|
||||||
|
def test_marking_known_at_level_100_keeps_words_outside_the_level(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
session = _open(tmp_path, Level(100))
|
||||||
|
session.next_card()
|
||||||
|
session.mark_known()
|
||||||
|
saved = _saved(tmp_path)
|
||||||
|
assert len(saved) == 999
|
||||||
|
assert saved[-900:] == ALL_CARDS[100:]
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_whose_words_are_all_known_is_finished_at_once(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
_learn_to_the_end(_open(tmp_path, Level(100)))
|
||||||
|
session = _open(tmp_path, Level(100))
|
||||||
|
assert session.remaining_in_level == 0
|
||||||
|
assert session.remaining == 900
|
||||||
|
assert session.next_card() is None
|
||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||||
from flash_card.application.use_cases.study_session import StudySession
|
from flash_card.application.use_cases.study_session import StudySession
|
||||||
from flash_card.domain.entities.card import Card
|
from flash_card.domain.entities.card import Card
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||||
CsvProgressRepository,
|
CsvProgressRepository,
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,7 @@ def _open(folder: Path, choose: Choose = _first) -> StudySession:
|
|||||||
CsvProgressRepository(folder / "words_to_learn.csv"),
|
CsvProgressRepository(folder / "words_to_learn.csv"),
|
||||||
NullSpeech(),
|
NullSpeech(),
|
||||||
choose,
|
choose,
|
||||||
|
Level(None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from flash_card.constants import (
|
|||||||
)
|
)
|
||||||
from flash_card.domain.entities.card import Card
|
from flash_card.domain.entities.card import Card
|
||||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
from flash_card.infrastructure.persistence.csv_progress_repository import (
|
||||||
CsvProgressRepository,
|
CsvProgressRepository,
|
||||||
)
|
)
|
||||||
@@ -34,7 +35,11 @@ def test_open_study_session_yields_card_from_real_file(tmp_path: Path) -> None:
|
|||||||
cards = repository.load().cards
|
cards = repository.load().cards
|
||||||
progress = CsvProgressRepository(tmp_path / PROGRESS_FILE_NAME)
|
progress = CsvProgressRepository(tmp_path / PROGRESS_FILE_NAME)
|
||||||
session = open_study_session(
|
session = open_study_session(
|
||||||
repository, progress, NullSpeech(), lambda candidates: candidates[0]
|
repository,
|
||||||
|
progress,
|
||||||
|
NullSpeech(),
|
||||||
|
lambda candidates: candidates[0],
|
||||||
|
Level(None),
|
||||||
)
|
)
|
||||||
|
|
||||||
card = session.next_card()
|
card = session.next_card()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from flash_card.domain.entities.deck import Deck
|
|||||||
from flash_card.domain.entities.vocabulary import Vocabulary
|
from flash_card.domain.entities.vocabulary import Vocabulary
|
||||||
from flash_card.domain.errors.vocabulary_errors import VocabularyNotFoundError
|
from flash_card.domain.errors.vocabulary_errors import VocabularyNotFoundError
|
||||||
from flash_card.domain.value_objects.language_pair import LanguagePair
|
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
|
||||||
|
|
||||||
class _FakeRepository:
|
class _FakeRepository:
|
||||||
@@ -52,7 +53,7 @@ def test_open_study_session_builds_session_over_all_cards() -> None:
|
|||||||
repository = _FakeRepository(Vocabulary(LanguagePair("ms", "da"), cards))
|
repository = _FakeRepository(Vocabulary(LanguagePair("ms", "da"), cards))
|
||||||
|
|
||||||
session = open_study_session(
|
session = open_study_session(
|
||||||
repository, _RecordingProgress(), _SilentSpeech(), _first
|
repository, _RecordingProgress(), _SilentSpeech(), _first, Level(None)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert session.remaining == 2
|
assert session.remaining == 2
|
||||||
@@ -62,7 +63,11 @@ def test_open_study_session_builds_session_over_all_cards() -> None:
|
|||||||
def test_open_study_session_propagates_repository_error() -> None:
|
def test_open_study_session_propagates_repository_error() -> None:
|
||||||
with pytest.raises(VocabularyNotFoundError):
|
with pytest.raises(VocabularyNotFoundError):
|
||||||
open_study_session(
|
open_study_session(
|
||||||
_MissingRepository(), _RecordingProgress(), _SilentSpeech(), _first
|
_MissingRepository(),
|
||||||
|
_RecordingProgress(),
|
||||||
|
_SilentSpeech(),
|
||||||
|
_first,
|
||||||
|
Level(None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -72,21 +77,83 @@ def test_open_study_session_prefers_saved_progress() -> None:
|
|||||||
progress = _RecordingProgress(Deck(pair, [Card("kau", "du")]))
|
progress = _RecordingProgress(Deck(pair, [Card("kau", "du")]))
|
||||||
|
|
||||||
session = open_study_session(
|
session = open_study_session(
|
||||||
_FakeRepository(vocabulary), progress, _SilentSpeech(), _first
|
_FakeRepository(vocabulary), progress, _SilentSpeech(), _first, Level(None)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert session.remaining == 1
|
assert session.remaining == 1
|
||||||
assert session.next_card() == CardDto("kau", "du", "ms", "da")
|
assert session.next_card() == CardDto("kau", "du", "ms", "da")
|
||||||
|
|
||||||
|
|
||||||
def test_open_study_session_skips_vocabulary_when_progress_exists() -> None:
|
PAIR = LanguagePair("ms", "da")
|
||||||
progress = _RecordingProgress(Deck(LanguagePair("ms", "da"), []))
|
THREE = (Card("aku", "mig"), Card("kau", "du"), Card("ini", "denne"))
|
||||||
|
|
||||||
|
|
||||||
|
def _last(cards: Sequence[Card]) -> Card:
|
||||||
|
return cards[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_study_session_propagates_error_when_list_is_missing() -> None:
|
||||||
|
progress = _RecordingProgress(Deck(PAIR, []))
|
||||||
|
|
||||||
|
with pytest.raises(VocabularyNotFoundError):
|
||||||
|
open_study_session(
|
||||||
|
_MissingRepository(), progress, _SilentSpeech(), _first, Level(None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_study_session_limits_draws_to_first_cards_of_level() -> None:
|
||||||
session = open_study_session(
|
session = open_study_session(
|
||||||
_MissingRepository(), progress, _SilentSpeech(), _first
|
_FakeRepository(Vocabulary(PAIR, THREE)),
|
||||||
|
_RecordingProgress(),
|
||||||
|
_SilentSpeech(),
|
||||||
|
_last,
|
||||||
|
Level(2),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert session.remaining == 0
|
assert session.remaining == 3
|
||||||
|
assert session.remaining_in_level == 2
|
||||||
|
assert session.next_card() == CardDto("kau", "du", "ms", "da")
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_study_session_uses_vocabulary_order_with_saved_progress() -> None:
|
||||||
|
progress = _RecordingProgress(Deck(PAIR, [THREE[2], THREE[1]]))
|
||||||
|
session = open_study_session(
|
||||||
|
_FakeRepository(Vocabulary(PAIR, THREE)),
|
||||||
|
progress,
|
||||||
|
_SilentSpeech(),
|
||||||
|
_first,
|
||||||
|
Level(2),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert session.remaining == 2
|
||||||
|
assert session.remaining_in_level == 1
|
||||||
|
assert session.next_card() == CardDto("kau", "du", "ms", "da")
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_study_session_is_finished_when_level_has_nothing_left() -> None:
|
||||||
|
progress = _RecordingProgress(Deck(PAIR, [THREE[1]]))
|
||||||
|
session = open_study_session(
|
||||||
|
_FakeRepository(Vocabulary(PAIR, THREE)),
|
||||||
|
progress,
|
||||||
|
_SilentSpeech(),
|
||||||
|
_first,
|
||||||
|
Level(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert session.next_card() is None
|
||||||
|
assert session.remaining == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_study_session_uses_all_cards_when_level_exceeds_list() -> None:
|
||||||
|
session = open_study_session(
|
||||||
|
_FakeRepository(Vocabulary(PAIR, THREE)),
|
||||||
|
_RecordingProgress(),
|
||||||
|
_SilentSpeech(),
|
||||||
|
_first,
|
||||||
|
Level(1000),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert session.remaining_in_level == 3
|
||||||
|
|
||||||
|
|
||||||
def test_open_study_session_never_saves_on_start() -> None:
|
def test_open_study_session_never_saves_on_start() -> None:
|
||||||
@@ -94,7 +161,7 @@ def test_open_study_session_never_saves_on_start() -> None:
|
|||||||
progress = _RecordingProgress()
|
progress = _RecordingProgress()
|
||||||
|
|
||||||
session = open_study_session(
|
session = open_study_session(
|
||||||
_FakeRepository(vocabulary), progress, _SilentSpeech(), _first
|
_FakeRepository(vocabulary), progress, _SilentSpeech(), _first, Level(None)
|
||||||
)
|
)
|
||||||
session.next_card()
|
session.next_card()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Tests of the StudySession use case."""
|
"""Tests of the StudySession use case."""
|
||||||
|
|
||||||
from collections.abc import Sequence
|
import itertools
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
|
||||||
from flash_card.application.dto.card_dto import CardDto
|
from flash_card.application.dto.card_dto import CardDto
|
||||||
from flash_card.application.use_cases.study_session import StudySession
|
from flash_card.application.use_cases.study_session import StudySession
|
||||||
@@ -150,3 +151,94 @@ def test_mark_known_does_not_speak_but_following_draw_does() -> None:
|
|||||||
assert speech.spoken == [("aku", "ms")]
|
assert speech.spoken == [("aku", "ms")]
|
||||||
session.next_card()
|
session.next_card()
|
||||||
assert speech.spoken == [("aku", "ms"), ("kau", "ms")]
|
assert speech.spoken == [("aku", "ms"), ("kau", "ms")]
|
||||||
|
|
||||||
|
|
||||||
|
C = Card("ini", "denne")
|
||||||
|
D = Card("itu", "den")
|
||||||
|
|
||||||
|
|
||||||
|
def _level_session(
|
||||||
|
deck_cards: Sequence[Card],
|
||||||
|
level_cards: Sequence[Card] | None,
|
||||||
|
choose: Callable[[Sequence[Card]], Card] = _first,
|
||||||
|
progress: _RecordingProgress | None = None,
|
||||||
|
speech: _RecordingSpeech | None = None,
|
||||||
|
) -> StudySession:
|
||||||
|
store = _RecordingProgress() if progress is None else progress
|
||||||
|
voice = _RecordingSpeech() if speech is None else speech
|
||||||
|
return StudySession(Deck(PAIR, deck_cards), choose, store, voice, level_cards)
|
||||||
|
|
||||||
|
|
||||||
|
def _last(cards: Sequence[Card]) -> Card:
|
||||||
|
return cards[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_card_draws_only_cards_of_the_level_over_many_draws() -> None:
|
||||||
|
chooser = itertools.cycle([_first, _last])
|
||||||
|
session = _level_session(
|
||||||
|
[A, B, C, D], [A, B], choose=lambda cards: next(chooser)(cards)
|
||||||
|
)
|
||||||
|
drawn = {session.next_card() for _ in range(40)}
|
||||||
|
assert drawn == {
|
||||||
|
CardDto("aku", "mig", "ms", "da"),
|
||||||
|
CardDto("kau", "du", "ms", "da"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_card_never_draws_a_card_outside_the_level() -> None:
|
||||||
|
session = _level_session([A, B, C], [B], choose=_last)
|
||||||
|
for _ in range(5):
|
||||||
|
assert session.next_card() == CardDto("kau", "du", "ms", "da")
|
||||||
|
|
||||||
|
|
||||||
|
def test_remaining_counts_whole_deck_and_remaining_in_level_counts_level() -> None:
|
||||||
|
session = _level_session([A, B, C], [A, B])
|
||||||
|
assert session.remaining == 3
|
||||||
|
assert session.remaining_in_level == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_remaining_in_level_counts_whole_deck_without_level() -> None:
|
||||||
|
assert _level_session([A, B, C], None).remaining_in_level == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_remaining_in_level_ignores_level_cards_missing_from_deck() -> None:
|
||||||
|
assert _level_session([B], [A, B]).remaining_in_level == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_mark_known_keeps_cards_outside_the_level_and_saves_whole_deck() -> None:
|
||||||
|
progress = _RecordingProgress()
|
||||||
|
session = _level_session([A, B, C], [A], progress=progress)
|
||||||
|
session.next_card()
|
||||||
|
session.mark_known()
|
||||||
|
assert progress.saved == [(B, C)]
|
||||||
|
assert session.remaining == 2
|
||||||
|
assert session.remaining_in_level == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_card_is_none_when_nothing_of_the_level_is_left() -> None:
|
||||||
|
session = _level_session([B, C], [A])
|
||||||
|
assert session.next_card() is None
|
||||||
|
assert session.current_card is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_finishes_after_last_card_of_level_is_known() -> None:
|
||||||
|
session = _level_session([A, B], [A])
|
||||||
|
session.next_card()
|
||||||
|
session.mark_known()
|
||||||
|
assert session.next_card() is None
|
||||||
|
assert session.remaining == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_card_speaks_only_cards_of_the_level() -> None:
|
||||||
|
speech = _RecordingSpeech()
|
||||||
|
session = _level_session([A, B, C], [B], choose=_last, speech=speech)
|
||||||
|
session.next_card()
|
||||||
|
session.next_card()
|
||||||
|
assert speech.spoken == [("kau", "ms"), ("kau", "ms")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_card_speaks_nothing_when_level_is_finished() -> None:
|
||||||
|
speech = _RecordingSpeech()
|
||||||
|
session = _level_session([B], [A], speech=speech)
|
||||||
|
session.next_card()
|
||||||
|
assert speech.spoken == []
|
||||||
|
|||||||
@@ -71,3 +71,47 @@ def test_remove_raises_when_card_is_absent() -> None:
|
|||||||
def test_is_empty_reflects_cards() -> None:
|
def test_is_empty_reflects_cards() -> None:
|
||||||
assert Deck(PAIR, []).is_empty
|
assert Deck(PAIR, []).is_empty
|
||||||
assert not Deck(PAIR, [A]).is_empty
|
assert not Deck(PAIR, [A]).is_empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_within_returns_deck_cards_found_in_given_cards_in_deck_order() -> None:
|
||||||
|
c = Card("ini", "denne")
|
||||||
|
deck = Deck(PAIR, [A, B, c])
|
||||||
|
assert deck.within([c, A]) == (A, c)
|
||||||
|
|
||||||
|
|
||||||
|
def test_within_respects_multiplicity() -> None:
|
||||||
|
deck = Deck(PAIR, [A, A, B])
|
||||||
|
assert deck.within([A, B]) == (A, B)
|
||||||
|
assert deck.within([A, A, A]) == (A, A)
|
||||||
|
|
||||||
|
|
||||||
|
def test_within_returns_empty_when_no_card_matches() -> None:
|
||||||
|
assert Deck(PAIR, [A]).within([B]) == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_within_does_not_change_the_deck() -> None:
|
||||||
|
deck = Deck(PAIR, [A, B])
|
||||||
|
deck.within([A])
|
||||||
|
assert deck.cards == (A, B)
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_chooses_among_given_candidates_only() -> None:
|
||||||
|
deck = Deck(PAIR, [A, B])
|
||||||
|
assert deck.draw(_first, among=[B]) == B
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_among_avoids_excluded_when_another_candidate_exists() -> None:
|
||||||
|
c = Card("ini", "denne")
|
||||||
|
deck = Deck(PAIR, [A, B, c])
|
||||||
|
assert deck.draw(_first, exclude=B, among=[B, c]) == c
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_among_returns_excluded_when_it_is_the_only_candidate() -> None:
|
||||||
|
deck = Deck(PAIR, [A, B])
|
||||||
|
assert deck.draw(_first, exclude=B, among=[B]) == B
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_among_empty_raises_empty_deck_error() -> None:
|
||||||
|
deck = Deck(PAIR, [A, B])
|
||||||
|
with pytest.raises(EmptyDeckError):
|
||||||
|
deck.draw(_first, among=[])
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Tests of the Level value object."""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flash_card.domain.entities.card import Card
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
|
||||||
|
CARDS = tuple(Card(f"w{i}", f"v{i}") for i in range(1000))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("limit", [0, -1, -100])
|
||||||
|
def test_level_refuses_limit_not_above_zero(limit: int) -> None:
|
||||||
|
with pytest.raises(ValueError, match="above zero"):
|
||||||
|
Level(limit)
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_accepts_no_limit() -> None:
|
||||||
|
assert Level(None).limit is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_level_is_frozen() -> None:
|
||||||
|
level = Level(100)
|
||||||
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||||
|
level.limit = 5 # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_returns_first_hundred_cards_when_limit_is_100() -> None:
|
||||||
|
assert Level(100).first(CARDS) == CARDS[:100]
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_returns_all_cards_when_there_is_no_limit() -> None:
|
||||||
|
assert Level(None).first(CARDS) == CARDS
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_returns_all_cards_when_limit_exceeds_the_list() -> None:
|
||||||
|
assert Level(5000).first(CARDS) == CARDS
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_returns_empty_when_list_is_empty() -> None:
|
||||||
|
assert Level(100).first(()) == ()
|
||||||
+254
-9
@@ -1,45 +1,290 @@
|
|||||||
"""Tests of the entry point.
|
"""Tests of the entry point.
|
||||||
|
|
||||||
@file test_app.py
|
@file test_app.py
|
||||||
@brief Checks main of flash_card.presentation.app without opening a window.
|
@brief Checks main and Application of flash_card.presentation.app without Tk.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from flash_card.domain.entities.card import Card
|
||||||
|
from flash_card.domain.entities.deck import Deck
|
||||||
|
from flash_card.domain.entities.vocabulary import Vocabulary
|
||||||
|
from flash_card.domain.errors.vocabulary_errors import VocabularyNotFoundError
|
||||||
|
from flash_card.domain.value_objects.language_pair import LanguagePair
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
from flash_card.presentation import app
|
from flash_card.presentation import app
|
||||||
|
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
||||||
|
FlashCardViewModel,
|
||||||
|
)
|
||||||
|
from flash_card.presentation.features.level.level_view_model import LevelViewModel
|
||||||
|
|
||||||
|
PAIR = LanguagePair("ms", "da")
|
||||||
|
CARDS = (Card("aku", "mig"), Card("kau", "du"), Card("ini", "denne"))
|
||||||
|
|
||||||
|
|
||||||
class _FakeRoot:
|
class _FakeRoot:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.mainloop_calls = 0
|
self.mainloop_calls = 0
|
||||||
|
self.destroyed = False
|
||||||
|
|
||||||
def mainloop(self) -> None:
|
def mainloop(self) -> None:
|
||||||
self.mainloop_calls += 1
|
self.mainloop_calls += 1
|
||||||
|
|
||||||
|
def destroy(self) -> None:
|
||||||
|
self.destroyed = True
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeApplication:
|
||||||
|
def __init__(self, root: _FakeRoot, exit_code: int = 0) -> None:
|
||||||
|
self.root = root
|
||||||
|
self.exit_code = exit_code
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeLevelView:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
root: object,
|
||||||
|
view_model: LevelViewModel,
|
||||||
|
on_choose: Callable[[Level], None],
|
||||||
|
) -> None:
|
||||||
|
self.view_model = view_model
|
||||||
|
self.on_choose = on_choose
|
||||||
|
self.destroyed = False
|
||||||
|
|
||||||
|
def destroy(self) -> None:
|
||||||
|
self.destroyed = True
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeFlashCardView:
|
||||||
|
def __init__(self, root: object, view_model: FlashCardViewModel) -> None:
|
||||||
|
self.view_model = view_model
|
||||||
|
|
||||||
|
|
||||||
|
class _Vocabulary:
|
||||||
|
def __init__(self, error: bool = False) -> None:
|
||||||
|
self._error = error
|
||||||
|
|
||||||
|
def load(self) -> Vocabulary:
|
||||||
|
if self._error:
|
||||||
|
raise VocabularyNotFoundError("word list not found")
|
||||||
|
return Vocabulary(PAIR, CARDS)
|
||||||
|
|
||||||
|
|
||||||
|
class _Progress:
|
||||||
|
def load(self) -> Deck | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save(self, deck: Deck) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _Speech:
|
||||||
|
def speak(self, text: str, language_code: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _first(cards: Sequence[Card]) -> Card:
|
||||||
|
return cards[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_views(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(app, "LevelView", _FakeLevelView)
|
||||||
|
monkeypatch.setattr(app, "FlashCardView", _FakeFlashCardView)
|
||||||
|
|
||||||
|
|
||||||
|
def _application(root: _FakeRoot, error: bool = False) -> app.Application:
|
||||||
|
return app.Application(
|
||||||
|
cast(tk.Tk, root), _Vocabulary(error), _Progress(), _Speech(), _first
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _recording_create(
|
||||||
|
seen: list[Level | None], exit_code: int = 0
|
||||||
|
) -> Callable[..., _FakeApplication]:
|
||||||
|
def fake_create(
|
||||||
|
data_dir: Path | None = None,
|
||||||
|
speech_enabled: bool = True,
|
||||||
|
level: Level | None = None,
|
||||||
|
) -> _FakeApplication:
|
||||||
|
seen.append(level)
|
||||||
|
return _FakeApplication(_FakeRoot(), exit_code)
|
||||||
|
|
||||||
|
return fake_create
|
||||||
|
|
||||||
|
|
||||||
def test_main_runs_main_loop_and_returns_zero(
|
def test_main_runs_main_loop_and_returns_zero(
|
||||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
root = _FakeRoot()
|
root = _FakeRoot()
|
||||||
seen: list[Path | None] = []
|
seen: list[tuple[Path | None, Level | None]] = []
|
||||||
|
|
||||||
def fake_create(
|
def fake_create(
|
||||||
data_dir: Path | None = None, speech_enabled: bool = True
|
data_dir: Path | None = None,
|
||||||
) -> tuple[_FakeRoot, object]:
|
speech_enabled: bool = True,
|
||||||
seen.append(data_dir)
|
level: Level | None = None,
|
||||||
return root, object()
|
) -> _FakeApplication:
|
||||||
|
seen.append((data_dir, level))
|
||||||
|
return _FakeApplication(root)
|
||||||
|
|
||||||
monkeypatch.setattr(app, "create_application", fake_create)
|
monkeypatch.setattr(app, "create_application", fake_create)
|
||||||
|
|
||||||
assert app.main(["--data-dir", str(tmp_path)]) == 0
|
assert app.main(["--data-dir", str(tmp_path)]) == 0
|
||||||
assert root.mainloop_calls == 1
|
assert root.mainloop_calls == 1
|
||||||
assert seen == [tmp_path]
|
assert seen == [(tmp_path, None)]
|
||||||
|
|
||||||
|
|
||||||
def test_main_returns_one_when_word_list_is_missing(
|
def test_main_returns_one_when_word_list_is_missing_with_level(
|
||||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
assert app.main(["--data-dir", str(tmp_path / "nowhere")]) == 1
|
code = app.main(["--data-dir", str(tmp_path / "nowhere"), "--level", "all"])
|
||||||
|
assert code == 1
|
||||||
assert "word list not found" in caplog.text
|
assert "word list not found" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("value", "limit"), [("100", 100), ("1000", 1000), ("all", None)]
|
||||||
|
)
|
||||||
|
def test_main_passes_level_of_level_option(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str, limit: int | None
|
||||||
|
) -> None:
|
||||||
|
seen: list[Level | None] = []
|
||||||
|
monkeypatch.setattr(app, "create_application", _recording_create(seen))
|
||||||
|
|
||||||
|
assert app.main(["--data-dir", str(tmp_path), "--level", value]) == 0
|
||||||
|
assert seen == [Level(limit)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_refuses_unknown_level(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
with pytest.raises(SystemExit) as raised:
|
||||||
|
app.main(["--level", "50"])
|
||||||
|
assert raised.value.code == 2
|
||||||
|
assert "invalid level" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_returns_exit_code_of_application(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
seen: list[Level | None] = []
|
||||||
|
monkeypatch.setattr(app, "create_application", _recording_create(seen, 1))
|
||||||
|
|
||||||
|
assert app.main(["--data-dir", str(tmp_path)]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_shows_no_view_before_a_screen_is_requested(
|
||||||
|
fake_views: None,
|
||||||
|
) -> None:
|
||||||
|
assert _application(_FakeRoot()).view is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_level_screen_offers_the_three_levels(fake_views: None) -> None:
|
||||||
|
application = _application(_FakeRoot())
|
||||||
|
application.show_level_screen()
|
||||||
|
view = application.view
|
||||||
|
assert isinstance(view, _FakeLevelView)
|
||||||
|
assert [c.level.limit for c in view.view_model.choices] == [100, 1000, None]
|
||||||
|
|
||||||
|
|
||||||
|
def test_choosing_a_level_replaces_level_screen_by_card_screen(
|
||||||
|
fake_views: None,
|
||||||
|
) -> None:
|
||||||
|
application = _application(_FakeRoot())
|
||||||
|
application.show_level_screen()
|
||||||
|
level_view = application.view
|
||||||
|
assert isinstance(level_view, _FakeLevelView)
|
||||||
|
|
||||||
|
level_view.on_choose(Level(100))
|
||||||
|
|
||||||
|
assert level_view.destroyed
|
||||||
|
assert isinstance(application.view, _FakeFlashCardView)
|
||||||
|
assert application.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_draws_the_first_card_of_the_level(fake_views: None) -> None:
|
||||||
|
application = _application(_FakeRoot())
|
||||||
|
application.start(Level(2))
|
||||||
|
view = application.view
|
||||||
|
assert isinstance(view, _FakeFlashCardView)
|
||||||
|
assert view.view_model.text == "aku"
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_without_level_screen_shows_card_screen_only(
|
||||||
|
fake_views: None,
|
||||||
|
) -> None:
|
||||||
|
application = _application(_FakeRoot())
|
||||||
|
application.start(Level(None))
|
||||||
|
assert isinstance(application.view, _FakeFlashCardView)
|
||||||
|
|
||||||
|
|
||||||
|
def test_choosing_a_level_closes_window_with_code_one_on_vocabulary_error(
|
||||||
|
fake_views: None, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
root = _FakeRoot()
|
||||||
|
application = _application(root, error=True)
|
||||||
|
application.show_level_screen()
|
||||||
|
level_view = application.view
|
||||||
|
assert isinstance(level_view, _FakeLevelView)
|
||||||
|
|
||||||
|
level_view.on_choose(Level(100))
|
||||||
|
|
||||||
|
assert root.destroyed
|
||||||
|
assert application.exit_code == 1
|
||||||
|
assert "word list not found" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_raises_vocabulary_error_for_the_caller(fake_views: None) -> None:
|
||||||
|
application = _application(_FakeRoot(), error=True)
|
||||||
|
with pytest.raises(VocabularyNotFoundError):
|
||||||
|
application.start(Level(100))
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_data_files_uses_bundled_list_when_default_folder_has_none(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
bundled = tmp_path / "bundle" / "ms-da.csv"
|
||||||
|
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", tmp_path / "data")
|
||||||
|
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", bundled)
|
||||||
|
vocabulary, progress = app.resolve_data_files(None)
|
||||||
|
assert vocabulary == bundled
|
||||||
|
assert progress == tmp_path / "data" / "words_to_learn.csv"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_data_files_prefers_the_default_folder_list(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
(tmp_path / "ms-da.csv").write_text("x", encoding="utf-8")
|
||||||
|
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", tmp_path)
|
||||||
|
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", tmp_path / "other.csv")
|
||||||
|
vocabulary, _ = app.resolve_data_files(None)
|
||||||
|
assert vocabulary == tmp_path / "ms-da.csv"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_data_files_uses_a_given_folder_alone(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", tmp_path / "other.csv")
|
||||||
|
vocabulary, progress = app.resolve_data_files(tmp_path / "given")
|
||||||
|
assert vocabulary == tmp_path / "given" / "ms-da.csv"
|
||||||
|
assert progress == tmp_path / "given" / "words_to_learn.csv"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_application_draws_first_card_from_bundled_list(
|
||||||
|
fake_views: None, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
bundled = tmp_path / "bundle" / "ms-da.csv"
|
||||||
|
bundled.parent.mkdir()
|
||||||
|
bundled.write_text("ms;da\nsaya;jeg\n", encoding="utf-8")
|
||||||
|
data = tmp_path / "data"
|
||||||
|
monkeypatch.setattr(app.tk, "Tk", _FakeRoot)
|
||||||
|
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", data)
|
||||||
|
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", bundled)
|
||||||
|
application = app.create_application(None, False, Level(None))
|
||||||
|
view = cast(_FakeFlashCardView, application.view)
|
||||||
|
assert view.view_model.text == "saya"
|
||||||
|
view.view_model.is_known()
|
||||||
|
assert (data / "words_to_learn.csv").is_file()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
from flash_card.infrastructure.speech.gtts_speech import GttsSpeech
|
from flash_card.infrastructure.speech.gtts_speech import GttsSpeech
|
||||||
from flash_card.infrastructure.speech.null_speech import NullSpeech
|
from flash_card.infrastructure.speech.null_speech import NullSpeech
|
||||||
from flash_card.presentation import app
|
from flash_card.presentation import app
|
||||||
@@ -16,6 +17,11 @@ class _FakeRoot:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeApplication:
|
||||||
|
root = _FakeRoot()
|
||||||
|
exit_code = 0
|
||||||
|
|
||||||
|
|
||||||
def _found(name: str) -> ModuleSpec:
|
def _found(name: str) -> ModuleSpec:
|
||||||
return ModuleSpec(name, None)
|
return ModuleSpec(name, None)
|
||||||
|
|
||||||
@@ -58,10 +64,12 @@ def test_main_passes_speech_disabled_with_no_speech_option(
|
|||||||
seen: list[bool] = []
|
seen: list[bool] = []
|
||||||
|
|
||||||
def fake_create(
|
def fake_create(
|
||||||
data_dir: Path | None = None, speech_enabled: bool = True
|
data_dir: Path | None = None,
|
||||||
) -> tuple[_FakeRoot, object]:
|
speech_enabled: bool = True,
|
||||||
|
level: Level | None = None,
|
||||||
|
) -> _FakeApplication:
|
||||||
seen.append(speech_enabled)
|
seen.append(speech_enabled)
|
||||||
return _FakeRoot(), object()
|
return _FakeApplication()
|
||||||
|
|
||||||
monkeypatch.setattr(app, "create_application", fake_create)
|
monkeypatch.setattr(app, "create_application", fake_create)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""Tests of the executable build tool.
|
||||||
|
|
||||||
|
@file test_build_executable.py
|
||||||
|
@brief Checks the PyInstaller arguments and the missing PyInstaller message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flash_card.constants import EXECUTABLE_NAME, ICON_FILE_NAME
|
||||||
|
|
||||||
|
_TOOL_PATH = Path(__file__).resolve().parents[2] / "tools" / "build_executable.py"
|
||||||
|
ROOT = Path("project")
|
||||||
|
ASSETS = ROOT / "src" / "flash_card" / "presentation" / "assets"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_tool() -> ModuleType:
|
||||||
|
spec = importlib.util.spec_from_file_location("build_executable", _TOOL_PATH)
|
||||||
|
assert spec is not None
|
||||||
|
assert spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def _option(arguments: list[str], name: str) -> str:
|
||||||
|
return arguments[arguments.index(name) + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_start_with_the_packaging_flags() -> None:
|
||||||
|
arguments = _load_tool().build_arguments(ROOT)
|
||||||
|
assert arguments[:3] == ["--onefile", "--windowed", "--noconfirm"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_name_icon_and_folders() -> None:
|
||||||
|
arguments = _load_tool().build_arguments(ROOT)
|
||||||
|
assert _option(arguments, "--name") == EXECUTABLE_NAME == "flash-card"
|
||||||
|
assert _option(arguments, "--icon") == str(ASSETS / ICON_FILE_NAME)
|
||||||
|
assert _option(arguments, "--paths") == str(ROOT / "src")
|
||||||
|
assert _option(arguments, "--distpath") == str(ROOT / "dist")
|
||||||
|
assert _option(arguments, "--workpath") == str(ROOT / "build")
|
||||||
|
assert _option(arguments, "--specpath") == str(ROOT / "build")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_add_assets_and_word_list() -> None:
|
||||||
|
arguments = _load_tool().build_arguments(ROOT)
|
||||||
|
pairs = [
|
||||||
|
arguments[i + 1] for i, value in enumerate(arguments) if value == "--add-data"
|
||||||
|
]
|
||||||
|
assert pairs == [
|
||||||
|
f"{ASSETS}{os.pathsep}flash_card/presentation/assets",
|
||||||
|
f"{ROOT / 'data' / 'ms-da.csv'}{os.pathsep}data",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_end_with_the_entry_script() -> None:
|
||||||
|
arguments = _load_tool().build_arguments(ROOT, clean=True)
|
||||||
|
assert arguments[-1] == str(ROOT / "src" / "flash_card" / "__main__.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_include_speech_packages_by_default() -> None:
|
||||||
|
arguments = _load_tool().build_arguments(ROOT)
|
||||||
|
assert arguments.count("--collect-all") == 2
|
||||||
|
assert "gtts" in arguments
|
||||||
|
assert "playsound" in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_leave_out_speech_packages_without_speech() -> None:
|
||||||
|
arguments = _load_tool().build_arguments(ROOT, speech=False)
|
||||||
|
assert "--collect-all" not in arguments
|
||||||
|
assert "--hidden-import" not in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_arguments_clean_only_when_asked() -> None:
|
||||||
|
tool = _load_tool()
|
||||||
|
assert "--clean" not in tool.build_arguments(ROOT)
|
||||||
|
assert "--clean" in tool.build_arguments(ROOT, clean=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_returns_two_and_explains_when_pyinstaller_is_missing(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
def refuse(name: str, package: str | None = None) -> ModuleType:
|
||||||
|
raise ImportError(name)
|
||||||
|
|
||||||
|
tool = _load_tool()
|
||||||
|
monkeypatch.setattr(importlib, "import_module", refuse)
|
||||||
|
assert tool.main([]) == 2
|
||||||
|
assert 'pip install -e ".[build]"' in capsys.readouterr().err
|
||||||
@@ -35,3 +35,27 @@ def test_project_root_contains_pyproject() -> None:
|
|||||||
|
|
||||||
def test_csv_encoding_drops_byte_order_mark() -> None:
|
def test_csv_encoding_drops_byte_order_mark() -> None:
|
||||||
assert constants.CSV_ENCODING == "utf-8-sig"
|
assert constants.CSV_ENCODING == "utf-8-sig"
|
||||||
|
|
||||||
|
|
||||||
|
def test_assets_dir_holds_the_four_images_and_the_icon() -> None:
|
||||||
|
names = [
|
||||||
|
constants.CARD_FRONT_IMAGE_NAME,
|
||||||
|
constants.CARD_BACK_IMAGE_NAME,
|
||||||
|
constants.RIGHT_IMAGE_NAME,
|
||||||
|
constants.WRONG_IMAGE_NAME,
|
||||||
|
constants.ICON_FILE_NAME,
|
||||||
|
]
|
||||||
|
assert len(set(names)) == 5
|
||||||
|
assert all((constants.ASSETS_DIR / name).is_file() for name in names)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bundled_vocabulary_is_the_word_list_of_the_bundled_data_dir() -> None:
|
||||||
|
assert constants.BUNDLED_VOCABULARY_PATH == (
|
||||||
|
constants.BUNDLED_DATA_DIR / constants.VOCABULARY_FILE_NAME
|
||||||
|
)
|
||||||
|
assert constants.BUNDLED_VOCABULARY_PATH.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_executable_and_icon_names() -> None:
|
||||||
|
assert constants.EXECUTABLE_NAME == "flash-card"
|
||||||
|
assert constants.ICON_FILE_NAME == "flash_card.ico"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from flash_card.constants import (
|
|||||||
CARD_FRONT_IMAGE_NAME,
|
CARD_FRONT_IMAGE_NAME,
|
||||||
CARD_HEIGHT,
|
CARD_HEIGHT,
|
||||||
CARD_WIDTH,
|
CARD_WIDTH,
|
||||||
|
ICON_FILE_NAME,
|
||||||
RIGHT_IMAGE_NAME,
|
RIGHT_IMAGE_NAME,
|
||||||
WRONG_IMAGE_NAME,
|
WRONG_IMAGE_NAME,
|
||||||
)
|
)
|
||||||
@@ -56,3 +57,18 @@ def test_generate_images_writes_png_with_expected_size(
|
|||||||
assert data.startswith(_SIGNATURE)
|
assert data.startswith(_SIGNATURE)
|
||||||
assert data[12:16] == b"IHDR"
|
assert data[12:16] == b"IHDR"
|
||||||
assert struct.unpack(">II", data[16:24]) == (width, height)
|
assert struct.unpack(">II", data[16:24]) == (width, height)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_images_writes_icon_with_one_256_pixel_png(generated: Path) -> None:
|
||||||
|
data = (generated / ICON_FILE_NAME).read_bytes()
|
||||||
|
reserved, kind, count = struct.unpack("<HHH", data[:6])
|
||||||
|
width, height, _colors, _zero, planes, bpp, size, offset = struct.unpack(
|
||||||
|
"<BBBBHHII", data[6:22]
|
||||||
|
)
|
||||||
|
assert (reserved, kind, count) == (0, 1, 1)
|
||||||
|
assert (width, height, planes, bpp) == (0, 0, 1, 32)
|
||||||
|
assert offset == 22
|
||||||
|
assert size == len(data) - 22
|
||||||
|
png = data[offset:]
|
||||||
|
assert png.startswith(_SIGNATURE)
|
||||||
|
assert struct.unpack(">II", png[16:24]) == (256, 256)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Tests of the level view model."""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from flash_card.constants import LEVEL_TITLE
|
||||||
|
from flash_card.domain.value_objects.level import Level
|
||||||
|
from flash_card.presentation.features.level import level_view_model
|
||||||
|
from flash_card.presentation.features.level.level_view_model import (
|
||||||
|
LevelChoice,
|
||||||
|
LevelViewModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_title_is_choose_your_level() -> None:
|
||||||
|
assert LevelViewModel().title == LEVEL_TITLE == "Choose your level"
|
||||||
|
|
||||||
|
|
||||||
|
def test_choices_are_three_levels_in_order() -> None:
|
||||||
|
choices = LevelViewModel().choices
|
||||||
|
assert [choice.label for choice in choices] == [
|
||||||
|
"100 words",
|
||||||
|
"1000 words",
|
||||||
|
"All words",
|
||||||
|
]
|
||||||
|
assert [choice.level for choice in choices] == [
|
||||||
|
Level(100),
|
||||||
|
Level(1000),
|
||||||
|
Level(None),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_choices_come_from_given_choices() -> None:
|
||||||
|
model = LevelViewModel([("Ten", 10)])
|
||||||
|
assert model.choices == (LevelChoice("Ten", Level(10)),)
|
||||||
|
|
||||||
|
|
||||||
|
def test_choice_is_frozen() -> None:
|
||||||
|
choice = LevelChoice("Ten", Level(10))
|
||||||
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||||
|
choice.label = "x" # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_model_module_does_not_import_tkinter() -> None:
|
||||||
|
assert "tkinter" not in vars(level_view_model)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Tests of the runtime helpers.
|
||||||
|
|
||||||
|
@file test_runtime.py
|
||||||
|
@brief Checks where resources and the executable are, frozen or from source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flash_card import runtime
|
||||||
|
|
||||||
|
BUNDLE = Path("bundle")
|
||||||
|
EXECUTABLE = Path("apps") / "flash-card.exe"
|
||||||
|
ROOT = Path("project")
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_resource_root_returns_bundle_when_frozen() -> None:
|
||||||
|
assert runtime.pick_resource_root(True, BUNDLE, ROOT) == BUNDLE
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_resource_root_returns_root_when_not_frozen() -> None:
|
||||||
|
assert runtime.pick_resource_root(False, BUNDLE, ROOT) == ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_resource_root_returns_root_when_bundle_is_unknown() -> None:
|
||||||
|
assert runtime.pick_resource_root(True, None, ROOT) == ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_executable_dir_returns_executable_folder_when_frozen() -> None:
|
||||||
|
assert runtime.pick_executable_dir(True, EXECUTABLE, ROOT) == EXECUTABLE.parent
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_executable_dir_returns_root_when_not_frozen() -> None:
|
||||||
|
assert runtime.pick_executable_dir(False, EXECUTABLE, ROOT) == ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_frozen_is_false_in_a_test_run() -> None:
|
||||||
|
assert runtime.is_frozen() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_root_is_project_root_in_a_source_run() -> None:
|
||||||
|
assert (runtime.resource_root() / "pyproject.toml").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_executable_dir_is_project_root_in_a_source_run() -> None:
|
||||||
|
assert runtime.executable_dir() == runtime.source_root()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Build the single file Windows executable with PyInstaller.
|
||||||
|
|
||||||
|
@file build_executable.py
|
||||||
|
@brief Runs PyInstaller with the arguments of the flash card program.
|
||||||
|
Uses the standard library; PyInstaller is imported only when a build runs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_SRC_DIR = Path(__file__).resolve().parents[1] / "src"
|
||||||
|
if str(_SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_SRC_DIR))
|
||||||
|
|
||||||
|
from flash_card.constants import ( # noqa: E402
|
||||||
|
EXECUTABLE_NAME,
|
||||||
|
ICON_FILE_NAME,
|
||||||
|
VOCABULARY_FILE_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
_ASSETS_RELATIVE = Path("src") / "flash_card" / "presentation" / "assets"
|
||||||
|
_ASSETS_TARGET = "flash_card/presentation/assets"
|
||||||
|
_DATA_RELATIVE = Path("data")
|
||||||
|
_SPEECH_PACKAGES = ("gtts", "playsound")
|
||||||
|
_INSTALL_HINT = 'python -m pip install -e ".[build]"'
|
||||||
|
_EXIT_MISSING_PYINSTALLER = 2
|
||||||
|
|
||||||
|
|
||||||
|
def _add_data(source: Path, target: str) -> list[str]:
|
||||||
|
"""Return the --add-data option of one source and target.
|
||||||
|
|
||||||
|
@param source File or folder to bundle.
|
||||||
|
@param target Folder inside the bundle.
|
||||||
|
@return The option and its value.
|
||||||
|
"""
|
||||||
|
return ["--add-data", f"{source}{os.pathsep}{target}"]
|
||||||
|
|
||||||
|
|
||||||
|
def build_arguments(
|
||||||
|
root: Path, *, speech: bool = True, clean: bool = False
|
||||||
|
) -> list[str]:
|
||||||
|
"""Return the PyInstaller argument list.
|
||||||
|
|
||||||
|
@param root Project root folder.
|
||||||
|
@param speech True to bundle the optional gtts and playsound packages.
|
||||||
|
@param clean True to clear the PyInstaller cache first.
|
||||||
|
@return The arguments, the entry script last.
|
||||||
|
"""
|
||||||
|
arguments = [
|
||||||
|
"--onefile",
|
||||||
|
"--windowed",
|
||||||
|
"--noconfirm",
|
||||||
|
"--name",
|
||||||
|
EXECUTABLE_NAME,
|
||||||
|
"--icon",
|
||||||
|
str(root / _ASSETS_RELATIVE / ICON_FILE_NAME),
|
||||||
|
"--paths",
|
||||||
|
str(root / "src"),
|
||||||
|
"--distpath",
|
||||||
|
str(root / "dist"),
|
||||||
|
"--workpath",
|
||||||
|
str(root / "build"),
|
||||||
|
"--specpath",
|
||||||
|
str(root / "build"),
|
||||||
|
]
|
||||||
|
arguments += _add_data(root / _ASSETS_RELATIVE, _ASSETS_TARGET)
|
||||||
|
arguments += _add_data(root / _DATA_RELATIVE / VOCABULARY_FILE_NAME, "data")
|
||||||
|
if speech:
|
||||||
|
for package in _SPEECH_PACKAGES:
|
||||||
|
arguments += ["--hidden-import", package, "--collect-all", package]
|
||||||
|
if clean:
|
||||||
|
arguments.append("--clean")
|
||||||
|
arguments.append(str(root / "src" / "flash_card" / "__main__.py"))
|
||||||
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
"""Build the executable from the command line.
|
||||||
|
|
||||||
|
@param argv Command line arguments, or None for sys.argv.
|
||||||
|
@return 0 on success, 2 when PyInstaller is not installed.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(description="Build the flash card executable.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-speech", action="store_true", help="leave out gtts and playsound"
|
||||||
|
)
|
||||||
|
parser.add_argument("--clean", action="store_true", help="clear the cache first")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
pyinstaller = importlib.import_module("PyInstaller.__main__")
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"PyInstaller is not installed; run {_INSTALL_HINT} and try again\n"
|
||||||
|
)
|
||||||
|
return _EXIT_MISSING_PYINSTALLER
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
pyinstaller.run(build_arguments(root, speech=not args.no_speech, clean=args.clean))
|
||||||
|
sys.stdout.write(f"built {root / 'dist' / (EXECUTABLE_NAME + '.exe')}\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Capture screenshots of the running flash card window.
|
"""Capture screenshots of the running flash card window.
|
||||||
|
|
||||||
@file capture_screenshots.py
|
@file capture_screenshots.py
|
||||||
@brief Starts the program, grabs the front of a card and, three seconds later,
|
@brief Starts the program, grabs the level screen, the front of a card and,
|
||||||
the back, and saves both as PNG files.
|
three seconds later, the back, and saves them as PNG files.
|
||||||
|
|
||||||
The tool needs Pillow (`python -m pip install -e ".[tools]"`), which is a tool
|
The tool needs Pillow (`python -m pip install -e ".[tools]"`), which is a tool
|
||||||
dependency only and not a dependency of the program. It grabs the screen area
|
dependency only and not a dependency of the program. It grabs the screen area
|
||||||
@@ -31,7 +31,11 @@ from flash_card.constants import ( # noqa: E402
|
|||||||
PROJECT_ROOT,
|
PROJECT_ROOT,
|
||||||
VOCABULARY_FILE_NAME,
|
VOCABULARY_FILE_NAME,
|
||||||
)
|
)
|
||||||
|
from flash_card.domain.value_objects.level import Level # noqa: E402
|
||||||
from flash_card.presentation.app import create_application # noqa: E402
|
from flash_card.presentation.app import create_application # noqa: E402
|
||||||
|
from flash_card.presentation.features.flash_card.flash_card_view import ( # noqa: E402
|
||||||
|
FlashCardView,
|
||||||
|
)
|
||||||
|
|
||||||
## @brief Default folder of the screenshots.
|
## @brief Default folder of the screenshots.
|
||||||
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "docs" / "images"
|
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "docs" / "images"
|
||||||
@@ -60,21 +64,37 @@ def _pump(root: object, seconds: float) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def capture(output_dir: Path) -> list[Path]:
|
def capture(output_dir: Path) -> list[Path]:
|
||||||
"""Capture the front and the back of one card.
|
"""Capture the level screen, then the front and the back of one card.
|
||||||
|
|
||||||
@param output_dir Folder to write `screenshot-front.png` and
|
@param output_dir Folder to write `screenshot-level.png`,
|
||||||
`screenshot-back.png` into.
|
`screenshot-front.png` and `screenshot-back.png` into.
|
||||||
@return The two files written, front first.
|
@return The three files written: level, front, back.
|
||||||
"""
|
"""
|
||||||
_make_dpi_aware()
|
_make_dpi_aware()
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
random.seed(SCREENSHOT_SEED)
|
random.seed(SCREENSHOT_SEED)
|
||||||
|
level = output_dir / "screenshot-level.png"
|
||||||
|
front = output_dir / "screenshot-front.png"
|
||||||
|
back = output_dir / "screenshot-back.png"
|
||||||
with tempfile.TemporaryDirectory() as data_dir:
|
with tempfile.TemporaryDirectory() as data_dir:
|
||||||
shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, data_dir)
|
shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, data_dir)
|
||||||
root, view = create_application(Path(data_dir), speech_enabled=False)
|
application = create_application(Path(data_dir), speech_enabled=False)
|
||||||
|
root = application.root
|
||||||
root.lift()
|
root.lift()
|
||||||
root.attributes("-topmost", True)
|
root.attributes("-topmost", True)
|
||||||
_pump(root, SETTLE_SECONDS)
|
_pump(root, SETTLE_SECONDS)
|
||||||
|
window_box = (
|
||||||
|
root.winfo_rootx(),
|
||||||
|
root.winfo_rooty(),
|
||||||
|
root.winfo_rootx() + root.winfo_width(),
|
||||||
|
root.winfo_rooty() + root.winfo_height(),
|
||||||
|
)
|
||||||
|
ImageGrab.grab(bbox=window_box).save(level)
|
||||||
|
application.start(Level(None))
|
||||||
|
view = application.view
|
||||||
|
if not isinstance(view, FlashCardView):
|
||||||
|
raise RuntimeError("the card window did not open")
|
||||||
|
_pump(root, SETTLE_SECONDS)
|
||||||
box = (
|
box = (
|
||||||
view.canvas.winfo_rootx() - 20,
|
view.canvas.winfo_rootx() - 20,
|
||||||
view.canvas.winfo_rooty() - 20,
|
view.canvas.winfo_rooty() - 20,
|
||||||
@@ -84,13 +104,11 @@ def capture(output_dir: Path) -> list[Path]:
|
|||||||
+ view.right_button.winfo_height()
|
+ view.right_button.winfo_height()
|
||||||
+ 20,
|
+ 20,
|
||||||
)
|
)
|
||||||
front = output_dir / "screenshot-front.png"
|
|
||||||
back = output_dir / "screenshot-back.png"
|
|
||||||
ImageGrab.grab(bbox=box).save(front)
|
ImageGrab.grab(bbox=box).save(front)
|
||||||
_pump(root, FLIP_DELAY_MS / 1000 + SETTLE_SECONDS)
|
_pump(root, FLIP_DELAY_MS / 1000 + SETTLE_SECONDS)
|
||||||
ImageGrab.grab(bbox=box).save(back)
|
ImageGrab.grab(bbox=box).save(back)
|
||||||
root.destroy()
|
root.destroy()
|
||||||
return [front, back]
|
return [level, front, back]
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Sequence[str] | None = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Generate the PNG assets of the flash card program.
|
"""Generate the PNG assets of the flash card program.
|
||||||
|
|
||||||
@file generate_images.py
|
@file generate_images.py
|
||||||
@brief Draws card_front, card_back, right and wrong as antialiased RGBA PNGs.
|
@brief Draws card_front, card_back, right and wrong as antialiased RGBA PNGs,
|
||||||
|
plus the program icon flash_card.ico.
|
||||||
Uses the standard library only; run it to regenerate the assets.
|
Uses the standard library only; run it to regenerate the assets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ from flash_card.constants import ( # noqa: E402
|
|||||||
CARD_FRONT_IMAGE_NAME,
|
CARD_FRONT_IMAGE_NAME,
|
||||||
CARD_HEIGHT,
|
CARD_HEIGHT,
|
||||||
CARD_WIDTH,
|
CARD_WIDTH,
|
||||||
|
ICON_FILE_NAME,
|
||||||
RIGHT_IMAGE_NAME,
|
RIGHT_IMAGE_NAME,
|
||||||
WRONG_IMAGE_NAME,
|
WRONG_IMAGE_NAME,
|
||||||
)
|
)
|
||||||
@@ -57,6 +59,10 @@ _CROSS_SEGMENTS: tuple[Segment, ...] = (
|
|||||||
((68.0, 32.0), (32.0, 68.0)),
|
((68.0, 32.0), (32.0, 68.0)),
|
||||||
)
|
)
|
||||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||||
|
_ICON_SIZE = 256
|
||||||
|
_ICON_HEADER_SIZE = 6
|
||||||
|
_ICON_ENTRY_SIZE = 16
|
||||||
|
_ICON_BITS_PER_PIXEL = 32
|
||||||
|
|
||||||
|
|
||||||
class _Canvas:
|
class _Canvas:
|
||||||
@@ -309,20 +315,27 @@ def _render_card(fill: Color) -> _Canvas:
|
|||||||
|
|
||||||
|
|
||||||
def _render_button(
|
def _render_button(
|
||||||
gradient: tuple[Color, Color], segments: Sequence[Segment]
|
gradient: tuple[Color, Color],
|
||||||
|
segments: Sequence[Segment],
|
||||||
|
size: int = _BUTTON_SIZE,
|
||||||
) -> _Canvas:
|
) -> _Canvas:
|
||||||
"""Draw a rounded gradient square with a white symbol.
|
"""Draw a rounded gradient square with a white symbol.
|
||||||
|
|
||||||
@param gradient Top and bottom colour.
|
@param gradient Top and bottom colour.
|
||||||
@param segments Line segments of the symbol.
|
@param segments Line segments of the symbol, drawn for a 100 px button.
|
||||||
|
@param size Side of the square in pixels; the design is scaled to it.
|
||||||
@return The canvas.
|
@return The canvas.
|
||||||
"""
|
"""
|
||||||
size = _BUTTON_SIZE
|
scale = size / _BUTTON_SIZE
|
||||||
|
scaled = [
|
||||||
|
((a[0] * scale, a[1] * scale), (b[0] * scale, b[1] * scale))
|
||||||
|
for a, b in segments
|
||||||
|
]
|
||||||
canvas = _Canvas(size, size)
|
canvas = _Canvas(size, size)
|
||||||
box: Box = (0.0, 0.0, float(size), float(size))
|
box: Box = (0.0, 0.0, float(size), float(size))
|
||||||
square = _rounded_rect_mask(size, size, box, _BUTTON_RADIUS)
|
square = _rounded_rect_mask(size, size, box, _BUTTON_RADIUS * scale)
|
||||||
canvas.paint(square, _vertical_gradient(gradient[0], gradient[1], size))
|
canvas.paint(square, _vertical_gradient(gradient[0], gradient[1], size))
|
||||||
symbol = _stroke_mask(size, size, segments, _STROKE_WIDTH)
|
symbol = _stroke_mask(size, size, scaled, _STROKE_WIDTH * scale)
|
||||||
canvas.paint(symbol, _solid(_WHITE))
|
canvas.paint(symbol, _solid(_WHITE))
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
@@ -351,8 +364,24 @@ def encode_png(width: int, height: int, rgba: bytes) -> bytes:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_ico(png: bytes, size: int) -> bytes:
|
||||||
|
"""Wrap one PNG image in an ICO container.
|
||||||
|
|
||||||
|
@param png The PNG file content.
|
||||||
|
@param size Image side in pixels; 256 is stored as 0.
|
||||||
|
@return The ICO file content.
|
||||||
|
"""
|
||||||
|
stored = 0 if size >= _ICON_SIZE else size
|
||||||
|
offset = _ICON_HEADER_SIZE + _ICON_ENTRY_SIZE
|
||||||
|
header = struct.pack("<HHH", 0, 1, 1)
|
||||||
|
entry = struct.pack(
|
||||||
|
"<BBBBHHII", stored, stored, 0, 0, 1, _ICON_BITS_PER_PIXEL, len(png), offset
|
||||||
|
)
|
||||||
|
return header + entry + png
|
||||||
|
|
||||||
|
|
||||||
def generate_images(output_dir: Path) -> list[Path]:
|
def generate_images(output_dir: Path) -> list[Path]:
|
||||||
"""Write the four PNG assets into a folder.
|
"""Write the four PNG assets and the icon into a folder.
|
||||||
|
|
||||||
@param output_dir Folder to write into; created when missing.
|
@param output_dir Folder to write into; created when missing.
|
||||||
@return The paths of the written files.
|
@return The paths of the written files.
|
||||||
@@ -370,6 +399,11 @@ def generate_images(output_dir: Path) -> list[Path]:
|
|||||||
data = encode_png(canvas.width, canvas.height, canvas.to_rgba_bytes())
|
data = encode_png(canvas.width, canvas.height, canvas.to_rgba_bytes())
|
||||||
path.write_bytes(data)
|
path.write_bytes(data)
|
||||||
written.append(path)
|
written.append(path)
|
||||||
|
icon = _render_button(_RIGHT_GRADIENT, _CHECK_SEGMENTS, _ICON_SIZE)
|
||||||
|
png = encode_png(icon.width, icon.height, icon.to_rgba_bytes())
|
||||||
|
icon_path = output_dir / ICON_FILE_NAME
|
||||||
|
icon_path.write_bytes(encode_ico(png, _ICON_SIZE))
|
||||||
|
written.append(icon_path)
|
||||||
return written
|
return written
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user