Instructor participation report: per-student histograms, outcome mix by class day, and a sortable table including the fairness ratio (answered calls over questions present for, with opt-out days out of the denominator), plus CSV exports of students, calls, and opt-outs. Assessment scales are now per-course data: ordered levels with labels and points out of 100 (defaults carry the old R grading values), with calls referencing levels by id so renames follow through to history. Renaming, re-pointing, reordering, and adding levels are always allowed; deleting a level in use by recorded calls is blocked. Pronouns and course term dates come from Canvas custom variable substitutions, at launch and roster-wide via rlid-scoped NRPS; the student page notes that names/pronouns are Canvas-sourced. Rosters can also be refreshed outside launches: a "Sync roster now" button and a sync-rosters CLI command for an hourly cron job, skipping ended courses. Alembic now runs SQLite-compatible batch migrations with a constraint naming convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
38 lines
1.0 KiB
Python
38 lines
1.0 KiB
Python
from flask import current_app, g
|
|
from sqlalchemy import MetaData, create_engine
|
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
|
|
# Deterministic constraint names, required for SQLite batch migrations
|
|
# and portable to MariaDB/MySQL.
|
|
NAMING_CONVENTION = {
|
|
"ix": "ix_%(column_0_label)s",
|
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
"pk": "pk_%(table_name)s",
|
|
}
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
metadata = MetaData(naming_convention=NAMING_CONVENTION)
|
|
|
|
|
|
def make_engine(url, echo=False):
|
|
return create_engine(url, echo=echo)
|
|
|
|
|
|
def make_session_factory(engine):
|
|
return sessionmaker(bind=engine, expire_on_commit=False)
|
|
|
|
|
|
def get_db():
|
|
if "db_session" not in g:
|
|
g.db_session = current_app.extensions["db_session_factory"]()
|
|
return g.db_session
|
|
|
|
|
|
def close_db(exc=None):
|
|
session = g.pop("db_session", None)
|
|
if session is not None:
|
|
session.close()
|