"""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, # 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, # What this *question* says about the image, and nothing else. # # It used to fall back to the library's own description, which is # written to catalogue an image, not to sit beside a question — so a # radiograph attached to a stem about a limping child carried # "abnormalities in the right hip joint" underneath it, and the # question was answered before it was read. Every one of the 343 # figures in the bank was inheriting that description; not one had a # caption of its own. Most question figures want no caption at all, # and the ones that do want words chosen for the question they are on. # # The library description is still the library's, and still shown # there and in an article. It is not shown in a quiz. "caption": link.caption or None, "path": getattr(asset, "path", None), "position": link.position, # What the library knows, for an explanation figure only. # # Once the answer is in, the catalogue entry is worth having: the # title, the description written when the image was filed, where it # came from, and any marks an educator drew on it. Before the answer # it is a giveaway, which is why the caption fell out of the figure # itself — so it rides only on the rows that are already withheld # until answers are revealed. Nothing here is sent with a stem. "library": _library_json(asset) if link.role == "explanation" else None, } def _library_json(asset: MediaAsset) -> dict | None: if asset is None: return None return { "title": asset.title or None, "caption": asset.caption or None, "alt_text": asset.alt_text or None, "source": asset.source or None, "source_url": asset.source_url or None, "overlay": asset.overlay or None, } 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, [])