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.
- django_flashdeck-0.1.0.dist-info/METADATA +215 -0
- django_flashdeck-0.1.0.dist-info/RECORD +53 -0
- django_flashdeck-0.1.0.dist-info/WHEEL +4 -0
- django_flashdeck-0.1.0.dist-info/licenses/LICENSE +28 -0
- flashdeck/__init__.py +1 -0
- flashdeck/admin.py +68 -0
- flashdeck/apps.py +8 -0
- flashdeck/bundles.py +346 -0
- flashdeck/conf.py +19 -0
- flashdeck/images.py +85 -0
- flashdeck/management/__init__.py +0 -0
- flashdeck/management/commands/__init__.py +0 -0
- flashdeck/management/commands/flashdeck_export.py +23 -0
- flashdeck/management/commands/flashdeck_import.py +29 -0
- flashdeck/manifest.py +33 -0
- flashdeck/marking.py +107 -0
- flashdeck/migrations/0001_initial.py +146 -0
- flashdeck/migrations/0002_card_options.py +44 -0
- flashdeck/migrations/0003_reference.py +28 -0
- flashdeck/migrations/__init__.py +0 -0
- flashdeck/models.py +325 -0
- flashdeck/practice.py +232 -0
- flashdeck/reference.py +108 -0
- flashdeck/scheduling.py +75 -0
- flashdeck/static/flashdeck/flashdeck.css +241 -0
- flashdeck/static/flashdeck/flashdeck.js +137 -0
- flashdeck/stats.py +248 -0
- flashdeck/templates/flashdeck/_account.html +1 -0
- flashdeck/templates/flashdeck/_assets.html +2 -0
- flashdeck/templates/flashdeck/_crumbs.html +1 -0
- flashdeck/templates/flashdeck/_crumbs_root.html +1 -0
- flashdeck/templates/flashdeck/_forms.html +3 -0
- flashdeck/templates/flashdeck/_prompt.html +5 -0
- flashdeck/templates/flashdeck/_reference_dialog.html +8 -0
- flashdeck/templates/flashdeck/_search.html +5 -0
- flashdeck/templates/flashdeck/admin/bundles.html +24 -0
- flashdeck/templates/flashdeck/fragment.html +1 -0
- flashdeck/templates/flashdeck/index.html +29 -0
- flashdeck/templates/flashdeck/nothing_due.html +21 -0
- flashdeck/templates/flashdeck/page.html +23 -0
- flashdeck/templates/flashdeck/progress.html +80 -0
- flashdeck/templates/flashdeck/question.html +46 -0
- flashdeck/templates/flashdeck/reference_deck.html +28 -0
- flashdeck/templates/flashdeck/reference_item.html +18 -0
- flashdeck/templates/flashdeck/reference_subject.html +19 -0
- flashdeck/templates/flashdeck/result.html +56 -0
- flashdeck/templates/flashdeck/settings.html +44 -0
- flashdeck/templatetags/__init__.py +0 -0
- flashdeck/templatetags/flashdeck_tags.py +52 -0
- flashdeck/urls.py +31 -0
- flashdeck/views.py +392 -0
- flashdeck/wagtail_hooks.py +109 -0
- 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,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