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

1""" 

2crate_anon/crateweb/nlp_classification/views.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 

26CRATE NLP classification views. 

27 

28""" 

29 

30from typing import Any, Optional 

31 

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 

38 

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) 

77 

78 

79class AdminHomeView(TemplateView): 

80 template_name = "nlp_classification/admin/home.html" 

81 

82 

83class AdminTaskListView(TemplateView): 

84 template_name = "nlp_classification/admin/task_list.html" 

85 

86 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

87 context = super().get_context_data(**kwargs) 

88 

89 table = self._get_table() 

90 tables.RequestConfig(self.request).configure(table) 

91 context.update(table=table) 

92 

93 return context 

94 

95 def _get_table(self) -> tables.Table: 

96 return TaskTable(Task.objects.all()) 

97 

98 

99class AdminTaskCreateView(CreateView): 

100 model = Task 

101 template_name = "nlp_classification/admin/update_form.html" 

102 form_class = TaskForm 

103 

104 def get_success_url(self): 

105 return reverse("nlp_classification_admin_task_list") 

106 

107 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

108 context = super().get_context_data(**kwargs) 

109 context.update(title="New task") 

110 

111 return context 

112 

113 

114class AdminTaskEditView(UpdateView): 

115 model = Task 

116 template_name = "nlp_classification/admin/update_form.html" 

117 form_class = TaskForm 

118 

119 def get_success_url(self): 

120 return reverse("nlp_classification_admin_task_list") 

121 

122 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

123 context = super().get_context_data(**kwargs) 

124 context.update(title="Edit task") 

125 

126 return context 

127 

128 

129class AdminQuestionListView(TemplateView): 

130 template_name = "nlp_classification/admin/question_list.html" 

131 

132 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

133 context = super().get_context_data(**kwargs) 

134 

135 table = self._get_table() 

136 tables.RequestConfig(self.request).configure(table) 

137 context.update(table=table) 

138 

139 return context 

140 

141 def _get_table(self) -> tables.Table: 

142 return QuestionTable(Question.objects.all()) 

143 

144 

145class AdminQuestionCreateView(CreateView): 

146 model = Question 

147 template_name = "nlp_classification/admin/update_form.html" 

148 form_class = QuestionForm 

149 

150 def get_success_url(self): 

151 return reverse("nlp_classification_admin_question_list") 

152 

153 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

154 context = super().get_context_data(**kwargs) 

155 context.update(title="New question") 

156 

157 return context 

158 

159 

160class AdminQuestionEditView(UpdateView): 

161 model = Question 

162 template_name = "nlp_classification/admin/update_form.html" 

163 form_class = QuestionForm 

164 

165 def get_success_url(self): 

166 return reverse("nlp_classification_admin_question_list") 

167 

168 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

169 context = super().get_context_data(**kwargs) 

170 context.update(title="Edit question") 

171 

172 return context 

173 

174 

175class AdminOptionListView(TemplateView): 

176 template_name = "nlp_classification/admin/option_list.html" 

177 

178 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

179 context = super().get_context_data(**kwargs) 

180 

181 table = self._get_table() 

182 tables.RequestConfig(self.request).configure(table) 

183 context.update(table=table) 

184 

185 return context 

186 

187 def _get_table(self) -> tables.Table: 

188 return OptionTable(Option.objects.all()) 

189 

190 

191class AdminOptionCreateView(CreateView): 

192 model = Option 

193 template_name = "nlp_classification/admin/update_form.html" 

194 form_class = OptionForm 

195 

196 def get_success_url(self): 

197 return reverse("nlp_classification_admin_option_list") 

198 

199 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

200 context = super().get_context_data(**kwargs) 

201 context.update(title="New option") 

202 

203 return context 

204 

205 

206class AdminOptionEditView(UpdateView): 

207 model = Option 

208 template_name = "nlp_classification/admin/update_form.html" 

209 form_class = OptionForm 

210 

211 def get_success_url(self): 

212 return reverse("nlp_classification_admin_option_list") 

213 

214 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

215 context = super().get_context_data(**kwargs) 

216 context.update(title="Edit option") 

217 

218 return context 

219 

220 

221class AdminSampleSpecListView(TemplateView): 

222 template_name = "nlp_classification/admin/sample_spec_list.html" 

223 

224 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

225 context = super().get_context_data(**kwargs) 

226 

227 table = self._get_table() 

228 tables.RequestConfig(self.request).configure(table) 

229 context.update(table=table) 

230 

231 return context 

232 

233 def _get_table(self) -> tables.Table: 

234 return SampleSpecTable(SampleSpec.objects.all()) 

235 

236 

237class AdminSampleSpecCreateView(CreateView): 

238 model = SampleSpec 

239 template_name = "nlp_classification/admin/update_form.html" 

240 form_class = SampleSpecForm 

241 

242 def get_success_url(self): 

243 return reverse("nlp_classification_admin_sample_spec_list") 

244 

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

248 

249 return context 

250 

251 

252class AdminSampleSpecEditView(UpdateView): 

253 model = SampleSpec 

254 template_name = "nlp_classification/admin/update_form.html" 

255 form_class = SampleSpecForm 

256 

257 def get_success_url(self): 

258 return reverse("nlp_classification_admin_sample_spec_list") 

259 

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

263 

264 return context 

265 

266 

267class AdminTableDefinitionListView(TemplateView): 

268 template_name = "nlp_classification/admin/table_definition_list.html" 

269 

270 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

271 context = super().get_context_data(**kwargs) 

272 

273 table = self._get_table() 

274 tables.RequestConfig(self.request).configure(table) 

275 context.update(table=table) 

276 

277 return context 

278 

279 def _get_table(self) -> tables.Table: 

280 return TableDefinitionTable(TableDefinition.objects.all()) 

281 

282 

283class AdminTableDefinitionCreateView(CreateView): 

284 model = TableDefinition 

285 template_name = "nlp_classification/admin/update_form.html" 

286 form_class = TableDefinitionForm 

287 

288 def get_success_url(self): 

289 return reverse("nlp_classification_admin_table_definition_list") 

290 

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

294 

295 return context 

296 

297 

298class AdminTableDefinitionEditView(UpdateView): 

299 model = TableDefinition 

300 template_name = "nlp_classification/admin/update_form.html" 

301 form_class = TableDefinitionForm 

302 

303 def get_success_url(self): 

304 return reverse("nlp_classification_admin_table_definition_list") 

305 

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

309 

310 return context 

311 

312 

313class AdminAssignmentListView(TemplateView): 

314 template_name = "nlp_classification/admin/assignment_list.html" 

315 

316 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

317 context = super().get_context_data(**kwargs) 

318 

319 table = self._get_table() 

320 tables.RequestConfig(self.request).configure(table) 

321 context.update(table=table) 

322 

323 return context 

324 

325 def _get_table(self) -> tables.Table: 

326 return AdminAssignmentTable(Assignment.objects.all()) 

327 

328 

329class AdminAssignmentCreateView(CreateView): 

330 model = Assignment 

331 template_name = "nlp_classification/admin/update_form.html" 

332 form_class = AssignmentForm 

333 

334 def get_success_url(self): 

335 return reverse("nlp_classification_admin_assignment_list") 

336 

337 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

338 context = super().get_context_data(**kwargs) 

339 context.update(title="New assignment") 

340 

341 return context 

342 

343 

344class AdminAssignmentEditView(UpdateView): 

345 model = Assignment 

346 template_name = "nlp_classification/admin/update_form.html" 

347 form_class = AssignmentForm 

348 

349 def get_success_url(self): 

350 return reverse("nlp_classification_admin_assignment_list") 

351 

352 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

353 context = super().get_context_data(**kwargs) 

354 context.update(title="Edit assigment") 

355 

356 return context 

357 

358 

359class UserAssignmentView(TemplateView): 

360 template_name = "nlp_classification/user/assignment.html" 

361 

362 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

363 context = super().get_context_data(**kwargs) 

364 

365 table = self._get_table() 

366 tables.RequestConfig(self.request).configure(table) 

367 context.update(table=table) 

368 

369 return context 

370 

371 def _get_table(self) -> tables.Table: 

372 assignment = Assignment.objects.get(pk=self.kwargs["pk"]) 

373 

374 return UserAnswerTable( 

375 UserAnswer.objects.filter(assignment=assignment) 

376 ) 

377 

378 

379class UserHomeView(TemplateView): 

380 template_name = "nlp_classification/user/home.html" 

381 

382 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

383 context = super().get_context_data(**kwargs) 

384 

385 table = self._get_table() 

386 tables.RequestConfig(self.request).configure(table) 

387 context.update(table=table) 

388 

389 return context 

390 

391 def _get_table(self) -> tables.Table: 

392 return UserAssignmentTable(Assignment.objects.all()) 

393 

394 

395class UserAnswerView(UpdateView): 

396 model = UserAnswer 

397 template_name = "nlp_classification/user/useranswerupdate_form.html" 

398 form_class = UserAnswerForm 

399 

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 ) 

406 

407 if next_record is not None: 

408 return reverse( 

409 "nlp_classification_user_answer", kwargs={"pk": next_record.pk} 

410 ) 

411 

412 return reverse( 

413 "nlp_classification_user_assignment", 

414 kwargs={"pk": self.object.assignment.pk}, 

415 ) 

416 

417 def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 

418 context = super().get_context_data(**kwargs) 

419 

420 table = self._get_table() 

421 

422 if table: 

423 tables.RequestConfig(self.request).configure(table) 

424 

425 context.update(nlp_table=table) 

426 

427 return context 

428 

429 def _get_table(self) -> Optional[tables.Table]: 

430 table_data = [] 

431 

432 for name, value in self.object.source_record.extra_nlp_fields.items(): 

433 table_data.append({"name": name, "value": value}) 

434 

435 if table_data: 

436 return FieldTable(table_data) 

437 

438 return None 

439 

440 

441def should_create_task(wizard: SessionWizardView) -> bool: 

442 return not wizard.has_selected_task 

443 

444 

445def should_select_question(wizard: SessionWizardView) -> bool: 

446 return wizard.has_selected_task 

447 

448 

449def should_create_question(wizard: SessionWizardView) -> bool: 

450 return not wizard.has_selected_question 

451 

452 

453class NlpClassificationWizardView(SessionWizardView): 

454 template_name = "nlp_classification/admin/wizard_form.html" 

455 

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) 

459 

460 return context 

461 

462 def get_instructions(self) -> Optional[str]: 

463 raise NotImplementedError( 

464 "get_instructions() needs to be defined in " 

465 f"{self.__class__.__name__}" 

466 ) 

467 

468 

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 ] 

483 

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" 

487 

488 if step == ws.CREATE_TASK: 

489 return "Enter the details for the new task" 

490 

491 if step == ws.SELECT_QUESTION: 

492 return "Select an existing question or create a new question" 

493 

494 if step == ws.CREATE_QUESTION: 

495 return "Enter the details for the new question" 

496 

497 if step == ws.SELECT_OPTIONS: 

498 return self._get_select_options_instructions() 

499 

500 if step == ws.CREATE_OPTIONS: 

501 return self._get_create_options_instructions() 

502 

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 ) 

508 

509 def _get_create_options_instructions(self) -> str: 

510 return f"Create options for the question '{self.question_title}'." 

511 

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 

521 

522 def get_form_initial(self, step: str) -> dict[str, Any]: 

523 initial = super().get_form_initial(step) 

524 

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()] 

529 

530 return initial 

531 

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 

536 

537 return kwargs 

538 

539 @property 

540 def has_selected_task(self) -> bool: 

541 return self.selected_task is not None 

542 

543 @property 

544 def selected_task(self) -> Optional[Task]: 

545 cleaned_data = self.get_cleaned_data_for_step(ws.SELECT_TASK) or {} 

546 

547 return cleaned_data.get("task") 

548 

549 @property 

550 def has_selected_question(self) -> bool: 

551 return self.selected_question is not None 

552 

553 @property 

554 def question_title(self) -> str: 

555 question = self.selected_question 

556 if question is not None: 

557 return question.title 

558 

559 return self.created_question_title 

560 

561 @property 

562 def selected_question(self) -> Optional[Question]: 

563 cleaned_data = self.get_cleaned_data_for_step(ws.SELECT_QUESTION) or {} 

564 

565 return cleaned_data.get("question") 

566 

567 @property 

568 def created_question_title(self) -> Optional[str]: 

569 cleaned_data = self.get_cleaned_data_for_step(ws.CREATE_QUESTION) or {} 

570 

571 return cleaned_data.get("title") 

572 

573 @property 

574 def selected_options(self) -> Optional[list[Option]]: 

575 cleaned_data = self.get_cleaned_data_for_step(ws.SELECT_OPTIONS) or {} 

576 

577 options = cleaned_data.get("options") 

578 if options is not None: 

579 return list(options) 

580 

581 return None 

582 

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

590 

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 

595 

596 question.task = task 

597 question.save() 

598 

599 options = self.selected_options 

600 if options is not None: 

601 question.options.set(options) 

602 

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) 

608 

609 return HttpResponseRedirect(reverse("nlp_classification_admin_home")) 

610 

611 

612class SampleDataWizardView(NlpClassificationWizardView): 

613 form_list = [ 

614 ( 

615 ws.SELECT_SOURCE_TABLE_DEFINITION, 

616 WizardSelectSourceTableDefinitionForm, 

617 ), 

618 (ws.SELECT_SOURCE_TABLE, WizardSelectSourceTableForm), 

619 ] 

620 

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 ) 

627 

628 def done( 

629 self, form_list: list[Form], form_dict: dict[str, Form], **kwargs: Any 

630 ) -> HttpResponse: 

631 

632 return HttpResponseRedirect(reverse("nlp_classification_admin_home"))