test: add gradebook API coverage

This commit is contained in:
2026-05-02 07:53:30 -06:00
parent 0998ee0f4b
commit 274b5b1981

View File

@@ -1,5 +1,10 @@
import csv
import io
from datetime import timedelta
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from account.models import RoleChoices from account.models import RoleChoices
from prompt.models import Conversation, Message from prompt.models import Conversation, Message
@@ -10,16 +15,26 @@ from .models import Award, Submission, SubmissionAward
User = get_user_model() User = get_user_model()
def _make_user(username): def _make_user(username, role=RoleChoices.NORMAL, classname=""):
return User.objects.create_user(username=username, password="pw") user = User.objects.create_user(username=username, password="pw")
user.role = role
user.classname = classname
user.save(update_fields=["role", "classname"])
return user
def _make_task(): def _make_task(
title="Test Challenge",
task_type="challenge",
display=1,
is_public=True,
):
return Task.objects.create( return Task.objects.create(
title="Test Challenge", title=title,
task_type="challenge", task_type=task_type,
display=1, display=display,
content="", content="",
is_public=is_public,
) )
@@ -249,3 +264,272 @@ class ShowcaseManagementApiTest(TestCase):
f"/api/submission/showcase/{self.submission.id}/" f"/api/submission/showcase/{self.submission.id}/"
) )
self.assertEqual(inactive_detail_resp.status_code, 404) self.assertEqual(inactive_detail_resp.status_code, 404)
class GradebookApiTest(TestCase):
def setUp(self):
self.admin = _make_user("grade-admin", role=RoleChoices.ADMIN)
self.normal = _make_user("grade-normal", classname="blocked")
def _student(self, username, classname="10A"):
return _make_user(username, classname=classname)
def _submit(self, user, task, score, created=None):
submission = Submission.objects.create(
user=user,
task=task,
score=score,
html="",
css="",
js="",
)
if created is not None:
Submission.objects.filter(pk=submission.pk).update(created=created)
submission.refresh_from_db()
return submission
def test_gradebook_requires_classname(self):
self.client.force_login(self.admin)
resp = self.client.get("/api/submission/gradebook/")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.json()["detail"], "请选择班级")
def test_normal_user_cannot_access_gradebook(self):
self.client.force_login(self.normal)
resp = self.client.get("/api/submission/gradebook/?classname=10A")
export_resp = self.client.get(
"/api/submission/gradebook/export/?classname=10A"
)
self.assertIn(resp.status_code, (302, 403))
self.assertIn(export_resp.status_code, (302, 403))
def test_coverage_includes_tutorial_and_challenge_without_public_requirement(self):
students = [
self._student("alice"),
self._student("bob"),
self._student("carol"),
self._student("dave"),
]
tutorial = _make_task(
title="Intro",
task_type="tutorial",
display=1,
is_public=False,
)
challenge = _make_task(
title="Challenge One",
task_type="challenge",
display=1,
is_public=True,
)
low_coverage = _make_task(
title="Optional",
task_type="challenge",
display=2,
is_public=True,
)
self._submit(students[0], tutorial, 4.0)
self._submit(students[1], tutorial, 5.0)
self._submit(students[0], challenge, 3.0)
self._submit(students[1], challenge, 4.0)
self._submit(students[0], low_coverage, 5.0)
self.client.force_login(self.admin)
resp = self.client.get("/api/submission/gradebook/?classname=10A")
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(data["student_count"], 4)
self.assertEqual(data["coverage_threshold_count"], 2)
self.assertEqual(data["included_task_count"], 2)
self.assertEqual(
[task["id"] for task in data["tasks"]],
[tutorial.id, challenge.id],
)
self.assertTrue(all(task["included"] for task in data["tasks"]))
alice = next(row for row in data["rows"] if row["username"] == "alice")
carol = next(row for row in data["rows"] if row["username"] == "carol")
self.assertEqual(alice["tutorial_total"], 4.0)
self.assertEqual(alice["challenge_total"], 3.0)
self.assertEqual(alice["total_score"], 7.0)
self.assertEqual(alice["average_score"], 3.5)
self.assertEqual(alice["submitted_task_count"], 2)
self.assertEqual(alice["missing_task_count"], 0)
self.assertEqual(carol["total_score"], 0.0)
self.assertEqual(carol["submitted_task_count"], 0)
self.assertEqual(carol["missing_task_count"], 2)
include_all_resp = self.client.get(
"/api/submission/gradebook/?classname=10A&include_all_tasks=true"
)
include_all = include_all_resp.json()
optional = next(
task for task in include_all["tasks"] if task["id"] == low_coverage.id
)
alice_all = next(
row for row in include_all["rows"] if row["username"] == "alice"
)
self.assertFalse(optional["included"])
self.assertTrue(alice_all["scores"][str(low_coverage.id)]["submitted"])
self.assertEqual(alice_all["scores"][str(low_coverage.id)]["score"], 5.0)
self.assertEqual(alice_all["total_score"], 7.0)
self.assertEqual(alice_all["submitted_task_count"], 2)
def test_best_submission_uses_highest_score_and_latest_equal_score_link(self):
alice = self._student("alice")
bob = self._student("bob")
task = _make_task(title="Best Score", task_type="tutorial", display=3)
older = timezone.now() - timedelta(days=2)
newer = timezone.now() - timedelta(days=1)
self._submit(alice, task, 2.0)
old_best = self._submit(alice, task, 4.5, created=older)
new_best = self._submit(alice, task, 4.5, created=newer)
self._submit(bob, task, 3.0)
self.client.force_login(self.admin)
resp = self.client.get("/api/submission/gradebook/?classname=10A")
self.assertEqual(resp.status_code, 200)
alice_row = next(
row for row in resp.json()["rows"] if row["username"] == "alice"
)
cell = alice_row["scores"][str(task.id)]
self.assertEqual(cell["score"], 4.5)
self.assertEqual(cell["submission_id"], str(new_best.id))
self.assertNotEqual(cell["submission_id"], str(old_best.id))
def test_task_type_and_username_filters_keep_full_class_rank(self):
alice = self._student("alice")
bob = self._student("bob")
tutorial = _make_task(title="Tutorial", task_type="tutorial", display=1)
challenge = _make_task(title="Challenge", task_type="challenge", display=1)
self._submit(alice, tutorial, 1.0)
self._submit(bob, tutorial, 5.0)
self._submit(alice, challenge, 5.0)
self._submit(bob, challenge, 1.0)
self.client.force_login(self.admin)
resp = self.client.get(
"/api/submission/gradebook/?classname=10A&task_type=tutorial&username=alice"
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual([task["task_type"] for task in data["tasks"]], ["tutorial"])
self.assertEqual(len(data["rows"]), 1)
self.assertEqual(data["rows"][0]["username"], "alice")
self.assertEqual(data["rows"][0]["rank"], 2)
def test_missing_class_returns_empty_table_with_class_options(self):
self._student("alice")
self.client.force_login(self.admin)
resp = self.client.get("/api/submission/gradebook/?classname=missing")
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertIn("10A", data["classes"])
self.assertEqual(data["student_count"], 0)
self.assertEqual(data["coverage_threshold_count"], 0)
self.assertEqual(data["tasks"], [])
self.assertEqual(data["rows"], [])
def test_no_included_tasks_still_returns_student_rows(self):
students = [
self._student("alice"),
self._student("bob"),
self._student("carol"),
]
optional = _make_task(title="Low Coverage", task_type="challenge", display=8)
self._submit(students[0], optional, 5.0)
self.client.force_login(self.admin)
resp = self.client.get("/api/submission/gradebook/?classname=10A")
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(data["student_count"], 3)
self.assertEqual(data["coverage_threshold_count"], 2)
self.assertEqual(data["tasks"], [])
self.assertEqual(data["included_task_count"], 0)
self.assertEqual(len(data["rows"]), 3)
alice = next(row for row in data["rows"] if row["username"] == "alice")
self.assertEqual(alice["total_score"], 0.0)
self.assertIsNone(alice["average_score"])
self.assertEqual(alice["submitted_task_count"], 0)
self.assertEqual(alice["missing_task_count"], 0)
include_all_resp = self.client.get(
"/api/submission/gradebook/?classname=10A&include_all_tasks=true"
)
include_all = include_all_resp.json()
self.assertEqual(include_all["task_count"], 1)
self.assertFalse(include_all["tasks"][0]["included"])
alice_all = next(
row for row in include_all["rows"] if row["username"] == "alice"
)
self.assertTrue(alice_all["scores"][str(optional.id)]["submitted"])
self.assertEqual(alice_all["total_score"], 0.0)
def test_grade_boundaries_use_ceil_thresholds(self):
task = _make_task(title="Boundary", task_type="challenge", display=7)
for i in range(1, 21):
student = self._student(f"s{i:02d}", classname="10B")
self._submit(student, task, float(21 - i))
self.client.force_login(self.admin)
resp = self.client.get("/api/submission/gradebook/?classname=10B")
self.assertEqual(resp.status_code, 200)
rows_by_name = {row["username"]: row for row in resp.json()["rows"]}
self.assertEqual(rows_by_name["s01"]["grade"], "A")
self.assertEqual(rows_by_name["s06"]["grade"], "A")
self.assertEqual(rows_by_name["s07"]["grade"], "B")
self.assertEqual(rows_by_name["s14"]["grade"], "B")
self.assertEqual(rows_by_name["s15"]["grade"], "C")
self.assertEqual(rows_by_name["s18"]["grade"], "C")
self.assertEqual(rows_by_name["s19"]["grade"], "D")
self.assertEqual(rows_by_name["s20"]["grade"], "E")
def test_export_csv_matches_current_filters(self):
alice = self._student("alice")
bob = self._student("bob")
tutorial = _make_task(title="Intro", task_type="tutorial", display=1)
challenge = _make_task(title="Challenge", task_type="challenge", display=1)
self._submit(alice, tutorial, 4.0)
self._submit(bob, tutorial, 2.0)
self._submit(alice, challenge, 5.0)
self.client.force_login(self.admin)
resp = self.client.get(
"/api/submission/gradebook/export/?classname=10A&task_type=tutorial"
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp["Content-Type"], "text/csv; charset=utf-8")
self.assertIn("attachment;", resp["Content-Disposition"])
rows = list(csv.reader(io.StringIO(resp.content.decode("utf-8-sig"))))
self.assertEqual(
rows[0],
[
"排名",
"等级",
"用户名",
"班级",
"教程1-Intro",
"教程合计",
"挑战合计",
"总分",
"平均分",
"已提交任务数",
"未提交任务数",
],
)
self.assertEqual(rows[1][2], "alice")
self.assertEqual(rows[1][4], "4")
self.assertEqual(rows[1][7], "4")