1
0
Files
coldcall_lti/tests/test_report_ui.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

82 lines
2.9 KiB
Python

import datetime
import re
import pytest
from conftest import outcome_actions
TODAY = datetime.date.today().isoformat()
@pytest.fixture
def instructor(dev_client):
dev_client.post("/dev/launch/dev-instructor")
return dev_client
def record_one_call(client, action="GOOD"):
client.post("/instructor/live/next", data={"date": TODAY})
page = client.get(f"/instructor/live?date={TODAY}").get_data(as_text=True)
call_id = re.search(r"/instructor/call/(\d+)/outcome", page).group(1)
actions = outcome_actions(page)
value = actions.get(action) or actions[action.title()]
client.post(f"/instructor/call/{call_id}/outcome", data={"action": value})
def test_report_page_renders(instructor):
record_one_call(instructor, "GOOD")
record_one_call(instructor, "POOR")
record_one_call(instructor, "missing")
page = instructor.get("/instructor/report").get_data(as_text=True)
assert "3 questions asked over 1" in page
assert "never called" in page
assert 'class="hist"' in page
assert "Outcomes by class day" in page
def test_report_sorting(instructor):
record_one_call(instructor, "GOOD")
page = instructor.get("/instructor/report?sort=answered").get_data(as_text=True)
rows = re.findall(r"<td>([A-Z][a-z]+ [A-Za-z]+)</td>", page)
# The called student sorts first under the answered sort.
called = re.search(r"never called", page)
assert rows, "expected student rows"
assert called
def test_csv_exports(instructor):
record_one_call(instructor, "GOOD")
resp = instructor.get("/instructor/export/students.csv")
assert resp.mimetype == "text/csv"
lines = resp.get_data(as_text=True).strip().splitlines()
assert lines[0].startswith("name,")
assert len(lines) == 9 # header + 8 students
resp = instructor.get("/instructor/export/calls.csv")
lines = resp.get_data(as_text=True).strip().splitlines()
assert len(lines) == 2
assert ",answered,GOOD," in lines[1]
resp = instructor.get("/instructor/export/optouts.csv")
assert resp.get_data(as_text=True).strip().splitlines()[0].startswith("date,")
def test_report_requires_instructor(dev_client):
dev_client.post("/dev/launch/dev-instructor")
dev_client.post("/dev/launch/dev-student-1")
assert dev_client.get("/instructor/report").status_code == 403
assert dev_client.get("/instructor/export/calls.csv").status_code == 403
def test_pronouns_flow_to_pages(instructor):
# Dev roster carries pronouns; they should reach the print list.
instructor.post(f"/instructor/day/{TODAY}/generate", data={"n": "30"})
page = instructor.get(f"/instructor/day/{TODAY}/print").get_data(as_text=True)
assert "she/her" in page or "he/him" in page
instructor.post("/dev/launch/dev-student-1")
page = instructor.get("/me").get_data(as_text=True)
assert "Ada Lovelace (she/her)" in page
assert "drawn from Canvas" in page