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
« 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
4===============================================================================
6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CRATE.
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.
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.
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/>.
24===============================================================================
26Tests for CRATE NLP classification views.
28"""
30from typing import Any
31from unittest import mock
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)
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)
64 view = UserAnswerView()
65 view.object = this_answer
67 self.assertEqual(
68 view.get_success_url(),
69 reverse(
70 "nlp_classification_user_answer", kwargs={"pk": unanswered.pk}
71 ),
72 )
74 def test_success_url_is_assignment_list_if_all_answered(self) -> None:
75 this_answer = UserAnswerFactory(decision=None)
77 view = UserAnswerView()
78 view.object = this_answer
80 self.assertEqual(
81 view.get_success_url(),
82 reverse(
83 "nlp_classification_user_assignment",
84 kwargs={"pk": this_answer.assignment.pk},
85 ),
86 )
89class TestStorage(BaseStorage):
90 pass
93class NlpClassificationWizardViewTests(TestCase):
94 def setUp(self) -> None:
95 super().setUp()
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)
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
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}"
118 if isinstance(value, list):
119 self.mock_request.POST.setlist(name, value)
120 else:
121 self.mock_request.POST[name] = value
123 self.mock_request.POST[self.current_step_param] = step
125 self.view.dispatch(self.mock_request)
127 @property
128 def current_step_param(self) -> str:
129 prefix = self.view.get_prefix(self.mock_request)
130 return f"{prefix}-current_step"
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 )
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 )
146 @property
147 def first_step(self) -> str:
148 raise NotImplementedError(
149 f"first_step needs to be defined in {self.__class__.__name__}"
150 )
153class TaskAndQuestionWizardViewTests(NlpClassificationWizardViewTests):
154 view_class = TaskAndQuestionWizardView
156 @property
157 def first_step(self) -> str:
158 return ws.SELECT_TASK
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()
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 )
173 kwargs = self.view.get_form_kwargs(step=ws.SELECT_QUESTION)
175 self.assertEqual(kwargs.get("task"), task)
177 def test_question_saved_with_existing_task(self) -> None:
178 task = TaskFactory()
180 # Select task
181 self.post(ws.SELECT_TASK, {"task": task.id})
182 self.assert_next_step(ws.SELECT_QUESTION)
184 # Select question
185 self.post(ws.SELECT_QUESTION, {"question": ""})
186 self.assert_next_step(ws.CREATE_QUESTION)
188 # Create question
189 self.post(ws.CREATE_QUESTION, {"title": "Test Question"})
190 self.assert_next_step(ws.SELECT_OPTIONS)
192 # Select options
193 self.post(ws.SELECT_OPTIONS, {"options": []})
194 self.assert_next_step(ws.CREATE_OPTIONS)
196 # Create options
197 self.post(
198 ws.CREATE_OPTIONS,
199 {"description_1": "", "description_2": ""},
200 )
201 self.assert_finished()
203 self.assertTrue(
204 Question.objects.filter(task=task, title="Test Question").exists()
205 )
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)
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)
217 # Create question
218 self.post(ws.CREATE_QUESTION, {"title": "Test Question"})
219 self.assert_next_step(ws.SELECT_OPTIONS)
221 # Select options
222 self.post(ws.SELECT_OPTIONS, {"options": []})
223 self.assert_next_step(ws.CREATE_OPTIONS)
225 # Create options
226 self.post(
227 ws.CREATE_OPTIONS,
228 {"description_1": "", "description_2": ""},
229 )
230 self.assert_finished()
232 task = Task.objects.get(name="Test Task")
233 self.assertTrue(
234 Question.objects.filter(task=task, title="Test Question").exists()
235 )
237 def test_existing_task_and_question_selected(self) -> None:
238 question = QuestionFactory()
239 task = question.task
241 # Select task
242 self.post(ws.SELECT_TASK, {"task": task.id})
243 self.assert_next_step(ws.SELECT_QUESTION)
245 # Select question
246 self.post(ws.SELECT_QUESTION, {"question": question.id})
247 self.assert_next_step(ws.SELECT_OPTIONS)
249 def test_select_options_instructions_for_existing_question(self) -> None:
250 question = QuestionFactory()
252 post_data = {self.current_step_param: ws.SELECT_QUESTION}
253 self.mock_request.POST = post_data
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 )
263 instructions = self.view.get_instructions(ws.SELECT_OPTIONS)
265 self.assertIn(question.title, instructions)
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
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 )
280 instructions = self.view.get_instructions(ws.SELECT_OPTIONS)
282 self.assertIn("Test Question", instructions)
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")
289 question = QuestionFactory(options=[option_1, option_2])
291 post_data = {self.current_step_param: ws.SELECT_OPTIONS}
292 self.mock_request.POST = post_data
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 )
305 initial = self.view.get_form_initial(step=ws.SELECT_OPTIONS)
306 options = initial.get("options")
308 self.assertIn(option_1.id, options)
309 self.assertIn(option_2.id, options)
310 self.assertNotIn(option_3.id, options)
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")
317 question = QuestionFactory(options=[option_1, option_2])
319 # Select task
320 self.post(ws.SELECT_TASK, {"task": question.task.id})
321 self.assert_next_step(ws.SELECT_QUESTION)
323 # Select question
324 self.post(ws.SELECT_QUESTION, {"question": question.id})
325 self.assert_next_step(ws.SELECT_OPTIONS)
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)
334 # Create options
335 self.post(
336 ws.CREATE_OPTIONS,
337 {"description_1": "", "description_2": ""},
338 )
339 self.assert_finished()
341 options = question.options.all()
343 self.assertIn(option_1, options)
344 self.assertIn(option_2, options)
345 self.assertIn(option_3, options)
347 def test_existing_options_removed_from_question(self) -> None:
348 option_1 = OptionFactory(description="Yes")
349 option_2 = OptionFactory(description="No")
351 question = QuestionFactory(options=[option_1, option_2])
353 # Select task
354 self.post(ws.SELECT_TASK, {"task": question.task.id})
355 self.assert_next_step(ws.SELECT_QUESTION)
357 # Select question
358 self.post(ws.SELECT_QUESTION, {"question": question.id})
359 self.assert_next_step(ws.SELECT_OPTIONS)
361 # Select options
362 self.post(ws.SELECT_OPTIONS, {"options": []})
363 self.assert_next_step(ws.CREATE_OPTIONS)
365 # Create options
366 self.post(
367 ws.CREATE_OPTIONS,
368 {"description_1": "", "description_2": ""},
369 )
370 self.assert_finished()
372 options = list(question.options.all())
373 self.assertEqual(options, [])
375 def test_existing_options_added_to_new_question(self) -> None:
376 task = TaskFactory()
378 option_1 = OptionFactory(description="Yes")
379 option_2 = OptionFactory(description="No")
380 option_3 = OptionFactory(description="Maybe")
382 # Select task
383 self.post(ws.SELECT_TASK, {"task": task.id})
384 self.assert_next_step(ws.SELECT_QUESTION)
386 # Select question
387 self.post(ws.SELECT_QUESTION, {"question": ""})
388 self.assert_next_step(ws.CREATE_QUESTION)
390 # Create question
391 self.post(ws.CREATE_QUESTION, {"title": "Test Question"})
392 self.assert_next_step(ws.SELECT_OPTIONS)
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)
401 # Create options
402 self.post(
403 ws.CREATE_OPTIONS,
404 {"description_1": "", "description_2": ""},
405 )
406 self.assert_finished()
408 question = Question.objects.get(task=task, title="Test Question")
409 options = question.options.all()
411 self.assertIn(option_1, options)
412 self.assertIn(option_2, options)
413 self.assertNotIn(option_3, options)
415 def test_new_options_added_to_existing_question(self) -> None:
416 question = QuestionFactory()
418 # Select task
419 self.post(ws.SELECT_TASK, {"task": question.task.id})
420 self.assert_next_step(ws.SELECT_QUESTION)
422 # Select question
423 self.post(ws.SELECT_QUESTION, {"question": question.id})
424 self.assert_next_step(ws.SELECT_OPTIONS)
426 # Select options
427 self.post(ws.SELECT_OPTIONS, {"options": []})
428 self.assert_next_step(ws.CREATE_OPTIONS)
430 # Create options
431 self.post(
432 ws.CREATE_OPTIONS, {"description_1": "Yes", "description_2": "No"}
433 )
434 self.assert_finished()
436 descriptions = [o.description for o in list(question.options.all())]
438 self.assertIn("Yes", descriptions)
439 self.assertIn("No", descriptions)
441 def test_create_options_instructions_for_existing_question(self) -> None:
442 question = QuestionFactory()
444 post_data = {self.current_step_param: ws.SELECT_QUESTION}
445 self.mock_request.POST = post_data
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 )
455 instructions = self.view.get_instructions(ws.CREATE_OPTIONS)
457 self.assertIn(question.title, instructions)
460class SampleDataWizardViewTests(NlpClassificationWizardViewTests):
461 view_class = SampleDataWizardView
463 @property
464 def first_step(self) -> str:
465 return ws.SELECT_SOURCE_TABLE_DEFINITION
467 def test_existing_table_definition_selected_for_source(self) -> None:
468 table_definition = TableDefinitionFactory()
470 # Select table definition
471 self.post(
472 ws.SELECT_SOURCE_TABLE_DEFINITION,
473 {"table_definition": table_definition.id},
474 )
475 self.assert_finished()
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)
485 # Select table
486 self.post(
487 ws.SELECT_SOURCE_TABLE,
488 {"table_name": "note"},
489 )
490 self.assert_finished()
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 )