Coverage for server / validators / question_bank / field_validations.py: 93%
119 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 HTTPException, status
2from server.validators.question_bank.input_validations import QuestionType
3from server.validators.question_richtext import enforce_text_length, html_allows_large_content
6def validate_question_field(values):
7 if "question" not in values:
8 raise ValueError("question field is missing.")
9 return values
12def validate_choices_field(values):
13 """
14 Validate the choices field based on question type.
15 Some question types like Free-response don't require choices.
16 """
17 # Question types that don't require choices
18 no_choices_types = [
19 QuestionType.FREE_RESPONSE,
20 QuestionType.GRAPH,
21 QuestionType.GRAPH_MULTIPLE_SELECT,
22 ]
24 question_type = values.get("questionType")
25 choices = values.get("choices")
27 # For question types that don't need choices, ensure choices is None or empty
28 if question_type in no_choices_types:
29 if choices:
30 raise HTTPException(
31 status_code=status.HTTP_400_BAD_REQUEST,
32 detail=f"Question type '{question_type}' should not have choices",
33 )
34 values["choices"] = None
35 return values
37 if not choices:
38 raise HTTPException(
39 status_code=status.HTTP_400_BAD_REQUEST,
40 detail="The field 'choices' is required for this question type",
41 )
43 if not isinstance(choices, list):
44 raise HTTPException(
45 status_code=status.HTTP_400_BAD_REQUEST,
46 detail="The field 'choices' must be a list",
47 )
49 if len(choices) == 0:
50 raise HTTPException(
51 status_code=status.HTTP_400_BAD_REQUEST,
52 detail="The field 'choices' must contain at least one choice",
53 )
55 if len(choices) > 31:
56 raise HTTPException(
57 status_code=status.HTTP_400_BAD_REQUEST,
58 detail="Choices reached the maximum limit",
59 )
61 # Check for duplicate choice IDs
62 choice_ids = [choice.get("id") for choice in choices]
63 if len(choice_ids) != len(set(choice_ids)):
64 raise HTTPException(
65 status_code=status.HTTP_400_BAD_REQUEST,
66 detail="Each choice must have a unique ID",
67 )
69 _enforce_choice_text_lengths(choices)
70 return values
73def _enforce_choice_text_lengths(choices: list) -> None:
74 for i, choice in enumerate(choices):
75 if not isinstance(choice, dict):
76 continue
77 text = choice.get("text")
78 if isinstance(text, str) and text.strip():
79 enforce_text_length(text, f"choice {i} text")
80 for j, item in enumerate(choice.get("items") or []):
81 if isinstance(item, str) and item.strip():
82 enforce_text_length(item, f"choice {i} item {j} text")
83 elif isinstance(item, dict):
84 item_text = item.get("text")
85 if isinstance(item_text, str) and item_text.strip():
86 enforce_text_length(item_text, f"choice {i} item {j} text")
89def validate_correct_answer_field(values):
90 """
91 Validate the correctAnswer field based on question type.
92 Free-response questions can have empty string answers.
93 """
94 if "correctAnswer" not in values:
95 raise ValueError("correctAnswer field is missing.")
97 # For Free-response, allow empty string answer
98 if values.get("questionType") == "Free-response":
99 return values
101 # For other types, ensure answer is not empty
102 correct_answer = values.get("correctAnswer", {})
103 if isinstance(correct_answer, dict):
104 answers = correct_answer.get("answers")
105 if not answers and answers != "": # Allow empty string but not None
106 raise ValueError(
107 "answers field in correctAnswer is required for this question type"
108 )
110 if not html_allows_large_content(str(values["correctAnswer"])) and len(
111 str(values["correctAnswer"])
112 ) > 256:
113 raise ValueError("correctAnswer exceeded the maximum amount of characters.")
115 return values
118def validate_solutions_field(values):
119 if "solutions" not in values:
120 raise ValueError("solutions field is missing")
123def validate_question_details_field(values):
124 if "questionDetails" not in values:
125 raise ValueError("questionDetails field is missing.")
126 return values
129def validate_assignment_type_field(values):
130 if "assignmentType" not in values:
131 raise ValueError("assignmentType field is missing.")
132 return values
135def validate_question_type_field(values):
136 if "questionType" not in values:
137 raise ValueError("questionType field is missing.")
138 return values
141def validate_difficulty_field(values):
142 if "difficulty" not in values:
143 raise ValueError("difficulty type field is missing.")
144 return values
147def validate_teks_code_field(values):
148 if "teksCode" not in values and values["assignmentType"] == "STAAR":
149 raise ValueError("teksCode is missing.")
150 return values
153def validate_grade_level_field(values):
154 if "gradeLevel" not in values:
155 raise ValueError("grade level is missing")
156 elif values["gradeLevel"] not in range(3, 13):
157 raise ValueError("grade level is invalid")
158 return values
161def validate_points_field(values):
162 if "points" not in values:
163 raise ValueError("points field is missing.")
164 return values
167def validate_category_field(values):
168 assignment_type = values.get("assignmentType")
169 category = values.get("category")
171 # For SAT, TSI, ACT - category should not be provided
172 if assignment_type in ["SAT", "TSI", "ACT"] and category is not None:
173 raise ValueError(f"category field must not be provided for {assignment_type} tests.")
175 # For STAAR - category is required
176 if assignment_type == "STAAR" and "category" not in values:
177 raise ValueError("category field is missing.")
179 # For other assignment types, category can be optional
180 return values
183def validate_deleted_field(values):
184 if "deleted" in values:
185 raise ValueError("deleted field must not be included in the payload.")
186 return values
189def validate_deleted_date_field(values):
190 if "deletedDate" in values:
191 raise ValueError("deletedDate field must not be included in the payload.")
192 return values
195def validate_created_date_field(values):
196 if "createdDate" in values:
197 raise ValueError("createdDate field must not be included in the payload.")
198 return values
201def validate_updated_date_field(values):
202 if "updatedDate" in values:
203 raise ValueError("updatedDate field must not be included in the payload.")
204 return values
207def validate_release_date_field(values):
208 assignment_type = values.get("assignmentType")
209 release_date = values.get("releaseDate")
211 # For SAT, TSI, ACT - releaseDate should not be provided
212 if assignment_type in ["SAT", "TSI", "ACT"] and release_date is not None:
213 raise ValueError(f"releaseDate field must not be provided for {assignment_type} tests.")
215 # For other assignment types, releaseDate can be optional
216 return values