import datetime from coldcall_lti import models, reports D1 = datetime.date(2026, 10, 1) D2 = datetime.date(2026, 10, 3) def setup_course(db): course = models.Course(lti_context_id="ctx-1") db.add(course) db.flush() models.ensure_default_levels(db, course) students = [] for i in range(3): s = models.Student(canvas_user_id=f"u{i}", name=f"Student {i}") db.add(s) students.append(s) db.flush() for s in students: db.add(models.Enrollment(course_id=course.id, student_id=s.id)) db.flush() return course, students def add_call(db, course, student, day, status, assessment=None): level_ids = { lvl.label: lvl.id for lvl in models.assessment_levels(db, course.id) } db.add( models.Call( course_id=course.id, student_id=student.id, session_date=day, status=status, assessment_id=level_ids[assessment] if assessment else None, ) ) db.flush() def test_student_stats_fairness_denominator(db_session): course, (a, b, c) = setup_course(db_session) # Day 1: three questions; day 2: one question. b opted out day 1. add_call(db_session, course, a, D1, models.STATUS_ANSWERED, "GOOD") add_call(db_session, course, a, D1, models.STATUS_ANSWERED, "POOR") add_call(db_session, course, c, D1, models.STATUS_MISSING) add_call(db_session, course, b, D2, models.STATUS_ANSWERED, "GOOD") # Skipped and pending calls count nowhere. add_call(db_session, course, c, D2, models.STATUS_SKIPPED) add_call(db_session, course, c, D2, models.STATUS_PENDING) db_session.add( models.OptOut(course_id=course.id, student_id=b.id, date=D1) ) db_session.flush() rows = {r["student"].id: r for r in reports.student_stats(db_session, course)} assert rows[a.id]["answered"] == 2 assert rows[a.id]["questions_present"] == 4 assert rows[a.id]["prop_asked"] == 0.5 # b missed day 1's three questions by opting out. assert rows[b.id]["questions_present"] == 1 assert rows[b.id]["prop_asked"] == 1.0 assert rows[b.id]["optouts"] == 1 assert rows[c.id]["answered"] == 0 assert rows[c.id]["missing"] == 1 assert rows[c.id]["prop_asked"] == 0.0 def test_assessments_by_day(db_session): course, (a, b, c) = setup_course(db_session) add_call(db_session, course, a, D1, models.STATUS_ANSWERED, "GOOD") add_call(db_session, course, b, D1, models.STATUS_ANSWERED, "GOOD") add_call(db_session, course, c, D1, models.STATUS_MISSING) add_call(db_session, course, a, D2, models.STATUS_ANSWERED, "SATISFACTORY") by_day = reports.assessments_by_day(db_session, course.id) assert [d["day"] for d in by_day] == [D1, D2] assert by_day[0]["counts"] == {"GOOD": 2, "missing": 1} assert by_day[0]["total"] == 3 assert by_day[1]["counts"] == {"SATISFACTORY": 1} def test_histogram_includes_zero_bin(): h = reports.histogram([0, 0, 2]) assert h["bins"] == [ {"value": 0, "students": 2}, {"value": 1, "students": 0}, {"value": 2, "students": 1}, ] assert h["max_students"] == 2