pdf-quiz-generator/backend/app/services/question_figures.py
Daniel e311a6b5ad fix: choosing is answering; figures say only what an educator wrote
Study mode held a choice as a draft and waited for "Submit response" — a
second press to confirm something already decided, on every question.
Clicking an option marks it now, green or red, with the explanation.
Free text is the exception and keeps Enter, because typing is not
choosing.

Figures carried a generated caption: "Figure from question #3360 (from
images/doc_23/page_704_img_0.jpeg)". That describes the database, not
the picture, and showed a learner an internal file path. 341 of them are
cleared, the indexer no longer writes them, and an unlabelled figure now
says nothing rather than "Figure 1". A screen reader still gets the
label and caption when there are any, and the position when there are
not.

Suspend, Restart and Edit are gone from above the question. Three
buttons over a question nobody was looking away from to press them; Exit
is in the bar at the bottom with the session's own controls, and
restarting and editing belong to the session list and the editor.

And iOS Safari's zoom-on-focus is fixed once rather than per field.
Safari zooms the whole page in when a control smaller than 16px takes
focus and never zooms back out, leaving the layout scaled and broken. It
was being remembered at each individual field, which meant it was
forgotten at most of them — a dozen were still under 16px. One rule for
every control on a coarse pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 00:19:35 +02:00

59 lines
2.4 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.
# An educator's label, or nothing. "Figure 1" and "Figure from question
# #3360" told a learner only that an image was an image, and the second
# one told them the internal path it came from as well.
"label": link.label or None,
"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, [])