Coverage for server / routes / student / student_assignments.py: 100%
35 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1from fastapi import APIRouter, Depends, HTTPException, Request, status
2from server.authentication.auth0_bearer import Auth0Bearer
3from server.services.growthbook import require_feature
4from server.models.assignment import Submission
5from server.models.sharerequests import ShareRequest
6from server.services.student.student_assignment import StudentAssignmentsService
7from server.models.assignment_submit import (
8 AssignmentSubmitRequest,
9 ViolationReportRequest,
10)
11from server.connection.database import db
12from server.validators.class_code_validator import ClassCodePath
14router = APIRouter()
16student_assignments_service = StudentAssignmentsService()
19@router.get(
20 "/{class_code}/all/fetch",
21 dependencies=[
22 Depends(Auth0Bearer(access_levels=["student"])),
23 Depends(require_feature("student.assignment_access")),
24 ],
25 status_code=status.HTTP_200_OK,
26 response_description="Returns all created assignments",
27 summary="As a student, I can fetch all assignments",
28 description="As a student, I can fetch all assignments",
29)
30async def assignments_fetch(
31 class_code: ClassCodePath,
32 request: Request,
33) -> dict:
34 """
35 Fetch all assignments for a student in a specific class.
37 This endpoint ensures:
38 - The class exists (valid `class_code`).
39 - The student is enrolled in the class before retrieving assignments.
41 Args:
42 class_code (str): The unique identifier for the class.
43 request (Request): The request object containing user details.
45 Returns:
46 dict: A response containing either assignments or an error message.
47 """
48 return await student_assignments_service.assignments_fetch(class_code, request)
51@router.get(
52 "/{class_code}/grade/average/fetch",
53 dependencies=[
54 Depends(Auth0Bearer(access_levels=["student"])),
55 Depends(require_feature("student.assignment_access")),
56 ],
57 status_code=status.HTTP_200_OK,
58 response_description="Returns the student's average grade across all assignments in a class",
59 summary="As a student, I can fetch my total average grade for a class",
60 description="As a student, I can fetch my total average grade for a class",
61)
62async def class_grade_average_fetch(
63 class_code: ClassCodePath,
64 request: Request,
65) -> dict:
66 """
67 Fetch the student's average grade across all graded assignments in a specific class.
69 Args:
70 class_code (str): The unique identifier for the class.
71 request (Request): The request object containing user details.
73 Returns:
74 dict: A response containing the average grade, count of graded assignments,
75 and total assignment count.
76 """
77 return await student_assignments_service.class_grade_average_fetch(
78 class_code, request
79 )
82@router.get(
83 "/{assignment_uuid}/fetch",
84 dependencies=[
85 Depends(Auth0Bearer(access_levels=["student"])),
86 Depends(require_feature("student.assignment_access")),
87 ],
88 status_code=status.HTTP_200_OK,
89 response_description="Returns detailed information about a specific assignment",
90 summary="As a student, I can fetch a specific assignment",
91 description="As a student, I can fetch a specific assignment",
92)
93async def assignment_details_fetch(
94 assignment_uuid: str,
95 request: Request,
96) -> dict:
97 """
98 Retrieve a specific assignment by its UUID. This endpoint is restricted to students only
99 and requires JWT authentication.
101 Args:
102 assignment_uuid (str): Unique identifier of the assignment. Must be a valid UUID string
103 that exists in the database.
104 request (Request): The incoming request object containing user context, including:
105 - JWT token in headers
106 - User authentication information
107 - User role and permissions
109 Returns:
110 dict: Assignment details including:
111 - assignment_id: str (UUID of the assignment)
112 - title: str (Title of the assignment)
113 - description: str (Detailed description)
114 - due_date: datetime (Assignment deadline)
115 - created_by: str (Teacher's ID who created it)
116 - created_at: datetime (Creation timestamp)
117 - status: str (Current status of the assignment)
118 - questions: list (List of questions and their details)
119 - settings: dict (Assignment configuration settings)
120 - timeUsed: str (Time spent on the student's current/most recent
121 submission, formatted "HH:MM:SS"; "00:00:00" if no submission exists)
123 Raises:
124 HTTPException(404): If the assignment UUID doesn't exist
125 HTTPException(403): If the user doesn't have permission to view this assignment
126 HTTPException(401): If the authentication token is invalid or expired
128 Example:
129 GET /assignments/view/123e4567-e89b-12d3-a456-426614174000
130 """
131 # Delegate the actual retrieval to the assignments service
132 # The service layer handles database interactions and permission checks
133 return await student_assignments_service.fetch_specific_assignment(
134 assignment_uuid, request
135 )
138@router.get(
139 "/{assignment_uuid}/questions/fetch",
140 dependencies=[Depends(Auth0Bearer(access_levels=["student"]))],
141 status_code=status.HTTP_200_OK,
142 response_description="Returns detailed information about a specific assignment",
143 summary="As a student, I can answer a specific assignment",
144 description="As a student, I can answer a specific assignment",
145)
146async def assignment_questions_fetch(
147 assignment_uuid: str,
148 request: Request,
149) -> dict:
150 """
151 Fetches the questions for a specific assignment based on the student's enrollment and submission status.
153 This method performs the following operations:
154 1. Validates the assignment UUID format.
155 2. Retrieves the assignment document from the database.
156 3. Verifies that the student is enrolled in one of the classes to which the assignment is assigned.
157 4. Checks if the student has an existing submission:
158 - If yes, returns the questions from the existing submission.
159 - If no, fetches questions from the question bank, optionally shuffles them (and their choices),
160 initializes a new submission record with default answers, and stores it in the database.
161 5. Enforces assignment settings such as allowed attempts and time allowed.
162 6. Updates attempt count if within the allowed limits.
163 7. Returns the assignment details, list of questions, student's answers, and the remaining time.
165 Args:
166 assignment_uuid (str): The UUID of the assignment to be fetched.
167 request (Request): The FastAPI request object, expected to contain the student's details in `request.state.user_details`.
169 Returns:
170 dict: A structured response containing:
171 - assignment: {
172 details (dict): Metadata of the assignment (excluding questions),
173 questions (list): List of question objects for the assignment,
174 student_answers (list): The student's existing or initialized answers,
175 remaining_time (str): Time left to complete the assignment in "HH:MM:SS" format
176 }
178 Raises:
179 HTTPException:
180 - 400: If the assignment UUID is invalid.
181 - 403: If the student is not enrolled or exceeds allowed attempts or time.
182 - 404: If the assignment does not exist.
183 - 500: For any unexpected internal error.
184 """
185 return await student_assignments_service.fetch_assignment_questions(
186 assignment_uuid, request
187 )
190@router.post(
191 "/{assignment_uuid:str}/answers/save",
192 dependencies=[Depends(Auth0Bearer(access_levels=["student"]))],
193 status_code=status.HTTP_200_OK,
194 description="As a student, I can save all my answers for an assignment",
195 summary="As a student, I can save all my answers for an assignment",
196)
197async def answer_assignment_update(
198 assignment_uuid: str,
199 student_assignment_answers: AssignmentSubmitRequest,
200 request: Request,
201) -> dict:
202 """
203 Updates the answers and flag status for a student's submission in the assignment collection.
205 This method finds the student's submission document by matching the provided `assignment_uuid`
206 and `student_id`, and then updates the answers for the corresponding questions based on the
207 data received in the `submit_request`. Each answer update includes the question ID, the student's
208 answer, and the optional flag status.
210 The updated submission is then retrieved and returned in the response.
212 Args:
213 assignment_uuid (str): The unique identifier for the assignment.
214 submit_request (AssignmentSubmitRequest): A Pydantic model containing the answers to be updated.
215 request (Request): The HTTP request object containing user details, specifically the student ID.
217 Returns:
218 dict: A dictionary with a message and the updated answers list.
220 Raises:
221 Exception: If the submission document is not found for the given assignment and student.
222 """
223 return await student_assignments_service.assignment_answers_save(
224 assignment_uuid, student_assignment_answers, request
225 )
228@router.post(
229 "/{assignment_uuid}/answers/submit",
230 dependencies=[
231 Depends(Auth0Bearer(access_levels=["student"])),
232 Depends(require_feature("student.assignment_submission")),
233 ],
234 status_code=status.HTTP_200_OK,
235 description="As a student, I can submit an answer for an assignment",
236 summary="As a student, I can submit an answer for an assignment",
237)
238async def assignment_answers_submit(
239 assignment_uuid: str,
240 student_assignment_answers: AssignmentSubmitRequest,
241 request: Request,
242) -> dict:
243 """
244 Submits and evaluates a student's answers for a specific assignment.
246 This method performs the following steps:
247 1. Validates whether the student is authorized to submit the assignment by checking class membership.
248 2. Retrieves the existing submission record for the student and assignment.
249 3. Updates the submission's answers by matching submitted responses with existing question IDs while preserving order.
250 4. Calculates the total score, correctness per question, and evaluation details.
251 5. Updates the submission document with the evaluated results and marks it as submitted.
253 Args:
254 assignment_uuid (str): The UUID of the assignment to be submitted.
255 submit_request (AssignmentSubmitRequest): The student's submitted answers and flags.
256 request (Request): The current HTTP request, used to extract student identity.
258 Returns:
259 dict: A response indicating success, along with scoring details:
260 - message (str): Status message.
261 - totalScore (int): Total points earned.
262 - totalCorrectAnswers (int): Number of correctly answered questions.
263 - totalQuestions (int): Total number of questions in the assignment.
264 - totalAnswersSubmitted (int): Number of answers the student attempted.
265 - details (list): Breakdown of scoring per question, including correct answer, student answer, and points earned.
267 Raises:
268 HTTPException: If the student is unauthorized, the assignment is not found, or no submission record exists.
269 """
270 return await student_assignments_service.assignment_submit(
271 assignment_uuid, student_assignment_answers, request
272 )
275@router.post(
276 "/{assignment_uuid}/violations",
277 dependencies=[Depends(Auth0Bearer(access_levels=["student"]))],
278 status_code=status.HTTP_200_OK,
279 description="As a student, I have a browser-lockdown violation recorded against my current attempt",
280 summary="As a student, I can report a browser-lockdown violation for an assignment attempt",
281)
282async def assignment_violation_report(
283 assignment_uuid: str,
284 violation: ViolationReportRequest,
285 request: Request,
286) -> dict:
287 """
288 Records one browser-lockdown violation (fullscreen exit, tab switch, blocked
289 clipboard action, etc. — see the client's useAssessmentLockdown hook) against
290 the student's current in-progress attempt, and reports whether the
291 max-violations threshold has been exceeded.
293 See docs/implementation/browser_lockdown_violation_tracking.md (client repo) for
294 the full feature spec. This backend is the source of truth for the running
295 count — the client calls this endpoint once per violation as it happens and
296 acts on the response, rather than counting client-side, so the count survives
297 a page refresh or reconnect mid-attempt.
299 Args:
300 assignment_uuid (str): The UUID of the assignment being taken.
301 violation (ViolationReportRequest): The reported violation; `type` is
302 informational only.
303 request (Request): The current HTTP request, used to extract student identity.
305 Returns:
306 dict:
307 - violation_count (int): The running total for this attempt, capped at
308 max_violations.
309 - max_violations (int): The configured threshold. It is
310 MAX_LOCKDOWN_VIOLATIONS in server/services/student/student_assignment.py,
311 currently 3. Read it from this field rather than hard-coding a
312 number; a client that hard-codes one will disagree with the server
313 the day it changes, which is exactly what happened.
314 - threshold_exceeded (bool): True once violation_count reaches max_violations.
315 - auto_submit (bool): True exactly once, on the violation that first
316 reaches the threshold (the 3rd) — the client should submit the
317 assignment now. Note the server ALSO stops accepting further answer
318 saves and question fetches for the attempt at that point (403), so
319 this is not merely a request.
320 - violation_breakdown (dict): Running per-type counts for this attempt,
321 e.g. {"tab_hidden": 3, "fullscreen_exit": 2}.
323 Raises:
324 HTTPException:
325 - 400: If the assignment UUID is invalid.
326 - 404: If no submission exists yet for this student/assignment (the
327 student must open the assignment via questions/fetch first).
328 """
329 return await student_assignments_service.report_violation(
330 assignment_uuid, violation, request
331 )
334@router.get(
335 "/{assignment_uuid}/submission/fetch",
336 dependencies=[
337 Depends(Auth0Bearer(access_levels=["student"])),
338 Depends(require_feature("student.assignment_review")),
339 ],
340 status_code=status.HTTP_200_OK,
341 description="As a student, I can fetch a specific submission for review",
342 summary="As a student, I can fetch a specific submission for review",
343)
344async def assignment_submission_fetch(
345 assignment_uuid: str,
346 request: Request,
347) -> dict:
348 """
349 Fetch detailed submission data for a given assignment and student.
351 This method performs the following:
352 - Validates if the student is enrolled in a class assigned to the given assignment.
353 - Retrieves the submission document for the assignment and student.
354 - Calculates the remaining time allowed for the submission.
355 - Scores the student's answers against the correct answers.
356 - Returns detailed feedback including score, correctness, and flagged status.
358 Args:
359 assignment_uuid (str): The unique identifier of the assignment.
360 request (Request): The FastAPI request object containing the student's details.
362 Returns:
363 dict: A dictionary containing:
364 - message (str): A success message.
365 - _id (str): The submission document ID.
366 - remaining_time (str): Time left in HH:MM:SS format.
367 - totalScore (int): Total score the student earned.
368 - totalCorrectAnswers (int): Number of correct answers.
369 - totalQuestions (int): Number of questions answered.
370 - totalAnswersSubmitted (int): Number of meaningful answers submitted.
371 - teacherComments (dict): questionId -> comment text left by the teacher, if any.
372 - settings (dict): The assignment's settings sub-document (time_allowed, allowed_attempts,
373 show_score_after_submit, show_correct_answers_after_submit, shuffle_questions,
374 shuffle_choices, allow_calculator, allow_feedback_after_submit, allow_late_submissions, ...).
375 - details (List[dict]): Per-question feedback including:
376 - questionId (str)
377 - questionType (str)
378 - points (int)
379 - isFlagged (bool)
380 - correctAnswer (Any)
381 - studentAnswer (dict):
382 - answer (Any)
383 - isCorrect (bool)
384 - earnedPoints (int)
386 Raises:
387 HTTPException:
388 - 403: If the student is unauthorized or the assignment is not found.
389 - 400: If `time_allowed` or `date_created` is missing or improperly formatted.
391 Example:
392 {
393 "message": "Successfully fetched submission summary.",
394 "_id": "64c5f1234567890abcdef123",
395 "remaining_time": "00:10:15",
396 "totalScore": 15,
397 "totalCorrectAnswers": 3,
398 "totalQuestions": 5,
399 "totalAnswersSubmitted": 4,
400 "settings": {
401 "time_allowed": "01:00:00",
402 "allowed_attempts": 3,
403 "show_score_after_submit": True,
404 "show_correct_answers_after_submit": True
405 },
406 "details": [
407 {
408 "questionId": "64c5f...",
409 "questionType": "multiple-choice",
410 "points": 5,
411 "isFlagged": False,
412 "correctAnswer": ["B"],
413 "studentAnswer": {
414 "answer": ["B"],
415 "isCorrect": True,
416 "earnedPoints": 5
417 }
418 },
419 ...
420 ]
421 }
422 """
423 return await student_assignments_service.fetch_submission_details(
424 assignment_uuid, request
425 )