from coldcall_lti import models from coldcall_lti.launch import ROLE_INSTRUCTOR, ROLE_LEARNER from coldcall_lti.roster import sync_roster def member(uid, name="A Student", roles=(ROLE_LEARNER,), status="Active", **kw): return { "user_id": uid, "name": name, "roles": list(roles), "status": status, **kw, } def make_course(db): course = models.Course(lti_context_id="ctx-1") db.add(course) db.flush() return course def test_sync_creates_students_and_enrollments(db_session): course = make_course(db_session) active, deactivated = sync_roster( db_session, course, [ member("u1", "Ada Lovelace", given_name="Ada", family_name="Lovelace"), member("u2", "Grace Hopper"), member("t1", "Teacher One", roles=[ROLE_INSTRUCTOR]), ], ) assert (active, deactivated) == (3, 0) students = { s.canvas_user_id: s for s in db_session.query(models.Student).all() } assert students["u1"].sortable_name == "Lovelace, Ada" roles = { e.student.canvas_user_id: e.role for e in db_session.query(models.Enrollment).all() } assert roles == {"u1": "student", "u2": "student", "t1": "instructor"} def test_sync_deactivates_dropped_and_reactivates_returning(db_session): course = make_course(db_session) sync_roster(db_session, course, [member("u1"), member("u2")]) active, deactivated = sync_roster(db_session, course, [member("u1")]) assert (active, deactivated) == (1, 1) dropped = ( db_session.query(models.Enrollment) .join(models.Student) .filter(models.Student.canvas_user_id == "u2") .one() ) assert not dropped.active sync_roster(db_session, course, [member("u1"), member("u2")]) assert dropped.active def test_sync_skips_inactive_members(db_session): course = make_course(db_session) active, _ = sync_roster( db_session, course, [member("u1"), member("u2", status="Inactive")], ) assert active == 1 assert db_session.query(models.Student).count() == 1 def test_sync_extracts_pronouns_from_nrps_message(db_session): course = make_course(db_session) custom_claim = "https://purl.imsglobal.org/spec/lti/claim/custom" sync_roster( db_session, course, [ member("u1", message=[{custom_claim: {"pronouns": "they/them"}}]), # Unexpanded variable (platform without pronouns) is ignored. member( "u2", message=[{custom_claim: {"pronouns": "$com.instructure.Person.pronouns"}}], ), ], ) students = { s.canvas_user_id: s for s in db_session.query(models.Student).all() } assert students["u1"].pronouns == "they/them" assert students["u2"].pronouns is None def test_sync_updates_changed_names(db_session): course = make_course(db_session) sync_roster(db_session, course, [member("u1", "Old Name")]) sync_roster(db_session, course, [member("u1", "New Name")]) student = db_session.query(models.Student).one() assert student.name == "New Name"