Coverage for crateweb/consent/views.py: 27%

324 statements  

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

1""" 

2crate_anon/crateweb/consent/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 

26**Django views for the consent-to-contact system.** 

27 

28""" 

29 

30import logging 

31import mimetypes 

32from typing import List 

33 

34from cardinal_pythonlib.django.serve import ( 

35 serve_buffer, 

36 serve_concatenated_pdf_from_disk, 

37 serve_file, 

38) 

39from cardinal_pythonlib.httpconst import ContentType 

40from cardinal_pythonlib.nhs import generate_random_nhs_number 

41from django.conf import settings 

42from django.contrib.auth.decorators import user_passes_test 

43from django.core.exceptions import PermissionDenied 

44from django.db import transaction 

45from django.http import HttpResponse, Http404, HttpResponseForbidden 

46from django.http.response import HttpResponseBase 

47from django.http.request import HttpRequest 

48from django.shortcuts import get_object_or_404, render 

49 

50from crate_anon.crateweb.consent.constants import EthicsInfo 

51from crate_anon.crateweb.consent.forms import ( 

52 ClinicianResponseForm, 

53 SingleNhsNumberForm, 

54 SuperuserSubmitContactRequestForm, 

55 ResearcherSubmitContactRequestForm, 

56 ClinicianSubmitContactRequestForm, 

57) 

58from crate_anon.crateweb.consent.lookup import lookup_consent, lookup_patient 

59from crate_anon.crateweb.consent.models import ( 

60 CharityPaymentRecord, 

61 ClinicianResponse, 

62 ConsentMode, 

63 ContactRequest, 

64 Email, 

65 EmailAttachment, 

66 Leaflet, 

67 Letter, 

68 make_dummy_objects, 

69 PatientLookup, 

70 Study, 

71 TEST_ID_STRINGS, 

72) 

73from crate_anon.crateweb.consent.storage import privatestorage 

74from crate_anon.crateweb.consent.tasks import ( 

75 finalize_clinician_response, 

76 test_email_rdbm_task, 

77) 

78from crate_anon.crateweb.consent.utils import ( 

79 days_to_years, 

80 render_pdf_html_to_string, 

81) 

82from crate_anon.crateweb.core.utils import ( 

83 is_developer, 

84 is_superuser, 

85) 

86from crate_anon.crateweb.extra.pdf import serve_html_or_pdf 

87from crate_anon.crateweb.research.research_db_info import ( 

88 get_research_db_info, 

89) 

90from crate_anon.crateweb.research.models import PidLookup, get_mpid 

91from crate_anon.crateweb.userprofile.models import UserProfile 

92 

93log = logging.getLogger(__name__) 

94 

95 

96# ============================================================================= 

97# Additional validators 

98# ============================================================================= 

99 

100 

101def validate_email_request( 

102 user: settings.AUTH_USER_MODEL, email: Email 

103) -> None: 

104 """ 

105 Checks that the current user has permission to view the specified e-mail. 

106 

107 Args:class 

108 user: current Django user object 

109 email: :class:`crate_anon.crateweb.consent.models.Email` 

110 

111 Raises: 

112 :exc:`django.core.exceptions.PermissionDenied` on failure 

113 

114 """ 

115 if user.profile.is_developer: 

116 return # the developer sees all 

117 if email.to_researcher: 

118 # e-mails to clinicians/patients may be restricted 

119 if user.is_superuser: 

120 return # RDBM can see any e-mail to a researcher 

121 studies = Study.filter_studies_for_researcher( 

122 Study.objects.all(), user 

123 ) 

124 if email.study in studies: 

125 return # this e-mail belongs to this researcher 

126 elif email.to_patient: 

127 if user.is_superuser: 

128 return # RDBM can see any e-mail to a patient 

129 raise PermissionDenied("Not authorized") 

130 

131 

132def validate_letter_request( 

133 user: settings.AUTH_USER_MODEL, letter: Letter 

134) -> None: 

135 """ 

136 Checks that the current user has permission to view the specified letter. 

137 

138 Args: 

139 user: current Django user object 

140 letter: :class:`crate_anon.crateweb.consent.models.Letter` 

141 

142 Raises: 

143 :exc:`django.core.exceptions.PermissionDenied` on failure 

144 

145 """ 

146 if user.profile.is_developer: 

147 return # the developer sees all 

148 if user.is_superuser: 

149 return # RDBM can see any letters generated 

150 if letter.to_researcher: 

151 studies = Study.filter_studies_for_researcher( 

152 Study.objects.all(), user 

153 ) 

154 if letter.study in studies: 

155 return # this e-mail belongs to this researcher 

156 raise PermissionDenied("Not authorized") 

157 

158 

159# ============================================================================= 

160# Fetchers 

161# ============================================================================= 

162 

163 

164def get_contact_request( 

165 request: HttpRequest, contact_request_id: str 

166) -> ContactRequest: 

167 """ 

168 Return the specified contact request. 

169 

170 Args: 

171 request: the :class:`django.http.request.HttpRequest` 

172 contact_request_id: PK 

173 

174 Returns: 

175 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

176 

177 Raises: 

178 :exc:`django.http.Http404` if not found 

179 """ 

180 if contact_request_id in TEST_ID_STRINGS: 

181 cr = make_dummy_objects(request, contact_request_id).contact_request 

182 cr.mockup() 

183 return cr 

184 return get_object_or_404( 

185 ContactRequest, id=contact_request_id 

186 ) # type: ContactRequest 

187 

188 

189def get_patient_lookup( 

190 request: HttpRequest, patient_lookup_id: str 

191) -> PatientLookup: 

192 """ 

193 Return the specified patient lookup. 

194 

195 Args: 

196 request: the :class:`django.http.request.HttpRequest` 

197 patient_lookup_id: PK 

198 

199 Returns: 

200 :class:`crate_anon.crateweb.consent.models.PatientLookup` 

201 

202 Raises: 

203 :exc:`django.http.Http404` if not found 

204 """ 

205 if patient_lookup_id in TEST_ID_STRINGS: 

206 return make_dummy_objects(request, patient_lookup_id).patient_lookup 

207 return get_object_or_404( 

208 PatientLookup, id=patient_lookup_id 

209 ) # type: PatientLookup 

210 

211 

212def get_consent_mode( 

213 request: HttpRequest, consent_mode_id: str 

214) -> ConsentMode: 

215 """ 

216 Return the specified consent mode. 

217 

218 Args: 

219 request: the :class:`django.http.request.HttpRequest` 

220 consent_mode_id: PK 

221 

222 Returns: 

223 :class:`crate_anon.crateweb.consent.models.ConsentMode` 

224 

225 Raises: 

226 :exc:`django.http.Http404` if not found 

227 """ 

228 if consent_mode_id in TEST_ID_STRINGS: 

229 return make_dummy_objects(request, consent_mode_id).consent_mode 

230 return get_object_or_404( 

231 ConsentMode, id=consent_mode_id 

232 ) # type: ConsentMode 

233 

234 

235# ============================================================================= 

236# Views 

237# ============================================================================= 

238 

239 

240# noinspection PyUnusedLocal 

241def study_details(request: HttpRequest, study_id: str) -> HttpResponseBase: 

242 """ 

243 View details of a study. 

244 

245 Args: 

246 request: the :class:`django.http.request.HttpRequest` 

247 study_id: PK for :class:`crate_anon.crateweb.consent.models.Study` 

248 """ 

249 if study_id in TEST_ID_STRINGS: 

250 study = make_dummy_objects(request, study_id).study 

251 else: 

252 study = get_object_or_404(Study, pk=study_id) # type: Study 

253 if not study.study_details_pdf: 

254 raise Http404("No details") 

255 # noinspection PyUnresolvedReferences 

256 return serve_file( 

257 study.study_details_pdf.path, 

258 content_type=ContentType.PDF, 

259 as_inline=True, 

260 ) 

261 

262 

263study_details.login_required = False 

264 

265 

266# noinspection PyUnusedLocal 

267def study_form(request: HttpRequest, study_id: str) -> HttpResponseBase: 

268 """ 

269 For a study, view the PDF form that researchers would like clinicians to 

270 complete. 

271 

272 Args: 

273 request: the :class:`django.http.request.HttpRequest` 

274 study_id: PK for :class:`crate_anon.crateweb.consent.models.Study` 

275 """ 

276 study = get_object_or_404(Study, pk=study_id) # type: Study 

277 if not study.subject_form_template_pdf: 

278 raise Http404("No study form for clinicians to complete") 

279 # noinspection PyUnresolvedReferences 

280 return serve_file( 

281 study.subject_form_template_pdf.path, 

282 content_type=ContentType.PDF, 

283 as_inline=True, 

284 ) 

285 

286 

287study_form.login_required = False 

288 

289 

290# noinspection PyUnusedLocal 

291def study_pack(request: HttpRequest, study_id: str) -> HttpResponseBase: 

292 """ 

293 View a PDF "pack" for a study, including the study details and its 

294 additional form for clinicians, if provided. 

295 

296 Args: 

297 request: the :class:`django.http.request.HttpRequest` 

298 study_id: PK for :class:`crate_anon.crateweb.consent.models.Study` 

299 """ 

300 study = get_object_or_404(Study, pk=study_id) # type: Study 

301 # noinspection PyUnresolvedReferences 

302 filenames = filter( 

303 None, 

304 [ 

305 study.study_details_pdf.path if study.study_details_pdf else None, 

306 ( 

307 study.subject_form_template_pdf.path 

308 if study.subject_form_template_pdf 

309 else None 

310 ), 

311 ], 

312 ) 

313 if not filenames: 

314 raise Http404("No leaflets") 

315 return serve_concatenated_pdf_from_disk( 

316 filenames, offered_filename=f"study_{study_id}_pack.pdf" 

317 ) 

318 

319 

320study_pack.login_required = False 

321 

322 

323# noinspection PyUnusedLocal 

324@user_passes_test(is_superuser) 

325def download_privatestorage( 

326 request: HttpRequest, filename: str 

327) -> HttpResponseBase: 

328 """ 

329 Download a file from the private storage area. 

330 

331 Superuser access function, used for admin interface only. 

332 

333 Args: 

334 request: the :class:`django.http.request.HttpRequest` 

335 filename: filename within the private storage area 

336 """ 

337 fullpath = privatestorage.path(filename) 

338 content_type = mimetypes.guess_type(filename, strict=False)[0] 

339 # ... guess_type returns a (content_type, encoding) tuple 

340 return serve_file(fullpath, content_type=content_type, as_inline=True) 

341 

342 

343@user_passes_test(is_developer) 

344def generate_random_nhs(request: HttpRequest, n: str = 10) -> HttpResponse: 

345 """ 

346 Display random NHS numbers to the requestor. 

347 

348 Args: 

349 request: the :class:`django.http.request.HttpRequest` 

350 n: how many random NHS numbers to generate? 

351 """ 

352 nhs_numbers = [generate_random_nhs_number() for _ in range(int(n))] 

353 return render( 

354 request, "generate_random_nhs.html", {"nhs_numbers": nhs_numbers} 

355 ) 

356 

357 

358def view_email_html(request: HttpRequest, email_id: str) -> HttpResponse: 

359 """ 

360 View the HTML for an e-mail. 

361 

362 Args: 

363 request: the :class:`django.http.request.HttpRequest` 

364 email_id: PK for :class:`crate_anon.crateweb.consent.models.Email` 

365 

366 Raises: 

367 :exc:`django.http.Http404` if not found 

368 """ 

369 email = get_object_or_404(Email, pk=email_id) # type: Email 

370 # noinspection PyTypeChecker 

371 validate_email_request(request.user, email) 

372 return HttpResponse(email.msg_html) 

373 

374 

375def view_email_attachment( 

376 request: HttpRequest, attachment_id: str 

377) -> HttpResponseBase: 

378 """ 

379 View the HTML for an e-mail. 

380 

381 Args: 

382 request: the :class:`django.http.request.HttpRequest` 

383 attachment_id: PK for 

384 :class:`crate_anon.crateweb.consent.models.EmailAttachment` 

385 

386 Raises: 

387 :exc:`django.http.Http404` if not found 

388 """ 

389 attachment = get_object_or_404( 

390 EmailAttachment, pk=attachment_id 

391 ) # type: EmailAttachment 

392 # noinspection PyTypeChecker 

393 validate_email_request(request.user, attachment.email) 

394 if not attachment.file: 

395 raise Http404("Attachment missing") 

396 return serve_file( 

397 attachment.file.path, 

398 content_type=attachment.content_type, 

399 as_inline=True, 

400 ) 

401 

402 

403@user_passes_test(is_developer) 

404def test_patient_lookup(request: HttpRequest) -> HttpResponse: 

405 """ 

406 Looks up a patient's details and shows the results without saving the 

407 lookup. 

408 

409 Args: 

410 request: the :class:`django.http.request.HttpRequest` 

411 """ 

412 form = SingleNhsNumberForm( 

413 request.POST if request.method == "POST" else None 

414 ) 

415 if form.is_valid(): 

416 lookup = lookup_patient( 

417 nhs_number=form.cleaned_data["nhs_number"], save=False 

418 ) 

419 # Don't use a Form. https://code.djangoproject.com/ticket/17031 

420 return render( 

421 request, "patient_lookup_result.html", {"lookup": lookup} 

422 ) 

423 return render(request, "patient_lookup_get_nhs.html", {"form": form}) 

424 

425 

426@user_passes_test(is_developer) 

427def test_consent_lookup(request: HttpRequest) -> HttpResponse: 

428 """ 

429 Looks up a patient's consent mode and shows the results without saving the 

430 lookup. 

431 

432 Args: 

433 request: the :class:`django.http.request.HttpRequest` 

434 """ 

435 form = SingleNhsNumberForm( 

436 request.POST if request.method == "POST" else None 

437 ) 

438 if form.is_valid(): 

439 decisions = [] # type: List[str] 

440 nhs_number = form.cleaned_data["nhs_number"] 

441 consent_mode = lookup_consent( 

442 nhs_number=nhs_number, decisions=decisions 

443 ) 

444 # Don't use a Form. https://code.djangoproject.com/ticket/17031 

445 return render( 

446 request, 

447 "consent_lookup_result.html", 

448 { 

449 "consent_mode": consent_mode, 

450 "nhs_number": nhs_number, 

451 "decisions": decisions, 

452 }, 

453 ) 

454 return render(request, "consent_lookup_get_nhs.html", {"form": form}) 

455 

456 

457# noinspection PyUnusedLocal 

458def view_leaflet(request: HttpRequest, leaflet_name: str) -> HttpResponseBase: 

459 """ 

460 Views a system-wide leaflet. 

461 

462 Args: 

463 request: the :class:`django.http.request.HttpRequest` 

464 leaflet_name: name of the leaflet; see 

465 :class:`crate_anon.crateweb.consent.models.Leaflet` 

466 """ 

467 leaflet = get_object_or_404(Leaflet, name=leaflet_name) # type: Leaflet 

468 if not leaflet.pdf: 

469 raise Http404("Missing leaflet") 

470 # noinspection PyUnresolvedReferences 

471 return serve_file( 

472 leaflet.pdf.path, content_type=ContentType.PDF, as_inline=True 

473 ) 

474 

475 

476view_leaflet.login_required = False 

477 

478 

479def view_letter(request: HttpRequest, letter_id: str) -> HttpResponseBase: 

480 """ 

481 View a letter (as a PDF). 

482 

483 Args: 

484 request: the :class:`django.http.request.HttpRequest` 

485 letter_id: PK for :class:`crate_anon.crateweb.consent.models.Letter` 

486 

487 Raises: 

488 :exc:`django.http.Http404` if not found 

489 """ 

490 letter = get_object_or_404(Letter, pk=letter_id) # type: Letter 

491 # noinspection PyTypeChecker 

492 validate_letter_request(request.user, letter) 

493 if not letter.pdf: 

494 raise Http404("Missing letter") 

495 return serve_file( 

496 letter.pdf.path, content_type=ContentType.PDF, as_inline=True 

497 ) 

498 

499 

500def submit_contact_request(request: HttpRequest) -> HttpResponse: 

501 """ 

502 Submits a contact request for a study, potentially for multiple patients. 

503 

504 Args: 

505 request: the :class:`django.http.request.HttpRequest` 

506 """ 

507 research_database_info = get_research_db_info() 

508 dbinfo = research_database_info.dbinfo_for_contact_lookup 

509 if request.user.is_superuser: 

510 form = SuperuserSubmitContactRequestForm( 

511 request.POST if request.method == "POST" else None, dbinfo=dbinfo 

512 ) 

513 else: 

514 form = ResearcherSubmitContactRequestForm( 

515 user=request.user, 

516 data=request.POST if request.method == "POST" else None, 

517 dbinfo=dbinfo, 

518 ) 

519 if not form.is_valid(): 

520 return render( 

521 request, 

522 "contact_request_submit.html", 

523 { 

524 "db_description": dbinfo.description, 

525 "form": form, 

526 }, 

527 ) 

528 

529 study = form.cleaned_data["study"] 

530 request_direct_approach = form.cleaned_data["request_direct_approach"] 

531 contact_requests = [] # type: List[ContactRequest] 

532 

533 # NHS numbers 

534 if request.user.is_superuser: 

535 for nhs_number in form.cleaned_data["nhs_numbers"]: 

536 contact_requests.append( 

537 ContactRequest.create( 

538 request=request, 

539 study=study, 

540 request_direct_approach=request_direct_approach, 

541 lookup_nhs_number=nhs_number, 

542 ) 

543 ) 

544 # RIDs 

545 for rid in form.cleaned_data["rids"]: 

546 contact_requests.append( 

547 ContactRequest.create( 

548 request=request, 

549 study=study, 

550 request_direct_approach=request_direct_approach, 

551 lookup_rid=rid, 

552 ) 

553 ) 

554 # MRIDs 

555 for mrid in form.cleaned_data["mrids"]: 

556 contact_requests.append( 

557 ContactRequest.create( 

558 request=request, 

559 study=study, 

560 request_direct_approach=request_direct_approach, 

561 lookup_mrid=mrid, 

562 ) 

563 ) 

564 

565 # Show results. 

566 # Don't use a Form. https://code.djangoproject.com/ticket/17031 

567 return render( 

568 request, 

569 "contact_request_result.html", 

570 { 

571 "contact_requests": contact_requests, 

572 }, 

573 ) 

574 

575 

576def clinician_initiated_contact_request(request: HttpRequest) -> HttpResponse: 

577 """ 

578 For clinicians to request that their patient is contacted about a study. 

579 

580 Args: 

581 request: the :class:`django.http.request.HttpRequest` 

582 """ 

583 research_database_info = get_research_db_info() 

584 dbinfo = research_database_info.dbinfo_for_contact_lookup 

585 email = request.user.email 

586 userprofile = UserProfile.objects.get(user=request.user) 

587 title = userprofile.title 

588 firstname = request.user.first_name 

589 lastname = request.user.last_name 

590 form = ClinicianSubmitContactRequestForm( 

591 data=request.POST if request.method == "POST" else None, 

592 dbinfo=dbinfo, 

593 email_addr=email, 

594 title=title, 

595 firstname=firstname, 

596 lastname=lastname, 

597 ) 

598 if not form.is_valid(): 

599 return render( 

600 request, 

601 "clinician_contact_request_submit.html", 

602 { 

603 "db_description": dbinfo.description, 

604 "form": form, 

605 "permitted_to_contact_discharged_patients_for_n_days": settings.PERMITTED_TO_CONTACT_DISCHARGED_PATIENTS_FOR_N_DAYS, # noqa: E501 

606 "permitted_to_contact_discharged_patients_for_n_years": days_to_years( # noqa: E501 

607 settings.PERMITTED_TO_CONTACT_DISCHARGED_PATIENTS_FOR_N_DAYS # noqa: E501 

608 ), 

609 }, 

610 ) 

611 study = form.cleaned_data["study"] 

612 let_rdbm_contact_pt = form.cleaned_data["let_rdbm_contact_pt"] 

613 signatory_name = "{} {} {}".format( 

614 form.cleaned_data["title"], 

615 form.cleaned_data["firstname"], 

616 form.cleaned_data["lastname"], 

617 ) 

618 signatory_title = form.cleaned_data["signatory_title"] 

619 contact_requests = [] # type: List[ContactRequest] 

620 msgs = [] # type: List[str] 

621 

622 # NHS numbers 

623 for nhs_number in form.cleaned_data["nhs_numbers"]: 

624 patient = PatientLookup.objects.filter(nhs_number=nhs_number).first() 

625 if patient: 

626 contact_requests.append( 

627 ContactRequest.create( 

628 request=request, 

629 study=study, 

630 request_direct_approach=False, 

631 lookup_nhs_number=nhs_number, 

632 clinician_initiated=True, 

633 clinician_email=form.cleaned_data["email"], 

634 rdbm_to_contact_pt=let_rdbm_contact_pt, 

635 clinician_signatory_name=signatory_name, 

636 clinician_signatory_title=signatory_title, 

637 ) 

638 ) 

639 else: 

640 msgs.append( 

641 "Patient with nhs number {} does not exist".format(nhs_number) 

642 ) 

643 

644 # RIDs 

645 for rid in form.cleaned_data["rids"]: 

646 patient_does_not_exist = False 

647 try: 

648 get_mpid(dbinfo=dbinfo, rid=rid) 

649 except PidLookup.DoesNotExist: 

650 msgs.append("Patient with rid {} does not exist".format(rid)) 

651 patient_does_not_exist = True 

652 if not patient_does_not_exist: 

653 contact_requests.append( 

654 ContactRequest.create( 

655 request=request, 

656 study=study, 

657 request_direct_approach=False, 

658 lookup_rid=rid, 

659 clinician_initiated=True, 

660 clinician_email=form.cleaned_data["email"], 

661 rdbm_to_contact_pt=let_rdbm_contact_pt, 

662 clinician_signatory_name=signatory_name, 

663 clinician_signatory_title=signatory_title, 

664 ) 

665 ) 

666 

667 # MRIDs 

668 for mrid in form.cleaned_data["mrids"]: 

669 patient_does_not_exist = False 

670 try: 

671 get_mpid(dbinfo=dbinfo, mrid=mrid) 

672 except PidLookup.DoesNotExist: 

673 msgs.append("Patient with mrid {} does not exist".format(mrid)) 

674 patient_does_not_exist = True 

675 if not patient_does_not_exist: 

676 contact_requests.append( 

677 ContactRequest.create( 

678 request=request, 

679 study=study, 

680 request_direct_approach=False, 

681 lookup_mrid=mrid, 

682 clinician_initiated=True, 

683 clinician_email=form.cleaned_data["email"], 

684 rdbm_to_contact_pt=let_rdbm_contact_pt, 

685 clinician_signatory_name=signatory_name, 

686 clinician_signatory_title=signatory_title, 

687 ) 

688 ) 

689 

690 # for contact_request in contact_requests: 

691 # # consent_mode = contact_request.consent_mode.consent_mode 

692 # # if (consent_mode == ConsentMode.GREEN or 

693 # # consent_mode == ConsentMode.YELLOW): 

694 # generate_automatic_yes.delay( 

695 # contact_request.id, 

696 # rdbm_to_contact_pt=let_rdbm_contact_pt) 

697 

698 return render( 

699 request, 

700 "clinician_contact_request_result.html", 

701 { 

702 "contact_requests": contact_requests, 

703 "msgs": msgs, 

704 "let_rdbm_contact_pt": let_rdbm_contact_pt, 

705 }, 

706 ) 

707 

708 

709def finalize_clinician_response_in_background( 

710 request: HttpRequest, clinician_response: ClinicianResponse 

711) -> HttpResponse: 

712 """ 

713 Submits a background processing job to complete processing a clinician's 

714 response, and return a short thank-you response to the clinician. 

715 

716 Args: 

717 request: the :class:`django.http.request.HttpRequest` 

718 clinician_response: 

719 :class:`crate_anon.crateweb.consent.models.ClinicianResponse` 

720 object 

721 """ 

722 clinician_response.finalize_a() # first part of processing 

723 transaction.on_commit( 

724 lambda: finalize_clinician_response.delay(clinician_response.id) 

725 ) # Asynchronous 

726 return render( 

727 request, 

728 "clinician_confirm_response.html", 

729 { 

730 "clinician_response": clinician_response, 

731 }, 

732 ) 

733 

734 

735def clinician_response_view( 

736 request: HttpRequest, clinician_response_id: str 

737) -> HttpResponse: 

738 """ 

739 Shows the response choices to the clinician. They'll get here by clicking 

740 on a link in an e-mail. 

741 

742 **REC DOCUMENTS 09, 11, 13 (B): Web form for clinicians to respond with.** 

743 

744 Args: 

745 request: the :class:`django.http.request.HttpRequest` 

746 clinician_response_id: PK for 

747 :class:`crate_anon.crateweb.consent.models.ClinicianResponse` 

748 """ 

749 if clinician_response_id in TEST_ID_STRINGS: 

750 dummies = make_dummy_objects(request, clinician_response_id) 

751 clinician_response = dummies.clinician_response 

752 contact_request = dummies.contact_request 

753 study = dummies.study 

754 patient_lookup = dummies.patient_lookup 

755 consent_mode = dummies.consent_mode 

756 else: 

757 clinician_response = get_object_or_404( 

758 ClinicianResponse, pk=clinician_response_id 

759 ) # type: ClinicianResponse 

760 contact_request = clinician_response.contact_request 

761 study = contact_request.study 

762 patient_lookup = contact_request.patient_lookup 

763 consent_mode = contact_request.consent_mode 

764 

765 # Build form. 

766 # - We have an existing clinician_response and wish to modify it 

767 # (potentially). 

768 # - If the clinician is responding to an e-mail, they will be passing 

769 # a couple of parameters (including the token) via GET query parameters. 

770 # If they're clicking "Submit", they'll be using POST. 

771 if request.method == "GET": 

772 from_email = True 

773 clinician_response.response_route = ClinicianResponse.ROUTE_EMAIL 

774 data = request.GET 

775 else: 

776 from_email = False 

777 clinician_response.response_route = ClinicianResponse.ROUTE_WEB 

778 data = request.POST 

779 form = ClinicianResponseForm(instance=clinician_response, data=data) 

780 

781 # Token valid? Check raw data. Say goodbye otherwise. 

782 # - The raw data in the form is not influenced by the form's instance. 

783 if form.data["token"] != clinician_response.token: 

784 return HttpResponseForbidden( 

785 "Not authorized. The token you passed doesn't match the one " 

786 "you were sent." 

787 ) 

788 

789 # Already responded? 

790 if clinician_response.responded: 

791 passed_to_pt = ( 

792 clinician_response.response == ClinicianResponse.RESPONSE_A 

793 ) 

794 return render( 

795 request, 

796 "clinician_already_responded.html", 

797 { 

798 "clinician_response": clinician_response, 

799 "consent_mode": consent_mode, 

800 "contact_request": contact_request, 

801 "Leaflet": Leaflet, 

802 "passed_to_pt": passed_to_pt, 

803 "patient_lookup": patient_lookup, 

804 "settings": settings, 

805 "study": study, 

806 }, 

807 ) 

808 

809 # Is the clinician saying yes or no (direct from e-mail)? 

810 if from_email and form.data["email_choice"] in ( 

811 ClinicianResponse.EMAIL_CHOICE_Y, 

812 ClinicianResponse.EMAIL_CHOICE_N, 

813 ): 

814 # We can't use form.save() as the data may not validate. 

815 # It won't validate because the response/clinician name is blank. 

816 # We can't write to the form directly. So... 

817 clinician_response.email_choice = form.data["email_choice"] 

818 if clinician_response.email_choice == ClinicianResponse.EMAIL_CHOICE_Y: 

819 # Ask RDBM to do the work 

820 clinician_response.response = ClinicianResponse.RESPONSE_R 

821 else: 

822 # Veto on clinical grounds 

823 clinician_response.response = ClinicianResponse.RESPONSE_B 

824 return finalize_clinician_response_in_background( 

825 request, clinician_response 

826 ) 

827 

828 # Has the clinician made a decision via the web form? 

829 if form.is_valid(): 

830 clinician_response = form.save(commit=False) # return unsaved instance 

831 return finalize_clinician_response_in_background( 

832 request, clinician_response 

833 ) 

834 

835 # If we get here, we need to offer the form up for editing, 

836 # and mark it as a web response. 

837 clinician_involvement_requested = ( 

838 contact_request.clinician_involvement 

839 == ContactRequest.CLINICIAN_INVOLVEMENT_REQUESTED 

840 ) 

841 clinician_involvement_required_yellow = ( 

842 contact_request.clinician_involvement 

843 == ContactRequest.CLINICIAN_INVOLVEMENT_REQUIRED_YELLOW 

844 ) 

845 clinician_involvement_required_unknown = ( 

846 contact_request.clinician_involvement 

847 == ContactRequest.CLINICIAN_INVOLVEMENT_REQUIRED_UNKNOWN 

848 ) 

849 extra_form = contact_request.is_extra_form() 

850 return render( 

851 request, 

852 "clinician_response.html", 

853 { 

854 "clinician_response": clinician_response, 

855 "ClinicianResponse": ClinicianResponse, 

856 "consent_mode": consent_mode, 

857 "contact_request": contact_request, 

858 "Leaflet": Leaflet, 

859 "patient_lookup": patient_lookup, 

860 "settings": settings, 

861 "study": study, 

862 "form": form, 

863 "clinician_involvement_requested": clinician_involvement_requested, 

864 "clinician_involvement_required_yellow": clinician_involvement_required_yellow, # noqa: E501 

865 "clinician_involvement_required_unknown": clinician_involvement_required_unknown, # noqa: E501 

866 # 'option_c_available': clinician_involvement_requested, 

867 "option_c_available": True, 

868 "option_r_available": not extra_form, 

869 "extra_form": extra_form, 

870 "unknown_consent_mode": contact_request.is_consent_mode_unknown(), 

871 "permitted_to_contact_discharged_patients_for_n_days": settings.PERMITTED_TO_CONTACT_DISCHARGED_PATIENTS_FOR_N_DAYS, # noqa: E501 

872 "permitted_to_contact_discharged_patients_for_n_years": days_to_years( # noqa: E501 

873 settings.PERMITTED_TO_CONTACT_DISCHARGED_PATIENTS_FOR_N_DAYS 

874 ), 

875 }, 

876 ) 

877 

878 

879clinician_response_view.login_required = False 

880 

881 

882# noinspection PyUnusedLocal 

883def clinician_pack( 

884 request: HttpRequest, clinician_response_id: str, token: str 

885) -> HttpResponse: 

886 """ 

887 Shows a clinician "pack" as a PDF, including a letter from them to the 

888 patient, details of the study, forms for the patient to respond, etc. 

889 

890 Args: 

891 request: the :class:`django.http.request.HttpRequest` 

892 clinician_response_id: PK for 

893 :class:`crate_anon.crateweb.consent.models.ClinicianResponse` 

894 token: security token (which we'll check against the one we sent to 

895 the clinician) 

896 """ 

897 if clinician_response_id in TEST_ID_STRINGS: 

898 dummies = make_dummy_objects(request, clinician_response_id) 

899 clinician_response = dummies.clinician_response 

900 contact_request = dummies.contact_request 

901 else: 

902 clinician_response = get_object_or_404( 

903 ClinicianResponse, pk=clinician_response_id 

904 ) # type: ClinicianResponse 

905 contact_request = clinician_response.contact_request 

906 # Check token authentication 

907 if token != clinician_response.token: 

908 return HttpResponseForbidden( 

909 "Not authorized. The token you passed doesn't match the one you " 

910 "were sent." 

911 ) 

912 # Build and serve 

913 pdf = contact_request.get_clinician_pack_pdf() 

914 offered_filename = "clinician_pack_{}.pdf".format(clinician_response_id) 

915 return serve_buffer( 

916 pdf, 

917 offered_filename=offered_filename, 

918 content_type=ContentType.PDF, 

919 as_attachment=False, 

920 as_inline=True, 

921 ) 

922 

923 

924clinician_pack.login_required = False 

925 

926 

927# ----------------------------------------------------------------------------- 

928# Draft e-mails 

929# ----------------------------------------------------------------------------- 

930 

931 

932@user_passes_test(is_developer) 

933def draft_clinician_email( 

934 request: HttpRequest, contact_request_id: str 

935) -> HttpResponse: 

936 """ 

937 Developer view: draft e-mail to clinician. 

938 

939 Args: 

940 request: the :class:`django.http.request.HttpRequest` 

941 contact_request_id: PK for 

942 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

943 """ 

944 contact_request = get_contact_request(request, contact_request_id) 

945 return HttpResponse(contact_request.get_clinician_email_html(save=False)) 

946 

947 

948@user_passes_test(is_developer) 

949def draft_approval_email( 

950 request: HttpRequest, contact_request_id: str 

951) -> HttpResponse: 

952 """ 

953 Developer view: draft e-mail to researcher, giving permission. 

954 

955 Args: 

956 request: the :class:`django.http.request.HttpRequest` 

957 contact_request_id: PK for 

958 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

959 """ 

960 contact_request = get_contact_request(request, contact_request_id) 

961 return HttpResponse(contact_request.get_approval_email_html()) 

962 

963 

964@user_passes_test(is_developer) 

965def draft_withdrawal_email( 

966 request: HttpRequest, contact_request_id: str 

967) -> HttpResponse: 

968 """ 

969 Developer view: draft e-mail to researcher, withdrawing permission. 

970 

971 Args: 

972 request: the :class:`django.http.request.HttpRequest` 

973 contact_request_id: PK for 

974 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

975 """ 

976 contact_request = get_contact_request(request, contact_request_id) 

977 return HttpResponse(contact_request.get_withdrawal_email_html()) 

978 

979 

980# ----------------------------------------------------------------------------- 

981# Draft letters 

982# ----------------------------------------------------------------------------- 

983 

984 

985@user_passes_test(is_developer) 

986def draft_approval_letter( 

987 request: HttpRequest, contact_request_id: str, viewtype: str 

988) -> HttpResponse: 

989 """ 

990 Developer view: draft letter to researcher, giving permission. 

991 

992 Args: 

993 request: the :class:`django.http.request.HttpRequest` 

994 contact_request_id: PK for 

995 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

996 viewtype: ``"pdf"`` or ``"html"`` 

997 """ 

998 contact_request = get_contact_request(request, contact_request_id) 

999 html = contact_request.get_approval_letter_html() 

1000 return serve_html_or_pdf( 

1001 html, viewtype, ethics_doccode=EthicsInfo.LTR_D_R_APPROVAL 

1002 ) 

1003 

1004 

1005@user_passes_test(is_developer) 

1006def draft_withdrawal_letter( 

1007 request: HttpRequest, contact_request_id: str, viewtype: str 

1008) -> HttpResponse: 

1009 """ 

1010 Developer view: draft letter to researcher, withdrawing permission. 

1011 

1012 Args: 

1013 request: the :class:`django.http.request.HttpRequest` 

1014 contact_request_id: PK for 

1015 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

1016 viewtype: ``"pdf"`` or ``"html"`` 

1017 """ 

1018 contact_request = get_contact_request(request, contact_request_id) 

1019 html = contact_request.get_withdrawal_letter_html() 

1020 return serve_html_or_pdf( 

1021 html, viewtype, ethics_doccode=EthicsInfo.LTR_D_R_WITHDRAWAL 

1022 ) 

1023 

1024 

1025@user_passes_test(is_developer) 

1026def draft_first_traffic_light_letter( 

1027 request: HttpRequest, patient_lookup_id: str, viewtype: str 

1028) -> HttpResponse: 

1029 """ 

1030 Developer view: draft first traffic-light letter to patient. 

1031 

1032 Args: 

1033 request: the :class:`django.http.request.HttpRequest` 

1034 patient_lookup_id: PK for 

1035 :class:`crate_anon.crateweb.consent.models.PatientLookup` 

1036 viewtype: ``"pdf"`` or ``"html"`` 

1037 """ 

1038 patient_lookup = get_patient_lookup(request, patient_lookup_id) 

1039 html = patient_lookup.get_first_traffic_light_letter_html() 

1040 return serve_html_or_pdf( 

1041 html, 

1042 viewtype, 

1043 ethics_doccode=EthicsInfo.LTR_PT_FIRST_TRAFFIC_LIGHT, 

1044 ) 

1045 

1046 

1047@user_passes_test(is_developer) 

1048def draft_confirm_traffic_light_letter( 

1049 request: HttpRequest, consent_mode_id: str, viewtype: str 

1050) -> HttpResponse: 

1051 """ 

1052 Developer view: draft letter to patient confirming traffic-light decision. 

1053 

1054 Args: 

1055 request: the :class:`django.http.request.HttpRequest` 

1056 consent_mode_id: PK for 

1057 :class:`crate_anon.crateweb.consent.models.ConsentMode` 

1058 viewtype: ``"pdf"`` or ``"html"`` 

1059 """ 

1060 consent_mode = get_consent_mode(request, consent_mode_id) 

1061 patient_lookup_override = ( 

1062 make_dummy_objects(request, consent_mode_id).patient_lookup 

1063 if consent_mode_id in TEST_ID_STRINGS 

1064 else None 

1065 ) 

1066 html = consent_mode.get_confirm_traffic_to_patient_letter_html( 

1067 patient_lookup_override=patient_lookup_override 

1068 ) 

1069 return serve_html_or_pdf( 

1070 html, viewtype, ethics_doccode=EthicsInfo.LTR_D_P_CONFIRM_TRAFFIC 

1071 ) 

1072 

1073 

1074@user_passes_test(is_developer) 

1075def draft_traffic_light_decision_form( 

1076 request: HttpRequest, patient_lookup_id: str, viewtype: str 

1077) -> HttpResponse: 

1078 """ 

1079 Developer view: draft traffic-light decision form. 

1080 

1081 Args: 

1082 request: the :class:`django.http.request.HttpRequest` 

1083 patient_lookup_id: PK for 

1084 :class:`crate_anon.crateweb.consent.models.PatientLookup` 

1085 viewtype: ``"pdf"`` or ``"html"`` 

1086 """ 

1087 patient_lookup = get_patient_lookup(request, patient_lookup_id) 

1088 html = patient_lookup.get_traffic_light_decision_form() 

1089 return serve_html_or_pdf( 

1090 html, viewtype, ethics_doccode=EthicsInfo.FORM_TRAFFIC_PERSONALIZED 

1091 ) 

1092 

1093 

1094@user_passes_test(is_developer) 

1095def draft_traffic_light_decision_form_generic( 

1096 request: HttpRequest, viewtype: str 

1097) -> HttpResponse: 

1098 """ 

1099 Developer view: draft traffic-light decision form, in the generic version 

1100 with no patient details. 

1101 

1102 Args: 

1103 request: the :class:`django.http.request.HttpRequest` 

1104 viewtype: ``"pdf"`` or ``"html"`` 

1105 """ 

1106 patient_lookup = PatientLookup() 

1107 html = patient_lookup.get_traffic_light_decision_form(generic=True) 

1108 return serve_html_or_pdf( 

1109 html, viewtype, ethics_doccode=EthicsInfo.FORM_TRAFFIC_GENERIC 

1110 ) 

1111 

1112 

1113@user_passes_test(is_developer) 

1114def draft_letter_clinician_to_pt_re_study( 

1115 request: HttpRequest, contact_request_id: str, viewtype: str 

1116) -> HttpResponse: 

1117 """ 

1118 Developer view: draft letter from clinician to patient, offering a study. 

1119 

1120 Args: 

1121 request: the :class:`django.http.request.HttpRequest` 

1122 contact_request_id: PK for 

1123 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

1124 viewtype: ``"pdf"`` or ``"html"`` 

1125 """ 

1126 contact_request = get_contact_request(request, contact_request_id) 

1127 html = contact_request.get_letter_clinician_to_pt_re_study() 

1128 return serve_html_or_pdf( 

1129 html, viewtype, ethics_doccode=EthicsInfo.LTR_C_P_STUDY 

1130 ) 

1131 

1132 

1133@user_passes_test(is_developer) 

1134def decision_form_to_pt_re_study( 

1135 request: HttpRequest, contact_request_id: str, viewtype: str 

1136) -> HttpResponse: 

1137 """ 

1138 Developer view: decision form to patient about a study. 

1139 

1140 Args: 

1141 request: the :class:`django.http.request.HttpRequest` 

1142 contact_request_id: PK for 

1143 :class:`crate_anon.crateweb.consent.models.ContactRequest` 

1144 viewtype: ``"pdf"`` or ``"html"`` 

1145 """ 

1146 contact_request = get_contact_request(request, contact_request_id) 

1147 html = contact_request.get_decision_form_to_pt_re_study() 

1148 return serve_html_or_pdf( 

1149 html, viewtype, ethics_doccode=EthicsInfo.FORM_STUDY 

1150 ) 

1151 

1152 

1153@user_passes_test(is_developer) 

1154def draft_researcher_cover_letter( 

1155 request: HttpRequest, viewtype: str 

1156) -> HttpResponse: 

1157 """ 

1158 Developer view: decision form to patient about a study. 

1159 

1160 Args: 

1161 request: the :class:`django.http.request.HttpRequest` 

1162 viewtype: ``"pdf"`` or ``"html"`` 

1163 """ 

1164 context = { 

1165 "rdbm_address_str": ", ".join(settings.RDBM_ADDRESS), 

1166 "settings": settings, 

1167 } 

1168 html = render_pdf_html_to_string( 

1169 "letter_researcher_to_patient_cover_letter_template.html", 

1170 context, 

1171 patient=True, 

1172 ) 

1173 return serve_html_or_pdf( 

1174 html, 

1175 viewtype, 

1176 ethics_doccode=EthicsInfo.LTR_R_P_STUDY_TEMPLATE, 

1177 ) 

1178 

1179 

1180# ----------------------------------------------------------------------------- 

1181# Reports for the superuser 

1182# ----------------------------------------------------------------------------- 

1183 

1184 

1185@user_passes_test(is_superuser) 

1186def charity_report(request: HttpRequest) -> HttpResponse: 

1187 """ 

1188 Show a summary of charity payments (triggered in response to clinicians 

1189 answering requests). 

1190 

1191 Args: 

1192 request: the :class:`django.http.request.HttpRequest` 

1193 """ 

1194 responses = ClinicianResponse.objects.filter(charity_amount_due__gt=0) 

1195 payments = CharityPaymentRecord.objects.all() 

1196 total_due = sum([x.charity_amount_due for x in responses]) 

1197 total_paid = sum([x.amount for x in payments]) 

1198 outstanding = total_due - total_paid 

1199 return render( 

1200 request, 

1201 "charity_report.html", 

1202 { 

1203 "responses": responses, 

1204 "payments": payments, 

1205 "total_due": total_due, 

1206 "total_paid": total_paid, 

1207 "outstanding": outstanding, 

1208 }, 

1209 ) 

1210 

1211 

1212@user_passes_test(is_superuser) 

1213def exclusion_report(request: HttpRequest) -> HttpResponse: 

1214 """ 

1215 Show NHS numbers of patients to exclude from the anonymised database 

1216 entirely. 

1217 

1218 Args: 

1219 request: the :class:`django.http.request.HttpRequest` 

1220 """ 

1221 consent_modes = ConsentMode.objects.filter( 

1222 current=True, exclude_entirely=True 

1223 ) 

1224 return render( 

1225 request, 

1226 "exclusion_report.html", 

1227 { 

1228 "consent_modes": consent_modes, 

1229 }, 

1230 ) 

1231 

1232 

1233# ----------------------------------------------------------------------------- 

1234# E-mail testing 

1235# ----------------------------------------------------------------------------- 

1236 

1237 

1238@user_passes_test(is_superuser) 

1239def test_email_rdbm(request: HttpRequest) -> HttpResponse: 

1240 """ 

1241 Tests the backend system (Celery etc.) by sending an e-mail to the RDBM. 

1242 

1243 Args: 

1244 request: the :class:`django.http.request.HttpRequest` 

1245 """ 

1246 test_email_rdbm_task.delay() 

1247 return render( 

1248 request, 

1249 "test_email_rdbm_ack.html", 

1250 { 

1251 "settings": settings, 

1252 }, 

1253 )