Flask application skeleton with SQLAlchemy models for courses (including per-course settings), students, enrollments, opt-outs, and calls; the weighted and cycle selection logic ported from the manual coldcall scripts; alembic migrations; and a pytest suite covering selection behavior and the model query helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
2.6 KiB
Python
88 lines
2.6 KiB
Python
import datetime
|
|
|
|
from coldcall_lti import models
|
|
|
|
|
|
def make_course_with_students(session, n=3):
|
|
course = models.Course(lti_context_id="ctx-1", title="Test Course")
|
|
session.add(course)
|
|
students = []
|
|
for i in range(n):
|
|
s = models.Student(canvas_user_id=f"user-{i}", name=f"Student {i}")
|
|
session.add(s)
|
|
students.append(s)
|
|
session.flush()
|
|
for s in students:
|
|
session.add(models.Enrollment(course_id=course.id, student_id=s.id))
|
|
session.flush()
|
|
return course, students
|
|
|
|
|
|
def test_answered_call_counts_ignores_skipped_and_pending(db_session):
|
|
course, students = make_course_with_students(db_session)
|
|
day = datetime.date(2026, 10, 1)
|
|
a, b, c = students
|
|
for status, student in [
|
|
(models.STATUS_ANSWERED, a),
|
|
(models.STATUS_ANSWERED, a),
|
|
(models.STATUS_SKIPPED, a),
|
|
(models.STATUS_ANSWERED, b),
|
|
(models.STATUS_MISSING, b),
|
|
(models.STATUS_PENDING, c),
|
|
]:
|
|
db_session.add(
|
|
models.Call(
|
|
course_id=course.id,
|
|
student_id=student.id,
|
|
session_date=day,
|
|
status=status,
|
|
)
|
|
)
|
|
db_session.flush()
|
|
|
|
counts = models.answered_call_counts(db_session, course.id)
|
|
assert counts == {a.id: 2, b.id: 1}
|
|
|
|
|
|
def test_students_present_excludes_optouts_and_inactive(db_session):
|
|
course, students = make_course_with_students(db_session)
|
|
day = datetime.date(2026, 10, 1)
|
|
a, b, c = students
|
|
|
|
db_session.add(
|
|
models.OptOut(course_id=course.id, student_id=a.id, date=day)
|
|
)
|
|
enrollment_b = (
|
|
db_session.query(models.Enrollment)
|
|
.filter_by(course_id=course.id, student_id=b.id)
|
|
.one()
|
|
)
|
|
enrollment_b.active = False
|
|
db_session.flush()
|
|
|
|
present = models.students_present(db_session, course.id, day)
|
|
assert [s.id for s in present] == [c.id]
|
|
|
|
# The opt-out only applies on its own date.
|
|
other_day = datetime.date(2026, 10, 2)
|
|
present = models.students_present(db_session, course.id, other_day)
|
|
assert {s.id for s in present} == {a.id, c.id}
|
|
|
|
|
|
def test_students_present_excludes_instructor_role(db_session):
|
|
course, students = make_course_with_students(db_session)
|
|
instructor = models.Student(canvas_user_id="teacher-1", name="Teacher")
|
|
db_session.add(instructor)
|
|
db_session.flush()
|
|
db_session.add(
|
|
models.Enrollment(
|
|
course_id=course.id, student_id=instructor.id, role="instructor"
|
|
)
|
|
)
|
|
db_session.flush()
|
|
|
|
present = models.students_present(
|
|
db_session, course.id, datetime.date(2026, 10, 1)
|
|
)
|
|
assert instructor.id not in {s.id for s in present}
|