Coverage for crateweb/nlp_classification/views.py: 67%
346 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/views.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===============================================================================
26CRATE NLP classification views.
28"""
30from typing import Any, Optional
32from django.http.response import HttpResponse, HttpResponseRedirect
33from django.forms import Form
34from django.urls import reverse
35from django.views.generic import CreateView, TemplateView, UpdateView
36import django_tables2 as tables
37from formtools.wizard.views import SessionWizardView
39from crate_anon.crateweb.nlp_classification.constants import WizardSteps as ws
40from crate_anon.crateweb.nlp_classification.forms import (
41 AssignmentForm,
42 OptionForm,
43 QuestionForm,
44 SampleSpecForm,
45 TableDefinitionForm,
46 TaskForm,
47 WizardCreateOptionsForm,
48 WizardCreateQuestionForm,
49 WizardCreateTaskForm,
50 WizardSelectOptionsForm,
51 WizardSelectQuestionForm,
52 WizardSelectSourceTableDefinitionForm,
53 WizardSelectSourceTableForm,
54 WizardSelectTaskForm,
55 UserAnswerForm,
56)
57from crate_anon.crateweb.nlp_classification.models import (
58 Assignment,
59 Option,
60 Question,
61 SampleSpec,
62 TableDefinition,
63 Task,
64 UserAnswer,
65)
66from crate_anon.crateweb.nlp_classification.tables import (
67 AdminAssignmentTable,
68 FieldTable,
69 OptionTable,
70 QuestionTable,
71 SampleSpecTable,
72 TableDefinitionTable,
73 TaskTable,
74 UserAnswerTable,
75 UserAssignmentTable,
76)
79class AdminHomeView(TemplateView):
80 template_name = "nlp_classification/admin/home.html"
83class AdminTaskListView(TemplateView):
84 template_name = "nlp_classification/admin/task_list.html"
86 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
87 context = super().get_context_data(**kwargs)
89 table = self._get_table()
90 tables.RequestConfig(self.request).configure(table)
91 context.update(table=table)
93 return context
95 def _get_table(self) -> tables.Table:
96 return TaskTable(Task.objects.all())
99class AdminTaskCreateView(CreateView):
100 model = Task
101 template_name = "nlp_classification/admin/update_form.html"
102 form_class = TaskForm
104 def get_success_url(self):
105 return reverse("nlp_classification_admin_task_list")
107 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
108 context = super().get_context_data(**kwargs)
109 context.update(title="New task")
111 return context
114class AdminTaskEditView(UpdateView):
115 model = Task
116 template_name = "nlp_classification/admin/update_form.html"
117 form_class = TaskForm
119 def get_success_url(self):
120 return reverse("nlp_classification_admin_task_list")
122 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
123 context = super().get_context_data(**kwargs)
124 context.update(title="Edit task")
126 return context
129class AdminQuestionListView(TemplateView):
130 template_name = "nlp_classification/admin/question_list.html"
132 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
133 context = super().get_context_data(**kwargs)
135 table = self._get_table()
136 tables.RequestConfig(self.request).configure(table)
137 context.update(table=table)
139 return context
141 def _get_table(self) -> tables.Table:
142 return QuestionTable(Question.objects.all())
145class AdminQuestionCreateView(CreateView):
146 model = Question
147 template_name = "nlp_classification/admin/update_form.html"
148 form_class = QuestionForm
150 def get_success_url(self):
151 return reverse("nlp_classification_admin_question_list")
153 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
154 context = super().get_context_data(**kwargs)
155 context.update(title="New question")
157 return context
160class AdminQuestionEditView(UpdateView):
161 model = Question
162 template_name = "nlp_classification/admin/update_form.html"
163 form_class = QuestionForm
165 def get_success_url(self):
166 return reverse("nlp_classification_admin_question_list")
168 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
169 context = super().get_context_data(**kwargs)
170 context.update(title="Edit question")
172 return context
175class AdminOptionListView(TemplateView):
176 template_name = "nlp_classification/admin/option_list.html"
178 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
179 context = super().get_context_data(**kwargs)
181 table = self._get_table()
182 tables.RequestConfig(self.request).configure(table)
183 context.update(table=table)
185 return context
187 def _get_table(self) -> tables.Table:
188 return OptionTable(Option.objects.all())
191class AdminOptionCreateView(CreateView):
192 model = Option
193 template_name = "nlp_classification/admin/update_form.html"
194 form_class = OptionForm
196 def get_success_url(self):
197 return reverse("nlp_classification_admin_option_list")
199 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
200 context = super().get_context_data(**kwargs)
201 context.update(title="New option")
203 return context
206class AdminOptionEditView(UpdateView):
207 model = Option
208 template_name = "nlp_classification/admin/update_form.html"
209 form_class = OptionForm
211 def get_success_url(self):
212 return reverse("nlp_classification_admin_option_list")
214 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
215 context = super().get_context_data(**kwargs)
216 context.update(title="Edit option")
218 return context
221class AdminSampleSpecListView(TemplateView):
222 template_name = "nlp_classification/admin/sample_spec_list.html"
224 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
225 context = super().get_context_data(**kwargs)
227 table = self._get_table()
228 tables.RequestConfig(self.request).configure(table)
229 context.update(table=table)
231 return context
233 def _get_table(self) -> tables.Table:
234 return SampleSpecTable(SampleSpec.objects.all())
237class AdminSampleSpecCreateView(CreateView):
238 model = SampleSpec
239 template_name = "nlp_classification/admin/update_form.html"
240 form_class = SampleSpecForm
242 def get_success_url(self):
243 return reverse("nlp_classification_admin_sample_spec_list")
245 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
246 context = super().get_context_data(**kwargs)
247 context.update(title="New sample specification")
249 return context
252class AdminSampleSpecEditView(UpdateView):
253 model = SampleSpec
254 template_name = "nlp_classification/admin/update_form.html"
255 form_class = SampleSpecForm
257 def get_success_url(self):
258 return reverse("nlp_classification_admin_sample_spec_list")
260 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
261 context = super().get_context_data(**kwargs)
262 context.update(title="Edit sample specification")
264 return context
267class AdminTableDefinitionListView(TemplateView):
268 template_name = "nlp_classification/admin/table_definition_list.html"
270 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
271 context = super().get_context_data(**kwargs)
273 table = self._get_table()
274 tables.RequestConfig(self.request).configure(table)
275 context.update(table=table)
277 return context
279 def _get_table(self) -> tables.Table:
280 return TableDefinitionTable(TableDefinition.objects.all())
283class AdminTableDefinitionCreateView(CreateView):
284 model = TableDefinition
285 template_name = "nlp_classification/admin/update_form.html"
286 form_class = TableDefinitionForm
288 def get_success_url(self):
289 return reverse("nlp_classification_admin_table_definition_list")
291 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
292 context = super().get_context_data(**kwargs)
293 context.update(title="New table definition")
295 return context
298class AdminTableDefinitionEditView(UpdateView):
299 model = TableDefinition
300 template_name = "nlp_classification/admin/update_form.html"
301 form_class = TableDefinitionForm
303 def get_success_url(self):
304 return reverse("nlp_classification_admin_table_definition_list")
306 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
307 context = super().get_context_data(**kwargs)
308 context.update(title="Edit table definition")
310 return context
313class AdminAssignmentListView(TemplateView):
314 template_name = "nlp_classification/admin/assignment_list.html"
316 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
317 context = super().get_context_data(**kwargs)
319 table = self._get_table()
320 tables.RequestConfig(self.request).configure(table)
321 context.update(table=table)
323 return context
325 def _get_table(self) -> tables.Table:
326 return AdminAssignmentTable(Assignment.objects.all())
329class AdminAssignmentCreateView(CreateView):
330 model = Assignment
331 template_name = "nlp_classification/admin/update_form.html"
332 form_class = AssignmentForm
334 def get_success_url(self):
335 return reverse("nlp_classification_admin_assignment_list")
337 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
338 context = super().get_context_data(**kwargs)
339 context.update(title="New assignment")
341 return context
344class AdminAssignmentEditView(UpdateView):
345 model = Assignment
346 template_name = "nlp_classification/admin/update_form.html"
347 form_class = AssignmentForm
349 def get_success_url(self):
350 return reverse("nlp_classification_admin_assignment_list")
352 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
353 context = super().get_context_data(**kwargs)
354 context.update(title="Edit assigment")
356 return context
359class UserAssignmentView(TemplateView):
360 template_name = "nlp_classification/user/assignment.html"
362 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
363 context = super().get_context_data(**kwargs)
365 table = self._get_table()
366 tables.RequestConfig(self.request).configure(table)
367 context.update(table=table)
369 return context
371 def _get_table(self) -> tables.Table:
372 assignment = Assignment.objects.get(pk=self.kwargs["pk"])
374 return UserAnswerTable(
375 UserAnswer.objects.filter(assignment=assignment)
376 )
379class UserHomeView(TemplateView):
380 template_name = "nlp_classification/user/home.html"
382 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
383 context = super().get_context_data(**kwargs)
385 table = self._get_table()
386 tables.RequestConfig(self.request).configure(table)
387 context.update(table=table)
389 return context
391 def _get_table(self) -> tables.Table:
392 return UserAssignmentTable(Assignment.objects.all())
395class UserAnswerView(UpdateView):
396 model = UserAnswer
397 template_name = "nlp_classification/user/useranswerupdate_form.html"
398 form_class = UserAnswerForm
400 def get_success_url(self, **kwargs) -> str:
401 next_record = (
402 UserAnswer.objects.filter(decision=None)
403 .exclude(pk=self.object.pk)
404 .first()
405 )
407 if next_record is not None:
408 return reverse(
409 "nlp_classification_user_answer", kwargs={"pk": next_record.pk}
410 )
412 return reverse(
413 "nlp_classification_user_assignment",
414 kwargs={"pk": self.object.assignment.pk},
415 )
417 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
418 context = super().get_context_data(**kwargs)
420 table = self._get_table()
422 if table:
423 tables.RequestConfig(self.request).configure(table)
425 context.update(nlp_table=table)
427 return context
429 def _get_table(self) -> Optional[tables.Table]:
430 table_data = []
432 for name, value in self.object.source_record.extra_nlp_fields.items():
433 table_data.append({"name": name, "value": value})
435 if table_data:
436 return FieldTable(table_data)
438 return None
441def should_create_task(wizard: SessionWizardView) -> bool:
442 return not wizard.has_selected_task
445def should_select_question(wizard: SessionWizardView) -> bool:
446 return wizard.has_selected_task
449def should_create_question(wizard: SessionWizardView) -> bool:
450 return not wizard.has_selected_question
453class NlpClassificationWizardView(SessionWizardView):
454 template_name = "nlp_classification/admin/wizard_form.html"
456 def get_context_data(self, form: Form, **kwargs: Any) -> dict[str, Any]:
457 context = super().get_context_data(form=form, **kwargs)
458 context["instructions"] = self.get_instructions(self.steps.current)
460 return context
462 def get_instructions(self) -> Optional[str]:
463 raise NotImplementedError(
464 "get_instructions() needs to be defined in "
465 f"{self.__class__.__name__}"
466 )
469class TaskAndQuestionWizardView(NlpClassificationWizardView):
470 condition_dict = {
471 ws.CREATE_TASK: should_create_task,
472 ws.SELECT_QUESTION: should_select_question,
473 ws.CREATE_QUESTION: should_create_question,
474 }
475 form_list = [
476 (ws.SELECT_TASK, WizardSelectTaskForm),
477 (ws.CREATE_TASK, WizardCreateTaskForm),
478 (ws.SELECT_QUESTION, WizardSelectQuestionForm),
479 (ws.CREATE_QUESTION, WizardCreateQuestionForm),
480 (ws.SELECT_OPTIONS, WizardSelectOptionsForm),
481 (ws.CREATE_OPTIONS, WizardCreateOptionsForm),
482 ]
484 def get_instructions(self, step: str) -> Optional[str]:
485 if step == ws.SELECT_TASK:
486 return "Select an existing task or create a new task"
488 if step == ws.CREATE_TASK:
489 return "Enter the details for the new task"
491 if step == ws.SELECT_QUESTION:
492 return "Select an existing question or create a new question"
494 if step == ws.CREATE_QUESTION:
495 return "Enter the details for the new question"
497 if step == ws.SELECT_OPTIONS:
498 return self._get_select_options_instructions()
500 if step == ws.CREATE_OPTIONS:
501 return self._get_create_options_instructions()
503 def _get_select_options_instructions(self) -> str:
504 return (
505 f"Select options for the question '{self.question_title}'. "
506 "You can create new options in the next step."
507 )
509 def _get_create_options_instructions(self) -> str:
510 return f"Create options for the question '{self.question_title}'."
512 def get_cleaned_data_for_step(self, step: str) -> Optional[dict[str, Any]]:
513 # https://github.com/jazzband/django-formtools/issues/266
514 # self.get_form() can raise a KeyError if the step does not exist in
515 # the dynamic form list because it has been excluded from
516 # condition_dict. The documentation does not mention this.
517 try:
518 return super().get_cleaned_data_for_step(step)
519 except KeyError:
520 return None
522 def get_form_initial(self, step: str) -> dict[str, Any]:
523 initial = super().get_form_initial(step)
525 if step == ws.SELECT_OPTIONS:
526 question = self.selected_question
527 if question is not None:
528 initial["options"] = [o.id for o in question.options.all()]
530 return initial
532 def get_form_kwargs(self, step=None) -> Any:
533 kwargs = super().get_form_kwargs(step)
534 if step == ws.SELECT_QUESTION:
535 kwargs["task"] = self.selected_task
537 return kwargs
539 @property
540 def has_selected_task(self) -> bool:
541 return self.selected_task is not None
543 @property
544 def selected_task(self) -> Optional[Task]:
545 cleaned_data = self.get_cleaned_data_for_step(ws.SELECT_TASK) or {}
547 return cleaned_data.get("task")
549 @property
550 def has_selected_question(self) -> bool:
551 return self.selected_question is not None
553 @property
554 def question_title(self) -> str:
555 question = self.selected_question
556 if question is not None:
557 return question.title
559 return self.created_question_title
561 @property
562 def selected_question(self) -> Optional[Question]:
563 cleaned_data = self.get_cleaned_data_for_step(ws.SELECT_QUESTION) or {}
565 return cleaned_data.get("question")
567 @property
568 def created_question_title(self) -> Optional[str]:
569 cleaned_data = self.get_cleaned_data_for_step(ws.CREATE_QUESTION) or {}
571 return cleaned_data.get("title")
573 @property
574 def selected_options(self) -> Optional[list[Option]]:
575 cleaned_data = self.get_cleaned_data_for_step(ws.SELECT_OPTIONS) or {}
577 options = cleaned_data.get("options")
578 if options is not None:
579 return list(options)
581 return None
583 def done(
584 self, form_list: list[Form], form_dict: dict[str, Form], **kwargs: Any
585 ) -> HttpResponse:
586 task = self.selected_task
587 if task is None:
588 create_task_form = form_dict[ws.CREATE_TASK]
589 task = create_task_form.save()
591 question = self.selected_question
592 if question is None:
593 create_question_form = form_dict[ws.CREATE_QUESTION]
594 question = create_question_form.instance
596 question.task = task
597 question.save()
599 options = self.selected_options
600 if options is not None:
601 question.options.set(options)
603 create_options_form = form_dict[ws.CREATE_OPTIONS]
604 for name in ["description_1", "description_2"]:
605 if description := create_options_form.cleaned_data[name]:
606 option = Option.objects.create(description=description)
607 question.options.add(option)
609 return HttpResponseRedirect(reverse("nlp_classification_admin_home"))
612class SampleDataWizardView(NlpClassificationWizardView):
613 form_list = [
614 (
615 ws.SELECT_SOURCE_TABLE_DEFINITION,
616 WizardSelectSourceTableDefinitionForm,
617 ),
618 (ws.SELECT_SOURCE_TABLE, WizardSelectSourceTableForm),
619 ]
621 def get_instructions(self, step: str) -> Optional[str]:
622 if step == ws.SELECT_SOURCE_TABLE_DEFINITION:
623 return (
624 "Select an existing source table definition or "
625 "create a new one"
626 )
628 def done(
629 self, form_list: list[Form], form_dict: dict[str, Form], **kwargs: Any
630 ) -> HttpResponse:
632 return HttpResponseRedirect(reverse("nlp_classification_admin_home"))