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 <noreply@anthropic.com>
124 lines
4.3 KiB
Python
124 lines
4.3 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")
|
|
# Speed: shrink the simulation for tests.
|
|
dev_client.post(
|
|
"/instructor/settings",
|
|
data={"selection_mode": "weighted", "weight_factor": "2",
|
|
"show_assessments": "on", "n_sims": "200", "sim_seed": "1"},
|
|
)
|
|
return dev_client
|
|
|
|
|
|
def record_calls(client, n=6):
|
|
for _ in range(n):
|
|
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)
|
|
client.post(
|
|
f"/instructor/call/{call_id}/outcome",
|
|
data={"action": outcome_actions(page)["GOOD"]},
|
|
)
|
|
|
|
|
|
def test_grades_page_before_compute(instructor):
|
|
page = instructor.get("/instructor/grades/").get_data(as_text=True)
|
|
assert "No grades computed yet" in page
|
|
|
|
|
|
def test_compute_and_view_grades(instructor):
|
|
record_calls(instructor)
|
|
resp = instructor.post(
|
|
"/instructor/grades/compute", follow_redirects=True
|
|
)
|
|
page = resp.get_data(as_text=True)
|
|
assert "SD of full-attendance counts" in page
|
|
assert "4.00" in page # someone answered GOOD -> full marks
|
|
assert "never answered" in page # not everyone got called
|
|
|
|
resp = instructor.get("/instructor/grades/export.csv")
|
|
assert resp.mimetype == "text/csv"
|
|
assert "final_points" in resp.get_data(as_text=True)
|
|
|
|
|
|
def test_student_report_and_publishing(instructor):
|
|
record_calls(instructor)
|
|
instructor.post("/instructor/grades/compute")
|
|
page = instructor.get("/instructor/grades/").get_data(as_text=True)
|
|
student_id = re.search(r"/instructor/grades/student/(\d+)", page).group(1)
|
|
report = instructor.get(
|
|
f"/instructor/grades/student/{student_id}"
|
|
).get_data(as_text=True)
|
|
assert "How your grade was computed" in report
|
|
|
|
# Unpublished: students see no report link and get 404.
|
|
instructor.post("/dev/launch/dev-student-1")
|
|
assert "participation grade report" not in instructor.get("/me").get_data(as_text=True)
|
|
assert instructor.get("/me/report").status_code == 404
|
|
|
|
# Publish, and the student can read their own report.
|
|
instructor.post("/dev/launch/dev-instructor")
|
|
instructor.post(
|
|
"/instructor/settings",
|
|
data={"selection_mode": "weighted", "weight_factor": "2",
|
|
"publish_grade_reports": "on"},
|
|
)
|
|
instructor.post("/dev/launch/dev-student-1")
|
|
page = instructor.get("/me").get_data(as_text=True)
|
|
assert "participation grade report" in page
|
|
report = instructor.get("/me/report").get_data(as_text=True)
|
|
assert "How your grade was computed" in report
|
|
assert "Ada Lovelace" in report
|
|
|
|
|
|
def test_push_flow_dev_mode(instructor):
|
|
record_calls(instructor)
|
|
instructor.post("/instructor/grades/compute")
|
|
|
|
# Push pages don't exist until AGS is enabled.
|
|
assert instructor.get("/instructor/grades/push").status_code == 404
|
|
|
|
instructor.post(
|
|
"/instructor/settings",
|
|
data={"selection_mode": "weighted", "weight_factor": "2",
|
|
"ags_enabled": "on"},
|
|
)
|
|
page = instructor.get("/instructor/grades/push").get_data(as_text=True)
|
|
assert "Review before pushing" in page
|
|
assert "Push 8 grades to the gradebook" in page
|
|
|
|
resp = instructor.post("/instructor/grades/push")
|
|
page = resp.get_data(as_text=True)
|
|
assert "simulated (dev mode)" in page
|
|
assert "8 of\n 8 pushed successfully" in page or "8 of" in page
|
|
|
|
|
|
def test_grades_require_instructor(dev_client):
|
|
dev_client.post("/dev/launch/dev-instructor")
|
|
dev_client.post("/dev/launch/dev-student-1")
|
|
assert dev_client.get("/instructor/grades/").status_code == 403
|
|
|
|
|
|
def test_scale_settings_and_canvas_import(instructor):
|
|
# Threshold rows via the settings form.
|
|
instructor.post(
|
|
"/instructor/settings",
|
|
data={"selection_mode": "weighted", "weight_factor": "2",
|
|
"scale_type": "table",
|
|
"scale_rows": "93 A\n90 A-\n0 F"},
|
|
)
|
|
record_calls(instructor, n=3)
|
|
instructor.post("/instructor/grades/compute")
|
|
page = instructor.get("/instructor/grades/").get_data(as_text=True)
|
|
assert "<strong>A</strong>" in page
|