Coverage for server / routes / teacher / teacher_assignment.py: 92%

80 statements  

« 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 ( 

4 require_feature, 

5 require_teacher_made_assignment_access, 

6) 

7from server.models.assignment import ( 

8 Assignment, 

9 Submission, 

10 UpdateAssignmentRequest, 

11 LateReviewRequest, 

12 LateRejectRequest, 

13 MarkStudentAnswerRequest, 

14 TeacherCommentRequest, 

15 UpdateQuestionPointsRequest, 

16) 

17from server.models.sharerequests import ShareRequest 

18from server.services.teacher.teacher_assignment import TeacherAssignmentsService 

19from server.validators.class_code_validator import ClassCodePath 

20 

21 

22router = APIRouter() 

23 

24teacher_assignments_service = TeacherAssignmentsService() 

25 

26 

27@router.post( 

28 "/create", 

29 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

30 status_code=status.HTTP_201_CREATED, 

31 description="As a teacher, I can create a new assignment", 

32 summary="As a teacher, I can create a new assignment", 

33) 

34async def create(new_assigment: Assignment, request: Request): 

35 """ 

36 Create a new assignment. 

37 

38 Args: 

39 new_assigment (Assignment): Assignment details to be created 

40 request (Request): The incoming request object containing user context 

41 

42 Returns: 

43 dict: Newly created assignment details 

44 """ 

45 if not new_assigment.assigned_class: 

46 raise HTTPException( 

47 status_code=status.HTTP_400_BAD_REQUEST, 

48 detail="Assigned Class is required.", 

49 ) 

50 return await teacher_assignments_service.create(new_assigment, request) 

51 

52 

53@router.get( 

54 "/{class_code:str}/all/fetch", 

55 dependencies=[ 

56 Depends(Auth0Bearer(access_levels=["teacher"])), 

57 Depends(require_teacher_made_assignment_access()), 

58 ], 

59 status_code=status.HTTP_200_OK, 

60 description="As a teacher, I can fetch all my created assignments", 

61 summary="As a teacher, I can fetch all my created assignments", 

62) 

63async def fetch_all(class_code: ClassCodePath, request: Request): 

64 """ 

65 Fetch all assignments created by the authenticated teacher. 

66 

67 This endpoint allows a teacher to retrieve a list of assignments they have created. 

68 The request must include a valid JWT token with teacher-level access. 

69 

70 Args: 

71 request (Request): The FastAPI request object containing user authentication details. 

72 

73 Returns: 

74 List[Dict]: A list of assignments created by the authenticated teacher. 

75 

76 Raises: 

77 HTTPException (401): If the user is not authenticated or does not have the required access level. 

78 HTTPException (500): If an internal server error occurs. 

79 """ 

80 return await teacher_assignments_service.fetch_all(class_code, request) 

81 

82 

83@router.post( 

84 "/staff/assignments/create", 

85 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

86 status_code=status.HTTP_201_CREATED, 

87 description="As a teacher, I can create a new assignment on behalf of staff", 

88 summary="As a teacher, I can create a new assignment on behalf of staff", 

89) 

90async def staff_assignment_create(new_assigment: Assignment, request: Request): 

91 """ 

92 Create a new assignment, tagged as staff-created. 

93 

94 Args: 

95 new_assigment (Assignment): Assignment details to be created 

96 request (Request): The incoming request object containing user context 

97 

98 Returns: 

99 dict: Newly created assignment details 

100 """ 

101 return await teacher_assignments_service.create_staff_assignment(new_assigment, request) 

102 

103 

104@router.get( 

105 "/staff/assignments/all/fetch", 

106 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

107 status_code=status.HTTP_200_OK, 

108 description="As a teacher, I can fetch all staff created assignments", 

109 summary="As a teacher, I can fetch all staff created assignments", 

110) 

111async def staff_assignments_fetch(request: Request): 

112 """ 

113 Fetch all global assignments that are not marked as deleted. 

114 Args: 

115 request (Request): The incoming request object, containing user details. 

116 Returns: 

117 dict: A dictionary containing a success message and a list of assignments. 

118 Raises: 

119 HTTPException: If an error occurs during the fetch operation. 

120 """ 

121 return await teacher_assignments_service.staff_assignments_fetch(request) 

122 

123 

124@router.get( 

125 "/staff/assignments/{assignment_uuid}/fetch", 

126 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

127 status_code=status.HTTP_200_OK, 

128 description="As a teacher, I can fetch all staff created assignments", 

129 summary="As a teacher, I can fetch all staff created assignments", 

130) 

131async def staff_assignment_fetch_by_id(assignment_uuid: str, request: Request): 

132 """ 

133 Fetch a specific global assignment by its UUID if it is not marked as deleted. 

134 Args: 

135 assignment_uuid (str): The UUID of the assignment to fetch. 

136 request (Request): The incoming FastAPI request object. 

137 Returns: 

138 dict: A response containing a detail message and the assignment object. 

139 Raises: 

140 HTTPException:  

141 - 404 if no assignment is found with the given UUID. 

142 - 500 if an unexpected error occurs. 

143 """ 

144 return await teacher_assignments_service.staff_specific_assignment_fetch(assignment_uuid, request) 

145 

146 

147@router.get( 

148 "/{assignment_uuid:str}/fetch", 

149 dependencies=[ 

150 Depends(Auth0Bearer(access_levels=["teacher"])), 

151 Depends(require_teacher_made_assignment_access()), 

152 ], 

153 status_code=status.HTTP_200_OK, 

154 description="As a teacher, I can fetch my created assignment", 

155 summary="As a teacher, I can fetch my created assignment", 

156) 

157async def fetch_specific_assignment(assignment_uuid: str, request: Request): 

158 """ 

159 Fetch a specific assignment created by the authenticated teacher. 

160 

161 This endpoint allows a teacher to retrieve an assignment they have created,  

162 including its details and associated questions. The request must be authenticated  

163 using a valid JWT token with "teacher" access level. 

164 

165 Args: 

166 assignment_uuid (str): The unique identifier of the assignment to fetch. 

167 request (Request): The HTTP request object containing authentication details. 

168 

169 Returns: 

170 dict: The assignment details if found, including questions and metadata. 

171 

172 Raises: 

173 HTTPException (400): If the provided assignment UUID is not a valid ObjectId. 

174 HTTPException (404): If the assignment is not found or the teacher does not have access. 

175 HTTPException (500): If an unexpected error occurs during processing. 

176 """ 

177 return await teacher_assignments_service.fetch_specific_assignment(assignment_uuid, request) 

178 

179 

180@router.put( 

181 "/{assignment_uuid}/update", 

182 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

183 status_code=status.HTTP_200_OK, 

184 description="As a teacher, I can update an existing assignment's details", 

185 summary="As a teacher, I can update an existing assignment's details", 

186) 

187async def update( 

188 assignment_uuid: str, updated_assignment: UpdateAssignmentRequest, request: Request 

189): 

190 """ 

191 Update an existing assignment's details. 

192 

193 Args: 

194 assignment_uuid (str): Unique identifier of the assignment to update 

195 updated_assignment (UpdateAssignmentRequest): Updated assignment details 

196 request (Request): The incoming request object containing teacher context 

197 

198 Returns: 

199 dict: Updated assignment details 

200 """ 

201 return await teacher_assignments_service.update( 

202 assignment_uuid, updated_assignment, request 

203 ) 

204 

205 

206@router.put( 

207 "/{assignment_uuid}/question/{question_id}/points/update", 

208 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

209 status_code=status.HTTP_200_OK, 

210 description="As a teacher, I can override a question's point value for this assignment only", 

211 summary="As a teacher, I can override a question's point value for this assignment only", 

212) 

213async def update_question_points( 

214 assignment_uuid: str, question_id: str, body: UpdateQuestionPointsRequest, request: Request 

215): 

216 """ 

217 Override the point value of one question within this assignment. 

218 

219 This edits only the assignment's own `question_points_overrides` map — the 

220 underlying question document in teacher_questionbank / global_questionbank 

221 (the shared "main" question record, potentially reused by other assignments) 

222 is never modified. 

223 

224 Args: 

225 assignment_uuid (str): Unique identifier of the assignment. 

226 question_id (str): Unique identifier of the question (must belong to this assignment). 

227 body (UpdateQuestionPointsRequest): `{ "points": <number> }`, 0-1000. 

228 request (Request): The incoming request object containing teacher context. 

229 

230 Returns: 

231 dict: {assignment_id, questionId, points, updatedAt}. 

232 

233 Raises: 

234 HTTPException 400: If assignment_uuid / question_id is not a valid ObjectId. 

235 HTTPException 404: If the assignment is not owned by the teacher, or the 

236 question is not part of this assignment. 

237 """ 

238 return await teacher_assignments_service.update_question_points( 

239 assignment_uuid, question_id, body.points, request 

240 ) 

241 

242 

243@router.delete( 

244 "/{assignment_uuid}/delete", 

245 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

246 status_code=status.HTTP_200_OK, 

247 description="As a teacher, I can delete a specific assignment", 

248 summary="As a teacher, I can delete a specific assignment", 

249) 

250async def assignment_delete(assignment_uuid: str, 

251 request: Request): 

252 """ 

253 Delete a specific assignment. 

254 

255 Args: 

256 assignment_uuid (str): Unique identifier of the assignment to delete 

257 request (Request): The incoming request object containing user context 

258 

259 Returns: 

260 dict: Deletion confirmation 

261 """ 

262 return await teacher_assignments_service.delete(assignment_uuid, request) 

263 

264 

265@router.get( 

266 "/{class_code:str}/{assignment_uuid:str}/analytics/summary/fetch", 

267 dependencies=[ 

268 Depends(Auth0Bearer(access_levels=["teacher"])), 

269 Depends(require_teacher_made_assignment_access()), 

270 ], 

271 status_code=status.HTTP_200_OK, 

272 description="As a teacher, I can get analytics for a specific assignment", 

273 summary="As a teacher, I can get analytics for a specific assignment", 

274) 

275async def analytics_summary_fetch(class_code: ClassCodePath, assignment_uuid: str, request: Request): 

276 """ 

277 Retrieves detailed analytics for a specific assignment created by a teacher, including overall 

278 score metrics, total submissions, and per-category statistics. 

279 

280 This method gathers: 

281 - Assignment-level metadata (title, description, date ranges) 

282 - Overall score statistics (e.g., average, highest, lowest, passed, failed) 

283 - Per-category breakdown: 

284 - Total questions in each category 

285 - Percentage of correct answers per category across all student submissions 

286 

287 Args: 

288 assignment_uuid (str): The unique identifier of the assignment to analyze. 

289 request (Request): The FastAPI request object containing authenticated teacher info. 

290 

291 Returns: 

292 dict: A response containing: 

293 - assignment_details (dict): Metadata about the assignment. 

294 - assignment_summary (dict): Aggregated analytics and score metrics. 

295 - categories (list): Per-category breakdown with question count and correctness percentage. 

296 

297 Raises: 

298 HTTPException: If the assignment is not found or not owned by the user (404). 

299 HTTPException: If there is an issue with score data format in submissions (400). 

300 HTTPException: For any unexpected server-side error (500). 

301 """ 

302 return await teacher_assignments_service.analytics_summary_fetch(class_code, assignment_uuid, request) 

303 

304 

305@router.get( 

306 "/{class_code:str}/{assignment_uuid:str}/analytics/item_analysis/fetch", 

307 dependencies=[ 

308 Depends(Auth0Bearer(access_levels=["teacher"])), 

309 Depends(require_feature("teacher.item_analysis")), 

310 # Parity with the sibling summary route's quota guard (EI-188). Ownership 

311 # itself is enforced in the service via created_by + teacher._id scoping. 

312 Depends(require_teacher_made_assignment_access()), 

313 ], 

314 status_code=status.HTTP_200_OK, 

315 description="As a teacher, I can get item analysis analytics for a specific assignment", 

316 summary="As a teacher, I can get item analysis analytics for a specific assignment", 

317) 

318async def analytics_item_analysis_fetch(class_code: ClassCodePath, assignment_uuid: str, request: Request): 

319 """ 

320 Perform item analysis on an assignment by aggregating student answers and comparing them with correct answers. 

321 

322 This method is used by a teacher to fetch analysis data of a given assignment. It ensures that the assignment 

323 belongs to the requesting teacher, gathers all student submissions for that assignment, and calculates per-question 

324 statistics such as total correct and incorrect answers, as well as the frequency of each student-selected option. 

325 

326 Args: 

327 assignment_uuid (str): The UUID string of the assignment to analyze. 

328 request (Request): The FastAPI request object, which contains authenticated teacher details. 

329 

330 Returns: 

331 dict: A dictionary with the key `"item_analysis"` containing a list of analysis results for each question.  

332 Each result includes: 

333 - question type 

334 - category 

335 - student expectation 

336 - correct answer (for multiple-choice and checkbox) 

337 - total correct and incorrect counts 

338 - a breakdown of student-selected answers 

339 

340 Example: 

341 { 

342 "total_submissions": 10, 

343 "item_analysis": [ 

344 { 

345 "_id": "questionId1", 

346 "question_type": "multiple-choice", 

347 "category": "Math", 

348 "student_expectation": "Apply multiplication", 

349 "correct_answer": "C", 

350 "points": "2", 

351 "total_correct": 10, 

352 "total_incorrect": 5, 

353 "student_answers": [ 

354 {"letter": "A", "text": "3", "total": 2}, 

355 {"letter": "B", "text": "5", "total": 1}, 

356 {"letter": "C", "text": "6", "total": 10}, 

357 {"letter": "D", "text": "9", "total": 2} 

358 ] 

359 }, 

360 ... 

361 ] 

362 } 

363 """ 

364 return await teacher_assignments_service.analytics_item_analysis_fetch(class_code, assignment_uuid, request) 

365 

366 

367@router.get( 

368 "/{class_code:str}/{assignment_uuid:str}/analytics/student-submission/{student_id:str}/fetch", 

369 dependencies=[ 

370 Depends(Auth0Bearer(access_levels=["teacher"])), 

371 # Parity with the sibling summary/item_analysis routes (EI-188). Ownership 

372 # itself is enforced in the service via created_by + teacher._id scoping, 

373 # plus a check that the target student is enrolled in this class. 

374 Depends(require_teacher_made_assignment_access()), 

375 ], 

376 status_code=status.HTTP_200_OK, 

377 description="As a teacher, I can fetch a specific student's submission for an assignment", 

378 summary="As a teacher, I can fetch a specific student's submission for an assignment", 

379) 

380async def analytics_student_submission_fetch(class_code: ClassCodePath, assignment_uuid: str, student_id: str, request: Request): 

381 """ 

382 Fetch a single enrolled student's submission for a teacher-made assignment, 

383 for teacher review (the "Individual Answers" analytics tab). 

384 

385 Unlike the student-facing `/{assignment_uuid}/submission/fetch` endpoint, this 

386 always reveals correct answers and scores regardless of the assignment's 

387 `show_correct_answers_after_submit` / `show_score_after_submit` settings — 

388 those settings gate what STUDENTS see, not what the assignment's own teacher 

389 can see while grading. 

390 

391 Args: 

392 class_code (str): Code of the class the assignment/student are scoped to. 

393 assignment_uuid (str): The assignment's ObjectId string. 

394 student_id (str): The target student's ObjectId string. 

395 request (Request): The FastAPI request object containing authenticated teacher info. 

396 

397 Returns: 

398 dict: A response containing: 

399 - _id (str): The submission document's ID. 

400 - assignmentDetails (dict): title, description, type, date_open, date_close. 

401 - remainingTime (str): Time left ("HH:MM:SS") in the student's attempt window. 

402 - isSubmitted (bool): Whether the student has a completed submission. 

403 - grade (str), remarks (str | None) 

404 - isLate (bool), reviewStatus (str): "none" | "pending" | "approved" | "rejected". 

405 - studentScore (str): Points the student actually earned. 

406 - totalScore (str): Max points possible (sum of question points). 

407 - totalCorrectAnswers, totalQuestions, totalAnswersSubmitted, 

408 totalAttemptsUsed, totalAttemptsAllowed (str) 

409 - details (list): Per-question data (question, choices, questionType, points, 

410 isFlagged, correctAnswer: {content, answerDetails}). 

411 - studentAnswers (list): Per-question student response 

412 (questionId, questionType, isFlagged, answer, isCorrect, earnedPoints). 

413 - teacherComments (dict): questionId -> comment text left by the teacher. 

414 

415 Raises: 

416 HTTPException: 

417 - 400: If `assignment_uuid` or `student_id` is not a valid ObjectId. 

418 - 404: If the assignment/class is not owned by the teacher, or the 

419 student is not enrolled in the class. 

420 """ 

421 return await teacher_assignments_service.analytics_student_submission_fetch( 

422 class_code, assignment_uuid, student_id, request 

423 ) 

424 

425 

426@router.post( 

427 "/{class_code:str}/{assignment_uuid:str}/analytics/student-submission/{student_id:str}/mark/{question_id:str}", 

428 dependencies=[ 

429 Depends(Auth0Bearer(access_levels=["teacher"])), 

430 Depends(require_teacher_made_assignment_access()), 

431 ], 

432 status_code=status.HTTP_200_OK, 

433 description="As a teacher, I can mark one student's written answer by hand", 

434 summary="As a teacher, I can mark one student's written answer by hand", 

435) 

436async def mark_student_answer( 

437 class_code: ClassCodePath, 

438 assignment_uuid: str, 

439 student_id: str, 

440 question_id: str, 

441 body: MarkStudentAnswerRequest, 

442 request: Request, 

443): 

444 """ 

445 Mark one student's answer to one written question, for the "Individual Answers" tab. 

446 

447 A written response cannot be graded by matching text — the student writes a 

448 paragraph and the stored model answer is a different paragraph — so an answer the 

449 auto-grader could not confirm is held rather than scored zero, and this is how a 

450 person resolves it. Partial credit is the point: written work is usually part right. 

451 

452 This is NOT update_question_points. That route changes what a question is worth for 

453 every student in the assignment; this awards one student's answer on its merits. 

454 

455 Args: 

456 class_code (str): Code of the class the assignment/student are scoped to. 

457 assignment_uuid (str): The assignment's ObjectId string. 

458 student_id (str): The target student's ObjectId string. 

459 question_id (str): The question's ObjectId string. 

460 body (MarkStudentAnswerRequest): `{points, feedback?}`. 

461 request (Request): The incoming request carrying the authenticated teacher. 

462 

463 Returns: 

464 dict: the new per-answer and submission-level numbers, including how many 

465 questions on this submission are still waiting to be marked. 

466 

467 Raises: 

468 HTTPException 400: Invalid ObjectId, a question type that is not marked by 

469 hand, or points outside 0..the question's own value. 

470 HTTPException 404: Assignment/class not owned by the teacher, student not 

471 enrolled, no submission, or the question is not part of this submission. 

472 """ 

473 return await teacher_assignments_service.mark_student_answer( 

474 class_code, 

475 assignment_uuid, 

476 student_id, 

477 question_id, 

478 body.points, 

479 body.feedback, 

480 request, 

481 ) 

482 

483 

484@router.post( 

485 "/{class_code:str}/{assignment_uuid:str}/analytics/student-submission/{student_id:str}/comment/{question_id:str}/add", 

486 dependencies=[ 

487 Depends(Auth0Bearer(access_levels=["teacher"])), 

488 Depends(require_teacher_made_assignment_access()), 

489 ], 

490 status_code=status.HTTP_200_OK, 

491 description="As a teacher, I can add a new comment on a student's answer to a specific question", 

492 summary="As a teacher, I can add a new comment on a student's answer to a specific question", 

493) 

494async def add_student_answer_comment( 

495 class_code: ClassCodePath, 

496 assignment_uuid: str, 

497 student_id: str, 

498 question_id: str, 

499 body: TeacherCommentRequest, 

500 request: Request, 

501): 

502 """ 

503 Add a new teacher comment on a specific student's answer to a specific 

504 question, for the "Individual Answers" analytics tab. There is at most 

505 one teacher comment per (submission, question) — use the /update route 

506 to change an existing one. 

507 

508 Args: 

509 class_code (str): Code of the class the assignment/student are scoped to. 

510 assignment_uuid (str): The assignment's ObjectId string. 

511 student_id (str): The target student's ObjectId string. 

512 question_id (str): The question's ObjectId string. 

513 body (TeacherCommentRequest): `{ "comment": "..." }`, 1-2000 chars, non-blank. 

514 request (Request): The FastAPI request object containing authenticated teacher info. 

515 

516 Returns: 

517 dict: {submission_id, questionId, comment, updatedAt}. 

518 

519 Raises: 

520 HTTPException: 

521 - 400: If `assignment_uuid` / `student_id` / `question_id` is not a valid ObjectId. 

522 - 404: If the assignment/class is not owned by the teacher, the student is not 

523 enrolled in the class, or the student has no submission for this assignment yet. 

524 - 409: If a comment already exists for this question. 

525 """ 

526 return await teacher_assignments_service.add_student_answer_comment( 

527 class_code, assignment_uuid, student_id, question_id, body.comment, request 

528 ) 

529 

530 

531@router.put( 

532 "/{class_code:str}/{assignment_uuid:str}/analytics/student-submission/{student_id:str}/comment/{question_id:str}/update", 

533 dependencies=[ 

534 Depends(Auth0Bearer(access_levels=["teacher"])), 

535 Depends(require_teacher_made_assignment_access()), 

536 ], 

537 status_code=status.HTTP_200_OK, 

538 description="As a teacher, I can update my existing comment on a student's answer to a specific question", 

539 summary="As a teacher, I can update my existing comment on a student's answer to a specific question", 

540) 

541async def update_student_answer_comment( 

542 class_code: ClassCodePath, 

543 assignment_uuid: str, 

544 student_id: str, 

545 question_id: str, 

546 body: TeacherCommentRequest, 

547 request: Request, 

548): 

549 """ 

550 Overwrite an existing teacher comment on a specific student's answer to a 

551 specific question. Use the /add route if no comment exists for this 

552 question yet. 

553 

554 Args: 

555 class_code (str): Code of the class the assignment/student are scoped to. 

556 assignment_uuid (str): The assignment's ObjectId string. 

557 student_id (str): The target student's ObjectId string. 

558 question_id (str): The question's ObjectId string. 

559 body (TeacherCommentRequest): `{ "comment": "..." }`, 1-2000 chars, non-blank. 

560 request (Request): The FastAPI request object containing authenticated teacher info. 

561 

562 Returns: 

563 dict: {submission_id, questionId, comment, updatedAt}. 

564 

565 Raises: 

566 HTTPException: 

567 - 400: If `assignment_uuid` / `student_id` / `question_id` is not a valid ObjectId. 

568 - 404: If the assignment/class is not owned by the teacher, the student is not 

569 enrolled in the class, the student has no submission yet, or no comment 

570 exists yet for this question. 

571 """ 

572 return await teacher_assignments_service.update_student_answer_comment( 

573 class_code, assignment_uuid, student_id, question_id, body.comment, request 

574 ) 

575 

576 

577@router.delete( 

578 "/{class_code:str}/{assignment_uuid:str}/analytics/student-submission/{student_id:str}/comment/{question_id:str}/delete", 

579 dependencies=[ 

580 Depends(Auth0Bearer(access_levels=["teacher"])), 

581 Depends(require_teacher_made_assignment_access()), 

582 ], 

583 status_code=status.HTTP_200_OK, 

584 description="As a teacher, I can delete my comment on a student's answer to a specific question", 

585 summary="As a teacher, I can delete my comment on a student's answer to a specific question", 

586) 

587async def delete_student_answer_comment( 

588 class_code: ClassCodePath, assignment_uuid: str, student_id: str, question_id: str, request: Request 

589): 

590 """ 

591 Delete the teacher's comment on a specific student's answer to a specific question. 

592 

593 Args: 

594 class_code (str): Code of the class the assignment/student are scoped to. 

595 assignment_uuid (str): The assignment's ObjectId string. 

596 student_id (str): The target student's ObjectId string. 

597 question_id (str): The question's ObjectId string. 

598 request (Request): The FastAPI request object containing authenticated teacher info. 

599 

600 Returns: 

601 dict: {submission_id, questionId, deleted: true}. 

602 

603 Raises: 

604 HTTPException: 

605 - 400: If `assignment_uuid` / `student_id` / `question_id` is not a valid ObjectId. 

606 - 404: If the assignment/class is not owned by the teacher, the student is not 

607 enrolled in the class, the student has no submission yet, or no comment 

608 exists for this question. 

609 """ 

610 return await teacher_assignments_service.delete_student_answer_comment( 

611 class_code, assignment_uuid, student_id, question_id, request 

612 ) 

613 

614 

615@router.get( 

616 "/next/{assignment_uuid}/fetch", 

617 include_in_schema=False, 

618 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

619 status_code=status.HTTP_200_OK, 

620 description="As a teacher, I can get next item for adaptive testing based on previous responses", 

621 summary="As a teacher, I can get next item for adaptive testing based on previous responses", 

622) 

623async def next_fetch( 

624 request: Request, 

625 assignment_uuid: str, 

626 prev_difficulty: str, 

627 prev_remarks: str, 

628 question_classification: str, 

629): 

630 """ 

631 Get next item for adaptive testing based on previous responses. 

632 

633 Args: 

634 request (Request): The incoming request object containing user context 

635 assignment_uuid (str): Unique identifier of the assignment 

636 prev_difficulty (str): Difficulty level of previous question 

637 prev_remarks (str): Remarks from previous question 

638 question_classification (str): Classification of question type 

639 

640 Returns: 

641 dict: Next question details 

642 """ 

643 return await teacher_assignments_service.next_fetch( 

644 request, assignment_uuid, prev_difficulty, prev_remarks, question_classification 

645 ) 

646 

647 

648@router.post( 

649 "/answer/create", 

650 include_in_schema=False, 

651 dependencies=[Depends(Auth0Bearer(access_levels=["teacher", "student"]))], 

652 status_code=status.HTTP_201_CREATED, 

653 description="As a teacher, I can submit an answer for an assignment", 

654 summary="As a teacher, I can submit an answer for an assignment", 

655) 

656async def answer_create(student_assignment_response: Submission, request: Request): 

657 """ 

658 Submit an answer for an assignment. 

659 

660 Args: 

661 student_assignment_response (Submission): Student's submission details 

662 request (Request): The incoming request object containing user context 

663 

664 Returns: 

665 dict: Submission result and feedback 

666 """ 

667 return await teacher_assignments_service.answer_update( 

668 student_assignment_response, request 

669 ) 

670 

671 

672@router.get( 

673 "/{submission_id}/fetch", 

674 include_in_schema=False, 

675 dependencies=[ 

676 Depends(Auth0Bearer(access_levels=["teacher"])), 

677 Depends(require_teacher_made_assignment_access()), 

678 ], 

679 status_code=status.HTTP_200_OK, 

680 description="As a teacher, I can retrieve a specific submission for review", 

681 summary="As a teacher, I can retrieve a specific submission for review", 

682) 

683async def assignment_fetch(submission_id: str, request: Request): 

684 """ 

685 Retrieve a specific submission for review. 

686 

687 Args: 

688 submission_id (str): Unique identifier of the submission 

689 request (Request): The incoming request object containing user context 

690 

691 Returns: 

692 dict: Submission details with answers and feedback 

693 """ 

694 return await teacher_assignments_service.fetch( 

695 submission_id, request 

696 ) 

697 

698@router.get( 

699 "/{assignment_uuid}/late-submissions/pending/fetch", 

700 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

701 status_code=status.HTTP_200_OK, 

702 description="As a teacher, I can list pending late submissions for a given assignment", 

703 summary="As a teacher, I can list pending late submissions for a given assignment", 

704) 

705async def list_pending_late_submissions(assignment_uuid: str, request: Request): 

706 """ 

707 List all late submissions currently pending teacher review for an assignment. 

708 

709 Args: 

710 assignment_uuid (str): The unique identifier of the assignment. 

711 request (Request): The incoming request object containing teacher context. 

712 

713 Returns: 

714 dict: {assignment_uuid, pending: [{submission_id, student_id, student_name, 

715 date_submitted, pre_penalty_grade, total_score}]} 

716 

717 Raises: 

718 HTTPException 400: If the assignment_uuid is not a valid ObjectId. 

719 HTTPException 404: If the assignment is not found or not owned by the teacher. 

720 """ 

721 return await teacher_assignments_service.list_pending_late_submissions(assignment_uuid, request) 

722 

723 

724@router.post( 

725 "/late-submission/{submission_id}/approve", 

726 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

727 status_code=status.HTTP_200_OK, 

728 description="As a teacher, I can approve a pending late submission", 

729 summary="As a teacher, I can approve a pending late submission", 

730) 

731async def approve_late_submission(submission_id: str, body: LateReviewRequest, request: Request): 

732 """ 

733 Approve a pending late submission, optionally applying a late penalty. 

734 

735 Args: 

736 submission_id (str): The unique identifier of the submission. 

737 body (LateReviewRequest): Payload containing late_penalty (0-100, default 0). 

738 request (Request): The incoming request object containing teacher context. 

739 

740 Returns: 

741 dict: {submission_id, review_status, grade, late_penalty} 

742 

743 Raises: 

744 HTTPException 400: If the submission_id is not a valid ObjectId. 

745 HTTPException 404: If the submission or owning assignment is not found. 

746 HTTPException 409: If the submission is not in 'pending' status. 

747 """ 

748 return await teacher_assignments_service.approve_late_submission( 

749 submission_id, body.late_penalty, request 

750 ) 

751 

752 

753@router.post( 

754 "/late-submission/{submission_id}/reject", 

755 dependencies=[Depends(Auth0Bearer(access_levels=["teacher"]))], 

756 status_code=status.HTTP_200_OK, 

757 description="As a teacher, I can reject a pending late submission", 

758 summary="As a teacher, I can reject a pending late submission", 

759) 

760async def reject_late_submission(submission_id: str, body: LateRejectRequest, request: Request): 

761 """ 

762 Reject a pending late submission. 

763 

764 Args: 

765 submission_id (str): The unique identifier of the submission. 

766 body (LateRejectRequest): Optional rejection reason (max 500 chars). 

767 request (Request): The incoming request object containing teacher context. 

768 

769 Returns: 

770 dict: {submission_id, review_status} 

771 

772 Raises: 

773 HTTPException 400: If the submission_id is not a valid ObjectId. 

774 HTTPException 404: If the submission or owning assignment is not found. 

775 HTTPException 409: If the submission is not in 'pending' status. 

776 """ 

777 return await teacher_assignments_service.reject_late_submission( 

778 submission_id, body.reason, request 

779 ) 

780 

781 

782@router.post( 

783 "/share", 

784 include_in_schema=False, 

785 dependencies=[ 

786 Depends(Auth0Bearer(access_levels=["teacher"])), 

787 Depends(require_teacher_made_assignment_access()), 

788 ], 

789 status_code=status.HTTP_200_OK, 

790 description="As a teacher, I can share an assignment with other users", 

791 summary="As a teacher, I can share an assignment with other users", 

792) 

793async def assignment_share(share_request: ShareRequest, request: Request): 

794 """ 

795 Share an assignment with other users. 

796 

797 Args: 

798 share_request (ShareRequest): Details about sharing preferences 

799 request (Request): The incoming request object containing user context 

800 

801 Returns: 

802 dict: Sharing confirmation details 

803 """ 

804 return await teacher_assignments_service.share(share_request, request)