Source code for crate_anon.crateweb.consent.views

#!/usr/bin/env python
# crate_anon/crateweb/consent/views.py

"""
===============================================================================

    Copyright (C) 2015-2018 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of CRATE.

    CRATE is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CRATE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CRATE. If not, see <http://www.gnu.org/licenses/>.

===============================================================================
"""

import logging
import mimetypes
from typing import List, Optional

from cardinal_pythonlib.django.serve import (
    serve_buffer,
    serve_concatenated_pdf_from_disk,
    serve_file,
)
from cardinal_pythonlib.nhs import generate_random_nhs_number
from django.conf import settings
from django.contrib.auth.decorators import user_passes_test
from django.db import transaction
from django.http import HttpResponse, Http404, HttpResponseForbidden
from django.http.response import HttpResponseBase
from django.http.request import HttpRequest
from django.shortcuts import get_object_or_404, render

from crate_anon.common.contenttypes import ContentType
from crate_anon.crateweb.consent.forms import (
    ClinicianResponseForm,
    SingleNhsNumberForm,
    SuperuserSubmitContactRequestForm,
    ResearcherSubmitContactRequestForm,
)
from crate_anon.crateweb.consent.lookup import lookup_consent, lookup_patient
from crate_anon.crateweb.consent.models import (
    CharityPaymentRecord,
    ClinicianResponse,
    ConsentMode,
    ContactRequest,
    Email,
    EmailAttachment,
    Leaflet,
    Letter,
    make_dummy_objects,
    PatientLookup,
    Study,
    TEST_ID_STR,
)
from crate_anon.crateweb.consent.storage import privatestorage
from crate_anon.crateweb.consent.tasks import (
    finalize_clinician_response,
    test_email_rdbm_task,
)
from crate_anon.crateweb.consent.utils import days_to_years
from crate_anon.crateweb.core.utils import (
    is_developer,
    is_superuser,
)
from crate_anon.crateweb.extra.pdf import serve_html_or_pdf
from crate_anon.crateweb.research.research_db_info import research_database_info  # noqa

log = logging.getLogger(__name__)


# =============================================================================
# Additional validators
# =============================================================================

def validate_email_request(user: settings.AUTH_USER_MODEL,
                           email: Email) -> Optional[HttpResponseForbidden]:
    if user.profile.is_developer:
        return  # the developer sees all
    if email.to_researcher:
        # e-mails to clinicians/patients may be restricted
        if user.is_superuser:
            return  # RDBM can see any e-mail to a researcher
        studies = Study.filter_studies_for_researcher(Study.objects.all(),
                                                      user)
        if email.study in studies:
            return  # this e-mail belongs to this researcher
    elif email.to_patient:
        if user.is_superuser:
            return  # RDBM can see any e-mail to a patient
    return HttpResponseForbidden("Not authorized")


def validate_letter_request(user: settings.AUTH_USER_MODEL,
                            letter: Letter) -> Optional[HttpResponseForbidden]:
    if user.profile.is_developer:
        return  # the developer sees all
    if user.is_superuser:
        return  # RDBM can see any letters generated
    if letter.to_researcher:
        studies = Study.filter_studies_for_researcher(Study.objects.all(),
                                                      user)
        if letter.study in studies:
            return  # this e-mail belongs to this researcher
    return HttpResponseForbidden("Not authorized")


# =============================================================================
# Fetchers
# =============================================================================

def get_contact_request(request: HttpRequest,
                        contact_request_id: str) -> ContactRequest:
    if contact_request_id == TEST_ID_STR:
        return make_dummy_objects(request).contact_request
    return get_object_or_404(
        ContactRequest, id=contact_request_id)  # type: ContactRequest


def get_patient_lookup(request: HttpRequest,
                       patient_lookup_id: str) -> PatientLookup:
    if patient_lookup_id == TEST_ID_STR:
        return make_dummy_objects(request).patient_lookup
    return get_object_or_404(
        PatientLookup, id=patient_lookup_id)  # type: PatientLookup


def get_consent_mode(request: HttpRequest,
                     consent_mode_id: str) -> ConsentMode:
    if consent_mode_id == TEST_ID_STR:
        return make_dummy_objects(request).consent_mode
    return get_object_or_404(
        ConsentMode, id=consent_mode_id)  # type: ConsentMode


# =============================================================================
# Views
# =============================================================================

# noinspection PyUnusedLocal
def study_details(request: HttpRequest, study_id: str) -> HttpResponseBase:
    if study_id == TEST_ID_STR:
        study = make_dummy_objects(request).study
    else:
        study = get_object_or_404(Study, pk=study_id)  # type: Study
    if not study.study_details_pdf:
        raise Http404("No details")
    return serve_file(study.study_details_pdf.path,
                      content_type=ContentType.PDF,
                      as_inline=True)


study_details.login_required = False


# noinspection PyUnusedLocal
def study_form(request: HttpRequest, study_id: str) -> HttpResponseBase:
    study = get_object_or_404(Study, pk=study_id)  # type: Study
    if not study.subject_form_template_pdf:
        raise Http404("No study form for clinicians to complete")
    return serve_file(study.subject_form_template_pdf.path,
                      content_type=ContentType.PDF,
                      as_inline=True)


study_form.login_required = False


# noinspection PyUnusedLocal
def study_pack(request: HttpRequest, study_id: str) -> HttpResponseBase:
    study = get_object_or_404(Study, pk=study_id)  # type: Study
    filenames = filter(None, [
        study.study_details_pdf.path
        if study.study_details_pdf else None,
        study.subject_form_template_pdf.path
        if study.subject_form_template_pdf else None,
    ])
    if not filenames:
        raise Http404("No leaflets")
    return serve_concatenated_pdf_from_disk(
        filenames,
        offered_filename="study_{}_pack.pdf".format(study_id)
    )


study_pack.login_required = False


# noinspection PyUnusedLocal
[docs]@user_passes_test(is_superuser) def download_privatestorage(request: HttpRequest, filename: str) -> HttpResponseBase: """Superuser access function, used for admin interface only.""" fullpath = privatestorage.path(filename) content_type = mimetypes.guess_type(filename, strict=False)[0] # ... guess_type returns a (content_type, encoding) tuple return serve_file(fullpath, content_type=content_type, as_inline=True)
@user_passes_test(is_developer) def generate_fake_nhs(request: HttpRequest, n: str = 10) -> HttpResponse: nhs_numbers = [generate_random_nhs_number() for _ in range(int(n))] return render(request, 'generate_fake_nhs.html', { 'nhs_numbers': nhs_numbers }) def view_email_html(request: HttpRequest, email_id: str) -> HttpResponse: email = get_object_or_404(Email, pk=email_id) # type: Email # noinspection PyTypeChecker validate_email_request(request.user, email) return HttpResponse(email.msg_html) def view_email_attachment(request: HttpRequest, attachment_id: str) -> HttpResponseBase: attachment = get_object_or_404(EmailAttachment, pk=attachment_id) # type: EmailAttachment # noqa # noinspection PyTypeChecker validate_email_request(request.user, attachment.email) if not attachment.file: raise Http404("Attachment missing") return serve_file(attachment.file.path, content_type=attachment.content_type, as_inline=True) @user_passes_test(is_developer) def test_patient_lookup(request: HttpRequest) -> HttpResponse: form = SingleNhsNumberForm( request.POST if request.method == 'POST' else None) if form.is_valid(): lookup = lookup_patient(nhs_number=form.cleaned_data['nhs_number'], save=False) # Don't use a Form. https://code.djangoproject.com/ticket/17031 return render(request, 'patient_lookup_result.html', {'lookup': lookup}) return render(request, 'patient_lookup_get_nhs.html', {'form': form}) @user_passes_test(is_developer) def test_consent_lookup(request: HttpRequest) -> HttpResponse: form = SingleNhsNumberForm( request.POST if request.method == 'POST' else None) if form.is_valid(): decisions = [] # type: List[str] nhs_number = form.cleaned_data['nhs_number'] consent_mode = lookup_consent(nhs_number=nhs_number, decisions=decisions) # Don't use a Form. https://code.djangoproject.com/ticket/17031 return render( request, 'consent_lookup_result.html', { 'consent_mode': consent_mode, 'nhs_number': nhs_number, 'decisions': decisions, } ) return render(request, 'consent_lookup_get_nhs.html', {'form': form}) # noinspection PyUnusedLocal def view_leaflet(request: HttpRequest, leaflet_name: str) -> HttpResponseBase: leaflet = get_object_or_404(Leaflet, name=leaflet_name) # type: Leaflet if not leaflet.pdf: raise Http404("Missing leaflet") return serve_file(leaflet.pdf.path, content_type=ContentType.PDF, as_inline=True) view_leaflet.login_required = False def view_letter(request: HttpRequest, letter_id: str) -> HttpResponseBase: letter = get_object_or_404(Letter, pk=letter_id) # type: Letter # noinspection PyTypeChecker validate_letter_request(request.user, letter) if not letter.pdf: raise Http404("Missing letter") return serve_file(letter.pdf.path, content_type=ContentType.PDF, as_inline=True) def submit_contact_request(request: HttpRequest) -> HttpResponse: dbinfo = research_database_info.dbinfo_for_contact_lookup if request.user.is_superuser: form = SuperuserSubmitContactRequestForm( request.POST if request.method == 'POST' else None, dbinfo=dbinfo) else: form = ResearcherSubmitContactRequestForm( user=request.user, data=request.POST if request.method == 'POST' else None, dbinfo=dbinfo) if not form.is_valid(): return render(request, 'contact_request_submit.html', { 'db_description': dbinfo.description, 'form': form, }) study = form.cleaned_data['study'] request_direct_approach = form.cleaned_data['request_direct_approach'] contact_requests = [] # NHS numbers if request.user.is_superuser: for nhs_number in form.cleaned_data['nhs_numbers']: contact_requests.append( ContactRequest.create( request=request, study=study, request_direct_approach=request_direct_approach, lookup_nhs_number=nhs_number)) # RIDs for rid in form.cleaned_data['rids']: contact_requests.append( ContactRequest.create( request=request, study=study, request_direct_approach=request_direct_approach, lookup_rid=rid)) # MRIDs for mrid in form.cleaned_data['mrids']: contact_requests.append( ContactRequest.create( request=request, study=study, request_direct_approach=request_direct_approach, lookup_mrid=mrid)) # Show results. # Don't use a Form. https://code.djangoproject.com/ticket/17031 return render(request, 'contact_request_result.html', { 'contact_requests': contact_requests, }) def finalize_clinician_response_in_background( request: HttpRequest, clinician_response: ClinicianResponse) -> HttpResponse: clinician_response.finalize_a() # first part of processing transaction.on_commit( lambda: finalize_clinician_response.delay(clinician_response.id) ) # Asynchronous return render(request, 'clinician_confirm_response.html', { 'clinician_response': clinician_response, })
[docs]def clinician_response_view(request: HttpRequest, clinician_response_id: str) -> HttpResponse: """ REC DOCUMENTS 09, 11, 13 (B): Web form for clinicians to respond with """ if clinician_response_id == TEST_ID_STR: dummies = make_dummy_objects(request) clinician_response = dummies.clinician_response contact_request = dummies.contact_request study = dummies.study patient_lookup = dummies.patient_lookup consent_mode = dummies.consent_mode else: clinician_response = get_object_or_404( ClinicianResponse, pk=clinician_response_id) # type: ClinicianResponse # noqa contact_request = clinician_response.contact_request study = contact_request.study patient_lookup = contact_request.patient_lookup consent_mode = contact_request.consent_mode # Build form. # - We have an existing clinician_response and wish to modify it # (potentially). # - If the clinician is responding to an e-mail, they will be passing # a couple of parameters (including the token) via GET query parameters. # If they're clicking "Submit", they'll be using POST. if request.method == 'GET': from_email = True clinician_response.response_route = ClinicianResponse.ROUTE_EMAIL data = request.GET else: from_email = False clinician_response.response_route = ClinicianResponse.ROUTE_WEB data = request.POST form = ClinicianResponseForm(instance=clinician_response, data=data) # log.debug("Form data: {}".format(form.data)) # Token valid? Check raw data. Say goodbye otherwise. # - The raw data in the form is not influenced by the form's instance. if form.data['token'] != clinician_response.token: # log.critical("Token from user: {!r}".format(form.data['token'])) # log.critical("Original token: {!r}".format(clinician_response.token)) return HttpResponseForbidden( "Not authorized. The token you passed doesn't match the one you " "were sent.") # Already responded? if clinician_response.responded: passed_to_pt = (clinician_response.response == ClinicianResponse.RESPONSE_A) return render(request, 'clinician_already_responded.html', { 'clinician_response': clinician_response, 'consent_mode': consent_mode, 'contact_request': contact_request, 'Leaflet': Leaflet, 'passed_to_pt': passed_to_pt, 'patient_lookup': patient_lookup, 'settings': settings, 'study': study, }) # Is the clinician saying yes or no (direct from e-mail)? if (from_email and form.data['email_choice'] in ( ClinicianResponse.EMAIL_CHOICE_Y, ClinicianResponse.EMAIL_CHOICE_N)): # We can't use form.save() as the data may not validate. # It won't validate because the response/clinician name is blank. # We can't write to the form directly. So... clinician_response.email_choice = form.data['email_choice'] if clinician_response.email_choice == ClinicianResponse.EMAIL_CHOICE_Y: # Ask RDBM to do the work clinician_response.response = ClinicianResponse.RESPONSE_R else: # Veto on clinical grounds clinician_response.response = ClinicianResponse.RESPONSE_B return finalize_clinician_response_in_background(request, clinician_response) # Has the clinician made a decision via the web form? if form.is_valid(): clinician_response = form.save(commit=False) # return unsaved instance return finalize_clinician_response_in_background(request, clinician_response) # If we get here, we need to offer the form up for editing, # and mark it as a web response. clinician_involvement_requested = ( contact_request.clinician_involvement == ContactRequest.CLINICIAN_INVOLVEMENT_REQUESTED) clinician_involvement_required_yellow = ( contact_request.clinician_involvement == ContactRequest.CLINICIAN_INVOLVEMENT_REQUIRED_YELLOW) clinician_involvement_required_unknown = ( contact_request.clinician_involvement == ContactRequest.CLINICIAN_INVOLVEMENT_REQUIRED_UNKNOWN) extra_form = contact_request.is_extra_form() return render(request, 'clinician_response.html', { 'clinician_response': clinician_response, 'ClinicianResponse': ClinicianResponse, 'consent_mode': consent_mode, 'contact_request': contact_request, 'Leaflet': Leaflet, 'patient_lookup': patient_lookup, 'settings': settings, 'study': study, 'form': form, 'clinician_involvement_requested': clinician_involvement_requested, 'clinician_involvement_required_yellow': clinician_involvement_required_yellow, 'clinician_involvement_required_unknown': clinician_involvement_required_unknown, 'option_c_available': clinician_involvement_requested, 'option_r_available': not extra_form, 'extra_form': extra_form, 'unknown_consent_mode': contact_request.is_consent_mode_unknown(), 'permitted_to_contact_discharged_patients_for_n_days': settings.PERMITTED_TO_CONTACT_DISCHARGED_PATIENTS_FOR_N_DAYS, 'permitted_to_contact_discharged_patients_for_n_years': days_to_years( settings.PERMITTED_TO_CONTACT_DISCHARGED_PATIENTS_FOR_N_DAYS), })
clinician_response_view.login_required = False # noinspection PyUnusedLocal def clinician_pack(request: HttpRequest, clinician_response_id: str, token: str) -> HttpResponse: if clinician_response_id == TEST_ID_STR: dummies = make_dummy_objects(request) clinician_response = dummies.clinician_response contact_request = dummies.contact_request else: clinician_response = get_object_or_404( ClinicianResponse, pk=clinician_response_id) # type: ClinicianResponse # noqa contact_request = clinician_response.contact_request # Check token authentication if token != clinician_response.token: return HttpResponseForbidden( "Not authorized. The token you passed doesn't match the one you " "were sent.") # Build and serve pdf = contact_request.get_clinician_pack_pdf() offered_filename = "clinician_pack_{}.pdf".format(clinician_response_id) return serve_buffer(pdf, offered_filename=offered_filename, content_type=ContentType.PDF, as_attachment=False, as_inline=True) clinician_pack.login_required = False # ----------------------------------------------------------------------------- # Draft e-mails # ----------------------------------------------------------------------------- @user_passes_test(is_developer) def draft_clinician_email(request: HttpRequest, contact_request_id: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) return HttpResponse( contact_request.get_clinician_email_html(save=False) ) @user_passes_test(is_developer) def draft_approval_email(request: HttpRequest, contact_request_id: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) return HttpResponse(contact_request.get_approval_email_html()) @user_passes_test(is_developer) def draft_withdrawal_email(request: HttpRequest, contact_request_id: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) return HttpResponse(contact_request.get_withdrawal_email_html()) # ----------------------------------------------------------------------------- # Draft letters # ----------------------------------------------------------------------------- @user_passes_test(is_developer) def draft_approval_letter(request: HttpRequest, contact_request_id: str, viewtype: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) html = contact_request.get_approval_letter_html() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_developer) def draft_withdrawal_letter(request: HttpRequest, contact_request_id: str, viewtype: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) html = contact_request.get_withdrawal_letter_html() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_developer) def draft_first_traffic_light_letter(request: HttpRequest, patient_lookup_id: str, viewtype: str) -> HttpResponse: patient_lookup = get_patient_lookup(request, patient_lookup_id) html = patient_lookup.get_first_traffic_light_letter_html() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_developer) def draft_confirm_traffic_light_letter(request: HttpRequest, consent_mode_id: str, viewtype: str) -> HttpResponse: consent_mode = get_consent_mode(request, consent_mode_id) html = consent_mode.get_confirm_traffic_to_patient_letter_html() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_developer) def draft_traffic_light_decision_form(request: HttpRequest, patient_lookup_id: str, viewtype: str) -> HttpResponse: patient_lookup = get_patient_lookup(request, patient_lookup_id) html = patient_lookup.get_traffic_light_decision_form() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_developer) def draft_letter_clinician_to_pt_re_study(request: HttpRequest, contact_request_id: str, viewtype: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) html = contact_request.get_letter_clinician_to_pt_re_study() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_developer) def decision_form_to_pt_re_study(request: HttpRequest, contact_request_id: str, viewtype: str) -> HttpResponse: contact_request = get_contact_request(request, contact_request_id) html = contact_request.get_decision_form_to_pt_re_study() return serve_html_or_pdf(html, viewtype) @user_passes_test(is_superuser) def charity_report(request: HttpRequest) -> HttpResponse: responses = ClinicianResponse.objects.filter(charity_amount_due__gt=0) payments = CharityPaymentRecord.objects.all() total_due = sum([x.charity_amount_due for x in responses]) total_paid = sum([x.amount for x in payments]) outstanding = total_due - total_paid return render(request, 'charity_report.html', { 'responses': responses, 'payments': payments, 'total_due': total_due, 'total_paid': total_paid, 'outstanding': outstanding, }) @user_passes_test(is_superuser) def exclusion_report(request: HttpRequest) -> HttpResponse: consent_modes = ConsentMode.objects.filter(current=True, exclude_entirely=True) return render(request, 'exclusion_report.html', { 'consent_modes': consent_modes, }) @user_passes_test(is_superuser) def test_email_rdbm(request: HttpRequest) -> HttpResponse: test_email_rdbm_task.delay() return render(request, 'test_email_rdbm_ack.html', { 'settings': settings, })