The objective did almost nothing It scoped question counts and nothing else, which is why changing it appeared to have no effect. An exam now carries a family (USMLE, COMLEX, boards), a description, and the article views it offers, and `/exams/` reports what the current objective actually changes rather than leaving the learner to guess. Reading follows from it: an article returns only the views its objective allows, so someone revising a basic-science step is never shown bedside dosing they must not act on — a view you can open but must never use is worse than one you were never offered. An editor still gets the whole article, because they cannot edit what they cannot see. An objective configured to show nothing falls back to all three; that is a configuration mistake, not a preference worth honouring. Unused figures deleted, at the user's request 3,262 figures — 334 MB — that nothing had ever used. "Unused" was defined by exclusion and every exclusion was checked rather than assumed: kept if any question uses it as a stem or explanation image, if any question version mentions it, or if it appears in article prose or a flashcard. 440 kept, and five question figures spot-checked as still readable afterwards. MinIO is now 596 objects, 520 MB, down from 3,858 and 854 MB. This is not reversible from the application; the nightly borg backup of the volume is the only way back, and that is stated in the script rather than assumed. For the record, since it was asked: the extraction is PyMuPDF, with an MD5 skip list for repeated branding images. It pulled every embedded image from all 18 source PDFs, which is why one 767-page document alone produced 908 of them. 208 backend tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
43 lines
1.6 KiB
Python
43 lines
1.6 KiB
Python
"""A study objective decides what a learner is shown, not just what is counted.
|
|
|
|
Revision ID: c7d8e9f0a1b2
|
|
Revises: b6c7d8e9f0a1
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy import inspect
|
|
|
|
revision = "c7d8e9f0a1b2"
|
|
down_revision = "b6c7d8e9f0a1"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _has_column(table: str, column: str) -> bool:
|
|
return column in {c["name"] for c in inspect(op.get_bind()).get_columns(table)}
|
|
|
|
|
|
def upgrade():
|
|
# `create_all` at startup may already have added these; each step checks.
|
|
for column in (
|
|
# Objectives are chosen from a list of families — USMLE, COMLEX, boards —
|
|
# because a flat list of every exam is not a choice anyone can make.
|
|
sa.Column("family", sa.String(80), nullable=True),
|
|
sa.Column("description", sa.String(300), nullable=True),
|
|
# Which readings this objective shows. Someone revising a basic-science
|
|
# step has no use for bedside dosing, and a view they can open but must
|
|
# never act on is worse than one they were never offered.
|
|
sa.Column("article_views", sa.JSON, nullable=True),
|
|
):
|
|
if not _has_column("exams", column.name):
|
|
op.add_column("exams", column)
|
|
|
|
op.execute("UPDATE exams SET family = 'Boards' WHERE slug = 'pediatrics-boards' AND family IS NULL")
|
|
op.execute("UPDATE exams SET family = 'USMLE' WHERE slug LIKE 'usmle-%' AND family IS NULL")
|
|
op.execute("UPDATE exams SET family = 'Other' WHERE family IS NULL")
|
|
|
|
|
|
def downgrade():
|
|
for column in ("article_views", "description", "family"):
|
|
if _has_column("exams", column):
|
|
op.drop_column("exams", column)
|