django-flashdeck 0.1.0__py3-none-any.whl

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 (53) hide show
  1. django_flashdeck-0.1.0.dist-info/METADATA +215 -0
  2. django_flashdeck-0.1.0.dist-info/RECORD +53 -0
  3. django_flashdeck-0.1.0.dist-info/WHEEL +4 -0
  4. django_flashdeck-0.1.0.dist-info/licenses/LICENSE +28 -0
  5. flashdeck/__init__.py +1 -0
  6. flashdeck/admin.py +68 -0
  7. flashdeck/apps.py +8 -0
  8. flashdeck/bundles.py +346 -0
  9. flashdeck/conf.py +19 -0
  10. flashdeck/images.py +85 -0
  11. flashdeck/management/__init__.py +0 -0
  12. flashdeck/management/commands/__init__.py +0 -0
  13. flashdeck/management/commands/flashdeck_export.py +23 -0
  14. flashdeck/management/commands/flashdeck_import.py +29 -0
  15. flashdeck/manifest.py +33 -0
  16. flashdeck/marking.py +107 -0
  17. flashdeck/migrations/0001_initial.py +146 -0
  18. flashdeck/migrations/0002_card_options.py +44 -0
  19. flashdeck/migrations/0003_reference.py +28 -0
  20. flashdeck/migrations/__init__.py +0 -0
  21. flashdeck/models.py +325 -0
  22. flashdeck/practice.py +232 -0
  23. flashdeck/reference.py +108 -0
  24. flashdeck/scheduling.py +75 -0
  25. flashdeck/static/flashdeck/flashdeck.css +241 -0
  26. flashdeck/static/flashdeck/flashdeck.js +137 -0
  27. flashdeck/stats.py +248 -0
  28. flashdeck/templates/flashdeck/_account.html +1 -0
  29. flashdeck/templates/flashdeck/_assets.html +2 -0
  30. flashdeck/templates/flashdeck/_crumbs.html +1 -0
  31. flashdeck/templates/flashdeck/_crumbs_root.html +1 -0
  32. flashdeck/templates/flashdeck/_forms.html +3 -0
  33. flashdeck/templates/flashdeck/_prompt.html +5 -0
  34. flashdeck/templates/flashdeck/_reference_dialog.html +8 -0
  35. flashdeck/templates/flashdeck/_search.html +5 -0
  36. flashdeck/templates/flashdeck/admin/bundles.html +24 -0
  37. flashdeck/templates/flashdeck/fragment.html +1 -0
  38. flashdeck/templates/flashdeck/index.html +29 -0
  39. flashdeck/templates/flashdeck/nothing_due.html +21 -0
  40. flashdeck/templates/flashdeck/page.html +23 -0
  41. flashdeck/templates/flashdeck/progress.html +80 -0
  42. flashdeck/templates/flashdeck/question.html +46 -0
  43. flashdeck/templates/flashdeck/reference_deck.html +28 -0
  44. flashdeck/templates/flashdeck/reference_item.html +18 -0
  45. flashdeck/templates/flashdeck/reference_subject.html +19 -0
  46. flashdeck/templates/flashdeck/result.html +56 -0
  47. flashdeck/templates/flashdeck/settings.html +44 -0
  48. flashdeck/templatetags/__init__.py +0 -0
  49. flashdeck/templatetags/flashdeck_tags.py +52 -0
  50. flashdeck/urls.py +31 -0
  51. flashdeck/views.py +392 -0
  52. flashdeck/wagtail_hooks.py +109 -0
  53. flashdeck/wagtail_views.py +106 -0
@@ -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,53 @@
1
+ flashdeck/__init__.py,sha256=OXGh4SZWEbxifOyC8UyJTB8k4dVPQqs1wFfNUXWnK30,35
2
+ flashdeck/admin.py,sha256=ddSroFlCPAJ9jIUEJ8DgJIg3-nfWzKYqWar5UxSsAiQ,2325
3
+ flashdeck/apps.py,sha256=ICmdf55jKdMOA9o4yOkA-1u9g_KRQY2-_GTSY8k98-I,264
4
+ flashdeck/bundles.py,sha256=_aJ6-r6VPYT_uDinpcMbX5goP5UytdxKvRFv9Sc0vLE,13687
5
+ flashdeck/conf.py,sha256=6KFOYFnWJtBQ-QC8h1dz_HeI2GDN2jeX5E3_ZHkIM7Q,688
6
+ flashdeck/images.py,sha256=6xBj5zeH80qEpF1jpBFJs6uOIMofb1cBmn51_xszUjg,3222
7
+ flashdeck/manifest.py,sha256=whwp0PQe5AGOxTD-IURNgNlFHCN1FJQ4YK17v0p2K_Q,890
8
+ flashdeck/marking.py,sha256=Ok_u5AhZQ7LjWRiMBGdDvBNW7_WjLvaw9_MmnyoFApo,3953
9
+ flashdeck/models.py,sha256=c7m4FB4H-xUvvZZLwUW0Dhnb6PkcBUKzEDyBHYIlZGk,11902
10
+ flashdeck/practice.py,sha256=kKP9WGE5VCMcaWWpNO8W5L05L6UdtIUAIe-8tpDjbz4,8490
11
+ flashdeck/reference.py,sha256=18krxFRe8nPfAvli_B-bFVULZz2sap06o1JIQ6punQo,3478
12
+ flashdeck/scheduling.py,sha256=_NLgBKiAQT8P6LRLpAuQwrUKnsQDhh73agKGlwwKKOw,2404
13
+ flashdeck/stats.py,sha256=tbVNwZrbGJIntR65D6bHUDZ5wYOTVSOSMdyn162jrPc,7677
14
+ flashdeck/urls.py,sha256=ZQAGwN4zJlI4sTpjPFNrzRZlFDdzH3c5A373cDFoKas,917
15
+ flashdeck/views.py,sha256=GEb1oj5oAefhAZiuAT5-FaRStWPb1DsfmiZVFJuF9l8,13581
16
+ flashdeck/wagtail_hooks.py,sha256=S7Bka85ItAvNmqFg5UvKncU8nZYHOuY4OTn-nVqimVg,3134
17
+ flashdeck/wagtail_views.py,sha256=m7XliKQj8Q0vNb_BHJ-nlu31jJ8-j2p5Mfzu3ezbGwE,4058
18
+ flashdeck/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ flashdeck/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ flashdeck/management/commands/flashdeck_export.py,sha256=u1H--_0s7T0mWnVi42xidwHGKXcdwdqZ5PF3SXD-Cew,891
21
+ flashdeck/management/commands/flashdeck_import.py,sha256=kiZBEJUKb8DxKGMj48YySLxuHbqDJCjmkCZ4aqVtPmA,1189
22
+ flashdeck/migrations/0001_initial.py,sha256=TKzD-syNguOjj2bFAejhmJijIYzP_3aXe37LoqZJS_c,8752
23
+ flashdeck/migrations/0002_card_options.py,sha256=WZosaZAbl2ROQXS6nJajr9xYv8y_TKOVlmy7cZ1F7Zs,1521
24
+ flashdeck/migrations/0003_reference.py,sha256=b0oNwxobLdpNscqUEnAFZcgRCP284FHrf01pfzkL85Y,948
25
+ flashdeck/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ flashdeck/static/flashdeck/flashdeck.css,sha256=IF8JceAo_4k8h2IPlayM-F4CrrZbk-lvm8yZYdG6cNw,13347
27
+ flashdeck/static/flashdeck/flashdeck.js,sha256=IKmd8che7DcYXaHojBJOKMLvznsPdFwBdwZFKoZgmLo,5031
28
+ flashdeck/templates/flashdeck/_account.html,sha256=PxhQ-dN6tEgJ5LcbUb5XAoIsqMLgNbUW1M9pcKgNd_w,423
29
+ flashdeck/templates/flashdeck/_assets.html,sha256=LOmqGSxvTeG5ybI3u5zHkzFdloZFA0UBNJdBMsxYsL0,155
30
+ flashdeck/templates/flashdeck/_crumbs.html,sha256=KkLgskUgIIEzqX67RvK9nLbTysXBSVjDjNDGF7Edy9o,417
31
+ flashdeck/templates/flashdeck/_crumbs_root.html,sha256=lDj97fu3hpPEXv-DBwqf2XxVESyaqzlEkZUKIzVt5HU,157
32
+ flashdeck/templates/flashdeck/_forms.html,sha256=t0OAkyuuzWP6CeelZVPRwvEQCHvSXtz3usVuVi006Ps,478
33
+ flashdeck/templates/flashdeck/_prompt.html,sha256=QNNX4U_YKNLAb1YlCtU-rL_tKXHR5Iv2Q3ySJDU8C9E,315
34
+ flashdeck/templates/flashdeck/_reference_dialog.html,sha256=PU06tXAtU6I0vTpO32rUT-zRfTcbt-vVShiMwJt9ZK4,529
35
+ flashdeck/templates/flashdeck/_search.html,sha256=EJoz_Zt45ttUSp-IgfxNYo5eTyw-5uwaFvdNeYyp2Cs,497
36
+ flashdeck/templates/flashdeck/fragment.html,sha256=SyltzO6OsiHNnEJC_jKu1ovJm7RVLewKfcqhtobBS-Q,173
37
+ flashdeck/templates/flashdeck/index.html,sha256=KW2Z9j38pLaFW7EziBH4DIQjQsP3V4ghHEJTQeX_9XA,1618
38
+ flashdeck/templates/flashdeck/nothing_due.html,sha256=Cjpti4vpJorVlbHlWt_EuDFlF2RwDECeGYOnFwFOOHs,935
39
+ flashdeck/templates/flashdeck/page.html,sha256=jUzLDrbXo3kpqkSm-_sfYSguBGpCo4y13vW7XzVMqwM,841
40
+ flashdeck/templates/flashdeck/progress.html,sha256=Kcsvj3QaEOgHSez2tjd72EAXu2BBXBn6ZNtT-Ylox7c,4482
41
+ flashdeck/templates/flashdeck/question.html,sha256=w8QLrGpcZ-7re69Lb6eGScOBT0fLWfsY5w_A6Lmkx1U,2692
42
+ flashdeck/templates/flashdeck/reference_deck.html,sha256=6tzILnHbMmONxK1STWHeAqkWKwT-A8ly4_Wgyu5sDko,1773
43
+ flashdeck/templates/flashdeck/reference_item.html,sha256=O2HX752e4TAvW0YopznGAtuZ7VmOseN6RMUofvXyBnE,1400
44
+ flashdeck/templates/flashdeck/reference_subject.html,sha256=EBaJ99C41TjOpmOKB_fkvZlCxuFKT1Je0DXq4H0JXSM,1374
45
+ flashdeck/templates/flashdeck/result.html,sha256=THCCyml65JEEFP_saL457e54ghgCL1YuG8PiWgkQGZE,4374
46
+ flashdeck/templates/flashdeck/settings.html,sha256=ny7IXgbuRrihoooHaZFeS1bhTwb-_MZt7_vfGVeKaNA,1772
47
+ flashdeck/templates/flashdeck/admin/bundles.html,sha256=duJyGRjDvOXyBS_Dtoxa5HOFICDKMP0BHQN9rooguZ8,1168
48
+ flashdeck/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ flashdeck/templatetags/flashdeck_tags.py,sha256=3PyMg_pEXa7gn6kdiaprDGP8CIP2PxolHma3wPaZXtU,1668
50
+ django_flashdeck-0.1.0.dist-info/METADATA,sha256=ENdk7YPxXFf2SMwQKpoBTHeJ7DkVmiGEQDFAUEQIixA,14176
51
+ django_flashdeck-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
52
+ django_flashdeck-0.1.0.dist-info/licenses/LICENSE,sha256=ATVszkWIu32IrI6wo8Sdxbr05TILannMDG-0wD_lNKQ,1501
53
+ django_flashdeck-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.
flashdeck/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Flashcards for Django sites."""
flashdeck/admin.py ADDED
@@ -0,0 +1,68 @@
1
+ """Editing in Django's admin. (On a Wagtail site, wagtail_hooks.py also adds the
2
+ same models to Wagtail's admin, as snippets.)"""
3
+
4
+ from django.contrib import admin
5
+
6
+ from .models import Answer, Card, ClaimedAnswer, Deck, Subject
7
+
8
+
9
+ @admin.register(Subject)
10
+ class SubjectAdmin(admin.ModelAdmin):
11
+ list_display = ["title", "slug", "language", "position"]
12
+ prepopulated_fields = {"slug": ["title"]}
13
+
14
+
15
+ @admin.register(Deck)
16
+ class DeckAdmin(admin.ModelAdmin):
17
+ list_display = ["title", "subject", "is_public", "position"]
18
+ list_filter = ["subject", "is_public"]
19
+ prepopulated_fields = {"slug": ["title"]}
20
+
21
+
22
+ class AnswerInline(admin.TabularInline):
23
+ model = Answer
24
+ fields = ["label", "text", "alternatives"]
25
+ extra = 1
26
+
27
+
28
+ @admin.register(Card)
29
+ class CardAdmin(admin.ModelAdmin):
30
+ list_display = ["__str__", "deck", "key"]
31
+ list_filter = ["deck__subject", "deck"]
32
+ search_fields = ["prompt", "key"]
33
+ inlines = [AnswerInline]
34
+
35
+ def save_formset(self, request, form, formset, change):
36
+ super().save_formset(request, form, formset, change)
37
+ # Keep the answers in the order in which they appear on the form.
38
+ kept = [
39
+ f.instance
40
+ for f in formset.forms
41
+ if f.instance.pk and f not in formset.deleted_forms
42
+ ]
43
+ for sort_order, answer in enumerate(kept):
44
+ if answer.sort_order != sort_order:
45
+ answer.sort_order = sort_order
46
+ answer.save(update_fields=["sort_order"])
47
+
48
+
49
+ @admin.register(ClaimedAnswer)
50
+ class ClaimedAnswerAdmin(admin.ModelAdmin):
51
+ list_display = ["text", "answer", "user", "status", "created"]
52
+ list_filter = ["status"]
53
+ fields = ["answer", "user", "text", "status", "created"]
54
+ readonly_fields = ["answer", "user", "text", "created"]
55
+ actions = ["accept", "reject"]
56
+
57
+ def has_add_permission(self, request):
58
+ return False
59
+
60
+ @admin.action(description="Accept: mark these answers right for everyone")
61
+ def accept(self, request, queryset):
62
+ for claimed in queryset.select_related("answer__card__deck__subject"):
63
+ claimed.status = ClaimedAnswer.Status.ACCEPTED
64
+ claimed.save()
65
+
66
+ @admin.action(description="Reject")
67
+ def reject(self, request, queryset):
68
+ queryset.update(status=ClaimedAnswer.Status.REJECTED)
flashdeck/apps.py ADDED
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class FlashdeckConfig(AppConfig):
5
+ name = "flashdeck"
6
+ verbose_name = "Flashcards"
7
+ # Set here, so that the app's migrations do not depend on the site's setting.
8
+ default_auto_field = "django.db.models.BigAutoField"