Add running class markbook API
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import routers.markbook as markbook_mod
|
||||
from routers.exam.dependencies import ExamContext
|
||||
from routers.markbook import router
|
||||
|
||||
TEACHER = "00000000-0000-0000-0000-000000000001"
|
||||
INST = "10000000-0000-0000-0000-000000000001"
|
||||
CLASS = "c-1"
|
||||
|
||||
|
||||
class FakeResult:
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
|
||||
class FakeQuery:
|
||||
def __init__(self, store, table):
|
||||
self.store = store
|
||||
self.table = table
|
||||
self.rows = list(store.get(table, []))
|
||||
self._filters = []
|
||||
self._op = None
|
||||
self._payload = None
|
||||
self._limit = None
|
||||
|
||||
def select(self, *_a, **_k):
|
||||
self._op = "select"; return self
|
||||
|
||||
def insert(self, payload):
|
||||
self._op = "insert"; self._payload = payload; return self
|
||||
|
||||
def upsert(self, payload, **_kwargs):
|
||||
self._op = "upsert"; self._payload = payload; return self
|
||||
|
||||
def eq(self, k, v):
|
||||
self._filters.append(("eq", k, v)); self.rows = [r for r in self.rows if r.get(k) == v]; return self
|
||||
|
||||
def in_(self, k, vals):
|
||||
vals = set(vals); self._filters.append(("in", k, vals)); self.rows = [r for r in self.rows if r.get(k) in vals]; return self
|
||||
|
||||
def order(self, *_a, **_k):
|
||||
return self
|
||||
|
||||
def limit(self, n):
|
||||
self._limit = n; return self
|
||||
|
||||
def _match(self, row):
|
||||
for op, k, v in self._filters:
|
||||
if op == "eq" and row.get(k) != v:
|
||||
return False
|
||||
if op == "in" and row.get(k) not in v:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self):
|
||||
backing = self.store.setdefault(self.table, [])
|
||||
if self._op in ("insert", "upsert"):
|
||||
payloads = self._payload if isinstance(self._payload, list) else [self._payload]
|
||||
out = []
|
||||
for p in payloads:
|
||||
row = dict(p)
|
||||
if self._op == "upsert" and self.table == "assessment_marks":
|
||||
existing = next((r for r in backing if r.get("assessment_id") == row.get("assessment_id") and r.get("student_id") == row.get("student_id")), None)
|
||||
if existing:
|
||||
existing.update(row); out.append(existing); continue
|
||||
if self._op == "upsert" and row.get("id") is not None:
|
||||
existing = next((r for r in backing if r.get("id") == row["id"]), None)
|
||||
if existing:
|
||||
existing.update(row); out.append(existing); continue
|
||||
row.setdefault("id", f"gen-{self.table}-{len(backing)}")
|
||||
backing.append(row); out.append(row)
|
||||
return FakeResult(out)
|
||||
rows = self.rows[: self._limit] if self._limit is not None else self.rows
|
||||
return FakeResult(rows)
|
||||
|
||||
|
||||
class FakeSupabase:
|
||||
def __init__(self, store):
|
||||
self.store = store
|
||||
|
||||
def table(self, name):
|
||||
return FakeQuery(self.store, name)
|
||||
|
||||
|
||||
def make_client(store):
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/markbook")
|
||||
from routers.exam.dependencies import get_exam_context
|
||||
app.dependency_overrides[get_exam_context] = lambda: ExamContext(TEACHER, "tok", FakeSupabase(store), [INST])
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def base_store(**extra):
|
||||
store = {
|
||||
"classes": [{"id": CLASS, "name": "10A Maths", "institute_id": INST}],
|
||||
"class_students": [
|
||||
{"class_id": CLASS, "student_id": "s1", "status": "active"},
|
||||
{"class_id": CLASS, "student_id": "s2", "status": "active"},
|
||||
{"class_id": CLASS, "student_id": "s3", "status": "inactive"},
|
||||
],
|
||||
}
|
||||
store.update(extra)
|
||||
return store
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def names(monkeypatch):
|
||||
monkeypatch.setattr(markbook_mod, "resolve_student_names", lambda ids: {sid: {"s1": "Alice", "s2": "Bob"}.get(sid, sid) for sid in ids})
|
||||
|
||||
|
||||
def test_create_assessment_sets_class_and_tenant():
|
||||
store = base_store()
|
||||
c = make_client(store)
|
||||
r = c.post(f"/api/markbook/classes/{CLASS}/assessments", json={"title": "Homework 1", "date": "2026-09-10", "max_marks": 20})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["class_id"] == CLASS
|
||||
assert body["tenant_id"] == INST
|
||||
assert body["title"] == "Homework 1"
|
||||
assert body["max_marks"] == 20
|
||||
|
||||
|
||||
def test_grid_reuses_active_roster_and_computes_summaries():
|
||||
store = base_store(
|
||||
class_assessments=[
|
||||
{"id": "a1", "class_id": CLASS, "tenant_id": INST, "title": "HW", "date": "2026-09-01", "max_marks": 10},
|
||||
{"id": "a2", "class_id": CLASS, "tenant_id": INST, "title": "Quiz", "date": "2026-09-08", "max_marks": 20},
|
||||
],
|
||||
assessment_marks=[
|
||||
{"assessment_id": "a1", "student_id": "s1", "mark": 8},
|
||||
{"assessment_id": "a2", "student_id": "s1", "mark": 18},
|
||||
{"assessment_id": "a1", "student_id": "s2", "mark": 6},
|
||||
],
|
||||
)
|
||||
body = make_client(store).get(f"/api/markbook/classes/{CLASS}/grid").json()
|
||||
assert [s["student_name"] for s in body["students"]] == ["Alice", "Bob"]
|
||||
alice = body["students"][0]
|
||||
assert alice["marks"] == {"a1": 8, "a2": 18}
|
||||
assert alice["total"] == 26
|
||||
assert alice["percentage"] == 86.7
|
||||
assert body["assessment_summaries"][0]["average_mark"] == 7.0
|
||||
assert body["summary"]["entered_mark_count"] == 3
|
||||
|
||||
|
||||
def test_mark_upsert_rejects_over_max_and_inactive_students():
|
||||
store = base_store(class_assessments=[{"id": "a1", "class_id": CLASS, "tenant_id": INST, "title": "HW", "max_marks": 10}])
|
||||
c = make_client(store)
|
||||
assert c.put(f"/api/markbook/classes/{CLASS}/assessments/a1/marks/s1", json={"mark": 11}).status_code == 422
|
||||
assert c.put(f"/api/markbook/classes/{CLASS}/assessments/a1/marks/s3", json={"mark": 5}).status_code == 404
|
||||
ok = c.put(f"/api/markbook/classes/{CLASS}/assessments/a1/marks/s1", json={"mark": 9})
|
||||
assert ok.status_code == 200
|
||||
assert ok.json()["mark"]["mark"] == 9
|
||||
|
||||
|
||||
def test_csv_export_has_roster_rows_and_assessment_columns():
|
||||
store = base_store(
|
||||
class_assessments=[{"id": "a1", "class_id": CLASS, "tenant_id": INST, "title": "HW", "max_marks": 10}],
|
||||
assessment_marks=[{"assessment_id": "a1", "student_id": "s1", "mark": 8}],
|
||||
)
|
||||
text = make_client(store).get(f"/api/markbook/classes/{CLASS}/csv").text
|
||||
lines = text.strip().splitlines()
|
||||
assert lines[0] == "row,student_name,student_id,HW,total,percentage"
|
||||
assert lines[1].startswith("1,Alice,s1,8,8")
|
||||
assert lines[2].startswith("2,Bob,s2,,,")
|
||||
Reference in New Issue
Block a user