1
0
Files
coldcall_lti/README.md
Benjamin Mako Hill 46f7f55633 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>
2026-07-31 18:38:22 -07:00

7.7 KiB

coldcall-lti

A Canvas external tool (LTI 1.3) for managing cold calls in case-based classes: it selects students to call using weighted randomness, records what happened with each call, lets students report planned absences, and reports participation data back to both instructor and students. It replaces a manual workflow built on exported rosters, Google Forms, and local scripts.

How it works

The tool is a Flask application that Canvas launches over LTI 1.3. A single URL serves everyone: Canvas identifies the person and course on each launch, so instructors get the call-list and reporting views while students get the absence form and their own history. The roster comes from Canvas through the Names and Role Provisioning Service, which means adds and drops are picked up automatically rather than reconciled by hand.

Selection uses the same weighting as the manual system it replaces: each answered call divides a student's weight by the course's weight factor (default 2), so students who have answered more questions become progressively less likely to be called. Each course can instead use "cycle" mode, which shuffles the roster and calls everyone exactly once. These, along with whether students can see their own assessments, are per-course settings.

Layout

  • coldcall_lti/models.py — SQLAlchemy models: courses (with their settings), students, enrollments, opt-outs, and calls, plus the query helpers that feed selection.
  • coldcall_lti/selection.py — the weighted and cycle selection logic. Kept free of database and web dependencies so it can be tested and reasoned about on its own.
  • coldcall_lti/__init__.py — the Flask application factory.
  • migrations/ — alembic migrations. The schema avoids database-specific types so the same migrations run on SQLite (the default) and MariaDB/MySQL; switching is a matter of changing COLDCALL_DATABASE_URL.
  • tests/ — pytest suite covering selection behavior and the model helpers.

Setup

Development uses a virtualenv that shares the system's Debian-packaged libraries (Flask, SQLAlchemy, alembic, pytest) and adds the one PyPI-only dependency, the maintained pylti1p3next fork of PyLTI1p3:

python3 -m venv --system-site-packages .venv
.venv/bin/pip install pylti1p3next
.venv/bin/pip install -e .

Create the database and run the tests:

.venv/bin/python -m alembic upgrade head
.venv/bin/python -m pytest tests/

Configuration is by environment variable: COLDCALL_DATABASE_URL (any SQLAlchemy URL; defaults to an SQLite file under instance/), COLDCALL_SECRET_KEY for Flask sessions, COLDCALL_LTI_CONFIG (path to the LTI platform configuration), and COLDCALL_DEV_MODE=1 to enable the fake-launch pages.

Connecting to Canvas

The tool speaks LTI 1.3, which requires a Developer Key created by a Canvas account admin. The key points Canvas at three endpoints here: /lti/login (OIDC initiation), /lti/launch (the launch target), and /lti/jwks (this tool's public keys). The platform side is described in a JSON file — copy lti_config.example.json to instance/lti_config.json and fill in the client id and deployment id from the Developer Key. Generate the tool's keypair alongside it:

openssl genrsa -out instance/private.key 4096
openssl rsa -in instance/private.key -pubout -out instance/public.key

On each instructor launch the tool refreshes the course roster from Canvas through the Names and Role Provisioning Service, so enrollment changes appear without any manual step. There is also a "Sync roster now" button on the instructor page, and a management command suitable for an hourly cron job on the server, which keeps rosters current even when nobody has launched the tool (worth having during the add/drop churn at the start of a term):

17 * * * * cd /path/to/coldcall_lti && .venv/bin/flask --app coldcall_lti sync-rosters

Courses whose Canvas end date has passed are skipped automatically.

Student names come from Canvas display names, which already reflect preferred names. Pronouns require one extra piece of Developer Key configuration: add a custom parameter

pronouns=$com.instructure.Person.pronouns
course_start=$Canvas.course.startAt
course_end=$Canvas.course.endAt
grading_scheme=$com.instructure.Course.gradingScheme

The course dates bound the schedule and date pickers; the tool works fine without them when a course has no dates set in Canvas.

to the key. Canvas then includes each person's pronouns in launches and in the roster data (the tool requests memberships scoped to the resource link, which is what makes Canvas attach per-member custom fields). Pronouns appear on the live call card, printed call lists, and each student's own page. If the account has pronouns disabled, everything simply shows without them.

Opt-outs

Students remove themselves from a day's cold-call list by picking the date on their page; nothing else is asked. There is deliberately no enforced submission deadline: an instructor running live calls in class gets up-to-the-second opt-outs automatically, while one who prints a call list beforehand should just tell students how much lead time they need (for example, "an hour or two before class"), since opt-outs after printing won't be on the paper.

Withdrawing an opt-out is bounded, because un-opting-out after the fact would rewrite a student's availability record (and with it their participation grade): withdrawal closes when class begins, using the start time recorded on the schedule page, or at the start of the class day when no time is recorded. Withdrawals are also recorded rather than deleted — the opt-out export includes a withdrawn_at column, so the full history of changes survives.

Grading

Final participation grades use the timing-neutral foregone- participation scheme (a port of the earlier participation_grades.R): a student's grade is the quality of their answers minus a deduction for participation they missed by being unavailable, estimated by Monte Carlo simulation of the actual weighted draw and averaged over when the absences fall, plus a small incentive penalty for being drawn while absent with no opt-out filed. The reason for an absence never matters, and luck of the draw never moves a grade. Parameters (allowance, passing line, penalties, simulation size) are per-course settings; grades are computed on demand, reviewed on the instructor's grades page, and shown to students only when the instructor publishes reports. With gradebook passback enabled, a review-then-push page sends the reviewed scores to Canvas via the Assignment and Grade Services; nothing is ever sent without explicit confirmation.

Grades are computed in points out of 100 and displayed through a per-course scale: the built-in linear UW 4.0 map, a threshold table (letter grades, importable in one click from the course's own Canvas grading scheme), or raw points.

The port was verified against the R engine's rendered reports from a real course: answer quality and availability match exactly; the simulated penalty agrees within Monte Carlo noise. flask --app coldcall_lti import-legacy <dir> imports a manual-era class directory for this kind of testing.

Developing without Canvas

Because a Developer Key takes institutional approval to get, the app has a fake-launch mode for local development:

COLDCALL_DEV_MODE=1 .venv/bin/flask --app coldcall_lti run --debug

Then open http://localhost:5000/dev and launch as the fake instructor or any of the fake students. This sets up exactly the session state a real launch would, and the fake roster flows through the same sync code as real NRPS data, so everything past the launch behaves identically. Dev mode also relaxes the cookie settings that Canvas's iframe embedding requires in production (SameSite=None; Secure), which would otherwise break plain-http localhost use.