Coverage for crateweb/nlp_classification/tests/views_tests.py: 99%

217 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-27 10:34 -0500

1""" 

2crate_anon/crateweb/nlp_classification/tests/views_tests.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CRATE. 

10 

11 CRATE is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CRATE is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CRATE. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26Tests for CRATE NLP classification views. 

27 

28""" 

29 

30from typing import Any 

31from unittest import mock 

32 

33from django.http import QueryDict 

34from django.test import TestCase 

35from django.urls import reverse 

36from formtools.wizard.storage import BaseStorage 

37from crate_anon.crateweb.core.constants import RESEARCH_DB_CONNECTION_NAME 

38from crate_anon.crateweb.nlp_classification.constants import WizardSteps as ws 

39from crate_anon.crateweb.nlp_classification.models import ( 

40 Question, 

41 TableDefinition, 

42 Task, 

43) 

44from crate_anon.crateweb.nlp_classification.tests.factories import ( 

45 OptionFactory, 

46 QuestionFactory, 

47 TableDefinitionFactory, 

48 TaskFactory, 

49 UserAnswerFactory, 

50) 

51from crate_anon.crateweb.nlp_classification.views import ( 

52 SampleDataWizardView, 

53 TaskAndQuestionWizardView, 

54 UserAnswerView, 

55) 

56 

57 

58class UserAnswerViewTests(TestCase): 

59 def test_success_url_is_next_unanswered(self) -> None: 

60 this_answer = UserAnswerFactory(decision=None) 

61 UserAnswerFactory() # answered 

62 unanswered = UserAnswerFactory(decision=None) 

63 

64 view = UserAnswerView() 

65 view.object = this_answer 

66 

67 self.assertEqual( 

68 view.get_success_url(), 

69 reverse( 

70 "nlp_classification_user_answer", kwargs={"pk": unanswered.pk} 

71 ), 

72 ) 

73 

74 def test_success_url_is_assignment_list_if_all_answered(self) -> None: 

75 this_answer = UserAnswerFactory(decision=None) 

76 

77 view = UserAnswerView() 

78 view.object = this_answer 

79 

80 self.assertEqual( 

81 view.get_success_url(), 

82 reverse( 

83 "nlp_classification_user_assignment", 

84 kwargs={"pk": this_answer.assignment.pk}, 

85 ), 

86 ) 

87 

88 

89class TestStorage(BaseStorage): 

90 pass 

91 

92 

93class NlpClassificationWizardViewTests(TestCase): 

94 def setUp(self) -> None: 

95 super().setUp() 

96 

97 self.mock_request = mock.Mock(method="POST", FILES={}) 

98 self.mock_request.POST = QueryDict(mutable=True) 

99 self.storage = TestStorage("test", request=self.mock_request) 

100 self.storage.init_data() 

101 self.mock_get_storage = mock.Mock(return_value=self.storage) 

102 

103 initkwargs = self.view_class.get_initkwargs() 

104 self.view = self.view_class(**initkwargs) 

105 self.view.setup(self.mock_request) 

106 self.view.storage = self.storage 

107 # In normal use, a GET request would do this 

108 self.storage.current_step = self.first_step 

109 

110 def post(self, step: str, post_dict: dict[str, Any]) -> None: 

111 with mock.patch.multiple( 

112 "formtools.wizard.views", get_storage=self.mock_get_storage 

113 ): 

114 self.mock_request.POST.clear() 

115 for key, value in post_dict.items(): 

116 name = f"{step}-{key}" 

117 

118 if isinstance(value, list): 

119 self.mock_request.POST.setlist(name, value) 

120 else: 

121 self.mock_request.POST[name] = value 

122 

123 self.mock_request.POST[self.current_step_param] = step 

124 

125 self.view.dispatch(self.mock_request) 

126 

127 @property 

128 def current_step_param(self) -> str: 

129 prefix = self.view.get_prefix(self.mock_request) 

130 return f"{prefix}-current_step" 

131 

132 def assert_next_step(self, expected: str) -> None: 

133 self.assertEqual( 

134 self.view.steps.current, 

135 expected, 

136 msg="Did not go to the next step. Are there form errors?", 

137 ) 

138 

139 def assert_finished(self) -> None: 

140 self.assertEqual( 

141 self.view.steps.current, 

142 self.first_step, 

143 msg="Did not complete. Are there form errors?", 

144 ) 

145 

146 @property 

147 def first_step(self) -> str: 

148 raise NotImplementedError( 

149 f"first_step needs to be defined in {self.__class__.__name__}" 

150 ) 

151 

152 

153class TaskAndQuestionWizardViewTests(NlpClassificationWizardViewTests): 

154 view_class = TaskAndQuestionWizardView 

155 

156 @property 

157 def first_step(self) -> str: 

158 return ws.SELECT_TASK 

159 

160 def test_selected_task_passed_to_select_question_form(self) -> None: 

161 post_data = {self.current_step_param: ws.SELECT_QUESTION} 

162 self.mock_request.POST = post_data 

163 task = TaskFactory() 

164 

165 self.storage.data.update( 

166 step_data={ 

167 ws.SELECT_TASK: { # previous step 

168 f"{ws.SELECT_TASK}-task": [task.id], 

169 } 

170 } 

171 ) 

172 

173 kwargs = self.view.get_form_kwargs(step=ws.SELECT_QUESTION) 

174 

175 self.assertEqual(kwargs.get("task"), task) 

176 

177 def test_question_saved_with_existing_task(self) -> None: 

178 task = TaskFactory() 

179 

180 # Select task 

181 self.post(ws.SELECT_TASK, {"task": task.id}) 

182 self.assert_next_step(ws.SELECT_QUESTION) 

183 

184 # Select question 

185 self.post(ws.SELECT_QUESTION, {"question": ""}) 

186 self.assert_next_step(ws.CREATE_QUESTION) 

187 

188 # Create question 

189 self.post(ws.CREATE_QUESTION, {"title": "Test Question"}) 

190 self.assert_next_step(ws.SELECT_OPTIONS) 

191 

192 # Select options 

193 self.post(ws.SELECT_OPTIONS, {"options": []}) 

194 self.assert_next_step(ws.CREATE_OPTIONS) 

195 

196 # Create options 

197 self.post( 

198 ws.CREATE_OPTIONS, 

199 {"description_1": "", "description_2": ""}, 

200 ) 

201 self.assert_finished() 

202 

203 self.assertTrue( 

204 Question.objects.filter(task=task, title="Test Question").exists() 

205 ) 

206 

207 def test_question_saved_with_new_task(self) -> None: 

208 # Select task 

209 self.post(ws.SELECT_TASK, {"task": ""}) 

210 self.assert_next_step(ws.CREATE_TASK) 

211 

212 # Create task 

213 self.post(ws.CREATE_TASK, {"name": "Test Task"}) 

214 # Because we can't select an existing question for a new task 

215 self.assert_next_step(ws.CREATE_QUESTION) 

216 

217 # Create question 

218 self.post(ws.CREATE_QUESTION, {"title": "Test Question"}) 

219 self.assert_next_step(ws.SELECT_OPTIONS) 

220 

221 # Select options 

222 self.post(ws.SELECT_OPTIONS, {"options": []}) 

223 self.assert_next_step(ws.CREATE_OPTIONS) 

224 

225 # Create options 

226 self.post( 

227 ws.CREATE_OPTIONS, 

228 {"description_1": "", "description_2": ""}, 

229 ) 

230 self.assert_finished() 

231 

232 task = Task.objects.get(name="Test Task") 

233 self.assertTrue( 

234 Question.objects.filter(task=task, title="Test Question").exists() 

235 ) 

236 

237 def test_existing_task_and_question_selected(self) -> None: 

238 question = QuestionFactory() 

239 task = question.task 

240 

241 # Select task 

242 self.post(ws.SELECT_TASK, {"task": task.id}) 

243 self.assert_next_step(ws.SELECT_QUESTION) 

244 

245 # Select question 

246 self.post(ws.SELECT_QUESTION, {"question": question.id}) 

247 self.assert_next_step(ws.SELECT_OPTIONS) 

248 

249 def test_select_options_instructions_for_existing_question(self) -> None: 

250 question = QuestionFactory() 

251 

252 post_data = {self.current_step_param: ws.SELECT_QUESTION} 

253 self.mock_request.POST = post_data 

254 

255 self.storage.data.update( 

256 step_data={ 

257 ws.SELECT_QUESTION: { # previous step 

258 f"{ws.SELECT_QUESTION}-question": [question.id], 

259 } 

260 } 

261 ) 

262 

263 instructions = self.view.get_instructions(ws.SELECT_OPTIONS) 

264 

265 self.assertIn(question.title, instructions) 

266 

267 def test_select_options_instructions_for_new_question(self) -> None: 

268 post_data = {self.current_step_param: ws.SELECT_QUESTION} 

269 self.mock_request.POST = post_data 

270 

271 self.storage.data.update( 

272 step_data={ 

273 ws.SELECT_QUESTION: {f"{ws.SELECT_QUESTION}-question": ""}, 

274 ws.CREATE_QUESTION: { 

275 f"{ws.CREATE_QUESTION}-title": ["Test Question"], 

276 }, 

277 } 

278 ) 

279 

280 instructions = self.view.get_instructions(ws.SELECT_OPTIONS) 

281 

282 self.assertIn("Test Question", instructions) 

283 

284 def test_existing_question_options_selected(self) -> None: 

285 option_1 = OptionFactory(description="Yes") 

286 option_2 = OptionFactory(description="No") 

287 option_3 = OptionFactory(description="Maybe") 

288 

289 question = QuestionFactory(options=[option_1, option_2]) 

290 

291 post_data = {self.current_step_param: ws.SELECT_OPTIONS} 

292 self.mock_request.POST = post_data 

293 

294 self.storage.data.update( 

295 step_data={ # Earlier steps 

296 ws.SELECT_TASK: { 

297 f"{ws.SELECT_TASK}-task": [question.task.id], 

298 }, 

299 ws.SELECT_QUESTION: { 

300 f"{ws.SELECT_QUESTION}-question": [question.id], 

301 }, 

302 } 

303 ) 

304 

305 initial = self.view.get_form_initial(step=ws.SELECT_OPTIONS) 

306 options = initial.get("options") 

307 

308 self.assertIn(option_1.id, options) 

309 self.assertIn(option_2.id, options) 

310 self.assertNotIn(option_3.id, options) 

311 

312 def test_existing_option_added_to_existing_question(self) -> None: 

313 option_1 = OptionFactory(description="Yes") 

314 option_2 = OptionFactory(description="No") 

315 option_3 = OptionFactory(description="Maybe") 

316 

317 question = QuestionFactory(options=[option_1, option_2]) 

318 

319 # Select task 

320 self.post(ws.SELECT_TASK, {"task": question.task.id}) 

321 self.assert_next_step(ws.SELECT_QUESTION) 

322 

323 # Select question 

324 self.post(ws.SELECT_QUESTION, {"question": question.id}) 

325 self.assert_next_step(ws.SELECT_OPTIONS) 

326 

327 # Select options 

328 self.post( 

329 ws.SELECT_OPTIONS, 

330 {"options": [option_1.id, option_2.id, option_3.id]}, 

331 ) 

332 self.assert_next_step(ws.CREATE_OPTIONS) 

333 

334 # Create options 

335 self.post( 

336 ws.CREATE_OPTIONS, 

337 {"description_1": "", "description_2": ""}, 

338 ) 

339 self.assert_finished() 

340 

341 options = question.options.all() 

342 

343 self.assertIn(option_1, options) 

344 self.assertIn(option_2, options) 

345 self.assertIn(option_3, options) 

346 

347 def test_existing_options_removed_from_question(self) -> None: 

348 option_1 = OptionFactory(description="Yes") 

349 option_2 = OptionFactory(description="No") 

350 

351 question = QuestionFactory(options=[option_1, option_2]) 

352 

353 # Select task 

354 self.post(ws.SELECT_TASK, {"task": question.task.id}) 

355 self.assert_next_step(ws.SELECT_QUESTION) 

356 

357 # Select question 

358 self.post(ws.SELECT_QUESTION, {"question": question.id}) 

359 self.assert_next_step(ws.SELECT_OPTIONS) 

360 

361 # Select options 

362 self.post(ws.SELECT_OPTIONS, {"options": []}) 

363 self.assert_next_step(ws.CREATE_OPTIONS) 

364 

365 # Create options 

366 self.post( 

367 ws.CREATE_OPTIONS, 

368 {"description_1": "", "description_2": ""}, 

369 ) 

370 self.assert_finished() 

371 

372 options = list(question.options.all()) 

373 self.assertEqual(options, []) 

374 

375 def test_existing_options_added_to_new_question(self) -> None: 

376 task = TaskFactory() 

377 

378 option_1 = OptionFactory(description="Yes") 

379 option_2 = OptionFactory(description="No") 

380 option_3 = OptionFactory(description="Maybe") 

381 

382 # Select task 

383 self.post(ws.SELECT_TASK, {"task": task.id}) 

384 self.assert_next_step(ws.SELECT_QUESTION) 

385 

386 # Select question 

387 self.post(ws.SELECT_QUESTION, {"question": ""}) 

388 self.assert_next_step(ws.CREATE_QUESTION) 

389 

390 # Create question 

391 self.post(ws.CREATE_QUESTION, {"title": "Test Question"}) 

392 self.assert_next_step(ws.SELECT_OPTIONS) 

393 

394 # Select options 

395 self.post( 

396 ws.SELECT_OPTIONS, 

397 {"options": [option_1.id, option_2.id]}, 

398 ) 

399 self.assert_next_step(ws.CREATE_OPTIONS) 

400 

401 # Create options 

402 self.post( 

403 ws.CREATE_OPTIONS, 

404 {"description_1": "", "description_2": ""}, 

405 ) 

406 self.assert_finished() 

407 

408 question = Question.objects.get(task=task, title="Test Question") 

409 options = question.options.all() 

410 

411 self.assertIn(option_1, options) 

412 self.assertIn(option_2, options) 

413 self.assertNotIn(option_3, options) 

414 

415 def test_new_options_added_to_existing_question(self) -> None: 

416 question = QuestionFactory() 

417 

418 # Select task 

419 self.post(ws.SELECT_TASK, {"task": question.task.id}) 

420 self.assert_next_step(ws.SELECT_QUESTION) 

421 

422 # Select question 

423 self.post(ws.SELECT_QUESTION, {"question": question.id}) 

424 self.assert_next_step(ws.SELECT_OPTIONS) 

425 

426 # Select options 

427 self.post(ws.SELECT_OPTIONS, {"options": []}) 

428 self.assert_next_step(ws.CREATE_OPTIONS) 

429 

430 # Create options 

431 self.post( 

432 ws.CREATE_OPTIONS, {"description_1": "Yes", "description_2": "No"} 

433 ) 

434 self.assert_finished() 

435 

436 descriptions = [o.description for o in list(question.options.all())] 

437 

438 self.assertIn("Yes", descriptions) 

439 self.assertIn("No", descriptions) 

440 

441 def test_create_options_instructions_for_existing_question(self) -> None: 

442 question = QuestionFactory() 

443 

444 post_data = {self.current_step_param: ws.SELECT_QUESTION} 

445 self.mock_request.POST = post_data 

446 

447 self.storage.data.update( 

448 step_data={ 

449 ws.SELECT_QUESTION: { # previous step 

450 f"{ws.SELECT_QUESTION}-question": [question.id], 

451 } 

452 } 

453 ) 

454 

455 instructions = self.view.get_instructions(ws.CREATE_OPTIONS) 

456 

457 self.assertIn(question.title, instructions) 

458 

459 

460class SampleDataWizardViewTests(NlpClassificationWizardViewTests): 

461 view_class = SampleDataWizardView 

462 

463 @property 

464 def first_step(self) -> str: 

465 return ws.SELECT_SOURCE_TABLE_DEFINITION 

466 

467 def test_existing_table_definition_selected_for_source(self) -> None: 

468 table_definition = TableDefinitionFactory() 

469 

470 # Select table definition 

471 self.post( 

472 ws.SELECT_SOURCE_TABLE_DEFINITION, 

473 {"table_definition": table_definition.id}, 

474 ) 

475 self.assert_finished() 

476 

477 def test_new_source_table_definition_created(self) -> None: 

478 # Select table definition 

479 self.post( 

480 ws.SELECT_SOURCE_TABLE_DEFINITION, 

481 {"table_definition": ""}, 

482 ) 

483 self.assert_next_step(ws.SELECT_SOURCE_TABLE) 

484 

485 # Select table 

486 self.post( 

487 ws.SELECT_SOURCE_TABLE, 

488 {"table_name": "note"}, 

489 ) 

490 self.assert_finished() 

491 

492 self.assertTrue( 

493 TableDefinition.objects.filter( 

494 db_connection_name=RESEARCH_DB_CONNECTION_NAME, 

495 table_name="note", 

496 pk_column_name="_pk", 

497 ).exists() 

498 )