File size: 7,703 Bytes
aeda545 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """The work on the board: saved with every answer, seen and given points by the student's teacher (board-work 1.1-1.4)."""
import io
import uuid
import pytest
from fastapi.testclient import TestClient
from PIL import Image
from mathly2 import cleanup, clock
from mathly2.bank import load_bank
from mathly2.config import Settings
from mathly2.main import create_app
from mathly2.store import InMemoryStore
from mathly2.tests.test_quizzes_api import right_index, skill_of
TEACHER, KID, OTHER_TEACHER, STRANGER = "t@school.test", "kid@school.test", "t2@school.test", "other@school.test"
def auth(email):
return {"Authorization": f"Bearer dev:{email}"}
def uid(email):
return str(uuid.uuid5(uuid.NAMESPACE_DNS, email))
def png(color="white"):
out = io.BytesIO()
Image.new("RGB", (80, 50), color).save(out, "PNG")
return out.getvalue()
@pytest.fixture
def world():
bank = load_bank()
store = InMemoryStore()
lessons = [l["id"] for l in bank.lessons][:3]
mine = store.create_class("setup", "A", lessons=lessons, teacher_email=TEACHER)
store.add_member(mine["id"], KID)
store.add_member(store.create_class("setup", "B", lessons=lessons, teacher_email=OTHER_TEACHER)["id"], STRANGER)
client = TestClient(create_app(Settings(teachers=frozenset({TEACHER, OTHER_TEACHER})), bank=bank, store=store))
for who in (KID, STRANGER, TEACHER, OTHER_TEACHER):
client.get("/api/me", headers=auth(who)) # a first sign-in links the account to the class
lesson = next(l for l in lessons if any(s["unit"] == l and bank.question_keys(s["id"]) for s in bank.skills))
return client, store, bank, lesson
def send_work(client, ref, key, who=KID, data=None):
return client.post(f"/api/work?ref={ref}&key={key}", content=data or png(), headers={**auth(who), "Content-Type": "image/png"})
def quiz(client, lesson, who=KID):
response = client.post(f"/api/lessons/{lesson}/quiz/start", headers=auth(who))
assert response.status_code == 200, response.text
return response.json()
def choose(client, bank, attempt, key, right, who=KID):
response = client.put(f"/api/attempts/{attempt['id']}/answers", headers=auth(who),
json={"key": key, "choice": right_index(bank, skill_of(key), key, right)})
assert response.status_code == 200, response.text
def handed_in(client, bank, lesson, rights):
attempt = quiz(client, lesson)
for q, right in zip(attempt["questions"], rights):
choose(client, bank, attempt, q["key"], right)
assert send_work(client, attempt["id"], q["key"]).status_code == 201
client.post(f"/api/attempts/{attempt['id']}/submit", headers=auth(KID))
return attempt
def test_a_quiz_says_work_is_required_and_lists_the_questions_whose_board_is_saved(world):
client, store, bank, lesson = world
attempt = quiz(client, lesson)
assert attempt["work_required"] is True and attempt["work"] == []
key = attempt["questions"][0]["key"]
assert send_work(client, attempt["id"], key).status_code == 201
assert quiz(client, lesson)["work"] == [key]
picture = store.get_picture(store.work_of(uid(KID), attempt["id"])[key])
assert picture["kind"] == "work" and picture["owner_id"] == uid(KID)
def test_work_goes_only_to_your_own_attempt_and_its_own_questions(world):
client, _, bank, lesson = world
attempt = quiz(client, lesson)
assert send_work(client, attempt["id"], attempt["questions"][0]["key"], who=STRANGER).status_code == 404
assert send_work(client, attempt["id"], "nope").status_code == 404
assert send_work(client, attempt["id"], attempt["questions"][0]["key"], data=b"not a picture").status_code in (400, 415, 422)
def test_practice_work_is_kept_per_question_and_the_latest_wins(world):
client, store, bank, lesson = world
skill = next(s["id"] for s in bank.skills if s["unit"] == lesson and bank.question_keys(s["id"]))
key = bank.question_keys(skill)[0]
assert send_work(client, "practice", key).status_code == 201
first = store.work_of(uid(KID), "practice")[key]
assert send_work(client, "practice", key, data=png("yellow")).status_code == 201
second = store.work_of(uid(KID), "practice")[key]
assert first != second and store.picture_refs(first)["work"] == [] and store.picture_refs(second)["work"]
assert send_work(client, "practice", "missing-key").status_code == 404
def test_the_class_teacher_sees_the_choices_and_the_board_and_others_do_not(world):
client, store, bank, lesson = world
attempt = handed_in(client, bank, lesson, [True] * 13 + [False] * 2)
listed = client.get("/api/teaching/work", headers=auth(TEACHER)).json()
assert [i["id"] for i in listed] == [attempt["id"]] and listed[0]["student"] == KID and listed[0]["checked"] is False and listed[0]["grade"] == 87
assert client.get("/api/teaching/work", headers=auth(OTHER_TEACHER)).json() == []
view = client.get(f"/api/teaching/work/{attempt['id']}", headers=auth(TEACHER)).json()
q = view["questions"][0]
assert sum(c["correct"] for c in q["choices"]) == 1 and q["picked"] is not None and q["auto"] == 1 and q["work"]
assert client.get(f"/api/pictures/{q['work']}", headers=auth(TEACHER)).status_code == 200
assert client.get(f"/api/pictures/{q['work']}", headers=auth(OTHER_TEACHER)).status_code == 404
assert client.get(f"/api/teaching/work/{attempt['id']}", headers=auth(OTHER_TEACHER)).status_code == 404
assert client.get("/api/teaching/counts", headers=auth(TEACHER)).json()["work"] == 1
def test_points_for_the_way_recompute_the_grade(world):
client, store, bank, lesson = world
attempt = handed_in(client, bank, lesson, [True] * 12 + [False] * 3) # 80: not passed
wrong = [q["key"] for q in attempt["questions"]][12:]
right = attempt["questions"][0]["key"]
post = lambda credit: client.post(f"/api/teaching/work/{attempt['id']}", headers=auth(TEACHER), json={"credit": credit})
got = post({wrong[0]: 1, wrong[1]: 0.5})
assert got.status_code == 200 and got.json()["grade"] == 90 and got.json()["checked_at"] # 13.5 of 15
facts = client.get(f"/api/lessons/{lesson}/quiz", headers=auth(KID)).json()
assert facts["best"] == 90 # the lesson's grade follows
assert post({right: 0}).json()["grade"] == 83 # a right answer with no way: 12.5 of 15
assert post({right: None, wrong[1]: None}).json()["grade"] == 87 # back to automatic: 13 of 15
assert post({wrong[2]: 0.3}).status_code == 422 and post({"nope": 1}).status_code == 422
assert client.get("/api/teaching/work", headers=auth(TEACHER)).json()[0]["checked"] is True
assert client.post(f"/api/teaching/work/{attempt['id']}", headers=auth(OTHER_TEACHER), json={"credit": {right: 1}}).status_code == 404
def test_an_open_quiz_is_not_on_the_teachers_list_and_a_replaced_board_is_cleaned_up(world):
client, store, bank, lesson = world
attempt = quiz(client, lesson)
key = attempt["questions"][0]["key"]
send_work(client, attempt["id"], key)
old = store.work_of(uid(KID), attempt["id"])[key]
send_work(client, attempt["id"], key, data=png("blue"))
assert client.get("/api/teaching/work", headers=auth(TEACHER)).json() == []
removed = cleanup.run(store, client.app.state.pictures, clock.now() + cleanup.retention.ORPHAN_AGE * 2)
assert removed["orphans"] == 1 and store.get_picture(old)["deleted_at"] and not store.get_picture(store.work_of(uid(KID), attempt["id"])[key])["deleted_at"]
|