mathly / tests /test_my_grades.py
3v324v23's picture
Add tests for student task list and quiz modes functionality
0ba5932
Raw History Blame Contribute Delete
6.99 kB
"""The student's grades page (GET /api/math/my-grades): the server's grades by unit, the official one only from final
sittings and only once released, practice best and latest, checked worksheets, and only the student's own."""
import asyncio
import uuid
import pytest
from app.answer_grading import validate_assessment
from app.main import create_app
from app.math_skills import algebra9, question_version, quiz_snapshot
from scan_support import OTHER, OWNER, answer_store, postgres_server # noqa: F401
from test_answer_grading import assessment
from test_quiz_modes import EMAILS, MemoryQuizModes, SchoolAuth, at, client, final_attempts, photograph
T = next(u for u in algebra9()["units"] if u["id"] == "t")
class Auth(SchoolAuth):
developers = frozenset({EMAILS[OWNER]}) # checks straight with the (fake) AI
@pytest.fixture
def graded(answer_store, monkeypatch):
monkeypatch.setenv("MATHLY_OPEN_ENDED_LOCKED", "0")
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
app = create_app(auth=Auth())
app.state.answers = answer_store
app.state.quiz_modes = MemoryQuizModes()
class Grader:
def grade(self, image, snapshot):
return validate_assessment(assessment(score=100))
app.state.answer_grader = Grader()
return app
async def practice(c, photos=0):
ids = []
for i in range(len(T["quiz"]["questions"])):
r = await c.post("/api/math/quiz-attempts", json={"unit": "t", "index": i, "new": True, "first": i == 0,
"question_version": question_version(quiz_snapshot(T, i))})
ids.append(r.json()["attempt"]["id"])
for aid in ids[:photos]:
assert (await photograph(c, aid)).status_code == 200
got = (await c.post("/api/math/quiz-sittings", json={"unit_id": "t", "attempts": ids, "client_key": str(uuid.uuid4())})).json()
for aid in ids[:photos]:
await c.post(f"/api/math/answer-attempts/{aid}/grade", json={})
return got, ids
def test_grades_by_unit_official_only_from_a_released_final_and_practice_best_and_latest(graded):
app = graded
async def run():
async with client(app, OWNER) as student, client(app, OTHER) as classmate:
await practice(student, photos=1) # 25
await practice(student, photos=0) # 0, the latest
window = app.state.quiz_modes.add_window({"class_id": 1, "unit_id": "t", "opens_at": at(-5), "closes_at": at(120)})
start = (await student.post("/api/math/quiz-sittings/start", json={"unit_id": "t"})).json()["start"]
ids = await final_attempts(student, start["id"])
await photograph(student, ids[0])
await student.post("/api/math/quiz-sittings", json={"unit_id": "t", "attempts": ids, "start_id": start["id"]})
await student.post(f"/api/math/answer-attempts/{ids[0]}/grade", json={})
got = (await student.get("/api/math/my-grades")).json()
(t,) = [u for u in got["units"] if u["unit_id"] == "t"]
assert t["practice_best"]["grade"] == 25 and t["practice_latest"]["grade"] == 0
assert t["official"]["mode"] == "final" and t["official"]["grade"] is None and t["official"]["released"] is False
assert [s["mode"] for s in t["sittings"]] == ["final", "practice", "practice"] # newest first
app.state.quiz_modes.update_window(window["id"], {"released_at": at(0)})
t = [u for u in (await student.get("/api/math/my-grades")).json()["units"] if u["unit_id"] == "t"][0]
assert (t["official"]["grade"], t["official"]["released"]) == (25, True)
# a classmate sees only their own, whatever they ask for
theirs = (await classmate.get(f"/api/math/my-grades?student_id={OWNER}")).json()
assert all(u["sittings"] == [] and u["official"] is None for u in theirs["units"])
asyncio.run(run())
def test_a_sitting_with_an_answer_still_open_has_no_grade(graded):
app = graded
async def run():
async with client(app, OWNER) as student:
sitting, ids = await practice(student, photos=0)
# a photo with no check yet: open
r = await student.post("/api/math/quiz-attempts", json={"unit": "t", "index": 0, "new": True, "first": True,
"question_version": question_version(quiz_snapshot(T, 0))})
assert r.status_code == 200
ids2 = [r.json()["attempt"]["id"]]
for i in range(1, 4):
ids2.append((await student.post("/api/math/quiz-attempts", json={
"unit": "t", "index": i, "new": True, "question_version": question_version(quiz_snapshot(T, i))})).json()["attempt"]["id"])
await photograph(student, ids2[0])
await student.post("/api/math/quiz-sittings", json={"unit_id": "t", "attempts": ids2, "client_key": str(uuid.uuid4())})
t = [u for u in (await student.get("/api/math/my-grades")).json()["units"] if u["unit_id"] == "t"][0]
assert (t["practice_latest"]["grade"], t["practice_latest"]["open"]) == (None, 1)
assert t["practice_best"]["grade"] == 0
asyncio.run(run())
def test_checked_worksheets_come_with_their_assignment_and_unit():
from test_assignments import STUDENT, TEACHER, FocusAuth, MemoryAssignments, Sittings
from test_assignments import client as aclient
from test_worksheets import MemoryWorksheets, photos, upload
from app.answer_scans import ScanError
class NoReviews(Sittings):
def review(self, *args, **kwargs):
raise ScanError(503, "review_unavailable")
app = create_app(auth=FocusAuth())
app.state.assignments, app.state.answers, app.state.worksheets = MemoryAssignments(), NoReviews(), MemoryWorksheets()
app.state.quiz_modes = MemoryQuizModes()
async def run():
async with aclient(app, TEACHER) as teacher, aclient(app, STUDENT) as student:
wid = (await upload(teacher)).json()["id"]
app.state.assignments.add({"class_id": 3, "title": "פילוג", "note": "", "due_on": "2026-11-12", "student_emails": None,
"items": [{"kind": "skill", "id": "t_ext"}, {"kind": "worksheet", "id": wid}]})
sub = (await student.put(f"/api/math/worksheets/{wid}/submission", files=photos(1))).json()
await teacher.put(f"/api/classes/3/worksheets/{wid}/submissions/{sub['id']}", json={"score": 90, "comment": "יפה"})
got = (await student.get("/api/math/my-grades")).json()
(sheet,) = got["worksheets"]
assert (sheet["score"], sheet["comment"], sheet["assignment"], sheet["unit_id"], sheet["title"]) == (
90, "יפה", "פילוג", "t", "דף עבודה – פיתגורס")
assert got["comments"] is None # could not be read: said so, the rest still comes
asyncio.run(run())