question_media replaced the two filename columns months ago: any number of figures per question, each with a role, a label the prose can refer to, a caption and an order. Only the editor's own endpoint ever read them. The editor showed the two legacy text fields, and the player and the answer review rendered the legacy paths — so the model existed and nothing used it. - FigureManager in the question editor: add from the image bank, name, caption, reorder, remove, per role. A figure with no caption is called out, because a caption is how anyone finds it again. The image id is shown, since that is what the link survives a rename by. - FigureStrip on the player and the review. Explanation figures are labelled thumbnails that open full size and page between them — a stack of full-width radiographs between two paragraphs pushes the explanation off the screen, and "as in Figure 2" needs Figure 2 to be named where it sits. A stem figure stays full size: it is the question. - question_figures.py is the single place rows become what a page renders, so the three views cannot disagree. - Explanation figures are withheld until answers are revealed, the same rule the explanation itself follows. The legacy paths still render where a question was never backfilled, so nothing that worked before stops working. Backend 242/242, frontend 290/290. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
56 lines
2.2 KiB
Python
56 lines
2.2 KiB
Python
"""The figures attached to a question.
|
|
|
|
A question used to carry one stem image and one explanation image as two
|
|
filename columns. `question_media` replaced that with rows — any number of
|
|
figures, each with a role, a label the prose can refer to, a caption and an
|
|
order — but only the editor's own endpoint ever read them, so the quiz player
|
|
and the answer review still showed the two legacy paths.
|
|
|
|
This is the one place that turns those rows into what a page renders, so the
|
|
editor, the player and the review cannot disagree about what is on a question.
|
|
"""
|
|
from collections import defaultdict
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.media import MediaAsset
|
|
from app.models.question_media import QuestionMedia
|
|
|
|
|
|
def figure_json(link: QuestionMedia, asset: MediaAsset) -> dict:
|
|
return {
|
|
"id": link.id,
|
|
"media_id": link.media_id,
|
|
"role": link.role,
|
|
# The label the prose refers to. Falls back to a number so a figure is
|
|
# never nameless, which is what makes "see the figure" ambiguous.
|
|
"label": link.label or f"Figure {link.position + 1}",
|
|
"caption": link.caption or getattr(asset, "caption", None),
|
|
"title": getattr(asset, "title", None),
|
|
"path": getattr(asset, "path", None),
|
|
"position": link.position,
|
|
}
|
|
|
|
|
|
def figures_for_questions(db: Session, question_ids) -> dict[int, list[dict]]:
|
|
"""Figures for many questions at once, keyed by question id.
|
|
|
|
One query for a whole quiz rather than one per question — a 240-question
|
|
exam would otherwise fan out into 240 round trips to build one page.
|
|
"""
|
|
ids = [qid for qid in dict.fromkeys(question_ids) if qid]
|
|
if not ids:
|
|
return {}
|
|
rows = (db.query(QuestionMedia, MediaAsset)
|
|
.join(MediaAsset, MediaAsset.id == QuestionMedia.media_id)
|
|
.filter(QuestionMedia.question_id.in_(ids))
|
|
.order_by(QuestionMedia.role, QuestionMedia.position, QuestionMedia.id)
|
|
.all())
|
|
out: dict[int, list[dict]] = defaultdict(list)
|
|
for link, asset in rows:
|
|
out[link.question_id].append(figure_json(link, asset))
|
|
return out
|
|
|
|
|
|
def figures_for(db: Session, question_id: int) -> list[dict]:
|
|
return figures_for_questions(db, [question_id]).get(question_id, [])
|