from datetime import datetime from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, Numeric, String, UniqueConstraint from app.database import Base class Exam(Base): """A study objective — the top of the hierarchy, above systems and disciplines. A question can sit under more than one exam (paediatric cardiology counts for both a paediatrics board and a step exam), so membership is a link table rather than a column on the question. """ __tablename__ = "exams" id = Column(Integer, primary_key=True, index=True) slug = Column(String(80), unique=True, nullable=False, index=True) name = Column(String(160), nullable=False) sort_order = Column(Integer, default=100) is_active = Column(Integer, default=1) # 0 hides it from the switcher # Objectives are picked from families — USMLE, COMLEX, boards — because a # flat list of every exam is not a choice anyone can make. family = Column(String(80), nullable=True) description = Column(String(300), nullable=True) # Which article views 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. Null means all of them. article_views = Column(JSON, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) class QuestionExamLink(Base): __tablename__ = "question_exam_links" __table_args__ = (UniqueConstraint("question_id", "exam_id", name="uq_question_exam"),) id = Column(Integer, primary_key=True, index=True) question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False, index=True) exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True) class ArticleExamLink(Base): """Which reading belongs to which objective. An article reached an exam only by inference through its category, which cannot express the case that matters: the same article belongs to a basic science step and to a clinical one, showing different views in each. `Exam.article_views` decides what is shown once you are there; this decides whether it is in the group at all. """ __tablename__ = "article_exam_links" __table_args__ = (UniqueConstraint("article_id", "exam_id", name="uq_article_exam"),) id = Column(Integer, primary_key=True, index=True) article_id = Column(Integer, ForeignKey("articles.id", ondelete="CASCADE"), nullable=False, index=True) exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True) class ExamBlueprint(Base): """One line of an examining board's published content outline. A real exam is not a uniform draw from a bank: the ABP publishes that 12% of a general paediatrics paper is preventive care and 2% is rheumatology. Without that, a forty-question block is forty coin flips and tells a learner nothing about how they would do on the day. `code` is the board's own numbering ("1", "4.A") so a domain can be matched back to the published outline, and `weight` is a percentage of the whole paper — set on domains, left null on the subdomains beneath them. """ __tablename__ = "exam_blueprints" __table_args__ = (UniqueConstraint("exam_id", "code", name="uq_exam_blueprint_code"),) id = Column(Integer, primary_key=True, index=True) exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False, index=True) parent_id = Column(Integer, ForeignKey("exam_blueprints.id", ondelete="CASCADE"), nullable=True, index=True) code = Column(String(16), nullable=False) title = Column(String(240), nullable=False) #: Percent of the paper. Null on a subdomain, which inherits its domain's share. weight = Column(Numeric(5, 2), nullable=True) sort_order = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.utcnow) class BlueprintCategoryLink(Base): """Which of our categories feed one blueprint line. The taxonomy is not reshaped to match a board's outline — it is arranged for studying, and theirs is arranged for examining. This maps one onto the other, so both can be right. """ __tablename__ = "blueprint_category_links" __table_args__ = (UniqueConstraint("blueprint_id", "category_id", name="uq_blueprint_category"),) id = Column(Integer, primary_key=True, index=True) blueprint_id = Column(Integer, ForeignKey("exam_blueprints.id", ondelete="CASCADE"), nullable=False, index=True) category_id = Column(Integer, ForeignKey("question_categories.id", ondelete="CASCADE"), nullable=False, index=True)