From 49d037692b11f34f5871dd45325f4eac43de37b4 Mon Sep 17 00:00:00 2001 From: Benjamin Mako Hill Date: Mon, 3 Aug 2026 16:03:18 -0700 Subject: [PATCH] Speak of the LMS generically, not Canvas specifically The tool targets any LTI 1.3 platform; Canvas is the primary target but nothing outside the Canvas-specific custom variable substitutions depends on it. User-facing strings and the README now say "LMS" except where a mechanism genuinely is Canvas's (the $Canvas.* / com.instructure.* substitutions and their handling), and the README states plainly that the tool has so far been exercised only against the saltire emulator, not yet a production LMS. Co-Authored-By: Claude Fable 5 --- README.md | 56 ++++++++++++--------- coldcall_lti/__init__.py | 2 +- coldcall_lti/dev.py | 2 +- coldcall_lti/instructor.py | 2 +- coldcall_lti/models.py | 2 +- coldcall_lti/templates/index.html | 3 +- coldcall_lti/templates/instructor_home.html | 6 +-- coldcall_lti/templates/push_preview.html | 4 +- coldcall_lti/templates/settings.html | 4 +- coldcall_lti/templates/student_home.html | 7 +-- coldcall_lti/views.py | 2 +- tests/test_grades_ui.py | 2 +- tests/test_report_ui.py | 2 +- 13 files changed, 52 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index ff50e55..1a14cfb 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,23 @@ # coldcall-lti -A Canvas external tool (LTI 1.3) for managing cold calls in case-based +An LTI 1.3 external tool 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. +local scripts. It should work with any LMS that implements LTI 1.3 +(LTI Advantage); Canvas is the primary target and the platform the +Canvas-specific conveniences below are written for. So far it has been +exercised only against the saltire LTI emulator (see +`docs/SALTIRE.md`) — not yet against any production LMS. ## 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 +The tool is a Flask application that the LMS launches over LTI 1.3. A +single URL serves everyone: the LMS 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, +comes from the LMS through the Names and Role Provisioning Service, which means adds and drops are picked up automatically rather than reconciled by hand. @@ -105,15 +109,17 @@ SQLAlchemy URL; defaults to an SQLite file under `instance/`), to the LTI platform configuration), and `COLDCALL_DEV_MODE=1` to enable the fake-launch pages. -## Connecting to Canvas +## Connecting to an LMS -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: +Any LTI 1.3 platform can launch the tool; registration means telling +the LMS about three endpoints here: `/lti/login` (OIDC initiation), +`/lti/launch` (the launch target), and `/lti/jwks` (this tool's public +keys). In Canvas this takes a Developer Key created by an account +admin; other platforms have their own registration screens for the +same information. 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 registration. Generate the +tool's keypair alongside it: ``` openssl genrsa -out instance/private.key 4096 @@ -121,7 +127,7 @@ 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 +the LMS 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 @@ -132,11 +138,13 @@ 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. +Courses whose end date has passed are skipped automatically. -Student names come from Canvas display names, which already reflect -preferred names. Four custom parameters on the Developer Key give the -tool everything else it can use from Canvas: +Student names come from the LMS's display names, which already reflect +preferred names. Canvas users can get more through four custom +parameters on the Developer Key (these are Canvas-specific variable +substitutions; other platforms may offer equivalents under different +names, and the tool works fine without them): ``` pronouns=$com.instructure.Person.pronouns @@ -151,8 +159,8 @@ Canvas attach per-member custom fields) and appear on the live call card, printed lists, and each student's own page. The course dates bound the schedule and date pickers, and the grading scheme becomes importable into the grade display scale with one click in settings. -All four degrade gracefully: a course or account without them simply -does without. +All four degrade gracefully: a course, account, or platform without +them simply does without. ## Opt-outs @@ -186,12 +194,12 @@ never matters, and luck of the draw never moves a grade. Parameters 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 +sends the reviewed scores to the LMS gradebook 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 +(letter grades, importable in one click from the course's own LMS grading scheme), or raw points. The port was verified against the R engine's rendered reports from a @@ -200,7 +208,7 @@ simulated penalty agrees within Monte Carlo noise. `flask --app coldcall_lti import-legacy ` imports a manual-era class directory for this kind of testing. -## Developing without Canvas +## Developing without an LMS Because a Developer Key takes institutional approval to get, the app has a fake-launch mode for local development: @@ -213,7 +221,7 @@ 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 +Dev mode also relaxes the cookie settings that LMS iframe embedding requires in production (SameSite=None; Secure), which would otherwise break plain-http localhost use. diff --git a/coldcall_lti/__init__.py b/coldcall_lti/__init__.py index 01a626d..77f0c7b 100644 --- a/coldcall_lti/__init__.py +++ b/coldcall_lti/__init__.py @@ -17,7 +17,7 @@ def create_app(config=Config): app.extensions["db_session_factory"] = make_session_factory(engine) app.teardown_appcontext(close_db) - # Canvas launches the tool in an iframe, so the session cookie must + # The LMS launches the tool in an iframe, so the session cookie must # be usable in a third-party context. Dev mode runs over plain # http://localhost, where Secure cookies would be dropped. if not app.config["DEV_MODE"]: diff --git a/coldcall_lti/dev.py b/coldcall_lti/dev.py index 37fc242..689e3d2 100644 --- a/coldcall_lti/dev.py +++ b/coldcall_lti/dev.py @@ -1,4 +1,4 @@ -"""Fake-launch mode for development without Canvas. +"""Fake-launch mode for development without an LMS. When COLDCALL_DEV_MODE=1, /dev offers a page of personas (one instructor, a small roster of students) in a fake course. Launching as diff --git a/coldcall_lti/instructor.py b/coldcall_lti/instructor.py index 6653d31..5b1d5f8 100644 --- a/coldcall_lti/instructor.py +++ b/coldcall_lti/instructor.py @@ -666,7 +666,7 @@ def settings_import_scale(): db = get_db() course = current_course() if not course.canvas_grading_scheme: - abort(400, "Canvas has not provided a grading scheme for this course.") + abort(400, "The LMS has not provided a grading scheme for this course.") course.scale_config = course.canvas_grading_scheme course.scale_type = "table" db.commit() diff --git a/coldcall_lti/models.py b/coldcall_lti/models.py index 4c536c4..6ba9916 100644 --- a/coldcall_lti/models.py +++ b/coldcall_lti/models.py @@ -113,7 +113,7 @@ class Student(Base): __tablename__ = "students" id: Mapped[int] = mapped_column(primary_key=True) - # The LTI "sub" claim: stable, opaque, unique per Canvas user. + # The LTI "sub" claim: stable, opaque, unique per LMS user. canvas_user_id: Mapped[str] = mapped_column(String(255), unique=True) name: Mapped[str | None] = mapped_column(String(255)) sortable_name: Mapped[str | None] = mapped_column(String(255)) diff --git a/coldcall_lti/templates/index.html b/coldcall_lti/templates/index.html index fc28e07..ca5e51e 100644 --- a/coldcall_lti/templates/index.html +++ b/coldcall_lti/templates/index.html @@ -4,6 +4,7 @@ {% if course %}

{{ user.name }} in {{ course.title or course.lti_context_id }}.

{% else %} -

This tool is meant to be launched from a Canvas course.

+

This tool is meant to be launched from a course in your learning + management system.

{% endif %} {% endblock %} diff --git a/coldcall_lti/templates/instructor_home.html b/coldcall_lti/templates/instructor_home.html index 5bf1163..8158f05 100644 --- a/coldcall_lti/templates/instructor_home.html +++ b/coldcall_lti/templates/instructor_home.html @@ -10,7 +10,7 @@ opt-out withdrawals can't be locked to class start times. Set up the schedule — pick your weekdays and the range is prefilled from - the Canvas course dates when available.

+ the LMS course dates when available.

Also worth one look before your first class: course settings (selection mode, weight, the assessment scale). Grading parameters @@ -51,9 +51,9 @@

- + The roster also refreshes automatically every - time you open this tool from Canvas. + time you open this tool from the LMS.

Printable list

diff --git a/coldcall_lti/templates/push_preview.html b/coldcall_lti/templates/push_preview.html index 43e1a6c..48f9138 100644 --- a/coldcall_lti/templates/push_preview.html +++ b/coldcall_lti/templates/push_preview.html @@ -4,7 +4,7 @@

← back to grades

Review before pushing to the gradebook

This will write the scores below to a "Case discussion - participation" column in the Canvas gradebook (computed + participation" column in the LMS gradebook (computed {{ run.created_at.strftime("%Y-%m-%d %H:%M") }} UTC). Nothing is sent until you confirm at the bottom.

@@ -18,6 +18,6 @@ {% endfor %}
- +
{% endblock %} diff --git a/coldcall_lti/templates/settings.html b/coldcall_lti/templates/settings.html index c74f31b..aa3fde7 100644 --- a/coldcall_lti/templates/settings.html +++ b/coldcall_lti/templates/settings.html @@ -134,8 +134,8 @@ {% if course.canvas_grading_scheme %}

- - Copies this course's grading scheme from Canvas + + Copies this course's grading scheme from the LMS ({{ course.canvas_grading_scheme | fromjson | length }} levels) into the threshold table.

diff --git a/coldcall_lti/templates/student_home.html b/coldcall_lti/templates/student_home.html index 0ea3dbd..f6610eb 100644 --- a/coldcall_lti/templates/student_home.html +++ b/coldcall_lti/templates/student_home.html @@ -3,9 +3,10 @@ {% block body %}

{{ course.title or course.lti_context_id }}

Hi {{ user.name }}{% if user.pronouns %} ({{ user.pronouns }}){% endif %}.

-

Your name and pronouns here are drawn from Canvas. If - they are not right, update them in your Canvas account settings and - they will be picked up here automatically.

+

Your name and pronouns here are drawn from your + learning management system (e.g., Canvas). If they are not right, + update them in your account settings there and they will be picked + up here automatically.

{% if report_available %}

Your diff --git a/coldcall_lti/views.py b/coldcall_lti/views.py index c70deb0..1968a78 100644 --- a/coldcall_lti/views.py +++ b/coldcall_lti/views.py @@ -36,7 +36,7 @@ def require_launch(view): @functools.wraps(view) def wrapped(**kwargs): if "course_id" not in session: - abort(403, "Launch this tool from your Canvas course.") + abort(403, "Launch this tool from your course in the LMS.") return view(**kwargs) return wrapped diff --git a/tests/test_grades_ui.py b/tests/test_grades_ui.py index 3d52548..2af2131 100644 --- a/tests/test_grades_ui.py +++ b/tests/test_grades_ui.py @@ -95,7 +95,7 @@ def test_push_flow_dev_mode(instructor): ) page = instructor.get("/instructor/grades/push").get_data(as_text=True) assert "Review before pushing" in page - assert "Push 8 grades to Canvas" in page + assert "Push 8 grades to the gradebook" in page resp = instructor.post("/instructor/grades/push") page = resp.get_data(as_text=True) diff --git a/tests/test_report_ui.py b/tests/test_report_ui.py index 0914a48..e81f0b8 100644 --- a/tests/test_report_ui.py +++ b/tests/test_report_ui.py @@ -78,4 +78,4 @@ def test_pronouns_flow_to_pages(instructor): 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 + assert "drawn from your" in page