Exams (migration v4b5c6d7e8f9) "Pediatrics Boards" was a hardcoded checkbox that filtered nothing. Exams are now rows: Pediatrics Boards and USMLE Step 2 CK ship seeded, and everything already in the bank is linked to the boards. Membership is a link table, not a column, because one paediatric cardiology question can count towards several exams. The learner's choice lives on `users.active_exam_id`, so it follows them between devices instead of sitting in one browser's storage. Choosing an exam scopes the bank; a question with no exam links stays visible, since unlinked content is unclassified rather than excluded. A switcher sits in the navbar. AI mode — matching, never generating Both entry points build a test from the educator-reviewed questions that already exist, ranked against the request. Nothing is invented: - POST /questions/builder/describe turns "what I want to study" into a test. - POST /questions/builder/from-upload matches a document against the bank. The file is read in memory and never stored — it is a search query, not a source of questions, so there is nothing to retain or expire. 10 MB cap, 30 questions. Handing a whole document to `websearch_to_tsquery` builds one enormous conjunction that matches nothing, so text over 300 characters is reduced to its most distinctive terms, OR-joined, before it reaches the lexical ranker. Continue your study (migration w5c6d7e8f9a0) A dashboard panel with the sessions in flight and the articles most recently opened. `article_views` records one row per learner and article, written best effort so a reading page never fails because a bookkeeping write did. Tests: 5 new exam tests (active exams and counts, choice persisted and cleared, unknown/inactive refused, bank scoping including unlinked questions, moderator-only creation) and 7 for AI-mode matching (no questions created, invisible questions excluded, no-match reported rather than an empty test, upload limits enforced). Full suites green: 113 backend, 136 frontend, build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""Exams as real data, with a per-user active exam.
|
|
|
|
"Pediatrics Boards" was a hardcoded checkbox that filtered nothing. Exams are now
|
|
rows users can switch between, and question membership is a link table because a
|
|
question can count towards more than one exam.
|
|
|
|
Revision ID: v4b5c6d7e8f9
|
|
Revises: u3a4b5c6d7e8
|
|
"""
|
|
from alembic import op
|
|
|
|
revision = "v4b5c6d7e8f9"
|
|
down_revision = "u3a4b5c6d7e8"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade():
|
|
op.execute("""
|
|
CREATE TABLE IF NOT EXISTS exams (
|
|
id SERIAL PRIMARY KEY,
|
|
slug VARCHAR(80) UNIQUE NOT NULL,
|
|
name VARCHAR(160) NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 100,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
op.execute("""
|
|
CREATE TABLE IF NOT EXISTS question_exam_links (
|
|
id SERIAL PRIMARY KEY,
|
|
question_id INTEGER NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
|
|
exam_id INTEGER NOT NULL REFERENCES exams(id) ON DELETE CASCADE,
|
|
CONSTRAINT uq_question_exam UNIQUE (question_id, exam_id)
|
|
)
|
|
""")
|
|
op.execute("CREATE INDEX IF NOT EXISTS ix_question_exam_links_question_id ON question_exam_links(question_id)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS ix_question_exam_links_exam_id ON question_exam_links(exam_id)")
|
|
op.execute("ALTER TABLE users ADD COLUMN IF NOT EXISTS active_exam_id INTEGER REFERENCES exams(id) ON DELETE SET NULL")
|
|
|
|
op.execute("""
|
|
INSERT INTO exams (slug, name, sort_order) VALUES
|
|
('pediatrics-boards', 'Pediatrics Boards', 10),
|
|
('usmle-step-2-ck', 'USMLE Step 2 CK', 20)
|
|
ON CONFLICT (slug) DO NOTHING
|
|
""")
|
|
# Everything in the bank today was written for the paediatrics boards.
|
|
op.execute("""
|
|
INSERT INTO question_exam_links (question_id, exam_id)
|
|
SELECT q.id, e.id FROM questions q
|
|
CROSS JOIN exams e WHERE e.slug = 'pediatrics-boards'
|
|
ON CONFLICT DO NOTHING
|
|
""")
|
|
|
|
|
|
def downgrade():
|
|
op.execute("ALTER TABLE users DROP COLUMN IF EXISTS active_exam_id")
|
|
op.execute("DROP TABLE IF EXISTS question_exam_links")
|
|
op.execute("DROP TABLE IF EXISTS exams")
|