Coverage for server / routes / common / assignments.py: 97%
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.models.assignment import Assignment, Submission, UpdateAssignmentRequest
4from server.models.sharerequests import ShareRequest
5from server.services.common.assignments import AssignmentsService
8router = APIRouter()
10assignments_service = AssignmentsService()
13from fastapi.responses import JSONResponse
16@router.post(
17 "/create",
18 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
19 status_code=status.HTTP_200_OK,
20 # Modified by Allan Ninal — 2026-09-24 (EI-T113).
21 # `response_model=Assignment` was removed. The service returns an ENVELOPE —
22 # {"detail": "Successfully Created Assignment", "new_assignment": Assignment}
23 # — so FastAPI validated that envelope AS an Assignment, the model's root
24 # validator reported "semester is required" (the envelope has no semester
25 # key), and the request died with ResponseValidationError -> 500.
26 #
27 # The write had ALREADY succeeded by then, so every call created an
28 # assignment and reported failure; a retrying client duplicated it. This was
29 # the only response_model in the file — every other route here returns its
30 # envelope unvalidated — so removing it matches the existing contract rather
31 # than inventing one.
32 response_description="Newly created assignment details",
33 responses={
34 200: {
35 "description": "Newly created assignment details",
36 "content": {
37 "application/json": {
38 "example": {
39 "title": "Assignment 123 from staff",
40 "description": "test",
41 "type": "STAAR",
42 "category": "current",
43 "semester": "spring",
44 "assigned_class": ["6660821c60082c7a7abc6242"],
45 "date_open": "2025-05-25T00:00:00.000Z",
46 "date_close": "2025-10-27T00:00:00.000Z",
47 "format": "standard",
48 "questions": [
49 {
50 "id": "66146bc250b7d3e9e30e0021",
51 "category": "2",
52 "topic": "topic2",
53 },
54 {
55 "id": "67a43228d70921ca626e99b9",
56 "category": "1",
57 "topic": "test",
58 },
59 {
60 "id": "67e91274bb615cb417807ef7",
61 "category": "1",
62 "topic": "This is topic",
63 },
64 {
65 "id": "67f12c007c6d257620daf333",
66 "category": "1",
67 "topic": "test",
68 },
69 {
70 "id": "67f2137ad036733d714b8344",
71 "category": "1",
72 "topic": "Test topic",
73 },
74 {
75 "id": "67fa816a4a7de71ac565bdab",
76 "category": "1",
77 "topic": "test",
78 },
79 {
80 "id": "68035aa9add1d22b4660d131",
81 "category": "1",
82 "topic": "Slope and Rate of Change",
83 },
84 ],
85 "settings": {
86 "allowed_attempts": 3,
87 "shuffle_questions": True,
88 "shuffle_choices": True,
89 "allow_calculator": True,
90 "show_score_after_submit": True,
91 "show_correct_answers_after_submit": True,
92 "allow_feedback_after_submit": True,
93 "time_allowed": "01:00:00",
94 },
95 "passing_grade": 90,
96 "copy_of": "684c36d4a688b290bbfe0b16",
97 }
98 }
99 },
100 }
101 },
102)
103async def create_new_assignment(new_assigment: Assignment, request: Request) -> dict:
104 """
105 Create a new assignment.
107 Args:
108 new_assigment (Assignment): Assignment details to be created
109 request (Request): The incoming request object containing user context
111 Returns:
112 dict: {"detail": <message>, "new_assignment": <the created assignment>}
113 """
114 return await assignments_service.create_new_assignment(new_assigment, request)
117@router.get(
118 "/view/{assignment_uuid}",
119 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
120 status_code=status.HTTP_200_OK,
121 response_description="Returns detailed information about a specific assignment",
122 tags=["Assignments"],
123)
124async def get_specific_assignment_by_id(
125 assignment_uuid: str, # Path parameter for assignment identification
126 request: Request, # FastAPI request object containing user context
127) -> dict:
128 """
129 Retrieve a specific assignment by its UUID. This endpoint is restricted to teachers only
130 and requires JWT authentication.
132 Args:
133 assignment_uuid (str): Unique identifier of the assignment. Must be a valid UUID string
134 that exists in the database.
135 request (Request): The incoming request object containing user context, including:
136 - JWT token in headers
137 - User authentication information
138 - User role and permissions
140 Returns:
141 dict: Assignment details including:
142 - assignment_id: str (UUID of the assignment)
143 - title: str (Title of the assignment)
144 - description: str (Detailed description)
145 - due_date: datetime (Assignment deadline)
146 - created_by: str (Teacher's ID who created it)
147 - created_at: datetime (Creation timestamp)
148 - status: str (Current status of the assignment)
149 - questions: list (List of questions and their details)
150 - settings: dict (Assignment configuration settings)
152 Raises:
153 HTTPException(404): If the assignment UUID doesn't exist
154 HTTPException(403): If the user doesn't have permission to view this assignment
155 HTTPException(401): If the authentication token is invalid or expired
157 Example:
158 GET /assignments/view/123e4567-e89b-12d3-a456-426614174000
159 """
160 # Delegate the actual retrieval to the assignments service
161 # The service layer handles database interactions and permission checks
162 return await assignments_service.assignment_view_fetch(assignment_uuid, request)
165@router.get(
166 "/adoptive/{assignment_uuid}/next_item",
167 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
168 status_code=status.HTTP_200_OK,
169)
170async def adoptive_next_item(
171 request: Request,
172 assignment_uuid: str,
173 prev_difficulty: str,
174 prev_remarks: str,
175 question_classification: str,
176):
177 """
178 Get next item for adaptive testing based on previous responses.
180 Args:
181 request (Request): The incoming request object containing user context
182 assignment_uuid (str): Unique identifier of the assignment
183 prev_difficulty (str): Difficulty level of previous question
184 prev_remarks (str): Remarks from previous question
185 question_classification (str): Classification of question type
187 Returns:
188 dict: Next question details
189 """
190 return await assignments_service.assignment_adaptive_fetch(
191 request, assignment_uuid, prev_difficulty, prev_remarks, question_classification
192 )
195@router.post(
196 "/answer",
197 dependencies=[Depends(Auth0Bearer(access_levels=["teacher", "student"]))],
198 status_code=status.HTTP_200_OK,
199)
200async def answer_assignment(student_assignment_response: Submission, request: Request):
201 """
202 Submit an answer for an assignment.
204 Args:
205 student_assignment_response (Submission): Student's submission details
206 request (Request): The incoming request object containing user context
208 Returns:
209 dict: Submission result and feedback
210 """
211 # assignments_service.answer_assignment does not exist and never has, so
212 # every call to POST /v1/assignments/answer raised AttributeError and
213 # answered 500. The method that records a submission is
214 # assignment_answer_update, whose signature — (submission: Submission,
215 # request: Request) — matches what this route already passes, argument for
216 # argument. Found 2026-09-22 by pylint E1101 once the pre-commit hook was
217 # repaired; the route is in the OpenAPI spec and had no test.
218 return await assignments_service.assignment_answer_update(
219 student_assignment_response, request
220 )
223@router.get(
224 "/review/{submission_id}",
225 dependencies=[Depends(Auth0Bearer(access_levels=["teacher", "student"]))],
226 status_code=status.HTTP_200_OK,
227)
228async def review_submission(submission_id: str, request: Request):
229 """
230 Retrieve a specific submission for review.
232 Args:
233 submission_id (str): Unique identifier of the submission
234 request (Request): The incoming request object containing user context
236 Returns:
237 dict: Submission details with answers and feedback
238 """
239 return await assignments_service.assignment_review_fetch(submission_id, request)
242@router.post(
243 "/share",
244 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
245 status_code=status.HTTP_200_OK,
246)
247async def share_assignment(share_request: ShareRequest, request: Request):
248 """
249 Share an assignment with other users.
251 Args:
252 share_request (ShareRequest): Details about sharing preferences
253 request (Request): The incoming request object containing user context
255 Returns:
256 dict: Sharing confirmation details
257 """
258 return await assignments_service.assignment_share(share_request, request)
261@router.get(
262 "/show_analytics/{assignment_uuid}",
263 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
264 status_code=status.HTTP_200_OK,
265)
266async def show_assignment_analytics(assignment_uuid: str, request: Request):
267 """
268 Get analytics for a specific assignment.
270 Args:
271 assignment_uuid (str): Unique identifier of the assignment
273 Returns:
274 dict: Analytics data including completion rates, scores, and performance metrics
275 """
276 return await assignments_service.assignment_analytics_fetch(
277 assignment_uuid, request
278 )
281@router.put(
282 "/update/{assignment_uuid}",
283 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
284 status_code=status.HTTP_200_OK,
285)
286async def update_assignment_details(
287 assignment_uuid: str, updated_assignment: UpdateAssignmentRequest, request: Request
288):
289 """
290 Update an existing assignment's details.
292 Args:
293 assignment_uuid (str): Unique identifier of the assignment
294 updated_assignment (UpdateAssignmentRequest): Updated assignment details
295 (``semester`` required at the HTTP boundary — EI-T771 / EI-T583)
296 request (Request): The incoming request object containing user context
298 Returns:
299 dict: Updated assignment details
300 """
301 return await assignments_service.assignment_update(
302 assignment_uuid, updated_assignment, request
303 )
306@router.delete(
307 # Modified by Allan Ninal — 2026-09-24 (EI-T121).
308 # WAS: "/delete/{assignment_id}" while the handler below takes
309 # `assignment_uuid`. The names did not match, so FastAPI bound NOTHING
310 # from the path and treated `assignment_uuid` as a REQUIRED QUERY
311 # parameter. Every call to the documented path answered
312 # 422 {"field": "assignment_uuid", "msg": "Field required"} — the
313 # endpoint could not delete anything through its own URL, only through
314 # the accidental form /delete/<ignored>?assignment_uuid=<id>.
315 # Renaming the PATH (rather than the handler argument) is the safe
316 # direction: every caller already uses the path form — the automation
317 # endpoint helper, the auth-sweep tests, EI-T777 — and nothing anywhere uses
318 # the query form, because it was never documented. It also matches the
319 # sibling route "/update/{assignment_uuid}".
320 "/delete/{assignment_uuid}",
321 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))],
322 status_code=status.HTTP_200_OK,
323)
324async def delete_assignment_by_id(assignment_uuid: str, request: Request):
325 """
326 Delete a specific assignment.
328 Args:
329 assignment_uuid (str): Unique identifier of the assignment to delete
330 request (Request): The incoming request object containing user context
332 Returns:
333 dict: Deletion confirmation
334 """
335 return await assignments_service.assignment_delete(assignment_uuid, request)