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
28 lines
832 B
Python
28 lines
832 B
Python
"""Track which articles a learner has opened, for "recently viewed".
|
|
|
|
Revision ID: w5c6d7e8f9a0
|
|
Revises: v4b5c6d7e8f9
|
|
"""
|
|
from alembic import op
|
|
|
|
revision = "w5c6d7e8f9a0"
|
|
down_revision = "v4b5c6d7e8f9"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade():
|
|
op.execute("""
|
|
CREATE TABLE IF NOT EXISTS article_views (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
article_id INTEGER NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
|
|
viewed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT uq_article_view UNIQUE (user_id, article_id)
|
|
)
|
|
""")
|
|
op.execute("CREATE INDEX IF NOT EXISTS ix_article_views_user ON article_views(user_id, viewed_at DESC)")
|
|
|
|
|
|
def downgrade():
|
|
op.execute("DROP TABLE IF EXISTS article_views")
|