Coverage for server / routes / teacher / teacher_question.py: 100%

78 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-10-04 09:33 +0000

1from typing import Annotated, Optional, List 

2from enum import Enum 

3from fastapi import ( 

4 APIRouter, 

5 Body, 

6 Depends, 

7 File, 

8 HTTPException, 

9 Query, 

10 Request, 

11 UploadFile, 

12 status, 

13) 

14from fastapi.openapi.models import Example 

15from server.authentication.auth0_bearer import Auth0Bearer 

16from server.services.growthbook import require_feature 

17 

18# from server.models.question import Question, UpdatedQuestion, UpdateQuestionStatus 

19from server.utilities import sample_payloads 

20from server.utilities.sample_payloads import teacher_questionbank_payload 

21from server.models.question_bank import QuestionModelCreate, QuestionModelUpdate 

22from server.services.teacher.teacher_question import TeacherQuestionService 

23from server.utilities.user_id_helper import to_user_id 

24 

25 

26router = APIRouter() 

27 

28teacher_question_service = TeacherQuestionService() 

29 

30 

31# Define enums for the filter options 

32class AssignmentType(str, Enum): 

33 STAAR = "STAAR" 

34 TSI = "TSI" 

35 SAT = "SAT" 

36 ACT = "ACT" 

37 

38 

39class QuestionType(str, Enum): 

40 MULTIPLE_CHOICE = "Multiple-choice" 

41 CHECKBOX = "Checkbox" 

42 FREE_RESPONSE = "Free-response" 

43 GRAPH = "Graph" 

44 DROP_DOWN_MENU = "Drop-down-Menu" 

45 DRAG_AND_DROP = "Drag-and-Drop" 

46 EMBEDDED_MULTIPLE_CHOICE = "Embedded-Multiple-Choice" 

47 SINGLE_STIMULUS = "Single-Stimulus" 

48 MULTI_PART_QUESTION = "Multi-Part-Question" 

49 GRID_QUESTION = "Grid-Question" 

50 GRAPH_MULTIPLE_SELECT = "Graph-Multiple-Select" 

51 

52 

53@router.get( 

54 "/all/fetch", 

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

56 status_code=status.HTTP_200_OK, 

57 response_description="As a teacher, I can fetch all questions accessible to me", 

58 description="As a teacher, I can fetch all questions accessible to me", 

59 summary="As a teacher, I can fetch all questions accessible to me", 

60) 

61async def teacher_questions_fetch( 

62 request: Request, 

63 assignment_types: Optional[List[AssignmentType]] = Query( 

64 None, description="Filter by assignment types" 

65 ), 

66 question_types: Optional[List[QuestionType]] = Query( 

67 None, description="Filter by question types" 

68 ), 

69 categories: Optional[List[str]] = Query(None, description="Filter by categories"), 

70 difficulties: Optional[List[str]] = Query( 

71 None, description="Filter by difficulty levels" 

72 ), 

73 grade_levels: Optional[List[int]] = Query( 

74 None, description="Filter by grade levels" 

75 ), 

76 subjects: Optional[List[str]] = Query(None, description="Filter by subjects"), 

77) -> dict: 

78 """ 

79 Retrieve all questions accessible to teachers. 

80 

81 This endpoint returns a paginated list of questions that the authenticated teacher 

82 can access, including questions they've created and shared questions from other teachers. 

83 Results can be filtered and sorted using query parameters. 

84 

85 Args: 

86 request (Request): The incoming request object containing: 

87 - JWT token in Authorization header 

88 - Teacher authentication context 

89 - Query parameters for filtering: 

90 - assignment_types (List[AssignmentType], optional): Filter by assignment types 

91 - question_types (List[QuestionType], optional): Filter by question types 

92 - subject (str): Filter by subject area 

93 - difficulty (str): Filter by difficulty level 

94 - search (str): Search question text/title 

95 - page (int): Page number for pagination 

96 - limit (int): Items per page 

97 

98 Returns: 

99 dict: Paginated question listing containing: 

100 - items (list): List of question objects with: 

101 - id (str): Unique question identifier 

102 - title (str): Question title/summary 

103 - content (str): Full question text 

104 - type (str): Question type (multiple choice, essay, etc) 

105 - difficulty (str): Difficulty rating 

106 - subject (str): Subject area 

107 - author (dict): Teacher who created the question 

108 - created_at (datetime): Creation timestamp 

109 - total (int): Total number of matching questions 

110 - page (int): Current page number 

111 - pages (int): Total number of pages 

112 - has_more (bool): Whether more pages exist 

113 

114 Raises: 

115 HTTPException: 

116 - 401: Invalid or missing authentication token 

117 - 422: Invalid query parameters 

118 - 500: Error retrieving questions 

119 

120 The 403 this previously documented did not exist: no code path produced it, 

121 and the missing check it implied was EI-SEC-010. There is nothing to forbid 

122 now — the bank returned is always the caller's own. 

123 """ 

124 # EI-SEC-010. This route used to accept `teacher_id` as a query parameter and 

125 # fall back to the caller's own id only when the client sent nothing, with no 

126 # ownership check anywhere — so `?teacher_id=<any other teacher>` returned 

127 # that teacher's entire question bank to any authenticated teacher. 

128 # 

129 # The parameter is removed rather than guarded: nothing legitimate sent it. 

130 # No caller exists in either SPA or in admin-staff-automation, and the 

131 # cross-teacher sharing this route's docstring mentions is served by a 

132 # different endpoint (/v1/teacher/question/staff/questions/all/fetch). Its 

133 # only consumers were two automation edge tests probing malformed and unknown 

134 # ids, updated alongside this change. 

135 # 

136 # Deriving the owner from the token unconditionally is what 

137 # teacher_questions_fetch_all (line ~536) in this same file already does. 

138 teacher_id = to_user_id(request.state.user_details["uuid"]) 

139 

140 request.state.assignment_types = assignment_types 

141 request.state.question_types = question_types 

142 request.state.categories = categories 

143 request.state.difficulties = difficulties 

144 request.state.grade_levels = grade_levels 

145 request.state.subjects = subjects 

146 return await teacher_question_service.fetch(request, teacher_id) 

147 

148 

149@router.get( 

150 "/staff/questions/all/fetch", 

151 dependencies=[ 

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

153 Depends(require_feature("teacher.erudition_tests_library")), 

154 ], 

155 status_code=status.HTTP_200_OK, 

156 response_description="As a teacher, I can fetch all staff questions accessible to me", 

157 description="As a teacher, I can fetch all staff questions accessible to me", 

158 summary="As a teacher, I can fetch all staff questions accessible to me", 

159) 

160async def teacher_staff_questions_fetch( 

161 request: Request, 

162 assignment_types: Optional[List[AssignmentType]] = Query( 

163 None, description="Filter by assignment types" 

164 ), 

165 question_types: Optional[List[QuestionType]] = Query( 

166 None, description="Filter by question types" 

167 ), 

168 categories: Optional[List[str]] = Query(None, description="Filter by categories"), 

169 difficulties: Optional[List[str]] = Query( 

170 None, description="Filter by difficulty levels" 

171 ), 

172 grade_levels: Optional[List[int]] = Query( 

173 None, description="Filter by grade levels" 

174 ), 

175 subjects: Optional[List[str]] = Query(None, description="Filter by subjects"), 

176) -> dict: 

177 """ 

178 Retrieve all staff questions accessible to teachers. 

179 

180 This endpoint returns a paginated list of staff questions that the authenticated teacher can access. 

181 Results can be filtered and sorted using query parameters. 

182 

183 Args: 

184 request (Request): The incoming request object containing: 

185 - JWT token in Authorization header 

186 - Teacher authentication context 

187 - Query parameters for filtering: 

188 - assignment_types (List[AssignmentType], optional): Filter by assignment types 

189 - question_types (List[QuestionType], optional): Filter by question types 

190 - subject (str): Filter by subject area 

191 - difficulty (str): Filter by difficulty level 

192 - search (str): Search question text/title 

193 - page (int): Page number for pagination 

194 - limit (int): Items per page 

195 

196 Returns: 

197 dict: Paginated question listing containing: 

198 - items (list): List of question objects with: 

199 - id (str): Unique question identifier 

200 - title (str): Question title/summary 

201 - content (str): Full question text 

202 - type (str): Question type (multiple choice, essay, etc) 

203 - difficulty (str): Difficulty rating 

204 - subject (str): Subject area 

205 - author (dict): Teacher who created the question 

206 - created_at (datetime): Creation timestamp 

207 - total (int): Total number of matching questions 

208 - page (int): Current page number 

209 - pages (int): Total number of pages 

210 - has_more (bool): Whether more pages exist 

211 

212 Raises: 

213 HTTPException: 

214 - 401: Invalid or missing authentication token 

215 - 403: Insufficient permissions to access questions 

216 - 422: Invalid query parameters 

217 - 500: Error retrieving questions 

218 """ 

219 request.state.assignment_types = assignment_types 

220 request.state.question_types = question_types 

221 request.state.categories = categories 

222 request.state.difficulties = difficulties 

223 request.state.grade_levels = grade_levels 

224 request.state.subjects = subjects 

225 return await teacher_question_service.staff_questions_fetch(request) 

226 

227 

228@router.post( 

229 "/create", 

230 dependencies=[ 

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

232 Depends(require_feature("teacher.create_own_questions")), 

233 ], 

234 status_code=status.HTTP_201_CREATED, 

235 response_description="As a teacher, I can create a new question", 

236 description="As a teacher, I can create a new question", 

237 summary="As a teacher, I can create a new question", 

238) 

239async def teacher_create_question( 

240 request: Request, 

241 question_data: Annotated[ 

242 dict, 

243 Body( 

244 description="Question data must match one of these templates: Multiple-choice, Checkbox, Free-response, Graph, Drop-down-Menu, Drag-and-Drop, or Embedded-Multiple-Choice", 

245 openapi_examples={ 

246 k: Example(**v) 

247 for k, v in teacher_questionbank_payload.items() 

248 if k 

249 in { 

250 "Multiple-choice", 

251 "Checkbox", 

252 "Free-response", 

253 "Graph", 

254 "Drop-down-Menu", 

255 "Drag-and-Drop", 

256 "Embedded-Multiple-Choice", 

257 "Single-Stimulus", 

258 "Multi-Part-Question", 

259 "Grid-Question", 

260 "Graph-Multiple-Select", 

261 } 

262 }, 

263 ), 

264 ], 

265) -> dict: 

266 """ 

267 Create a new question in the question bank. 

268 

269 This endpoint allows teachers to create a new question with specified details. The question 

270 will be associated with the authenticated teacher as the author. Supports multiple question 

271 types including Multiple-choice, Checkbox, Free-response, Graph, Drop-down-Menu, 

272 Drag-and-Drop, and Embedded-Multiple-Choice. 

273 

274 Args: 

275 request (Request): The incoming request object containing: 

276 - JWT token in Authorization header 

277 - Teacher authentication context 

278 - Teacher role and permissions 

279 question_data (dict): Question details for creation including: 

280 - question (str, required): Main question text/prompt, supports HTML formatting 

281 - choices (list[dict], required for Multiple-choice/Checkbox): 

282 - id (int): Choice identifier 

283 - text (str): Choice text, supports HTML formatting 

284 - correctAnswer (dict, required): 

285 - answers (Union[str, list[str]]): Correct answer(s) 

286 - answerDetails (str, optional): Explanation for the correct answer 

287 - graph (str, optional): Graph data for Graph type questions 

288 - questionDetails (str, optional): Additional details or context for the question 

289 - assignmentType (str, required): Type of assignment (e.g., "STAAR") 

290 - questionType (str, required): One of: 

291 - "Multiple-choice" 

292 - "Checkbox" 

293 - "Free-response" 

294 - "Graph" 

295 - "Drop-down-Menu" 

296 - "Drag-and-Drop" 

297 - "Embedded-Multiple-Choice" 

298 - difficulty (str, required): Question difficulty (e.g., "Easy", "Advance") 

299 - teksCode (str, required): TEKS standard code 

300 - points (str/int, required): Point value for the question 

301 - category (str, required): Question category identifier 

302 

303 For Drop-down-Menu and Drag-and-Drop types: 

304 - choices (list[dict]): List of choice groups: 

305 - id (int): Group identifier 

306 - items (list): Available items for this group 

307 

308 For Embedded-Multiple-Choice: `question` is a passage with the answer 

309 choices marked inline as `<span data-embedded-choice-id="...">...</span>`; 

310 `choices` is a flat list of `{id, text}` (one per marked span) and 

311 `correctAnswer.answers` is a single-element list holding the correct 

312 choice's `id` (not its text — ids disambiguate a phrase that appears 

313 more than once in the passage). 

314 

315 Returns: 

316 dict: Created question details including: 

317 - id (str): Unique question identifier 

318 - question (str): Question text 

319 - choices (list): Answer choices if applicable 

320 - correctAnswer (dict): Correct answer information 

321 - questionDetails (str): Additional question details 

322 - assignmentType (str): Assignment type 

323 - questionType (str): Question type 

324 - difficulty (str): Difficulty level 

325 - teksCode (str): TEKS code 

326 - points (int): Point value 

327 - category (str): Question category 

328 - author (dict): Teacher who created it 

329 - created_at (datetime): Creation timestamp 

330 - status (str): Current status 

331 

332 Raises: 

333 HTTPException: 

334 - 401: Invalid or missing authentication token 

335 - 403: Insufficient permissions to create questions 

336 - 422: Invalid question details: 

337 - Missing required fields 

338 - Invalid question type 

339 - Missing choices for Multiple-choice/Checkbox 

340 - Invalid choice format 

341 - Missing or invalid correct answer format 

342 - 500: Error creating question 

343 """ 

344 

345 return await teacher_question_service.create(request, question_data) 

346 

347 

348@router.get( 

349 "/{question_id}/fetch", 

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

351 status_code=status.HTTP_200_OK, 

352 response_description="As a teacher, I can fetch a specific question by its ID", 

353 description="As a teacher, I can fetch a specific question by its ID", 

354 summary="As a teacher, I can fetch a specific question by its ID", 

355) 

356async def teacher_fetch_question(question_id: str, request: Request) -> dict: 

357 """ 

358 Retrieve detailed information about a specific question by its ID. 

359 

360 This endpoint returns comprehensive details about a question identified by its unique ID. 

361 Only teachers with appropriate permissions can access the question data. 

362 

363 Args: 

364 question_id (str): Unique identifier of the question to retrieve 

365 request (Request): The incoming request object containing: 

366 - JWT token in Authorization header 

367 - Teacher authentication context 

368 - Teacher role and permissions 

369 

370 Returns: 

371 dict: Question details including: 

372 - id (str): Unique question identifier 

373 - title (str): Question title/summary 

374 - content (str): Full question text/prompt 

375 - type (str): Question type (e.g. multiple choice, essay) 

376 - difficulty (str): Difficulty rating 

377 - subject (str): Subject area/discipline 

378 - options (list): Answer choices for multiple choice questions 

379 - correct_answer (str/list): Correct answer(s) 

380 - points (int): Point value 

381 - tags (list): Categorization tags 

382 - author (dict): Teacher who created the question 

383 - created_at (datetime): Creation timestamp 

384 - updated_at (datetime): Last modification timestamp 

385 - status (str): Current question status 

386 

387 Raises: 

388 HTTPException: 

389 - 401: Invalid or missing authentication token 

390 - 403: Insufficient permissions to access question 

391 - 404: Question not found 

392 - 500: Error retrieving question data 

393 """ 

394 return await teacher_question_service.detail_fetch(question_id, request) 

395 

396 

397@router.put( 

398 "/{question_id}/update", 

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

400 status_code=status.HTTP_200_OK, 

401 response_description="As a teacher, I can update a specific question by its ID", 

402 description="As a teacher, I can update a specific question by its ID", 

403 summary="As a teacher, I can update a specific question by its ID", 

404) 

405async def teacher_question_update( 

406 question_id: str, 

407 question_data: Annotated[ 

408 dict, 

409 Body( 

410 description="Question data must match one of these templates: Multiple-choice, Checkbox, Free-response, Graph, Drop-down-Menu, Drag-and-Drop, or Embedded-Multiple-Choice", 

411 openapi_examples={ 

412 k: Example(**v) 

413 for k, v in teacher_questionbank_payload.items() 

414 if k 

415 in { 

416 "Multiple-choice", 

417 "Checkbox", 

418 "Free-response", 

419 "Graph", 

420 "Drop-down-Menu", 

421 "Drag-and-Drop", 

422 "Embedded-Multiple-Choice", 

423 "Single-Stimulus", 

424 "Multi-Part-Question", 

425 "Grid-Question", 

426 "Graph-Multiple-Select", 

427 } 

428 }, 

429 ), 

430 ], 

431 request: Request, 

432) -> dict: 

433 """ 

434 Update a specific question by its ID. 

435 

436 This endpoint allows teachers to modify an existing question's content, answers, and metadata. 

437 Teachers can only update questions they have created. 

438 

439 Args: 

440 question_id (str): Unique identifier of the question to update 

441 question_data (dict): Updated question details including: 

442 - question (str, required): Main question text/prompt, supports HTML formatting 

443 - choices (list[dict], required for Multiple-choice/Checkbox): 

444 - id (int): Choice identifier 

445 - text (str): Choice text, supports HTML formatting 

446 - correctAnswer (dict, required): 

447 - answers (Union[str, list[str]]): Correct answer(s) 

448 - answerDetails (str, optional): Explanation for the correct answer 

449 - graph (str, optional): Graph data for Graph type questions 

450 - questionDetails (str, optional): Additional details or context 

451 - assignmentType (str, required): Type of assignment (e.g., "STAAR") 

452 - questionType (str, required): Question format type 

453 - difficulty (str, required): Question difficulty level 

454 - teksCode (str, required): TEKS standard code 

455 - points (str/int, required): Point value 

456 - category (str, required): Question category 

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

458 

459 Returns: 

460 dict: Updated question object containing: 

461 - updated_question (dict): Complete updated question details 

462 - _id (str): Question identifier 

463 - question (str): Updated question text 

464 - choices (list): Modified answer choices 

465 - correctAnswer (dict): New correct answer data 

466 - other fields: Additional updated metadata 

467 

468 Raises: 

469 HTTPException: 

470 - 400: Invalid question ID format 

471 - 401: Invalid or missing authentication token 

472 - 403: Insufficient permissions to update question 

473 - 404: Question not found or unauthorized access 

474 - 422: Invalid request body format 

475 - 500: Database or server error 

476 """ 

477 return await teacher_question_service.update(question_id, question_data, request) 

478 

479 

480@router.delete( 

481 "/{question_id}/delete", 

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

483 status_code=status.HTTP_200_OK, 

484 response_description="As a teacher, I can delete a specific question by its ID", 

485 description="As a teacher, I can delete a specific question by its ID", 

486 summary="As a teacher, I can delete a specific question by its ID", 

487) 

488async def teacher_question_delete(question_id: str, request: Request) -> dict: 

489 """ 

490 Delete a specific question by its ID. 

491 

492 Args: 

493 question_id (str): Unique identifier of the question to delete 

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

495 

496 Returns: 

497 dict: Deletion confirmation message 

498 

499 Raises: 

500 HTTPException: 

501 - 401: Invalid or missing authentication token 

502 - 403: Insufficient permissions to delete question 

503 - 404: Question not found 

504 - 500: Error deleting question 

505 """ 

506 return await teacher_question_service.delete(question_id, request) 

507 

508 

509@router.get( 

510 "/filter-options", 

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

512 status_code=status.HTTP_200_OK, 

513 response_description="As a teacher, I can get filter options with counts", 

514 description="As a teacher, I can get filter options with counts", 

515 summary="Get filter options with counts", 

516) 

517async def teacher_filter_options(request: Request) -> dict: 

518 """ 

519 Get available filter options with counts. 

520 

521 This endpoint returns counts of questions for each filter option, 

522 such as assignment types, question types, categories, and difficulties. 

523 

524 Args: 

525 request (Request): The incoming request object containing: 

526 - JWT token in Authorization header 

527 - Teacher authentication context 

528 

529 Returns: 

530 dict: Filter options with counts: 

531 - assignmentTypes: Count by assignment type (e.g., {"STAAR": 5, "TSI": 3}) 

532 - questionTypes: Count by question type (e.g., {"Multiple-choice": 7}) 

533 - categories: Count by category (e.g., {"math": 4, "science": 2}) 

534 - difficulties: Count by difficulty (e.g., {"easy": 3, "medium": 4}) 

535 - available_assignment_types: List of all possible assignment types from enum 

536 - available_question_types: List of all possible question types from enum 

537 

538 Raises: 

539 HTTPException: 

540 - 401: Invalid or missing authentication token 

541 - 403: Insufficient permissions 

542 - 500: Error retrieving filter options 

543 """ 

544 # Get filter options with counts from the service 

545 filter_options = await teacher_question_service.get_filter_options(request) 

546 

547 # Add available options from enums to help with checkbox rendering 

548 filter_options["available_assignment_types"] = [ 

549 {"value": type.value, "label": type.name} for type in AssignmentType 

550 ] 

551 filter_options["available_question_types"] = [ 

552 {"value": type.value, "label": type.name.replace("_", " ").title()} 

553 for type in QuestionType 

554 ] 

555 

556 return filter_options 

557 

558 

559@router.get( 

560 "/clear-filters", 

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

562 status_code=status.HTTP_200_OK, 

563 response_description="As a teacher, I can clear all filters", 

564 description="As a teacher, I can clear all filters", 

565 summary="Clear all filters", 

566) 

567async def teacher_clear_filters(request: Request) -> dict: 

568 """ 

569 Clear all filters and return all questions. 

570 

571 This endpoint is a convenience method that returns all questions 

572 without any filtering, equivalent to the "Clear All" button functionality. 

573 

574 Args: 

575 request (Request): The incoming request object containing: 

576 - JWT token in Authorization header 

577 - Teacher authentication context 

578 

579 Returns: 

580 dict: Same response as the /all/fetch endpoint but without any filters applied 

581 

582 Raises: 

583 HTTPException: 

584 - 401: Invalid or missing authentication token 

585 - 403: Insufficient permissions 

586 - 500: Error retrieving questions 

587 """ 

588 # Clear all filter states so no filters are applied 

589 request.state.assignment_types = None 

590 request.state.question_types = None 

591 request.state.categories = None 

592 request.state.difficulties = None 

593 teacher_id = to_user_id(request.state.user_details["uuid"]) 

594 return await teacher_question_service.fetch(request, teacher_id) 

595 

596 

597@router.post( 

598 "/image/upload", 

599 dependencies=[ 

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

601 Depends(require_feature("teacher.create_own_questions")), 

602 ], 

603 status_code=status.HTTP_201_CREATED, 

604 response_description="As a teacher, I can upload an image to embed in a question", 

605 description="As a teacher, I can upload an image to embed in a question", 

606 summary="As a teacher, I can upload an image to embed in a question", 

607) 

608async def teacher_upload_question_image( 

609 request: Request, 

610 file: UploadFile = File( 

611 ..., description="Image to embed in a question. PNG or JPEG, 10MB maximum." 

612 ), 

613) -> dict: 

614 """ 

615 Upload an image for use inside a question. 

616 

617 Stores the file in object storage and returns a public CDN URL for the editor to 

618 insert as an ``<img src>``. Previously the editor inlined images as base64 ``data:`` 

619 URIs, which embedded the entire image in the saved question HTML. 

620 

621 Gated on the same feature as question creation: uploading an image is part of 

622 authoring a question, so the two should not be able to disagree. 

623 

624 Args: 

625 request (Request): The incoming request object containing: 

626 - JWT token in Authorization header 

627 - Teacher authentication context 

628 file (UploadFile): The image file. Type is validated from its magic bytes, 

629 not the declared content type. 

630 

631 Returns: 

632 dict: Upload result containing: 

633 - success (bool): Always True on a 201 

634 - message (str): Human-readable confirmation 

635 - id (str): Stored filename, unique per upload 

636 - url (str): Public CDN URL to embed 

637 - path (str): Object key within the bucket 

638 - original_filename (str): Filename as supplied by the client 

639 - size (int): Size in bytes 

640 - content_type (str): Detected MIME type 

641 

642 Raises: 

643 HTTPException: 

644 - 413: File exceeds the 10MB limit 

645 - 415: File is not a supported image, or its type cannot be determined 

646 - 500: Object storage rejected the upload 

647 """ 

648 return await teacher_question_service.upload_question_image(request, file)