django-flashdeck 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. django_flashdeck-0.1.0/.github/dependabot.yml +13 -0
  2. django_flashdeck-0.1.0/.github/workflows/ci.yml +38 -0
  3. django_flashdeck-0.1.0/.github/workflows/release.yml +23 -0
  4. django_flashdeck-0.1.0/.gitignore +8 -0
  5. django_flashdeck-0.1.0/CHANGELOG.md +13 -0
  6. django_flashdeck-0.1.0/LICENSE +28 -0
  7. django_flashdeck-0.1.0/PKG-INFO +215 -0
  8. django_flashdeck-0.1.0/README.md +179 -0
  9. django_flashdeck-0.1.0/pyproject.toml +69 -0
  10. django_flashdeck-0.1.0/src/flashdeck/__init__.py +1 -0
  11. django_flashdeck-0.1.0/src/flashdeck/admin.py +68 -0
  12. django_flashdeck-0.1.0/src/flashdeck/apps.py +8 -0
  13. django_flashdeck-0.1.0/src/flashdeck/bundles.py +346 -0
  14. django_flashdeck-0.1.0/src/flashdeck/conf.py +19 -0
  15. django_flashdeck-0.1.0/src/flashdeck/images.py +85 -0
  16. django_flashdeck-0.1.0/src/flashdeck/management/__init__.py +0 -0
  17. django_flashdeck-0.1.0/src/flashdeck/management/commands/__init__.py +0 -0
  18. django_flashdeck-0.1.0/src/flashdeck/management/commands/flashdeck_export.py +23 -0
  19. django_flashdeck-0.1.0/src/flashdeck/management/commands/flashdeck_import.py +29 -0
  20. django_flashdeck-0.1.0/src/flashdeck/manifest.py +33 -0
  21. django_flashdeck-0.1.0/src/flashdeck/marking.py +107 -0
  22. django_flashdeck-0.1.0/src/flashdeck/migrations/0001_initial.py +146 -0
  23. django_flashdeck-0.1.0/src/flashdeck/migrations/0002_card_options.py +44 -0
  24. django_flashdeck-0.1.0/src/flashdeck/migrations/0003_reference.py +28 -0
  25. django_flashdeck-0.1.0/src/flashdeck/migrations/__init__.py +0 -0
  26. django_flashdeck-0.1.0/src/flashdeck/models.py +325 -0
  27. django_flashdeck-0.1.0/src/flashdeck/practice.py +232 -0
  28. django_flashdeck-0.1.0/src/flashdeck/reference.py +108 -0
  29. django_flashdeck-0.1.0/src/flashdeck/scheduling.py +75 -0
  30. django_flashdeck-0.1.0/src/flashdeck/static/flashdeck/flashdeck.css +241 -0
  31. django_flashdeck-0.1.0/src/flashdeck/static/flashdeck/flashdeck.js +137 -0
  32. django_flashdeck-0.1.0/src/flashdeck/stats.py +248 -0
  33. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_account.html +1 -0
  34. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_assets.html +2 -0
  35. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_crumbs.html +1 -0
  36. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_crumbs_root.html +1 -0
  37. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_forms.html +3 -0
  38. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_prompt.html +5 -0
  39. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_reference_dialog.html +8 -0
  40. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/_search.html +5 -0
  41. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/admin/bundles.html +24 -0
  42. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/fragment.html +1 -0
  43. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/index.html +29 -0
  44. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/nothing_due.html +21 -0
  45. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/page.html +23 -0
  46. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/progress.html +80 -0
  47. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/question.html +46 -0
  48. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/reference_deck.html +28 -0
  49. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/reference_item.html +18 -0
  50. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/reference_subject.html +19 -0
  51. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/result.html +56 -0
  52. django_flashdeck-0.1.0/src/flashdeck/templates/flashdeck/settings.html +44 -0
  53. django_flashdeck-0.1.0/src/flashdeck/templatetags/__init__.py +0 -0
  54. django_flashdeck-0.1.0/src/flashdeck/templatetags/flashdeck_tags.py +52 -0
  55. django_flashdeck-0.1.0/src/flashdeck/urls.py +31 -0
  56. django_flashdeck-0.1.0/src/flashdeck/views.py +392 -0
  57. django_flashdeck-0.1.0/src/flashdeck/wagtail_hooks.py +109 -0
  58. django_flashdeck-0.1.0/src/flashdeck/wagtail_views.py +106 -0
  59. django_flashdeck-0.1.0/tests/__init__.py +0 -0
  60. django_flashdeck-0.1.0/tests/conftest.py +70 -0
  61. django_flashdeck-0.1.0/tests/settings.py +40 -0
  62. django_flashdeck-0.1.0/tests/test_admin.py +25 -0
  63. django_flashdeck-0.1.0/tests/test_bundles.py +196 -0
  64. django_flashdeck-0.1.0/tests/test_card_options.py +134 -0
  65. django_flashdeck-0.1.0/tests/test_images.py +85 -0
  66. django_flashdeck-0.1.0/tests/test_marking.py +50 -0
  67. django_flashdeck-0.1.0/tests/test_practice.py +138 -0
  68. django_flashdeck-0.1.0/tests/test_progress.py +140 -0
  69. django_flashdeck-0.1.0/tests/test_reference.py +84 -0
  70. django_flashdeck-0.1.0/tests/urls.py +7 -0
  71. django_flashdeck-0.1.0/uv.lock +999 -0
@@ -0,0 +1,13 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: github-actions
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
11
+ groups:
12
+ actions:
13
+ patterns: ["*"]
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python: ["3.12", "3.13", "3.14"]
18
+ django: ["5.2", "6.0", "6.1"]
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: astral-sh/setup-uv@v7
22
+ with:
23
+ python-version: ${{ matrix.python }}
24
+ - run: uv sync --group dev
25
+ - run: uv pip install "django~=${{ matrix.django }}.0"
26
+ - run: uv run --no-sync ruff check .
27
+ - run: uv run --no-sync ruff format --check .
28
+ - run: uv run --no-sync pytest
29
+ - name: No model changes without a migration
30
+ run: uv run --no-sync django-admin makemigrations flashdeck --check --dry-run --settings=tests.settings --pythonpath=.
31
+
32
+ build:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+ - uses: astral-sh/setup-uv@v7
37
+ - run: uv build
38
+ - run: uvx twine check --strict dist/*
@@ -0,0 +1,23 @@
1
+ # Publishes a release to PyPI when a tag such as v0.2.0 is pushed, by trusted
2
+ # publishing (no token is stored): on PyPI, the project's publisher is this
3
+ # repository, this workflow (release.yml) and the environment "pypi".
4
+ name: Release
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ permissions:
15
+ contents: read
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: astral-sh/setup-uv@v7
20
+ - name: The tag matches the version in pyproject.toml
21
+ run: test "v$(uv version --short)" = "$GITHUB_REF_NAME"
22
+ - run: uv build
23
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[co]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (11 November 2026)
4
+
5
+ The first release, from the app that runs at https://scipython.com/apps/flashcards/.
6
+
7
+ - Subjects, decks and cards, edited in Django's admin and, on a Wagtail site, in Wagtail's.
8
+ - Typed answers marked right, nearly right (a difference only in accents, capitals or punctuation) or wrong; cards with several labelled answers; characters to type as buttons.
9
+ - Spaced repetition (FSRS) for logged-in users, with cards hidden or shown more or less often; random cards from the public decks, with nothing stored, for visitors.
10
+ - Answers claimed as right by a user, reviewed in the admin.
11
+ - A Progress page, and reference pages generated from the cards, with a search and a dialog that opens from a card.
12
+ - Card images reduced in size and stripped of metadata, under random names.
13
+ - Bundles (`cards.yaml` and its images) imported and exported with management commands or, on a Wagtail site, in the admin.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Christian Hill
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-flashdeck
3
+ Version: 0.1.0
4
+ Summary: Flashcards for Django sites: decks of cards with typed answers, marked right or wrong, and spaced repetition.
5
+ Project-URL: Homepage, https://github.com/xnx/django-flashdeck
6
+ Project-URL: Changelog, https://github.com/xnx/django-flashdeck/blob/main/CHANGELOG.md
7
+ Project-URL: Decks, https://github.com/xnx/flashcard-decks
8
+ Author: Christian Hill
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Keywords: django,flashcards,fsrs,spaced repetition,wagtail
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 5.2
16
+ Classifier: Framework :: Django :: 6.0
17
+ Classifier: Framework :: Django :: 6.1
18
+ Classifier: Framework :: Wagtail
19
+ Classifier: Intended Audience :: Education
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Education
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: django-modelcluster>=6.0
28
+ Requires-Dist: django>=5.2
29
+ Requires-Dist: fsrs>=6.3.2
30
+ Requires-Dist: markdown>=3.11.1
31
+ Requires-Dist: pillow>=10.0
32
+ Requires-Dist: pyyaml>=6.0
33
+ Provides-Extra: wagtail
34
+ Requires-Dist: wagtail>=7.0; extra == 'wagtail'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # django-flashdeck
38
+
39
+ The django-flashdeck package adds flashcards to a Django site. A card's prompt is text, an image or both, and its answers are typed and marked as right, nearly right or wrong. A card may have several answers, each marked separately, such as the six persons of a conjugated verb. Logged-in users get *spaced repetition*: each card comes back at increasing intervals, timed for just before it would be forgotten. Cards are grouped into *decks* (topics) within *subjects* (such as French or dog breeds), and a subject's decks are written, moved between sites and passed on as *bundles*: a YAML file and its images.
40
+
41
+ The package needs Python 3.12 or later and Django 5.2 or later, and works with or without Wagtail. Decks of French verbs and of the circuit symbols of GCSE Physics, as bundles, are in [flashcard-decks](https://github.com/xnx/flashcard-decks); it runs at https://scipython.com/apps/flashcards/.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install django-flashdeck # or "django-flashdeck[wagtail]"
47
+ ```
48
+
49
+ Add the app and django-modelcluster (which it uses for editing a card's answers inline) to `INSTALLED_APPS`, include its URLs and migrate:
50
+
51
+ ```python
52
+ INSTALLED_APPS = [
53
+ ...,
54
+ "modelcluster",
55
+ "flashdeck",
56
+ ]
57
+ ```
58
+
59
+ ```python
60
+ urlpatterns = [
61
+ ...,
62
+ path("flashcards/", include("flashdeck.urls")),
63
+ ]
64
+ ```
65
+
66
+ Subjects, decks, cards and claimed answers (see "Marking") are edited in Django's admin and, on a Wagtail site, in Wagtail's admin under *Flashcards*. A deck is either public, for anyone to practise, or private, for users with the permission *Can practise private decks* (`flashdeck.practise_private`), which superusers have.
67
+
68
+ Practising needs no account: a visitor who is not logged in is shown cards from the public decks at random, and nothing is stored. A logged-in user's answers are stored, for scheduling and scores. The top of every page links to the site's login page (Django's `LOGIN_URL`, and back), if the site has one; for a logged-in user, it shows their name and, if `FLASHDECK_LOGOUT_URL` is set, a Log out button.
69
+
70
+ ## Settings
71
+
72
+ | Setting | Default | Meaning |
73
+ |---|---|---|
74
+ | `FLASHDECK_NEW_CARDS_PER_DAY` | `20` | New cards that each user starts per day, across all subjects |
75
+ | `FLASHDECK_IMAGE_MAX_SIZE` | `1200` | The size, in pixels, of the square that uploaded images are reduced to fit |
76
+ | `FLASHDECK_UPLOAD_DIR` | `"flashdeck"` | The directory, within the site's media storage, that holds card images |
77
+ | `FLASHDECK_LOGOUT_URL` | `""` | The URL, or URL name, at which to log out (by POST), for a Log out button on the index; none if blank |
78
+
79
+ ## Templates and styles
80
+
81
+ Every page extends `flashdeck/page.html`, which by default is a page of its own. A site overrides it to put the app inside its own pages:
82
+
83
+ ```django
84
+ {% extends "base.html" %}
85
+ {% block title %}{% block flashdeck_title %}{% endblock %}{% endblock %}
86
+ {% block content %}{% block flashdeck_content %}{% endblock %}{% endblock %}
87
+ ```
88
+
89
+ The breadcrumb trail starts with `flashdeck/_crumbs_root.html`, which is empty by default; a site can override it to add its own links before *Flashcards*.
90
+
91
+ The app has its own stylesheet, with no dependence on a CSS framework. Its colours follow the page's text colour and `color-scheme` (light or dark), unless the site sets these custom properties on `.flashdeck`: `--flashdeck-ink`, `--flashdeck-muted`, `--flashdeck-line`, `--flashdeck-surface`, `--flashdeck-input`, `--flashdeck-accent`, `--flashdeck-accent-on`, `--flashdeck-right`, `--flashdeck-nearly` and `--flashdeck-wrong`.
92
+
93
+ ## Marking
94
+
95
+ An answer is *right* if it matches one of the accepted answers (the model answer and its alternatives), ignoring capitals, runs of white space, typographic apostrophes and quotation marks (’ and '), a space before ; : ! or ? (which French typography puts there), and a final full stop, exclamation mark or question mark. It is *nearly right* if it differs from an accepted answer only in accents, punctuation, or ß, æ and œ written as ss, ae and oe. For a *case-sensitive* subject (such as German, whose nouns have capitals), a difference in capitals also makes an answer nearly right, not right. Anything else is *wrong*. A card is wrong if any of its answers is wrong, otherwise nearly right if any is nearly right, and otherwise right.
96
+
97
+ After an answer is marked nearly right or wrong, a logged-in user can press *I was right*. The answer is then saved as a *claimed answer*, the card is rescheduled as if it had been right, and the same answer is marked right for that user from then on. Claimed answers are listed in the admin, where accepting one adds it to the card's alternatives, so that it is marked right for everyone; rejecting one stops it being marked right for its user.
98
+
99
+ ## Scheduling
100
+
101
+ Scheduling uses FSRS, the [Free Spaced Repetition Scheduler](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm), through the fsrs package. A right answer counts as *Good*, a nearly right one as *Hard* and a wrong one as *Again*, so a card answered wrongly comes back within minutes.
102
+
103
+ A logged-in user is shown the card most overdue, otherwise a new card (up to `FLASHDECK_NEW_CARDS_PER_DAY`), and otherwise a page saying when the next card is due, with links to start another new card or to practise ahead. A visitor who is not logged in is shown cards from the public decks at random, and nothing is stored.
104
+
105
+ How often a card comes back is set by the chance of remembering it that FSRS aims for when it next comes back (its *target recall*): 0.9 normally. After answering a card, a logged-in user can choose under *Card options* to see it *More often* (0.95), *Less often* (0.85) or *Rarely* (0.8), which reschedules it at once. The effect compounds: after the second, third and fourth right answers, a card comes back after about 2, 11 and 46 days normally, but after 1, 3 and 8 days (more often), 4, 31 and 174 (less often) or 8, 76 and 525 (rarely). A user can also hide a card, from its question or its result, so that it is never shown again; hiding a card not yet answered does not count towards the day's new cards. Hidden cards, and cards with a changed frequency, are listed at `settings/` (linked from the index), where each choice can be undone.
106
+
107
+ The app's own pages take the slugs `settings`, `progress` and `reference`, which a subject or deck therefore cannot have.
108
+
109
+
110
+ ## Progress
111
+
112
+ The Progress page (`progress/`, for logged-in users) shows:
113
+
114
+ - the number of cards answered today, the percentage right and the number of days in a row with at least one answer;
115
+ - the answers on each of the last 30 days, as a bar chart split into right, nearly right and wrong (with the same figures as a table);
116
+ - for each deck, the cards seen, those *well learnt* (coming back after 21 days or more), those due, and the percentage right over the last 30 days;
117
+ - the cards most often missed (answered wrong or nearly right), and, for decks whose cards have labelled answers, the labels most often missed, such as the *nous* form of the subjonctif.
118
+
119
+ A right answer given after looking at the reference counts as nearly right, as it does for scheduling. The result of each answer also shows a one-line summary of the day.
120
+
121
+ ## Reference
122
+
123
+ Each subject, deck and *item* has a reference page, generated from the cards. A card is named in them by the first line of its prompt or, if it has an image (whose prompt is usually a question such as "Name this symbol"), by its answer.
124
+
125
+
126
+ - a deck's page (`<subject>/<deck>/reference/`) shows the deck's cards as a table, with their answers and notes;
127
+ - an item's page (`<subject>/reference/<key>/`) shows the cards that share a key across the subject's decks; for a subject whose decks are the tenses of verbs, keyed by verb, that is one verb in every tense;
128
+ - a subject's page (`<subject>/reference/`) lists its decks and an index of its items.
129
+
130
+ A subject and a deck can also have reference text of their own (rules, tables of endings, exceptions), written in Markdown with tables, which heads its page. Only editors write it, and it is not sanitised. Each page has a search, which narrows the table or index as one types, ignoring accents, capitals and punctuation, and matches every prompt and every accepted answer, so that a form met in a text (*serais*) finds its verb.
131
+
132
+ On a card, the *Reference* button or ⌘K (Ctrl+K) opens the card's deck reference in a dialog over the card, so that what has been typed is kept; its links open within the dialog, and Esc closes it. Opening it before marking makes a right answer count as nearly right, so that the card comes back sooner.
133
+
134
+ ## Images
135
+
136
+ Card images are stored with the site's media files, in `<FLASHDECK_UPLOAD_DIR>/<subject>/`, under random names: a file named after its subject (`beagle.jpg`) would give the answer away in the page. Photographs (PNG, JPEG and WebP) are reduced to `FLASHDECK_IMAGE_MAX_SIZE`, turned the right way up and stripped of their metadata (EXIF, XMP and comments), which often names what they show; only their colour profile is kept. SVG images are stored as they are. An SVG file can contain scripts, so only trusted editors should be able to upload one. A replaced image's file is deleted, as is a deleted card's.
137
+
138
+ The images are not kept in Wagtail's image library, whose files all share one directory, which would make a subject's images hard to move to another site or pass on.
139
+
140
+ ## Bundles
141
+
142
+ A bundle is a directory, or a zip file, holding `cards.yaml` and the image files that it names:
143
+
144
+ ```yaml
145
+ format: flashdeck/1
146
+ subject:
147
+ slug: french
148
+ title: French
149
+ language: fr # for screen readers and phone keyboards
150
+ characters: à â ç é è ê î ô ù û # buttons for typing them
151
+ case_sensitive: false
152
+ reference: … # Markdown, for the subject's reference page
153
+ decks:
154
+ - slug: conditionnel
155
+ title: 'Irregular verbs: conditionnel présent'
156
+ public: false
157
+ reference: |-
158
+ The stem of the futur simple with the endings of the imparfait.
159
+ cards:
160
+ - key: etre # identifies the card within its deck
161
+ prompt: |-
162
+ être (to be)
163
+ conditionnel présent
164
+ answers:
165
+ - {label: je, text: serais}
166
+ - {label: tu, text: serais}
167
+ # …
168
+ notes: 'Stem: ser- (irregular).'
169
+ - slug: animals
170
+ title: Animals
171
+ public: true
172
+ cards:
173
+ - key: dog
174
+ prompt: dog
175
+ answer: le chien # a single answer
176
+ - key: beagle
177
+ image: images/beagle.jpg
178
+ image_alt: A photograph of a dog
179
+ image_credit: A. Photographer, Wikimedia Commons
180
+ image_licence: CC BY-SA 4.0
181
+ answers:
182
+ - {text: beagle, alternatives: [le beagle]}
183
+ ```
184
+
185
+ An answer is either its text, or a mapping with `text` and optionally `label` and `alternatives`. YAML reads an unquoted `yes`, `no`, `on` or `off` as true or false, so answers such as the French pronoun *on* must be quoted; the import stops with a message if one is not.
186
+
187
+ ```bash
188
+ python manage.py flashdeck_import decks/french # or a .zip file
189
+ python manage.py flashdeck_import decks/french --update
190
+ python manage.py flashdeck_export french french.zip # or a directory
191
+ ```
192
+
193
+ On a Wagtail site, *Flashcards → Import and export* in the admin uploads a bundle as a zip file (of up to 100 MB, with no file in it over 20 MB) and downloads any subject as one, for editors with the permissions to add and change cards.
194
+
195
+ An import adds the subject, decks and cards that the site does not have, matching them by slug and key, and leaves existing ones alone. With `--update`, it also updates existing ones from the bundle. Alternatives accepted on the site are kept, a card's image is replaced only if the bundle's is different, and whether a deck is public is left as it is. An export writes everything about a subject, including edits made in the admin, as a bundle that can be imported elsewhere.
196
+
197
+ A script that generates a bundle can write its `cards.yaml` with `flashdeck.manifest.dump_manifest(data)`, which needs no Django set-up and puts short lists on one line and text of more than one line in literal blocks.
198
+
199
+ ## Development
200
+
201
+ The package has its own tests and settings (in `tests/`), independent of any site:
202
+
203
+ ```bash
204
+ uv sync --group dev
205
+ uv run pytest
206
+ uv run ruff check . && uv run ruff format --check .
207
+ ```
208
+
209
+ CI runs them with Python 3.12 to 3.14 and Django 5.2 to 6.1, and treats any warning raised in the package's own code, such as of a deprecation, as an error.
210
+
211
+ To release a new version, change `version` in `pyproject.toml`, add it to `CHANGELOG.md`, commit, and push a tag such as `v0.2.0`. The Release workflow then publishes it to PyPI, by trusted publishing.
212
+
213
+ ## Licence
214
+
215
+ The django-flashdeck package is licensed under the BSD 3-Clause License (see `LICENSE`).
@@ -0,0 +1,179 @@
1
+ # django-flashdeck
2
+
3
+ The django-flashdeck package adds flashcards to a Django site. A card's prompt is text, an image or both, and its answers are typed and marked as right, nearly right or wrong. A card may have several answers, each marked separately, such as the six persons of a conjugated verb. Logged-in users get *spaced repetition*: each card comes back at increasing intervals, timed for just before it would be forgotten. Cards are grouped into *decks* (topics) within *subjects* (such as French or dog breeds), and a subject's decks are written, moved between sites and passed on as *bundles*: a YAML file and its images.
4
+
5
+ The package needs Python 3.12 or later and Django 5.2 or later, and works with or without Wagtail. Decks of French verbs and of the circuit symbols of GCSE Physics, as bundles, are in [flashcard-decks](https://github.com/xnx/flashcard-decks); it runs at https://scipython.com/apps/flashcards/.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install django-flashdeck # or "django-flashdeck[wagtail]"
11
+ ```
12
+
13
+ Add the app and django-modelcluster (which it uses for editing a card's answers inline) to `INSTALLED_APPS`, include its URLs and migrate:
14
+
15
+ ```python
16
+ INSTALLED_APPS = [
17
+ ...,
18
+ "modelcluster",
19
+ "flashdeck",
20
+ ]
21
+ ```
22
+
23
+ ```python
24
+ urlpatterns = [
25
+ ...,
26
+ path("flashcards/", include("flashdeck.urls")),
27
+ ]
28
+ ```
29
+
30
+ Subjects, decks, cards and claimed answers (see "Marking") are edited in Django's admin and, on a Wagtail site, in Wagtail's admin under *Flashcards*. A deck is either public, for anyone to practise, or private, for users with the permission *Can practise private decks* (`flashdeck.practise_private`), which superusers have.
31
+
32
+ Practising needs no account: a visitor who is not logged in is shown cards from the public decks at random, and nothing is stored. A logged-in user's answers are stored, for scheduling and scores. The top of every page links to the site's login page (Django's `LOGIN_URL`, and back), if the site has one; for a logged-in user, it shows their name and, if `FLASHDECK_LOGOUT_URL` is set, a Log out button.
33
+
34
+ ## Settings
35
+
36
+ | Setting | Default | Meaning |
37
+ |---|---|---|
38
+ | `FLASHDECK_NEW_CARDS_PER_DAY` | `20` | New cards that each user starts per day, across all subjects |
39
+ | `FLASHDECK_IMAGE_MAX_SIZE` | `1200` | The size, in pixels, of the square that uploaded images are reduced to fit |
40
+ | `FLASHDECK_UPLOAD_DIR` | `"flashdeck"` | The directory, within the site's media storage, that holds card images |
41
+ | `FLASHDECK_LOGOUT_URL` | `""` | The URL, or URL name, at which to log out (by POST), for a Log out button on the index; none if blank |
42
+
43
+ ## Templates and styles
44
+
45
+ Every page extends `flashdeck/page.html`, which by default is a page of its own. A site overrides it to put the app inside its own pages:
46
+
47
+ ```django
48
+ {% extends "base.html" %}
49
+ {% block title %}{% block flashdeck_title %}{% endblock %}{% endblock %}
50
+ {% block content %}{% block flashdeck_content %}{% endblock %}{% endblock %}
51
+ ```
52
+
53
+ The breadcrumb trail starts with `flashdeck/_crumbs_root.html`, which is empty by default; a site can override it to add its own links before *Flashcards*.
54
+
55
+ The app has its own stylesheet, with no dependence on a CSS framework. Its colours follow the page's text colour and `color-scheme` (light or dark), unless the site sets these custom properties on `.flashdeck`: `--flashdeck-ink`, `--flashdeck-muted`, `--flashdeck-line`, `--flashdeck-surface`, `--flashdeck-input`, `--flashdeck-accent`, `--flashdeck-accent-on`, `--flashdeck-right`, `--flashdeck-nearly` and `--flashdeck-wrong`.
56
+
57
+ ## Marking
58
+
59
+ An answer is *right* if it matches one of the accepted answers (the model answer and its alternatives), ignoring capitals, runs of white space, typographic apostrophes and quotation marks (’ and '), a space before ; : ! or ? (which French typography puts there), and a final full stop, exclamation mark or question mark. It is *nearly right* if it differs from an accepted answer only in accents, punctuation, or ß, æ and œ written as ss, ae and oe. For a *case-sensitive* subject (such as German, whose nouns have capitals), a difference in capitals also makes an answer nearly right, not right. Anything else is *wrong*. A card is wrong if any of its answers is wrong, otherwise nearly right if any is nearly right, and otherwise right.
60
+
61
+ After an answer is marked nearly right or wrong, a logged-in user can press *I was right*. The answer is then saved as a *claimed answer*, the card is rescheduled as if it had been right, and the same answer is marked right for that user from then on. Claimed answers are listed in the admin, where accepting one adds it to the card's alternatives, so that it is marked right for everyone; rejecting one stops it being marked right for its user.
62
+
63
+ ## Scheduling
64
+
65
+ Scheduling uses FSRS, the [Free Spaced Repetition Scheduler](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm), through the fsrs package. A right answer counts as *Good*, a nearly right one as *Hard* and a wrong one as *Again*, so a card answered wrongly comes back within minutes.
66
+
67
+ A logged-in user is shown the card most overdue, otherwise a new card (up to `FLASHDECK_NEW_CARDS_PER_DAY`), and otherwise a page saying when the next card is due, with links to start another new card or to practise ahead. A visitor who is not logged in is shown cards from the public decks at random, and nothing is stored.
68
+
69
+ How often a card comes back is set by the chance of remembering it that FSRS aims for when it next comes back (its *target recall*): 0.9 normally. After answering a card, a logged-in user can choose under *Card options* to see it *More often* (0.95), *Less often* (0.85) or *Rarely* (0.8), which reschedules it at once. The effect compounds: after the second, third and fourth right answers, a card comes back after about 2, 11 and 46 days normally, but after 1, 3 and 8 days (more often), 4, 31 and 174 (less often) or 8, 76 and 525 (rarely). A user can also hide a card, from its question or its result, so that it is never shown again; hiding a card not yet answered does not count towards the day's new cards. Hidden cards, and cards with a changed frequency, are listed at `settings/` (linked from the index), where each choice can be undone.
70
+
71
+ The app's own pages take the slugs `settings`, `progress` and `reference`, which a subject or deck therefore cannot have.
72
+
73
+
74
+ ## Progress
75
+
76
+ The Progress page (`progress/`, for logged-in users) shows:
77
+
78
+ - the number of cards answered today, the percentage right and the number of days in a row with at least one answer;
79
+ - the answers on each of the last 30 days, as a bar chart split into right, nearly right and wrong (with the same figures as a table);
80
+ - for each deck, the cards seen, those *well learnt* (coming back after 21 days or more), those due, and the percentage right over the last 30 days;
81
+ - the cards most often missed (answered wrong or nearly right), and, for decks whose cards have labelled answers, the labels most often missed, such as the *nous* form of the subjonctif.
82
+
83
+ A right answer given after looking at the reference counts as nearly right, as it does for scheduling. The result of each answer also shows a one-line summary of the day.
84
+
85
+ ## Reference
86
+
87
+ Each subject, deck and *item* has a reference page, generated from the cards. A card is named in them by the first line of its prompt or, if it has an image (whose prompt is usually a question such as "Name this symbol"), by its answer.
88
+
89
+
90
+ - a deck's page (`<subject>/<deck>/reference/`) shows the deck's cards as a table, with their answers and notes;
91
+ - an item's page (`<subject>/reference/<key>/`) shows the cards that share a key across the subject's decks; for a subject whose decks are the tenses of verbs, keyed by verb, that is one verb in every tense;
92
+ - a subject's page (`<subject>/reference/`) lists its decks and an index of its items.
93
+
94
+ A subject and a deck can also have reference text of their own (rules, tables of endings, exceptions), written in Markdown with tables, which heads its page. Only editors write it, and it is not sanitised. Each page has a search, which narrows the table or index as one types, ignoring accents, capitals and punctuation, and matches every prompt and every accepted answer, so that a form met in a text (*serais*) finds its verb.
95
+
96
+ On a card, the *Reference* button or ⌘K (Ctrl+K) opens the card's deck reference in a dialog over the card, so that what has been typed is kept; its links open within the dialog, and Esc closes it. Opening it before marking makes a right answer count as nearly right, so that the card comes back sooner.
97
+
98
+ ## Images
99
+
100
+ Card images are stored with the site's media files, in `<FLASHDECK_UPLOAD_DIR>/<subject>/`, under random names: a file named after its subject (`beagle.jpg`) would give the answer away in the page. Photographs (PNG, JPEG and WebP) are reduced to `FLASHDECK_IMAGE_MAX_SIZE`, turned the right way up and stripped of their metadata (EXIF, XMP and comments), which often names what they show; only their colour profile is kept. SVG images are stored as they are. An SVG file can contain scripts, so only trusted editors should be able to upload one. A replaced image's file is deleted, as is a deleted card's.
101
+
102
+ The images are not kept in Wagtail's image library, whose files all share one directory, which would make a subject's images hard to move to another site or pass on.
103
+
104
+ ## Bundles
105
+
106
+ A bundle is a directory, or a zip file, holding `cards.yaml` and the image files that it names:
107
+
108
+ ```yaml
109
+ format: flashdeck/1
110
+ subject:
111
+ slug: french
112
+ title: French
113
+ language: fr # for screen readers and phone keyboards
114
+ characters: à â ç é è ê î ô ù û # buttons for typing them
115
+ case_sensitive: false
116
+ reference: … # Markdown, for the subject's reference page
117
+ decks:
118
+ - slug: conditionnel
119
+ title: 'Irregular verbs: conditionnel présent'
120
+ public: false
121
+ reference: |-
122
+ The stem of the futur simple with the endings of the imparfait.
123
+ cards:
124
+ - key: etre # identifies the card within its deck
125
+ prompt: |-
126
+ être (to be)
127
+ conditionnel présent
128
+ answers:
129
+ - {label: je, text: serais}
130
+ - {label: tu, text: serais}
131
+ # …
132
+ notes: 'Stem: ser- (irregular).'
133
+ - slug: animals
134
+ title: Animals
135
+ public: true
136
+ cards:
137
+ - key: dog
138
+ prompt: dog
139
+ answer: le chien # a single answer
140
+ - key: beagle
141
+ image: images/beagle.jpg
142
+ image_alt: A photograph of a dog
143
+ image_credit: A. Photographer, Wikimedia Commons
144
+ image_licence: CC BY-SA 4.0
145
+ answers:
146
+ - {text: beagle, alternatives: [le beagle]}
147
+ ```
148
+
149
+ An answer is either its text, or a mapping with `text` and optionally `label` and `alternatives`. YAML reads an unquoted `yes`, `no`, `on` or `off` as true or false, so answers such as the French pronoun *on* must be quoted; the import stops with a message if one is not.
150
+
151
+ ```bash
152
+ python manage.py flashdeck_import decks/french # or a .zip file
153
+ python manage.py flashdeck_import decks/french --update
154
+ python manage.py flashdeck_export french french.zip # or a directory
155
+ ```
156
+
157
+ On a Wagtail site, *Flashcards → Import and export* in the admin uploads a bundle as a zip file (of up to 100 MB, with no file in it over 20 MB) and downloads any subject as one, for editors with the permissions to add and change cards.
158
+
159
+ An import adds the subject, decks and cards that the site does not have, matching them by slug and key, and leaves existing ones alone. With `--update`, it also updates existing ones from the bundle. Alternatives accepted on the site are kept, a card's image is replaced only if the bundle's is different, and whether a deck is public is left as it is. An export writes everything about a subject, including edits made in the admin, as a bundle that can be imported elsewhere.
160
+
161
+ A script that generates a bundle can write its `cards.yaml` with `flashdeck.manifest.dump_manifest(data)`, which needs no Django set-up and puts short lists on one line and text of more than one line in literal blocks.
162
+
163
+ ## Development
164
+
165
+ The package has its own tests and settings (in `tests/`), independent of any site:
166
+
167
+ ```bash
168
+ uv sync --group dev
169
+ uv run pytest
170
+ uv run ruff check . && uv run ruff format --check .
171
+ ```
172
+
173
+ CI runs them with Python 3.12 to 3.14 and Django 5.2 to 6.1, and treats any warning raised in the package's own code, such as of a deprecation, as an error.
174
+
175
+ To release a new version, change `version` in `pyproject.toml`, add it to `CHANGELOG.md`, commit, and push a tag such as `v0.2.0`. The Release workflow then publishes it to PyPI, by trusted publishing.
176
+
177
+ ## Licence
178
+
179
+ The django-flashdeck package is licensed under the BSD 3-Clause License (see `LICENSE`).
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "django-flashdeck"
3
+ version = "0.1.0"
4
+ description = "Flashcards for Django sites: decks of cards with typed answers, marked right or wrong, and spaced repetition."
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Christian Hill" }]
9
+ requires-python = ">=3.12"
10
+ keywords = ["django", "wagtail", "flashcards", "spaced repetition", "fsrs"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Web Environment",
14
+ "Framework :: Django",
15
+ "Framework :: Django :: 5.2",
16
+ "Framework :: Django :: 6.0",
17
+ "Framework :: Django :: 6.1",
18
+ "Framework :: Wagtail",
19
+ "Intended Audience :: Education",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Education",
26
+ ]
27
+ dependencies = [
28
+ "django>=5.2",
29
+ "django-modelcluster>=6.0",
30
+ "fsrs>=6.3.2",
31
+ "markdown>=3.11.1",
32
+ "pillow>=10.0",
33
+ "pyyaml>=6.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ wagtail = ["wagtail>=7.0"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/xnx/django-flashdeck"
41
+ Changelog = "https://github.com/xnx/django-flashdeck/blob/main/CHANGELOG.md"
42
+ Decks = "https://github.com/xnx/flashcard-decks"
43
+
44
+ [dependency-groups]
45
+ dev = ["pytest", "pytest-django", "ruff"]
46
+
47
+ [build-system]
48
+ requires = ["hatchling>=1.27"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/flashdeck"]
53
+
54
+ [tool.pytest.ini_options]
55
+ DJANGO_SETTINGS_MODULE = "tests.settings"
56
+ pythonpath = ["."]
57
+ testpaths = ["tests"]
58
+ # A warning raised in the package's own code, such as of a deprecation, fails.
59
+ filterwarnings = ["error:::flashdeck"]
60
+
61
+ [tool.ruff]
62
+ line-length = 88
63
+ target-version = "py312"
64
+ src = ["src", "."]
65
+ extend-exclude = ["*/migrations/*"]
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "B", "UP", "DJ"]
69
+ ignore = ["E501"] # line length is left to `ruff format`
@@ -0,0 +1 @@
1
+ """Flashcards for Django sites."""