Grants¶
A grant makes somebody an editor of something they do not otherwise reach. It
is handed out by a moderator or an admin on the Access page (/access),
never self-assigned, never derived from an SSO claim.
There are three grant tables behind four kinds of thing.
The tables¶
| Table | Names | Written from |
|---|---|---|
category_grants |
a category branch, an exam, a discipline tag, or a question folder | /access (category and folder only) |
media_library_grants |
one image library | /access |
flashcard_deck_grants |
one deck, or a category branch of decks | /flashcards/grants (moderator-only) |
category_grants is one row per grant with four nullable target columns —
category_id, exam_id, tag_id, folder_id — plus user_id, granted_by
and created_at. A single row may set more than one, and they combine with
AND: a row naming both a category and an exam covers only questions that
are in that branch and on that exam. An unset column means "any".
flashcard_deck_grants carries a check constraint: a row must name exactly
one of deck_id or category_id, never both and never neither.
The /access page deliberately exposes only three of these dimensions —
category, library, folder. Exam-scoped and tag-scoped grants exist in the
schema and in the permission predicate, but nothing in the UI writes them.
A category branch¶
The workhorse. It covers the category named and everything beneath it, and it keeps covering things filed there later — a branch is a description, not a list.
Branch descent is a plain Python walk over (id, parent_id) pairs
(_descendants in utils/category_grants.py), not a SQL recursive CTE. Two
other places in the codebase walk the same tree independently:
access.py for the reach counts, and quiz_builder.category_descendants for
deck and builder scoping.
granted_category_scope(db, user) returns:
Nonefor a moderator or admin — meaning every category, no restriction- a
set[int]of category ids for anybody else, possibly empty
A branch grant lets its holder edit:
- the questions filed in it, by primary category or by an additional category link
- the articles filed in it
- the decks whose category falls in it — only if a deck grant names that branch; a question-category grant alone does not carry decks
An image library¶
media_library_grants names one MediaLibrary. Its holder may:
- list the libraries they hold
- upload a new asset into one
- edit an asset's metadata, tags and overlay
- move an asset between libraries they hold both ends of
They may not: delete an asset (DELETE /media/{id} is moderator-only),
create a library, or grant one to anybody else.
Placing an existing picture on an article needs both halves: an article-editing grant covering where the article is filed, and the library the picture lives in. The branch says what you may write about; the library says which pictures you may use, and neither is a way into the other.
A deck¶
flashcard_deck_grants names one deck or one category branch of decks.
can_edit_deck says yes to, in order: a moderator; the deck's owner
(deck.user_id == user.id); anybody whose grants cover the deck's id.
Starting a new deck is moderator-only (may_start_a_deck returns
bool(user.is_moderator)) — a deck grant is permission to look after the decks
it names and write cards into them, not to add another deck to the tree.
Deleting, restoring and sharing a deck are not moderator-only: all three go
through can_edit_deck, so a deck-grant holder may do them on a deck they
hold. The bin (GET /flashcards/trash) answers with an empty list for anybody
who is not a moderator, so a grant holder who deletes a deck cannot restore it
themselves. Only force-unshare is admin-only.
A grant naming a since-deleted category silently covers nothing rather than erroring.
A known gap
Four card-linking endpoints — linking and unlinking a card to a question or
to an article — gate on is_moderator directly rather than going through
deck_access.can_edit_deck. A non-moderator deck-grant holder is refused
there even on a deck they can otherwise edit fully. The rest of
flashcards.py honours grants correctly; this is a real inconsistency in
_own_card_or_404, not a rule.
A question folder¶
A folder is a list, not a description: it covers exactly the questions somebody put in it and nothing filed there implicitly or later.
A folder grant reaches questions only. It has no effect on article editing,
deck access, or the manageable-categories calculation — all of those read only
the category_id dimension.
Folder membership is deliberately never editable by a grant holder. Only the folder's owner or a moderator can add or remove questions. Otherwise the holder of a folder grant could add any question to the folder and so widen their own grant.
What a grant does not carry¶
Verified against the routes:
| Action | Who |
|---|---|
| Create an article | moderator |
| Delete an article | moderator |
| Move an article through draft → in review → published | moderator |
| Create a question | moderator |
| Permanently delete a question | moderator |
| Change a question's category, or bulk-recategorise | moderator |
| Add, update or delete a question figure | moderator |
| Create a deck | moderator (deleting and sharing one you hold is not restricted — see above) |
| Create an image library; grant or revoke one | moderator |
| Delete or restore a media asset | moderator |
| Unlink a question from an article | moderator |
Everything on /access |
moderator |
| Set a role | admin (/admin) or moderator (/access, user/moderator only) |
An article with no category is in nobody's branch: can_edit_article
returns False for every non-moderator regardless of grants. Filing it under a
category is the fix.
Draft status is not an edit gate. can_edit_article never reads
article.status — a grantee edits a draft in their branch exactly as they edit
a published article in it. Status only gates reading: an unpublished article
404s or is hidden for a non-moderator.
Where a grant holder works¶
Editorial (/editorial). It is open to any grant holder, shows the same
queues a moderator sees over the branches they hold, and carries the library
browser below them. Reading (/articles) is a learner's surface and has no
editing controls at all.
The question manager (/questions/manage) and the question editor
(/questions/:id) are gated on questionManager rather than on the moderator
role, precisely so a grant holder can reach them; the server then refuses
anything outside the grant.
The bin, the taxonomy (/categories), the uploader, the job list and the
change log stay moderator-only — those are decisions about the whole bank
rather than about one branch of it.