Coverage for server / validators / question_request_root_validators.py: 87%
381 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 typing import IO
3import filetype
4from fastapi import HTTPException, status
5from datetime import datetime, timezone
7from server.validators.question_bank.input_validations import Category
8from server.validators.question_richtext import enforce_text_length
9from server.validators.question_class_enum import (
10 ClassificationEnum,
11 DifficultyEnum,
12 ResponseEnum,
13 StatusEnum,
14 TypeEnum,
15)
18def validate_staar_fields(cls, values):
19 values = is_not_empty(cls, values)
20 values = validate_missing_staar_keys(cls, values)
21 values["question_type"] = is_type_match(values["question_type"], "STAAR")
22 values["grade_level"] = validate_grade_level(values["grade_level"])
23 values["category"] = validate_category(values["category"]) # affected
24 values["keywords"] = validate_keywords(values["keywords"])
25 values["student_expectations"] = validate_student_expectations(
26 values["student_expectations"]
27 ) # affected
28 values["response_type"] = set_response_type(values["response_type"])
29 values["question_content"] = validate_question_content(values["question_content"])
30 values["question_img"] = set_image(values["question_img"])
31 if "release_date" in values and values["release_date"]:
32 values["release_date"] = validate_release_date(values["release_date"])
33 return values
36def validate_staff_staar_fields(cls, values):
37 if "assignmentType" not in values or values["assignmentType"] != "STAAR":
38 return values
40 values = is_not_empty(cls, values)
41 values = validate_staff_missing_staar_keys(cls, values)
42 values["assignmentType"] = is_type_match(values["assignmentType"], "STAAR")
43 values["gradeLevel"] = validate_grade_level(values["gradeLevel"])
44 values["category"] = validate_category(values["category"]) # affected
45 values["keywords"] = validate_keywords(values["keywords"])
47 values["studentExpectation"] = validate_student_expectations(
48 values["studentExpectation"]
49 ) # affected
50 # values["responseType"] = set_response_type(values["responseType"])
51 values["question"] = validate_question_content(values["question"])
52 if "questionImages" in values and values["questionImages"]:
53 values["questionImages"] = validate_image_urls(values["questionImages"])
54 if "releaseDate" in values and values["releaseDate"]:
55 values["releaseDate"] = validate_release_date(values["releaseDate"])
56 return values
59def validate_sat_tsi_act_fields(cls, values):
60 """
61 Validate that SAT, TSI, and ACT tests do not contain releaseDate or category fields.
62 These fields are not allowed for these test types.
63 """
64 assignment_type = values.get("assignmentType")
65 if assignment_type not in ["SAT", "TSI", "ACT"]:
66 return values
68 # Check for prohibited fields
69 prohibited_fields = ["releaseDate", "category"]
70 for field in prohibited_fields:
71 if field in values and values[field] is not None:
72 raise HTTPException(
73 status_code=status.HTTP_400_BAD_REQUEST,
74 detail=f"The field '{field}' is not allowed for {assignment_type} tests. Please remove this field from your request."
75 )
77 return values
80def validate_college_fields(cls, values):
81 values = is_not_empty(cls, values)
82 values = validate_missing_college_keys(cls, values)
83 values["classification"] = set_classification_type(values["classification"])
84 values["test_code"] = validate_test_code(values["test_code"])
85 values["question_type"] = is_type_match(values["question_type"], "College Level")
86 values["keywords"] = validate_keywords(values["keywords"])
87 values["response_type"] = set_response_type(values["response_type"])
88 values["question_content"] = validate_question_content(values["question_content"])
89 values["question_img"] = set_image(values["question_img"])
90 values["difficulty"] = validate_difficulty(values["difficulty"])
91 values["points"] = validate_points(values["points"], values["difficulty"])
92 return values
95def validate_mathworld_fields(cls, values):
96 values = is_not_empty(cls, values)
97 values = validate_missing_mathworld_keys(cls, values)
98 values["question_type"] = is_type_match(values["question_type"], "MathWorld")
99 values["grade_level"] = validate_grade_level(values["grade_level"])
100 values["category"] = validate_category(values["category"])
101 values["topic"] = validate_topic(values["topic"])
102 values["teks_code"] = validate_teks_code(values["teks_code"])
103 values["subject"] = validate_subject(values["subject"])
104 values["difficulty"] = validate_difficulty(values["difficulty"])
105 values["keywords"] = validate_keywords(values["keywords"])
106 values["student_expectations"] = validate_student_expectations(
107 values["student_expectations"]
108 )
109 values["points"] = validate_points(values["points"], values["difficulty"])
110 values["response_type"] = set_response_type(values["response_type"])
111 values["question_content"] = validate_question_content(values["question_content"])
112 values["question_img"] = set_image(values["question_img"])
113 return values
116def validate_option_fields(cls, values):
117 values = is_not_empty(cls, values)
118 values = validate_missing_option_keys(cls, values)
119 values["content"] = validate_option_content(values["content"])
120 values["letter "] = validate_option_letter(values["letter"])
121 values["is_answer"] = validate_is_answer(values["is_answer"])
122 values["image"] = set_option_image(values["image"])
123 if "unit" in values and values["unit"]:
124 values["unit"] = validate_unit(values["unit"])
125 return values
128def validate_updated_question_fields(cls, values):
129 values = validate_missing_update_keys(cls, values)
130 values["update_note"] = validate_update_note(values["update_note"])
131 return values
134def validate_updated_status_fields(cls, values):
135 # values['update_note'] = validate_update_note(values['update_note'])
136 values["status"] = validate_question_status(values["status"])
137 values = validate_missing_update_keys(cls, values)
138 return values
141# individual validators
144def is_not_empty(cls, values):
145 """
146 Validate that required fields are not empty.
147 Skips validation for optional fields.
148 """
149 optional_fields = {
150 "question_img", "image", "unit", "created_by",
151 "updated_by", "release_date", "questionImages",
152 "questionGraphs", "questionDetails", "drawingMode",
153 "deletedDate", "deletedBy", "updatedDate", "updatedBy",
154 "createdDate", "createdBy", "deleted",
155 # Optional staff catalog id: a client that sends `questionId: null`
156 # explicitly must not be told "the field is required" -- it is not.
157 "questionId",
158 }
160 # Add choices to optional fields if question type is Free-response or Graph
161 question_type = values.get("questionType")
162 if question_type in ["Free-response", "Graph", "Graph-Multiple-Select"]:
163 optional_fields.add("choices")
165 for attr, value in values.items():
166 if attr in optional_fields:
167 continue
169 if value is None:
170 raise HTTPException(
171 status.HTTP_400_BAD_REQUEST,
172 detail=f"The field '{attr}' is required but was not provided"
173 )
175 if isinstance(value, str) and value.strip() == "":
176 raise HTTPException(
177 status.HTTP_400_BAD_REQUEST,
178 detail=f"The field '{attr}' cannot be empty"
179 )
181 if isinstance(value, list) and len(value) == 0 and attr not in optional_fields:
182 raise HTTPException(
183 status.HTTP_400_BAD_REQUEST,
184 detail=f"The field '{attr}' must contain at least one item"
185 )
187 return values
190def validate_missing_staar_keys(cls, values):
191 keys = values.keys()
192 required_keys = [
193 "question_type",
194 "grade_level",
195 "category",
196 "keywords",
197 "student_expectations",
198 "response_type",
199 "question_content",
200 ]
201 for key in required_keys:
202 if key not in keys:
203 raise HTTPException(
204 status.HTTP_400_BAD_REQUEST, detail=key + " is required"
205 )
206 return values
209def validate_staff_missing_staar_keys(cls, values):
210 """
211 Validate that all required STAAR fields are present.
212 """
213 required_keys = [
214 "questionType",
215 "gradeLevel",
216 "category",
217 "keywords",
218 "studentExpectation",
219 "assignmentType",
220 "question",
221 "teksCode",
222 "questionSubject",
223 "correctAnswer",
224 "difficulty",
225 "points",
226 "solutions"
227 ]
229 missing_keys = []
230 for key in required_keys:
231 if key not in values:
232 missing_keys.append(key)
234 if missing_keys:
235 if len(missing_keys) == 1:
236 raise HTTPException(
237 status.HTTP_400_BAD_REQUEST,
238 detail=f"{missing_keys[0]} is required"
239 )
240 else:
241 raise HTTPException(
242 status.HTTP_400_BAD_REQUEST,
243 detail=f"The following fields are required: {', '.join(missing_keys)}"
244 )
246 return values
249def validate_missing_college_keys(cls, values):
250 keys = values.keys()
251 required_keys = [
252 "question_type",
253 "classification",
254 "test_code",
255 "keywords",
256 "response_type",
257 "question_content",
258 "difficulty",
259 "points",
260 ]
261 for key in required_keys:
262 if key not in keys:
263 raise HTTPException(
264 status.HTTP_400_BAD_REQUEST, detail=key + " is required"
265 )
266 return values
269def validate_missing_mathworld_keys(cls, values):
270 keys = values.keys()
271 required_keys = [
272 "question_type",
273 "grade_level",
274 "teks_code",
275 "subject",
276 "topic",
277 "category",
278 "keywords",
279 "student_expectations",
280 "difficulty",
281 "points",
282 "response_type",
283 "question_content",
284 ]
285 for key in required_keys:
286 if key not in keys:
287 raise HTTPException(
288 status.HTTP_400_BAD_REQUEST, detail=key + " is required"
289 )
290 return values
293def validate_missing_option_keys(cls, values):
294 keys = values.keys()
295 required_keys = ["letter", "content", "is_answer"]
296 for key in required_keys:
297 if key not in keys:
298 raise HTTPException(
299 status.HTTP_400_BAD_REQUEST,
300 detail=key + " is required in option object",
301 )
302 return values
305def validate_missing_update_keys(cls, values):
306 keys = values.keys()
307 required_keys = []
309 if "status" not in keys or (
310 "status" in keys and values["status"] in ["Rejected", "Reported"]
311 ):
312 required_keys = ["update_note"]
314 for key in required_keys:
315 if key not in keys:
316 raise HTTPException(
317 status.HTTP_400_BAD_REQUEST, detail=key + " is required"
318 )
319 return values
322def validate_category(v):
323 if str(type(v)) == "<class 'str'>":
324 v = v.strip()
325 if v == "":
326 raise HTTPException(
327 status.HTTP_400_BAD_REQUEST, detail="category should not be empty"
328 )
329 if v not in Category.__members__.values():
330 raise HTTPException(
331 status_code=status.HTTP_400_BAD_REQUEST,
332 detail=f"Valid category is a string from {', '.join(Category.__members__.values())}"
333 )
335 return v
336 else:
337 raise HTTPException(
338 status.HTTP_400_BAD_REQUEST, detail="category must be a string"
339 )
342def validate_topic(v):
343 if str(type(v)) == "<class 'str'>":
344 v = v.strip()
345 if v == "":
346 raise HTTPException(
347 status.HTTP_400_BAD_REQUEST, detail="topic should not be empty"
348 )
350 return v
351 else:
352 raise HTTPException(
353 status.HTTP_400_BAD_REQUEST, detail="topic must be a string"
354 )
357def validate_grade_level(v):
358 if str(type(v)) == "<class 'int'>":
359 if v > 12 or v < 3:
360 raise HTTPException(
361 status.HTTP_400_BAD_REQUEST,
362 detail="invalid grade level: should only be between 3 to 12",
363 )
364 return v
365 else:
366 raise HTTPException(
367 status.HTTP_400_BAD_REQUEST, detail="grade level must be an integer"
368 )
371def validate_points(v, difficulty):
372 if str(type(v)) == "<class 'int'>":
373 if v > 100 or v < 1:
374 raise HTTPException(
375 status.HTTP_400_BAD_REQUEST,
376 detail="invalid points value: should only be between 1 to 3",
377 )
378 match difficulty:
379 case "Easy":
380 if v != 1:
381 raise HTTPException(
382 status.HTTP_400_BAD_REQUEST,
383 detail="Difficulty level is incompatible with points assigned.",
384 )
385 case "Average":
386 if v != 2:
387 raise HTTPException(
388 status.HTTP_400_BAD_REQUEST,
389 detail="Difficulty level is incompatible with points assigned.",
390 )
391 case "Hard":
392 if v != 3:
393 raise HTTPException(
394 status.HTTP_400_BAD_REQUEST,
395 detail="Difficulty level is incompatible with points assigned.",
396 )
397 return v
398 else:
399 raise HTTPException(
400 status.HTTP_400_BAD_REQUEST, detail="points must be an integer"
401 )
404def validate_subject(v):
405 if str(type(v)) == "<class 'str'>":
406 v = v.strip()
407 if v == "":
408 raise HTTPException(
409 status.HTTP_400_BAD_REQUEST, detail="subject should not be empty"
410 )
412 if v not in ["Algebra I", "Algebra II", "Geometry", "Pre-Calculus"]:
413 raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Invalid Subject")
414 return v
415 else:
416 raise HTTPException(
417 status.HTTP_400_BAD_REQUEST, detail="subject must be a string"
418 )
421def validate_keywords(v):
422 if str(type(v)) == "<class 'list'>":
423 if len(v) > 10:
424 raise HTTPException(
425 status.HTTP_400_BAD_REQUEST, detail="Max number of keywords reached"
426 )
427 if len(v) > 0:
428 for value in v:
429 if str(type(value)) == "<class 'str'>":
430 if value.strip() == "":
431 raise HTTPException(
432 status.HTTP_400_BAD_REQUEST,
433 detail="a value in keywords should not be an empty string",
434 )
435 else:
436 raise HTTPException(
437 status.HTTP_400_BAD_REQUEST,
438 detail="all values in keywords must be string",
439 )
440 if len(value) > 50:
441 raise HTTPException(
442 status.HTTP_400_BAD_REQUEST,
443 detail="Max length of keyword reached",
444 )
445 return v
446 else:
447 raise HTTPException(
448 status.HTTP_400_BAD_REQUEST, detail="keywords must not be empty"
449 )
450 else:
451 raise HTTPException(
452 status.HTTP_400_BAD_REQUEST, detail="keywords must be a list"
453 )
456def validate_student_expectations(v):
457 if str(type(v)) == "<class 'str'>":
458 if v.strip() == "":
459 raise HTTPException(
460 status.HTTP_400_BAD_REQUEST,
461 detail="student_expectations should not be an empty string",
462 )
463 else:
464 raise HTTPException(
465 status.HTTP_400_BAD_REQUEST,
466 detail="student_expectations must be a string",
467 )
469 return v
472def validate_question_content(v):
473 if str(type(v)) == "<class 'str'>":
474 if v.strip() == "":
475 raise HTTPException(
476 status.HTTP_400_BAD_REQUEST,
477 detail="question content should not be empty",
478 )
479 return enforce_text_length(v, "question content")
480 else:
481 raise HTTPException(
482 status.HTTP_400_BAD_REQUEST, detail="question content must be a string"
483 )
486def validate_release_date(v):
487 if not isinstance(v, str):
488 raise HTTPException(
489 status.HTTP_400_BAD_REQUEST, detail="release date must be a string"
490 )
492 if v.strip() == "":
493 raise HTTPException(
494 status.HTTP_400_BAD_REQUEST, detail="release date should not be empty"
495 )
497 try:
498 # Convert the string to datetime
499 release_date = datetime.fromisoformat(v.replace("Z", "+00:00"))
500 except ValueError:
501 raise HTTPException(
502 status.HTTP_400_BAD_REQUEST,
503 detail="release date invalid format: expected ISO 8601 (e.g., 2025-01-01T00:00:00Z)",
504 )
506 # Check if the date is in the future
507 now_utc = datetime.now(timezone.utc)
508 if release_date > now_utc:
509 raise HTTPException(
510 status.HTTP_400_BAD_REQUEST,
511 detail="release date invalid - date should not be in the future",
512 )
514 return release_date # ✅ Return as datetime, not string
517def set_response_type(v):
518 if str(type(v)) == "<class 'str'>":
519 v = v.strip().title()
520 if v in [response.value for response in ResponseEnum]:
521 return v
522 else:
523 if v == "":
524 raise HTTPException(
525 status.HTTP_400_BAD_REQUEST,
526 detail="response_type should not be an empty string",
527 )
528 else:
529 raise HTTPException(
530 status.HTTP_400_BAD_REQUEST, detail="invalid response type"
531 )
532 else:
533 raise HTTPException(
534 status.HTTP_400_BAD_REQUEST, detail="response_type must be a string"
535 )
538def is_type_match(v, question_type):
539 if str(type(v)) == "<class 'str'>":
540 if question_type == "STAAR":
541 v = v.strip().upper()
542 if v == question_type and v in [q_type.value for q_type in TypeEnum]:
543 return v
544 else:
545 raise HTTPException(
546 status.HTTP_400_BAD_REQUEST,
547 detail="question type must match to the endpoint use: "
548 + question_type,
549 )
550 else:
551 v = v.strip().title()
552 if v == question_type.title() and v in [
553 q_type.value for q_type in TypeEnum
554 ]:
555 return v
556 else:
557 raise HTTPException(
558 status.HTTP_400_BAD_REQUEST,
559 detail="question type must match to the endpoint use: "
560 + question_type,
561 )
563 else:
564 raise HTTPException(
565 status.HTTP_400_BAD_REQUEST, detail="question_type must be a string"
566 )
569def set_image(v):
570 if str(type(v)) == "<class 'str'>":
571 if len(v) > 0:
572 if len(v.strip()) == "":
573 raise HTTPException(
574 status.HTTP_400_BAD_REQUEST,
575 detail="question image value not allowed",
576 )
578 raise HTTPException(
579 status.HTTP_400_BAD_REQUEST,
580 detail="invalid image insertion: image must be added through the Form, not in payload.",
581 )
582 return v
583 else:
584 raise HTTPException(
585 status.HTTP_400_BAD_REQUEST, detail="image must be a string"
586 )
589def validate_image_urls(v):
590 if str(type(v)) == "<class 'list'>":
591 for value in v:
592 if str(type(value)) == "<class 'str'>":
593 if value.strip() == "":
594 raise HTTPException(
595 status.HTTP_400_BAD_REQUEST,
596 detail="a value in questionImages should not be an empty string",
597 )
598 else:
599 raise HTTPException(
600 status.HTTP_400_BAD_REQUEST,
601 detail="all values in questionImages must be string",
602 )
603 if len(v) > 50:
604 raise HTTPException(
605 status.HTTP_400_BAD_REQUEST,
606 detail="Max length of questionImages reached",
607 )
608 return v
609 else:
610 raise HTTPException(
611 status.HTTP_400_BAD_REQUEST, detail="questionImages must be a list"
612 )
615def validate_test_code(v):
616 if str(type(v)) == "<class 'str'>":
617 if len(v) > 6:
618 raise HTTPException(
619 status.HTTP_400_BAD_REQUEST,
620 detail="test code must not exceed 6 characters",
621 )
622 return v
623 else:
624 raise HTTPException(
625 status.HTTP_400_BAD_REQUEST, detail="test code must be a string"
626 )
629def set_classification_type(v):
630 if str(type(v)) == "<class 'str'>":
631 v = v.strip().upper()
632 if v in [classification.value for classification in ClassificationEnum]:
633 return v
634 else:
635 raise HTTPException(
636 status.HTTP_400_BAD_REQUEST, detail="invalid classification type"
637 )
638 else:
639 raise HTTPException(
640 status.HTTP_400_BAD_REQUEST, detail="classification value must be a string"
641 )
644def validate_teks_code(v):
645 if str(type(v)) == "<class 'str'>":
646 v = v.strip()
647 if len(v) > 6:
648 raise HTTPException(
649 status.HTTP_400_BAD_REQUEST,
650 detail="teks code must not exceed 6 characters",
651 )
652 if v not in [
653 "A.1",
654 "A.2",
655 "A.3",
656 "A.4",
657 "A.5",
658 "A.6",
659 "A.7",
660 "A.8",
661 "A.9",
662 "A.10",
663 "A.11",
664 "A.12",
665 ]:
666 raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Invalid Teks Code")
667 return v
668 else:
669 raise HTTPException(
670 status.HTTP_400_BAD_REQUEST, detail="teks code must be a string"
671 )
674def validate_difficulty(v):
675 if str(type(v)) == "<class 'str'>":
676 v = v.strip().title()
677 if v in [difficulty.value for difficulty in DifficultyEnum]:
678 return v
679 else:
680 raise HTTPException(
681 status.HTTP_400_BAD_REQUEST, detail="invalid difficulty level"
682 )
683 else:
684 raise HTTPException(
685 status.HTTP_400_BAD_REQUEST, detail="difficulty value must be a string"
686 )
689def validate_option_content(v):
690 if str(type(v)) == "<class 'str'>":
691 if v.strip() == "":
692 raise HTTPException(
693 status.HTTP_400_BAD_REQUEST, detail="option content should not be empty"
694 )
695 return enforce_text_length(v, "option content")
696 else:
697 raise HTTPException(
698 status.HTTP_400_BAD_REQUEST, detail="option content must be a string"
699 )
702def validate_unit(v):
703 if v:
704 if str(type(v)) == "<class 'str'>":
705 if len(v) != 0:
706 if v.strip() == "":
707 raise HTTPException(
708 status.HTTP_400_BAD_REQUEST,
709 detail="unit should not be all whitespaces",
710 )
711 if len(v.strip()) > 20:
712 raise HTTPException(
713 status.HTTP_400_BAD_REQUEST,
714 detail="unit content should not exceed 20 characters",
715 )
716 return v.strip()
717 else:
718 raise HTTPException(
719 status.HTTP_400_BAD_REQUEST, detail="unit must be a string"
720 )
722 return v.strip()
725def validate_option_letter(v):
726 if str(type(v)) == "<class 'str'>":
727 if v.strip() == "":
728 raise HTTPException(
729 status.HTTP_400_BAD_REQUEST, detail="option letter should not be empty"
730 )
731 if len(v.strip()) > 1:
732 raise HTTPException(
733 status.HTTP_400_BAD_REQUEST,
734 detail="option letter should only be 1 character long",
735 )
736 return v.strip()
737 else:
738 raise HTTPException(
739 status.HTTP_400_BAD_REQUEST, detail="option letter must be a string"
740 )
743def validate_is_answer(v):
744 if str(type(v)) == "<class 'bool'>":
745 return v
746 else:
747 raise HTTPException(
748 status.HTTP_400_BAD_REQUEST, detail="is_answer should be type boolean"
749 )
752def set_option_image(v):
753 if str(type(v)) == "<class 'str'>":
754 if len(v) > 0:
755 if len(v.strip()) == "":
756 raise HTTPException(
757 status.HTTP_400_BAD_REQUEST, detail="option image value not allowed"
758 )
760 raise HTTPException(
761 status.HTTP_400_BAD_REQUEST,
762 detail="invalid option image insertion: image must be added through the Form, not in payload.",
763 )
764 return v
765 else:
766 raise HTTPException(
767 status.HTTP_400_BAD_REQUEST, detail="image must be a string"
768 )
771def validate_update_note(v):
772 if v:
773 if v.strip() == "":
774 raise HTTPException(
775 status.HTTP_400_BAD_REQUEST, detail="update_note is required"
776 )
778 return v
781def validate_question_status(v):
782 v = v.strip().title()
783 if v in [status.value for status in StatusEnum]:
784 return v.title()
785 else:
786 raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="invalid status")
789def validate_file_size_type(file: IO):
790 """
791 Validate file size and type for uploaded images.
793 Args:
794 file (IO): File-like object to validate
796 Raises:
797 HTTPException:
798 - 413: If file exceeds size limit
799 - 415: If file type is unsupported or cannot be determined
800 """
801 FILE_SIZE = 10485760 # 10MB
802 file_position = file.tell() # Save current position
804 try:
805 # Validate file type
806 accepted_file_types = ["image/png", "image/jpeg", "png", "jpeg", "jpg"]
807 file_info = filetype.guess(file)
808 if file_info is None:
809 raise HTTPException(
810 status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
811 detail="Unable to determine file type. Please upload a valid JPG or PNG image.",
812 )
814 detected_content_type = file_info.extension.lower()
816 if hasattr(file, 'content_type'):
817 content_type = file.content_type
818 else:
819 # Default to a safe value if content_type isn't available
820 content_type = detected_content_type
822 if (
823 content_type not in accepted_file_types
824 or detected_content_type not in accepted_file_types
825 ):
826 raise HTTPException(
827 status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
828 detail=f"Unsupported file type. Detected: {detected_content_type}. Please upload a JPG or PNG image.",
829 )
831 # Validate file size
832 file.seek(0) # Reset to beginning for size check
833 real_file_size = 0
834 for chunk in file:
835 real_file_size += len(chunk)
836 if real_file_size > FILE_SIZE:
837 raise HTTPException(
838 status_code=status.HTTP_413_CONTENT_TOO_LARGE,
839 detail=f"File size exceeds the maximum limit of {FILE_SIZE/1048576:.1f}MB."
840 )
841 finally:
842 # Always restore the file position
843 file.seek(file_position)