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
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-27 10:34 -0500
1"""
2crate_anon/crateweb/consent/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===============================================================================
26**Django views for the consent-to-contact system.**
28"""
30import logging
31import mimetypes
32from typing import List
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
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
93log = logging.getLogger(__name__)
96# =============================================================================
97# Additional validators
98# =============================================================================
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.
107 Args:class
108 user: current Django user object
109 email: :class:`crate_anon.crateweb.consent.models.Email`
111 Raises:
112 :exc:`django.core.exceptions.PermissionDenied` on failure
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")
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.
138 Args:
139 user: current Django user object
140 letter: :class:`crate_anon.crateweb.consent.models.Letter`
142 Raises:
143 :exc:`django.core.exceptions.PermissionDenied` on failure
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")
159# =============================================================================
160# Fetchers
161# =============================================================================
164def get_contact_request(
165 request: HttpRequest, contact_request_id: str
166) -> ContactRequest:
167 """
168 Return the specified contact request.
170 Args:
171 request: the :class:`django.http.request.HttpRequest`
172 contact_request_id: PK
174 Returns:
175 :class:`crate_anon.crateweb.consent.models.ContactRequest`
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
189def get_patient_lookup(
190 request: HttpRequest, patient_lookup_id: str
191) -> PatientLookup:
192 """
193 Return the specified patient lookup.
195 Args:
196 request: the :class:`django.http.request.HttpRequest`
197 patient_lookup_id: PK
199 Returns:
200 :class:`crate_anon.crateweb.consent.models.PatientLookup`
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
212def get_consent_mode(
213 request: HttpRequest, consent_mode_id: str
214) -> ConsentMode:
215 """
216 Return the specified consent mode.
218 Args:
219 request: the :class:`django.http.request.HttpRequest`
220 consent_mode_id: PK
222 Returns:
223 :class:`crate_anon.crateweb.consent.models.ConsentMode`
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
235# =============================================================================
236# Views
237# =============================================================================
240# noinspection PyUnusedLocal
241def study_details(request: HttpRequest, study_id: str) -> HttpResponseBase:
242 """
243 View details of a study.
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 )
263study_details.login_required = False
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.
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 )
287study_form.login_required = False
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.
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 )
320study_pack.login_required = False
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.
331 Superuser access function, used for admin interface only.
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)
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.
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 )
358def view_email_html(request: HttpRequest, email_id: str) -> HttpResponse:
359 """
360 View the HTML for an e-mail.
362 Args:
363 request: the :class:`django.http.request.HttpRequest`
364 email_id: PK for :class:`crate_anon.crateweb.consent.models.Email`
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)
375def view_email_attachment(
376 request: HttpRequest, attachment_id: str
377) -> HttpResponseBase:
378 """
379 View the HTML for an e-mail.
381 Args:
382 request: the :class:`django.http.request.HttpRequest`
383 attachment_id: PK for
384 :class:`crate_anon.crateweb.consent.models.EmailAttachment`
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 )
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.
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})
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.
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})
457# noinspection PyUnusedLocal
458def view_leaflet(request: HttpRequest, leaflet_name: str) -> HttpResponseBase:
459 """
460 Views a system-wide leaflet.
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 )
476view_leaflet.login_required = False
479def view_letter(request: HttpRequest, letter_id: str) -> HttpResponseBase:
480 """
481 View a letter (as a PDF).
483 Args:
484 request: the :class:`django.http.request.HttpRequest`
485 letter_id: PK for :class:`crate_anon.crateweb.consent.models.Letter`
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 )
500def submit_contact_request(request: HttpRequest) -> HttpResponse:
501 """
502 Submits a contact request for a study, potentially for multiple patients.
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 )
529 study = form.cleaned_data["study"]
530 request_direct_approach = form.cleaned_data["request_direct_approach"]
531 contact_requests = [] # type: List[ContactRequest]
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 )
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 )
576def clinician_initiated_contact_request(request: HttpRequest) -> HttpResponse:
577 """
578 For clinicians to request that their patient is contacted about a study.
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]
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 )
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 )
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 )
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)
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 )
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.
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 )
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.
742 **REC DOCUMENTS 09, 11, 13 (B): Web form for clinicians to respond with.**
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
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)
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 )
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 )
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 )
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 )
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 )
879clinician_response_view.login_required = False
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.
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 )
924clinician_pack.login_required = False
927# -----------------------------------------------------------------------------
928# Draft e-mails
929# -----------------------------------------------------------------------------
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.
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))
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.
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())
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.
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())
980# -----------------------------------------------------------------------------
981# Draft letters
982# -----------------------------------------------------------------------------
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.
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 )
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.
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 )
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.
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 )
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.
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 )
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.
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 )
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.
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 )
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.
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 )
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.
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 )
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.
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 )
1180# -----------------------------------------------------------------------------
1181# Reports for the superuser
1182# -----------------------------------------------------------------------------
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).
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 )
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.
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 )
1233# -----------------------------------------------------------------------------
1234# E-mail testing
1235# -----------------------------------------------------------------------------
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.
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 )