diff --git a/submission/api.py b/submission/api.py index 37d424a..89b4f3a 100644 --- a/submission/api.py +++ b/submission/api.py @@ -1,6 +1,9 @@ +import csv import threading -from typing import List, Optional +from typing import List, Literal, Optional +from urllib.parse import quote from uuid import UUID +from django.http import HttpResponse from ninja import Router, Query from ninja.errors import HttpError from ninja.pagination import paginate @@ -30,6 +33,7 @@ from .schemas import ( FlagIn, FlagStats, AwardOut, + GradebookOut, PromptRoundOut, ShowcaseDetailOut, ShowcaseItemOut, @@ -46,6 +50,7 @@ from .schemas import ( from .models import Award, ItemOrdering, Rating, Submission, SubmissionAward +from .gradebook import GradebookFilters, build_gradebook, gradebook_csv_rows from task.models import Task from account.models import RoleChoices, User @@ -471,6 +476,54 @@ def get_task_stats(request, task_id: int, classname: Optional[str] = None): ) +@router.get("/gradebook/", response=GradebookOut) +@admin_required +def get_gradebook( + request, + classname: str = "", + task_type: Optional[Literal["tutorial", "challenge"]] = None, + username: Optional[str] = None, + include_all_tasks: bool = False, +): + return build_gradebook( + GradebookFilters( + classname=classname, + task_type=task_type, + username=username, + include_all_tasks=include_all_tasks, + ) + ) + + +@router.get("/gradebook/export/") +@admin_required +def export_gradebook( + request, + classname: str = "", + task_type: Optional[Literal["tutorial", "challenge"]] = None, + username: Optional[str] = None, + include_all_tasks: bool = False, +): + gradebook = build_gradebook( + GradebookFilters( + classname=classname, + task_type=task_type, + username=username, + include_all_tasks=include_all_tasks, + ) + ) + response = HttpResponse(content_type="text/csv; charset=utf-8") + filename = f"gradebook-{gradebook['classname']}.csv" + response["Content-Disposition"] = ( + f"attachment; filename*=UTF-8''{quote(filename)}" + ) + response.write("\ufeff") + writer = csv.writer(response) + for row in gradebook_csv_rows(gradebook): + writer.writerow(row) + return response + + @router.get("/showcase/", response=List[AwardOut]) @login_required def list_showcase(request): diff --git a/submission/schemas.py b/submission/schemas.py index ac4aadb..7da6478 100644 --- a/submission/schemas.py +++ b/submission/schemas.py @@ -160,6 +160,48 @@ class TaskStatsOut(Schema): top_viewed: list[TopViewedItem] +class GradebookTask(Schema): + id: int + display: int + title: str + task_type: Literal["tutorial", "challenge"] + submitted_count: int + coverage: float + included: bool + + +class GradebookCell(Schema): + score: float + submitted: bool + submission_id: Optional[UUID] = None + + +class GradebookRow(Schema): + user_id: int + username: str + classname: str + rank: int + grade: Literal["A", "B", "C", "D", "E"] + scores: dict[int, GradebookCell] + tutorial_total: float + challenge_total: float + total_score: float + average_score: Optional[float] + submitted_task_count: int + missing_task_count: int + + +class GradebookOut(Schema): + classname: str + classes: list[str] + task_count: int + included_task_count: int + student_count: int + coverage_threshold_count: int + tasks: list[GradebookTask] + rows: list[GradebookRow] + + class ShowcaseItemOut(Schema): submission_id: UUID username: str