"""Drop the role from figure titles; the badge and the link already say it. Titles read "Stem figure — Occult Fracture". The role is already visible from where the figure sits on the question, and the link itself is the `question_media` row, not the words in the title. What is useful in a bank of 440 is the subject, so that is all the title should be. The id stays the identifier it always was, and no link is touched. docker compose exec backend python -m scripts.retitle_figures docker compose exec backend python -m scripts.retitle_figures --apply """ import re import sys from sqlalchemy import text as sa_text from app.database import SessionLocal from app.models.media import MediaAsset PREFIX = re.compile(r"^(stem|explanation) figure\s*—\s*", re.I) def main(): apply_changes = "--apply" in sys.argv db = SessionLocal() try: # Where a figure has no subject of its own, the question's category is # the best name available; the filename is not a name at all. subjects = {} for media_id, name in db.execute(sa_text(""" SELECT qm.media_id, c.name FROM question_media qm JOIN questions q ON q.id = qm.question_id LEFT JOIN question_categories c ON c.id = q.question_category_id """)).fetchall(): if name and media_id not in subjects: subjects[media_id] = name # A detached figure has no link to take a subject from, but its caption # records which question it came off. That is still the best name. detached = {} for asset in db.query(MediaAsset).all(): match = re.search(r"question #(\d+)", asset.caption or "") if match and asset.id not in subjects: detached[asset.id] = int(match.group(1)) if detached: names = dict(db.execute(sa_text(""" SELECT q.id, c.name FROM questions q LEFT JOIN question_categories c ON c.id = q.question_category_id WHERE q.id = ANY(:ids) """), {"ids": list(detached.values())}).fetchall()) for media_id, question_id in detached.items(): if names.get(question_id): subjects[media_id] = names[question_id] changed = [] for asset in db.query(MediaAsset).all(): title = asset.title or "" if PREFIX.search(title): new = PREFIX.sub("", title).strip() elif re.match(r"^page_\d+_img", title): new = subjects.get(asset.id) or title else: continue if new and new != title: changed.append((asset, title, new)) print(f" titles to change: {len(changed)}") for _asset, old, new in changed[:6]: print(f" {old:<44} -> {new}") if not apply_changes: print("\n Re-run with --apply.") return 0 for asset, _old, new in changed: asset.title = new[:300] db.flush() # A legend is what a reader sees in an explanation, so twelve figures all # called "Pediatric Surgery" is twelve legends that say nothing. Only the # repeats are numbered; a subject used once stays clean. from collections import defaultdict by_title = defaultdict(list) for asset in db.query(MediaAsset).order_by(MediaAsset.id).all(): by_title[asset.title].append(asset) questions = dict(db.execute(sa_text( "SELECT media_id, MIN(question_id) FROM question_media GROUP BY media_id")).fetchall()) numbered = 0 for title, assets in by_title.items(): if len(assets) < 2: continue for asset in assets: question_id = questions.get(asset.id) asset.title = (f"{title} · Q{question_id}" if question_id else f"{title} · #{asset.id}")[:300] numbered += 1 db.commit() print(f" disambiguated: {numbered} repeated subjects") print(f"\n retitled: {len(changed)} (no link changed; the id is the link)") finally: db.close() return 0 if __name__ == "__main__": sys.exit(main())