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

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 

4 

5 

6def validate_question_field(values): 

7 if "question" not in values: 

8 raise ValueError("question field is missing.") 

9 return values 

10 

11 

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 ] 

23 

24 question_type = values.get("questionType") 

25 choices = values.get("choices") 

26 

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 

36 

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 ) 

42 

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 ) 

48 

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 ) 

54 

55 if len(choices) > 31: 

56 raise HTTPException( 

57 status_code=status.HTTP_400_BAD_REQUEST, 

58 detail="Choices reached the maximum limit", 

59 ) 

60 

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 ) 

68 

69 _enforce_choice_text_lengths(choices) 

70 return values 

71 

72 

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") 

87 

88 

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.") 

96 

97 # For Free-response, allow empty string answer 

98 if values.get("questionType") == "Free-response": 

99 return values 

100 

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 ) 

109 

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.") 

114 

115 return values 

116 

117 

118def validate_solutions_field(values): 

119 if "solutions" not in values: 

120 raise ValueError("solutions field is missing") 

121 

122 

123def validate_question_details_field(values): 

124 if "questionDetails" not in values: 

125 raise ValueError("questionDetails field is missing.") 

126 return values 

127 

128 

129def validate_assignment_type_field(values): 

130 if "assignmentType" not in values: 

131 raise ValueError("assignmentType field is missing.") 

132 return values 

133 

134 

135def validate_question_type_field(values): 

136 if "questionType" not in values: 

137 raise ValueError("questionType field is missing.") 

138 return values 

139 

140 

141def validate_difficulty_field(values): 

142 if "difficulty" not in values: 

143 raise ValueError("difficulty type field is missing.") 

144 return values 

145 

146 

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 

151 

152 

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 

159 

160 

161def validate_points_field(values): 

162 if "points" not in values: 

163 raise ValueError("points field is missing.") 

164 return values 

165 

166 

167def validate_category_field(values): 

168 assignment_type = values.get("assignmentType") 

169 category = values.get("category") 

170 

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.") 

174 

175 # For STAAR - category is required 

176 if assignment_type == "STAAR" and "category" not in values: 

177 raise ValueError("category field is missing.") 

178 

179 # For other assignment types, category can be optional 

180 return values 

181 

182 

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 

187 

188 

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 

193 

194 

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 

199 

200 

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 

205 

206 

207def validate_release_date_field(values): 

208 assignment_type = values.get("assignmentType") 

209 release_date = values.get("releaseDate") 

210 

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.") 

214 

215 # For other assignment types, releaseDate can be optional 

216 return values