An admin can now say "you edit Step 1 Cardiology" rather than only "you edit this category". Each dimension on a grant is nullable and means "any"; a grant covers the questions matching all the dimensions it sets, and holding several grants is the union of their coverage (migration c1d2e3f4a5b6). A check constraint refuses a grant that names nothing, which would otherwise mean "everything". Permission checks now run against a predicate over Question rather than a set of category ids, so the exam and discipline dimensions actually take effect on edit, delete and bulk actions instead of being silently ignored. Pediatrics is unbound back to a global tag. With counts already scoped by the learner's active exam, one global row gives the right number per exam, so scoping the row bought nothing and duplicating a 6,740-tag vocabulary per exam would have to be repeated for every rename and merge. Tests: 123 backend green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01365DYKu14YtsBKv2ycW6eG
43 lines
1.9 KiB
Python
43 lines
1.9 KiB
Python
"""Grants can name an exam and a discipline, not only a category.
|
|
|
|
An admin should be able to say "you edit Step 1 Cardiology" rather than only
|
|
"you edit this category". Each dimension is nullable and means "any", so an
|
|
existing category-only grant keeps working unchanged.
|
|
|
|
Revision ID: c1d2e3f4a5b6
|
|
Revises: b0c1d2e3f4a5
|
|
"""
|
|
from alembic import op
|
|
|
|
revision = "c1d2e3f4a5b6"
|
|
down_revision = "b0c1d2e3f4a5"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade():
|
|
op.execute("ALTER TABLE category_grants ADD COLUMN IF NOT EXISTS exam_id INTEGER REFERENCES exams(id) ON DELETE CASCADE")
|
|
op.execute("ALTER TABLE category_grants ADD COLUMN IF NOT EXISTS tag_id INTEGER REFERENCES question_tags(id) ON DELETE CASCADE")
|
|
op.execute("ALTER TABLE category_grants ALTER COLUMN category_id DROP NOT NULL")
|
|
# A grant naming nothing at all would silently mean "everything".
|
|
op.execute("""
|
|
ALTER TABLE category_grants DROP CONSTRAINT IF EXISTS ck_grant_has_a_dimension
|
|
""")
|
|
op.execute("""
|
|
ALTER TABLE category_grants ADD CONSTRAINT ck_grant_has_a_dimension
|
|
CHECK (category_id IS NOT NULL OR exam_id IS NOT NULL OR tag_id IS NOT NULL)
|
|
""")
|
|
op.execute("ALTER TABLE category_grants DROP CONSTRAINT IF EXISTS uq_category_grant")
|
|
op.execute("""
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_grant_dimensions ON category_grants
|
|
(user_id, COALESCE(category_id, 0), COALESCE(exam_id, 0), COALESCE(tag_id, 0))
|
|
""")
|
|
|
|
|
|
def downgrade():
|
|
op.execute("DROP INDEX IF EXISTS uq_grant_dimensions")
|
|
op.execute("ALTER TABLE category_grants DROP CONSTRAINT IF EXISTS ck_grant_has_a_dimension")
|
|
op.execute("DELETE FROM category_grants WHERE category_id IS NULL")
|
|
op.execute("ALTER TABLE category_grants ALTER COLUMN category_id SET NOT NULL")
|
|
op.execute("ALTER TABLE category_grants DROP COLUMN IF EXISTS tag_id")
|
|
op.execute("ALTER TABLE category_grants DROP COLUMN IF EXISTS exam_id")
|