Phase 6: participation grading, gradebook passback, opt-out integrity
Ports the timing-neutral foregone-participation grading scheme from participation_grades.R: answer quality (per-course level points) minus a deduction for participation foregone through unavailability, estimated by Monte Carlo simulation of the actual weighted draw and averaged over when absences fall, plus a small form-filing incentive for drawn-while-absent-without-opt-out days. Reason-blind and luck-protected; zero-answer students floor to 0. Parameters (allowance in SD units, passing line, form penalty, simulation size/seed) are course settings. Verified against the R engine's rendered 2026q2 reports via the new import-legacy command: quality and availability match exactly, finals within Monte Carlo noise; dropped students import as inactive enrollments and are excluded identically. Grades are computed on demand into stored GradeRun snapshots and reviewed on a grades page with CSV export and per-student reports (also served to students via a publish toggle). Display scales map points to UW 4.0, a threshold table (one-click import of the Canvas course grading scheme), or raw points. Gradebook passback via AGS sits behind a settings toggle with a review-then-push flow. Opt-out withdrawals are now soft-deletes with a withdrawn_at audit trail, and close when class begins (class days gained optional start times), so availability records cannot be rewritten after the fact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""assessment levels, grading, and platform service details
|
||||
|
||||
Revision ID: 826e2835e289
|
||||
Revises: 487d155678ea
|
||||
Create Date: 2026-07-31 18:21:06.340614
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '826e2835e289'
|
||||
down_revision: Union[str, None] = '487d155678ea'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('assessment_levels',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('course_id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=64), nullable=False),
|
||||
sa.Column('points', sa.Float(), nullable=False),
|
||||
sa.Column('position', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['courses.id'], name=op.f('fk_assessment_levels_course_id_courses')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_assessment_levels')),
|
||||
sa.UniqueConstraint('course_id', 'label', name=op.f('uq_assessment_levels_course_id'))
|
||||
)
|
||||
op.create_table('grade_runs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('course_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('params', sa.Text(), nullable=False),
|
||||
sa.Column('results', sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['courses.id'], name=op.f('fk_grade_runs_course_id_courses')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_grade_runs'))
|
||||
)
|
||||
with op.batch_alter_table('calls', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('assessment_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key(batch_op.f('fk_calls_assessment_id_assessment_levels'), 'assessment_levels', ['assessment_id'], ['id'])
|
||||
batch_op.drop_column('assessment')
|
||||
|
||||
with op.batch_alter_table('courses', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('lti_issuer', sa.String(length=255), nullable=True))
|
||||
batch_op.add_column(sa.Column('lti_client_id', sa.String(length=255), nullable=True))
|
||||
batch_op.add_column(sa.Column('nrps_url', sa.String(length=1024), nullable=True))
|
||||
batch_op.add_column(sa.Column('allowance_sd_units', sa.Float(), nullable=False))
|
||||
batch_op.add_column(sa.Column('passing_points', sa.Float(), nullable=False))
|
||||
batch_op.add_column(sa.Column('form_penalty_points', sa.Float(), nullable=False))
|
||||
batch_op.add_column(sa.Column('n_sims', sa.Integer(), nullable=False))
|
||||
batch_op.add_column(sa.Column('sim_seed', sa.Integer(), nullable=False))
|
||||
batch_op.add_column(sa.Column('scale_type', sa.String(length=16), nullable=False))
|
||||
batch_op.add_column(sa.Column('scale_config', sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column('canvas_grading_scheme', sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column('publish_grade_reports', sa.Boolean(), nullable=False))
|
||||
batch_op.add_column(sa.Column('ags_enabled', sa.Boolean(), nullable=False))
|
||||
batch_op.add_column(sa.Column('ags_lineitems_url', sa.String(length=1024), nullable=True))
|
||||
batch_op.add_column(sa.Column('ags_scopes', sa.Text(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('courses', schema=None) as batch_op:
|
||||
batch_op.drop_column('ags_scopes')
|
||||
batch_op.drop_column('ags_lineitems_url')
|
||||
batch_op.drop_column('ags_enabled')
|
||||
batch_op.drop_column('publish_grade_reports')
|
||||
batch_op.drop_column('canvas_grading_scheme')
|
||||
batch_op.drop_column('scale_config')
|
||||
batch_op.drop_column('scale_type')
|
||||
batch_op.drop_column('sim_seed')
|
||||
batch_op.drop_column('n_sims')
|
||||
batch_op.drop_column('form_penalty_points')
|
||||
batch_op.drop_column('passing_points')
|
||||
batch_op.drop_column('allowance_sd_units')
|
||||
batch_op.drop_column('nrps_url')
|
||||
batch_op.drop_column('lti_client_id')
|
||||
batch_op.drop_column('lti_issuer')
|
||||
|
||||
with op.batch_alter_table('calls', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('assessment', sa.VARCHAR(length=32), nullable=True))
|
||||
batch_op.drop_constraint(batch_op.f('fk_calls_assessment_id_assessment_levels'), type_='foreignkey')
|
||||
batch_op.drop_column('assessment_id')
|
||||
|
||||
op.drop_table('grade_runs')
|
||||
op.drop_table('assessment_levels')
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user