Skip to content

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:

  • None for 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.