1
0
Files
coldcall_lti/coldcall_lti/db.py
Benjamin Mako Hill 7ec1be5dc6 Phase 5: reporting, exports, pronouns, and roster freshness
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>
2026-07-31 17:05:34 -07:00

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()