From 181a417d488ef56711ddfcfabf8f8ef33b75dd74 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Fri, 24 Jan 2025 20:03:03 +0000 Subject: [PATCH 01/24] by the by add lead organisation to context processor and template titles --- project/npda/context_processors.py | 2 ++ project/npda/general_functions/session.py | 3 ++- .../npda/templates/dashboard/dashboard_base.html | 14 +++----------- .../npda/templates/partials/npda_user_table.html | 2 +- .../templates/partials/submission_history.html | 2 +- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/project/npda/context_processors.py b/project/npda/context_processors.py index c3c691ee..2f44044e 100644 --- a/project/npda/context_processors.py +++ b/project/npda/context_processors.py @@ -2,6 +2,7 @@ from project.npda.general_functions import get_current_audit_year + def session_data(request): return { "can_complete_questionnaire": request.session.get( @@ -9,6 +10,7 @@ def session_data(request): ), "can_upload_csv": request.session.get("can_upload_csv", False), "pz_code": request.session.get("pz_code", None), + "lead_organisation": request.session.get("lead_organisation", None), "requested_audit_year": request.session.get("requested_audit_year", None), "audit_years": request.session.get("audit_years", []), } diff --git a/project/npda/general_functions/session.py b/project/npda/general_functions/session.py index d9bba47d..4f9a53cb 100644 --- a/project/npda/general_functions/session.py +++ b/project/npda/general_functions/session.py @@ -9,7 +9,7 @@ organisations_adapter, get_audit_period_for_date, get_current_audit_year, - SUPPORTED_AUDIT_YEARS + SUPPORTED_AUDIT_YEARS, ) logger = logging.getLogger(__name__) @@ -61,6 +61,7 @@ def create_session_object(user): session = { "pz_code": pz_code, + "lead_organisation": primary_organisation.paediatric_diabetes_unit.lead_organisation_name, "pdu_choices": list(pdu_choices), "can_upload_csv": can_upload_csv, "can_complete_questionnaire": can_complete_questionnaire, diff --git a/project/npda/templates/dashboard/dashboard_base.html b/project/npda/templates/dashboard/dashboard_base.html index 4ecac236..8ba53fc4 100644 --- a/project/npda/templates/dashboard/dashboard_base.html +++ b/project/npda/templates/dashboard/dashboard_base.html @@ -7,8 +7,8 @@ hx-trigger="dashboard from:body" hx-swap="innerHTML">
-

- PDU Dashboard for {{ pdu_lead_organisation.name }} ({{ pdu_object.pz_code }}) +

+ PDU Dashboard for {{ lead_organisation }} ({{ pdu_object.pz_code }})

Calculated at {{ kpi_calculations_object.calculation_datetime }} @@ -20,38 +20,30 @@

{% include 'dashboard/components/cards/pdu_card.html' %} {% include 'dashboard/components/cards/audit_details_card.html' %} -
{% include 'dashboard/components/cards/hba1c_card.html' %} {% include 'dashboard/components/cards/admissions_card.html' %}
- -

Unit Report

- {% include 'dashboard/components/cards/patient_characteristics_card.html' %} -
{% include 'dashboard/components/cards/treatment_regimen_card.html' %} {% include 'dashboard/components/cards/glucose_monitoring_card.html' %}
-
{% include 'dashboard/components/cards/total_eligible_patients_card.html' %} {% include 'dashboard/components/cards/hcl_use_card.html' %} -
-
@@ -76,5 +68,5 @@

Unit Report

Patient-Level Report

- {% include 'dashboard/pt_level_report_table_partial.html' with text=default_pt_level_menu_text selected=default_pt_level_menu_tab_selected highlight=default_highlight table_data=default_table_data %} + {% include 'dashboard/pt_level_report_table_partial.html' with text=default_pt_level_menu_text selected=default_pt_level_menu_tab_selected highlight=default_highlight table_data=default_table_data %}
diff --git a/project/npda/templates/partials/npda_user_table.html b/project/npda/templates/partials/npda_user_table.html index 9ad6b926..c6faedeb 100644 --- a/project/npda/templates/partials/npda_user_table.html +++ b/project/npda/templates/partials/npda_user_table.html @@ -1,6 +1,6 @@ {% load npda_tags %}

- NPDA Users at {{ chosen_pdu }} + NPDA Users at {{ lead_organisation }} ({{ chosen_pdu }})

{% if npdauser_list %} diff --git a/project/npda/templates/partials/submission_history.html b/project/npda/templates/partials/submission_history.html index c70eb329..020f691d 100644 --- a/project/npda/templates/partials/submission_history.html +++ b/project/npda/templates/partials/submission_history.html @@ -5,7 +5,7 @@ {% if request.user.view_preference == 1 %}

- All Submissions for {{ pz_code }} + All Submissions for {{ lead_organisation }} ({{ pz_code }})

{% elif request.user.view_preference == 2 %} From 383d3ba4f6c267d098d3632c98ed0a491cc98f11 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Fri, 24 Jan 2025 20:30:27 +0000 Subject: [PATCH 02/24] update session to return lead organisation on change --- project/npda/general_functions/session.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project/npda/general_functions/session.py b/project/npda/general_functions/session.py index 4f9a53cb..072438c2 100644 --- a/project/npda/general_functions/session.py +++ b/project/npda/general_functions/session.py @@ -82,6 +82,7 @@ def get_new_session_fields(user, pz_code): ret = {} Submission = apps.get_model("npda", "Submission") + PaediatricDiabetesUnit = apps.get_model("npda", "PaediatricDiabetesUnit") can_upload_csv = True can_complete_questionnaire = True @@ -127,6 +128,9 @@ def get_new_session_fields(user, pz_code): can_complete_questionnaire = True ret["pz_code"] = pz_code + ret["lead_organisation"] = PaediatricDiabetesUnit.objects.get( + pz_code=pz_code + ).lead_organisation_name ret["pdu_choices"] = list( organisations_adapter.paediatric_diabetes_units_to_populate_select_field( requesting_user=user, user_instance=None From 83414b7a09c0c6267f7bce59e3a74f6cb3a3793e Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Fri, 24 Jan 2025 23:00:07 +0000 Subject: [PATCH 03/24] add migration and filter for unique identifier in patient filter --- .../0021_alter_patient_options_and_more.py | 58 +++++++++++++++++++ project/npda/models/patient.py | 17 +++++- project/npda/templates/patients.html | 17 +++--- project/npda/views/patient.py | 34 ++++++----- 4 files changed, 101 insertions(+), 25 deletions(-) create mode 100644 project/npda/migrations/0021_alter_patient_options_and_more.py diff --git a/project/npda/migrations/0021_alter_patient_options_and_more.py b/project/npda/migrations/0021_alter_patient_options_and_more.py new file mode 100644 index 00000000..96d5beca --- /dev/null +++ b/project/npda/migrations/0021_alter_patient_options_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 5.1.5 on 2025-01-24 21:22 + +import project.npda.models.custom_validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("npda", "0020_patient_location_bng_patient_location_wgs_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="patient", + options={ + "ordering": ("pk", "nhs_number", "unique_reference_number"), + "permissions": [ + ( + "can_lock_child_patient_data_from_editing", + "Can lock a child's record from editing.", + ), + ( + "can_unlock_child_patient_data_from_editing", + "Can unlock a child's record from editing.", + ), + ( + "can_opt_out_child_from_inclusion_in_audit", + "Can sanction an opt out from participating in the audit. Note all the child's date except NPDA unique identifier are irretrievably deleted.", + ), + ], + "verbose_name": "Patient", + "verbose_name_plural": "Patients", + }, + ), + migrations.AddField( + model_name="patient", + name="unique_reference_number", + field=models.CharField( + blank=True, + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", + max_length=50, + null=True, + verbose_name="Unique Reference Number", + ), + ), + migrations.AlterField( + model_name="patient", + name="nhs_number", + field=models.CharField( + blank=True, + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", + null=True, + validators=[project.npda.models.custom_validators.validate_nhs_number], + verbose_name="NHS Number", + ), + ), + ] diff --git a/project/npda/models/patient.py b/project/npda/models/patient.py index 60ecd3d8..42c3d8c7 100644 --- a/project/npda/models/patient.py +++ b/project/npda/models/patient.py @@ -44,7 +44,21 @@ class Patient(models.Model): """ nhs_number = CharField( # the NHS number for England and Wales - "NHS Number", unique=False, validators=[validate_nhs_number] + "NHS Number", + unique=False, + validators=[validate_nhs_number], + null=True, + blank=True, + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", + ) + + unique_reference_number = CharField( + "Unique Reference Number", + max_length=50, + unique=False, + blank=True, + null=True, + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", ) sex = models.IntegerField("Stated gender", choices=SEX_TYPE, blank=True, null=True) @@ -124,6 +138,7 @@ class Meta: ordering = ( "pk", "nhs_number", + "unique_reference_number", ) permissions = [ CAN_LOCK_CHILD_PATIENT_DATA_FROM_EDITING, diff --git a/project/npda/templates/patients.html b/project/npda/templates/patients.html index 446f0491..bc1c05da 100644 --- a/project/npda/templates/patients.html +++ b/project/npda/templates/patients.html @@ -14,9 +14,10 @@ +
{% if perms.npda.add_patient %} - - - Add patient - + + + Add patient + {% endif %} -
- {% include 'partials/patient_table.html' %} -
+
{% include 'partials/patient_table.html' %}
{% endblock %} diff --git a/project/npda/views/patient.py b/project/npda/views/patient.py index 5b1a442c..8e9a0f4a 100644 --- a/project/npda/views/patient.py +++ b/project/npda/views/patient.py @@ -9,7 +9,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count, Case, When, Max, Q, F -from django.forms import BaseForm, ValidationError +from django.forms import BaseForm from django.http.response import HttpResponse from django.shortcuts import render from django.views.generic.edit import CreateView, UpdateView, DeleteView @@ -20,9 +20,7 @@ # Third party imports import nhs_number -from project.npda.general_functions import ( - organisations_adapter -) +from project.npda.general_functions import organisations_adapter from project.npda.general_functions.quarter_for_date import ( retrieve_quarter_for_date, ) @@ -64,8 +62,8 @@ def get_sort_by(self): # Check we are sorting by a fixed set of fields rather than the full Django __ notation if sort_by_param in ["nhs_number", "index_of_multiple_deprivation_quintile"]: sort_by = sort_by_param - - sort_by = f"-{sort_by}" if sort_param == "desc" else sort_by + + sort_by = f"-{sort_by}" if sort_param == "desc" else sort_by return sort_by @@ -78,15 +76,17 @@ def get_queryset(self): submissions__submission_active=True, submissions__audit_year=self.request.session.get("selected_audit_year"), ) - + # filter by contents of the search bar search = self.request.GET.get("search-input") if search: search = nhs_number.standardise_format(search) or search filtered_patients &= Q( - Q(nhs_number__icontains=search) | Q(pk__icontains=search) + Q(nhs_number__icontains=search) + | Q(pk__icontains=search) + | Q(unique_identifier__icontains=search) ) - + # filter patients to the view preference of the user if self.request.user.view_preference == 1: # PDU view @@ -108,7 +108,9 @@ def get_queryset(self): if sort_by: patient_queryset = patient_queryset.order_by(sort_by) else: - patient_queryset = patient_queryset.order_by("is_valid", "-visit_error_count") + patient_queryset = patient_queryset.order_by( + "is_valid", "-visit_error_count" + ) return patient_queryset @@ -139,20 +141,20 @@ def get_context_data(self, **kwargs): patient.latest_quarter = retrieve_quarter_for_date( patient.most_recent_visit_date ) - + # Highlight the separation between patients with errors and those without # unless we are sorting by a particular field in which case errors appear mixed if not context["sort_by"]: if not patient.is_valid or patient.visit_error_count > 0: if error_count_in_page == 0: patient.is_first_error = True - + error_count_in_page += 1 if patient.is_valid and patient.visit_error_count == 0: if valid_count_in_page == 0: patient.is_first_valid = True - + valid_count_in_page += 1 context["error_count_in_page"] = error_count_in_page @@ -162,9 +164,11 @@ def get_context_data(self, **kwargs): def get(self, request, *args: str, **kwargs) -> HttpResponse: response = super().get(request, *args, **kwargs) - + if request.htmx: - return render(request, "partials/patient_table.html", context=self.get_context_data()) + return render( + request, "partials/patient_table.html", context=self.get_context_data() + ) return response From d9b091629964d11426d14d05b84fb472d9558fe7 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sat, 25 Jan 2025 09:41:03 +0000 Subject: [PATCH 04/24] urn in patient model, refactor uses across forms and templates --- project/npda/admin.py | 3 +- project/npda/forms/patient_form.py | 13 +- project/npda/kpi_class/kpis.py | 384 +++++++++++++----- .../0022_alter_patient_nhs_number_and_more.py | 36 ++ project/npda/models/custom_validators.py | 10 + project/npda/models/patient.py | 4 +- project/npda/models/patientsubmission.py | 8 +- project/npda/templates/npda/patient_form.html | 24 +- project/npda/templates/visit.html | 6 +- project/npda/templatetags/npda_tags.py | 43 +- 10 files changed, 405 insertions(+), 126 deletions(-) create mode 100644 project/npda/migrations/0022_alter_patient_nhs_number_and_more.py diff --git a/project/npda/admin.py b/project/npda/admin.py index b182c3ae..472e1bf6 100644 --- a/project/npda/admin.py +++ b/project/npda/admin.py @@ -15,7 +15,6 @@ PaediatricDiabetesUnit = apps.get_model("npda", "PaediatricDiabetesUnit") - @admin.register(OrganisationEmployer) class OrganisationEmployerAdmin(admin.ModelAdmin): search_fields = ("name", "pk", "lead_organisation_ods_code", "pz_code") @@ -28,7 +27,7 @@ class NPDAUserAdmin(admin.ModelAdmin): @admin.register(Patient) class PatientAdmin(admin.ModelAdmin): - search_fields = ("nhs_number_icontains", "pk") + search_fields = ("nhs_number_icontains", "pk", "unique_reference_number_icontains") @admin.register(PaediatricDiabetesUnit) diff --git a/project/npda/forms/patient_form.py b/project/npda/forms/patient_form.py index 8582d068..e7c3cd11 100644 --- a/project/npda/forms/patient_form.py +++ b/project/npda/forms/patient_form.py @@ -13,7 +13,6 @@ # django imports from django.apps import apps from django.core.exceptions import ValidationError -from httpx import HTTPError from ...constants.styles.form_styles import * from ..models import Patient @@ -40,6 +39,13 @@ def validate(self, value): raise ValidationError("Invalid NHS number") +class UniqueReferenceNumberField(forms.CharField): + + def validate(self, value): + if value and not value.isdigit(): + raise ValidationError("Invalid Unique Reference Number") + + class PostcodeField(forms.CharField): def to_python(self, value): postcode = super().to_python(value) @@ -54,6 +60,7 @@ class Meta: model = Patient fields = [ "nhs_number", + "unique_reference_number", "sex", "date_of_birth", "postcode", @@ -66,6 +73,7 @@ class Meta: ] field_classes = { "nhs_number": NHSNumberField, + "unique_reference_number": UniqueReferenceNumberField, "postcode": PostcodeField, "gp_practice_postcode": PostcodeField, } @@ -73,6 +81,9 @@ class Meta: "nhs_number": forms.TextInput( attrs={"class": TEXT_INPUT}, ), + "unique_reference_number": forms.TextInput( + attrs={"class": TEXT_INPUT}, + ), "sex": forms.Select(), "date_of_birth": DateInput(), "postcode": forms.TextInput(attrs={"class": TEXT_INPUT}), diff --git a/project/npda/kpi_class/kpis.py b/project/npda/kpi_class/kpis.py index 5c10d67f..c07fbdc2 100644 --- a/project/npda/kpi_class/kpis.py +++ b/project/npda/kpi_class/kpis.py @@ -101,9 +101,13 @@ def __init__( """ # Set various attributes used in calculations - self.calculation_date = calculation_date if calculation_date is not None else date.today() + self.calculation_date = ( + calculation_date if calculation_date is not None else date.today() + ) # Set the start and end audit dates - self.audit_start_date, self.audit_end_date = self._get_audit_start_and_end_dates() + self.audit_start_date, self.audit_end_date = ( + self._get_audit_start_and_end_dates() + ) self.AUDIT_DATE_RANGE = (self.audit_start_date, self.audit_end_date) # Set the return_pt_querysets attribute @@ -118,7 +122,7 @@ def set_patients_for_calculation( pz_codes: list[str] = None, ): """Set patients for KPI calculations. - + Can only set patients or pz_codes, not both.""" # Mutex if (patients is None) == (pz_codes is None): @@ -133,7 +137,7 @@ def set_patients_for_calculation( paediatric_diabetes_units__paediatric_diabetes_unit__pz_code__in=pz_codes ) self.total_patients_count = self.patients.count() - + def calculate_kpis_for_patients( self, patients: QuerySet[Patient], @@ -211,13 +215,17 @@ def calculate_kpis_for_single_patient( if type(patient) != Patient: raise ValueError(f"patient must be a Patient instance, got {type(patient)}") if type(pdu) != PaediatricDiabetesUnit: - raise ValueError(f"pdu must be a PaediatricDiabetesUnit instance, got {type(pdu)}") + raise ValueError( + f"pdu must be a PaediatricDiabetesUnit instance, got {type(pdu)}" + ) # Get values that are simple look ups calculation_datetime = datetime.now() audit_start_date = self.audit_start_date audit_end_date = self.audit_end_date - gte_12yo = patient.date_of_birth <= calculation_datetime.date() - relativedelta(years=12) + gte_12yo = patient.date_of_birth <= calculation_datetime.date() - relativedelta( + years=12 + ) diagnosed_in_period = patient.diagnosis_date in self.AUDIT_DATE_RANGE died_in_period = patient.death_date in self.AUDIT_DATE_RANGE transfer_in_period = ( @@ -312,7 +320,7 @@ def _calculate_kpis( ) -> KPICalculationsObject: """Calculate KPIs for set self.patients and cohort range (self.audit_start_date and self.audit_end_date). - + If kpi_idxs is not provided, will calculate all KPIs 1 - 49, otherwise calculates the subset of KPIs in the kpi_idxs list. @@ -369,8 +377,8 @@ def _calculate_kpis( kpi_number = 320 + int(name_split[2]) # Assign the KPI label - return_obj["calculated_kpi_values"][kpi_name]["kpi_label"] = self._get_kpi_label( - kpi_number + return_obj["calculated_kpi_values"][kpi_name]["kpi_label"] = ( + self._get_kpi_label(kpi_number) ) return return_obj @@ -383,7 +391,9 @@ def _get_kpi_label(self, kpi_number: int) -> str: return kpi_registry.get_rendered_label(kpi_number) - def _run_kpi_calculation_method(self, kpi_method_name: str) -> Union[KPIResult | str]: + def _run_kpi_calculation_method( + self, kpi_method_name: str + ) -> Union[KPIResult | str]: """Will find and run kpi calculation method (name schema is calculation_KPI_NAME_MAP_VALUE) """ @@ -466,12 +476,15 @@ def calculate_kpi_1_total_eligible(self) -> KPIResult: # Set the query set as an attribute to be used in subsequent KPI calculations self.total_kpi_1_eligible_pts_base_query_set = self.patients.filter( # Valid attributes - Q(nhs_number__isnull=False) - & Q(date_of_birth__isnull=False) - # Visit / admisison date within audit period - & Q(visit__visit_date__range=(self.AUDIT_DATE_RANGE)) - # Below the age of 25 at the start of the audit period - & Q(date_of_birth__gt=self.audit_start_date - relativedelta(years=25)) + ( + Q(nhs_number__isnull=False) + | (Q(unique_reference_number__isnull=False)) + & Q(date_of_birth__isnull=False) + # Visit / admisison date within audit period + & Q(visit__visit_date__range=(self.AUDIT_DATE_RANGE)) + # Below the age of 25 at the start of the audit period + & Q(date_of_birth__gt=self.audit_start_date - relativedelta(years=25)) + ) ).distinct() # When you filter on a related model field # (visit__visit_date__range), Django performs a join between the # Patient model and the Visit model. If a patient has multiple visits @@ -762,9 +775,11 @@ def calculate_kpi_6_total_t1dm_complete_year_gte_12yo(self) -> KPIResult: discarded as they're set to the same value as eligible/ineligible in the returned KPIResult object. """ - - base_eligible_patients, total_eligible = self._get_total_kpi_5_eligible_pts_base_query_set_and_total_count() - + + base_eligible_patients, total_eligible = ( + self._get_total_kpi_5_eligible_pts_base_query_set_and_total_count() + ) + # Gte 12yo eligible_patients = base_eligible_patients.filter( # Age 12 and above at the start of the audit period @@ -785,7 +800,11 @@ def calculate_kpi_6_total_t1dm_complete_year_gte_12yo(self) -> KPIResult: | Q(total_cholesterol_date__range=(self.AUDIT_DATE_RANGE)) | Q(thyroid_function_date__range=(self.AUDIT_DATE_RANGE)) | Q(coeliac_screen_date__range=(self.AUDIT_DATE_RANGE)) - | Q(psychological_screening_assessment_date__range=(self.AUDIT_DATE_RANGE)) + | Q( + psychological_screening_assessment_date__range=( + self.AUDIT_DATE_RANGE + ) + ) ), patient=OuterRef("pk"), visit_date__range=self.AUDIT_DATE_RANGE, @@ -796,7 +815,9 @@ def calculate_kpi_6_total_t1dm_complete_year_gte_12yo(self) -> KPIResult: valid_kpi_6_visits=Exists(valid_visit_subquery) ) - eligible_patients = eligible_pts_annotated_kpi_6_visits.filter(valid_kpi_6_visits__gte=1) + eligible_patients = eligible_pts_annotated_kpi_6_visits.filter( + valid_kpi_6_visits__gte=1 + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -850,28 +871,55 @@ def calculate_kpi_7_total_new_diagnoses_t1dm(self) -> KPIResult: # query set eligible_patients = self.patients.filter( # Valid attributes - Q(nhs_number__isnull=False) - & Q(date_of_birth__isnull=False) - # * Age < 25y years at the start of the audit period - & Q(date_of_birth__gt=self.audit_start_date - relativedelta(years=25)) - # Diagnosis of Type 1 diabetes - & Q(diabetes_type=DIABETES_TYPES[0][0]) - & Q(diagnosis_date__range=self.AUDIT_DATE_RANGE) - & ( - # an observation within the audit period - # this requires checking for a date in any of the Visit model's - # observation fields (found simply by searching for date fields - # with the word 'observation' in the field verbose_name) - Q(visit__height_weight_observation_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__hba1c_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__blood_pressure_observation_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__foot_examination_observation_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__retinal_screening_observation_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__albumin_creatinine_ratio_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__total_cholesterol_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__thyroid_function_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__coeliac_screen_date__range=(self.AUDIT_DATE_RANGE)) - | Q(visit__psychological_screening_assessment_date__range=(self.AUDIT_DATE_RANGE)) + ( + Q(nhs_number__isnull=False) + | (Q(unique_reference_number__isnull=False)) + & Q(date_of_birth__isnull=False) + # * Age < 25y years at the start of the audit period + & Q(date_of_birth__gt=self.audit_start_date - relativedelta(years=25)) + # Diagnosis of Type 1 diabetes + & Q(diabetes_type=DIABETES_TYPES[0][0]) + & Q(diagnosis_date__range=self.AUDIT_DATE_RANGE) + & ( + # an observation within the audit period + # this requires checking for a date in any of the Visit model's + # observation fields (found simply by searching for date fields + # with the word 'observation' in the field verbose_name) + Q( + visit__height_weight_observation_date__range=( + self.AUDIT_DATE_RANGE + ) + ) + | Q(visit__hba1c_date__range=(self.AUDIT_DATE_RANGE)) + | Q( + visit__blood_pressure_observation_date__range=( + self.AUDIT_DATE_RANGE + ) + ) + | Q( + visit__foot_examination_observation_date__range=( + self.AUDIT_DATE_RANGE + ) + ) + | Q( + visit__retinal_screening_observation_date__range=( + self.AUDIT_DATE_RANGE + ) + ) + | Q( + visit__albumin_creatinine_ratio_date__range=( + self.AUDIT_DATE_RANGE + ) + ) + | Q(visit__total_cholesterol_date__range=(self.AUDIT_DATE_RANGE)) + | Q(visit__thyroid_function_date__range=(self.AUDIT_DATE_RANGE)) + | Q(visit__coeliac_screen_date__range=(self.AUDIT_DATE_RANGE)) + | Q( + visit__psychological_screening_assessment_date__range=( + self.AUDIT_DATE_RANGE + ) + ) + ) ) ).distinct() # the reason for distinct is same as KPI1 (see comments). # This time, was failing tests for KPI 41-42. @@ -970,7 +1018,11 @@ def calculate_kpi_9_total_service_transitions(self) -> KPIResult: eligible_patients = base_eligible_patients.filter( # a leaving date in the audit period - Q(paediatric_diabetes_units__date_leaving_service__range=(self.AUDIT_DATE_RANGE)) + Q( + paediatric_diabetes_units__date_leaving_service__range=( + self.AUDIT_DATE_RANGE + ) + ) ) # Count eligible patients @@ -1020,9 +1072,15 @@ def calculate_kpi_10_total_coeliacs(self) -> KPIResult: ) # Filter the Patient queryset based on the subquery - base_query_set, _ = self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + base_query_set, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) eligible_patients = base_query_set.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter(visit__in=latest_visit_subquery).values("id") + ) + ) ) # Count eligible patients @@ -1066,15 +1124,23 @@ def calculate_kpi_11_total_thyroids(self) -> KPIResult: """ # Define the subquery to find the latest visit where thyroid_treatment_status__in = 2 or 3 latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), thyroid_treatment_status__in=[2, 3]) + Visit.objects.filter( + patient=OuterRef("pk"), thyroid_treatment_status__in=[2, 3] + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery - base_query_set, _ = self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + base_query_set, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) eligible_patients = base_query_set.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter(visit__in=latest_visit_subquery).values("id") + ) + ) ) # Count eligible patients @@ -1124,9 +1190,15 @@ def calculate_kpi_12_total_ketone_test_equipment(self) -> KPIResult: ) # Filter the Patient queryset based on the subquery - base_query_set, _ = self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + base_query_set, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) eligible_patients = base_query_set.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter(visit__in=latest_visit_subquery).values("id") + ) + ) ) # Count eligible patients @@ -1178,13 +1250,21 @@ def calculate_kpi_13_one_to_three_injections_per_day( # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 1 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=1).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=1 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1223,15 +1303,23 @@ def calculate_kpi_14_four_or_more_injections_per_day( total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 2 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=2).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=2 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1270,15 +1358,23 @@ def calculate_kpi_15_insulin_pump( total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 3 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=3).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=3 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1313,15 +1409,23 @@ def calculate_kpi_16_one_to_three_injections_plus_other_medication( total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 4 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=4).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=4 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1355,7 +1459,7 @@ def calculate_kpi_17_four_or_more_injections_plus_other_medication( ) total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( Visit.objects.filter(patient=OuterRef("pk")) .order_by("-visit_date") @@ -1363,7 +1467,13 @@ def calculate_kpi_17_four_or_more_injections_plus_other_medication( ) # Filter the Patient queryset based on the subquery if treatment_regimen = 5 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=5).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=5 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1397,15 +1507,23 @@ def calculate_kpi_18_insulin_pump_plus_other_medication( ) total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 6 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=6).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=6 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1439,15 +1557,23 @@ def calculate_kpi_19_dietary_management_alone( ) total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 7 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=7).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=7 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1481,15 +1607,23 @@ def calculate_kpi_20_dietary_management_plus_other_medication( ) total_ineligible = self.total_patients_count - total_eligible - # Define the subquery to find the latest visit + # Define the subquery to find the latest visit latest_visit_subquery = ( - Visit.objects.filter(patient=OuterRef("pk"), ) + Visit.objects.filter( + patient=OuterRef("pk"), + ) .order_by("-visit_date") .values("pk")[:1] ) # Filter the Patient queryset based on the subquery if treatment_regimen = 8 passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery, visit__treatment=8).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery, visit__treatment=8 + ).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1531,7 +1665,11 @@ def calculate_kpi_21_flash_glucose_monitor( ) # Filter the Patient queryset based on the subquery passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter(visit__in=latest_visit_subquery).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1573,7 +1711,11 @@ def calculate_kpi_22_real_time_cgm_with_alarms( ) # Filter the Patient queryset based on the subquery passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter(visit__in=latest_visit_subquery).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1616,7 +1758,11 @@ def calculate_kpi_23_type1_real_time_cgm_with_alarms( ) # Filter the Patient queryset based on the subquery passed_patients = eligible_patients.filter( - Q(id__in=Subquery(Patient.objects.filter(visit__in=latest_visit_subquery).values("id"))) + Q( + id__in=Subquery( + Patient.objects.filter(visit__in=latest_visit_subquery).values("id") + ) + ) ) total_passed = passed_patients.count() total_failed = total_eligible - total_passed @@ -1672,9 +1818,9 @@ def calculate_kpi_24_hybrid_closed_loop_system( eligible_patients_kpi_24 = total_kpi_1_eligible_pts_base_query_set.filter( Q( id__in=Subquery( - Patient.objects.filter(visit__in=eligible_kpi_24_latest_visit_subquery).values( - "id" - ) + Patient.objects.filter( + visit__in=eligible_kpi_24_latest_visit_subquery + ).values("id") ) ) ) @@ -1791,7 +1937,12 @@ def get_kpi_24_hcl_use_stratified_by_quarter( "total_passed": total_passed, "total_eligible": total_eligible_kpi_24, "pct": round( - (total_passed / total_eligible_kpi_24) * 100 if total_eligible_kpi_24 else 0, 1 + ( + (total_passed / total_eligible_kpi_24) * 100 + if total_eligible_kpi_24 + else 0 + ), + 1, ), } @@ -2285,10 +2436,13 @@ def calculate_kpi_32_1_health_check_completion_rate( ] ) - actual_health_checks_overall = total_health_checks_lt_12yo + total_health_checks_gte_12yo + actual_health_checks_overall = ( + total_health_checks_lt_12yo + total_health_checks_gte_12yo + ) expected_total_health_checks = ( - eligible_patients_lt_12yo.count() * 3 + eligible_patients_gte_12yo.count() * 6 + eligible_patients_lt_12yo.count() * 3 + + eligible_patients_gte_12yo.count() * 6 ) # Also set pt querysets to be returned if required @@ -2826,8 +2980,10 @@ def calculate_kpi_38_patients_attending_additional_dietetic_appointment( ), ) ) - total_passed_query_set = eligible_pts_annotated_dietician_additional_visits.filter( - dietician_additional_valid_visits__gte=1 + total_passed_query_set = ( + eligible_pts_annotated_dietician_additional_visits.filter( + dietician_additional_valid_visits__gte=1 + ) ) total_passed = total_passed_query_set.count() @@ -2867,15 +3023,13 @@ def calculate_kpi_39_influenza_immunisation_recommended( # Find patients with at least one entry for Influzena Immunisation # Recommended (item 24) within the audit period - eligible_pts_annotated_flu_immunisation_recommended_date_visits = ( - eligible_patients.annotate( - flu_immunisation_recommended_date_valid_visits=Count( - "visit", - filter=Q( - visit__visit_date__range=self.AUDIT_DATE_RANGE, - visit__flu_immunisation_recommended_date__range=self.AUDIT_DATE_RANGE, - ), - ) + eligible_pts_annotated_flu_immunisation_recommended_date_visits = eligible_patients.annotate( + flu_immunisation_recommended_date_valid_visits=Count( + "visit", + filter=Q( + visit__visit_date__range=self.AUDIT_DATE_RANGE, + visit__flu_immunisation_recommended_date__range=self.AUDIT_DATE_RANGE, + ), ) ) total_passed_query_set = ( @@ -2978,8 +3132,10 @@ def calculate_kpi_41_coeliac_disease_screening( "visit", # NOTE: relativedelta not supported filter=Q( - visit__coeliac_screen_date__gte=F("diagnosis_date") - timedelta(days=90), - visit__coeliac_screen_date__lte=F("diagnosis_date") + timedelta(days=90), + visit__coeliac_screen_date__gte=F("diagnosis_date") + - timedelta(days=90), + visit__coeliac_screen_date__lte=F("diagnosis_date") + + timedelta(days=90), ), ) ) @@ -3028,8 +3184,10 @@ def calculate_kpi_42_thyroid_disease_screening( "visit", # NOTE: relativedelta not supported filter=Q( - visit__thyroid_function_date__gte=F("diagnosis_date") - timedelta(days=90), - visit__thyroid_function_date__lte=F("diagnosis_date") + timedelta(days=90), + visit__thyroid_function_date__gte=F("diagnosis_date") + - timedelta(days=90), + visit__thyroid_function_date__lte=F("diagnosis_date") + + timedelta(days=90), ), ) ) @@ -3086,9 +3244,13 @@ def calculate_kpi_43_carbohydrate_counting_education( # Date of Diabetes Diagnosis (item 7) valid_visit_subquery = Visit.objects.filter( patient=OuterRef("pk"), - carbohydrate_counting_level_three_education_date__gte=F("patient__diagnosis_date") + carbohydrate_counting_level_three_education_date__gte=F( + "patient__diagnosis_date" + ) - timedelta(days=7), - carbohydrate_counting_level_three_education_date__lte=F("patient__diagnosis_date") + carbohydrate_counting_level_three_education_date__lte=F( + "patient__diagnosis_date" + ) + timedelta(days=14), ) @@ -3268,8 +3430,12 @@ def calculate_kpi_hba1c_vals_stratified_by_diabetes_type(self): total_ineligible = self.total_patients_count - total_eligible # Filter eligible patients to just relevant diabetes types - eligible_patients_t1dm = eligible_patients.filter(diabetes_type=DIABETES_TYPES[0][0]) - eligible_patients_t2dm = eligible_patients.filter(diabetes_type=DIABETES_TYPES[1][0]) + eligible_patients_t1dm = eligible_patients.filter( + diabetes_type=DIABETES_TYPES[0][0] + ) + eligible_patients_t2dm = eligible_patients.filter( + diabetes_type=DIABETES_TYPES[1][0] + ) hba1c_vals = { "t1dm": {}, @@ -3285,8 +3451,12 @@ def calculate_kpi_hba1c_vals_stratified_by_diabetes_type(self): eligible_patients=eligible_pts, ).values("patient__pk", "hba1c") - hba1c_vals[key]["mean"] = round(self._calculate_mean_hba1cs(valid_visits), 1) - hba1c_vals[key]["median"] = round(self._calculate_median_hba1cs(valid_visits), 1) + hba1c_vals[key]["mean"] = round( + self._calculate_mean_hba1cs(valid_visits), 1 + ) + hba1c_vals[key]["median"] = round( + self._calculate_median_hba1cs(valid_visits), 1 + ) return hba1c_vals @@ -3361,7 +3531,9 @@ def calculate_kpi_46_number_of_admissions( | Q(hospital_discharge_date__range=self.AUDIT_DATE_RANGE) ), # valid reason for admission - hospital_admission_reason__in=[choice[0] for choice in HOSPITAL_ADMISSION_REASONS], + hospital_admission_reason__in=[ + choice[0] for choice in HOSPITAL_ADMISSION_REASONS + ], patient=OuterRef("pk"), visit_date__range=self.AUDIT_DATE_RANGE, ) @@ -3713,7 +3885,9 @@ def _get_total_pts_new_t1dm_diag_90D_before_audit_end_base_query_set_and_total_c ) # First get new T1DM diagnoses pts - base_query_set, _ = self._get_total_kpi_7_eligible_pts_base_query_set_and_total_count() + base_query_set, _ = ( + self._get_total_kpi_7_eligible_pts_base_query_set_and_total_count() + ) # Filter for those diagnoses at least 90 days before audit end date self.t1dm_pts_diagnosed_90D_before_end_base_query_set = base_query_set.filter( diff --git a/project/npda/migrations/0022_alter_patient_nhs_number_and_more.py b/project/npda/migrations/0022_alter_patient_nhs_number_and_more.py new file mode 100644 index 00000000..e7d262e4 --- /dev/null +++ b/project/npda/migrations/0022_alter_patient_nhs_number_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.5 on 2025-01-24 23:32 + +import project.npda.models.custom_validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("npda", "0021_alter_patient_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="patient", + name="nhs_number", + field=models.CharField( + blank=True, + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", + null=True, + validators=[project.npda.models.custom_validators.validate_nhs_number], + verbose_name="NHS Number", + ), + ), + migrations.AlterField( + model_name="patient", + name="unique_reference_number", + field=models.CharField( + blank=True, + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", + max_length=50, + null=True, + verbose_name="Unique Reference Number", + ), + ), + ] diff --git a/project/npda/models/custom_validators.py b/project/npda/models/custom_validators.py index 349f4850..1d2965e7 100644 --- a/project/npda/models/custom_validators.py +++ b/project/npda/models/custom_validators.py @@ -6,6 +6,7 @@ # Logging logger = logging.getLogger(__name__) + def validate_nhs_number(value): """Validate the NHS number using the nhs_number package.""" if not nhs_number.is_valid(value): @@ -13,3 +14,12 @@ def validate_nhs_number(value): f"{value} is not a valid NHS number.", params={"value": value}, ) + + +def validate_unique_reference_number(value): + """Validate the Unique Reference Number.""" + if not value.isdigit(): + raise ValidationError( + f"{value} is not a valid Unique Reference Number.", + params={"value": value}, + ) diff --git a/project/npda/models/patient.py b/project/npda/models/patient.py index 42c3d8c7..3744c943 100644 --- a/project/npda/models/patient.py +++ b/project/npda/models/patient.py @@ -49,7 +49,7 @@ class Patient(models.Model): validators=[validate_nhs_number], null=True, blank=True, - help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", + help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", ) unique_reference_number = CharField( @@ -58,7 +58,7 @@ class Patient(models.Model): unique=False, blank=True, null=True, - help_text="This is the NHS number for England and Wales. It is used to identify the patient in the audit.", + help_text="This is a unique reference number for Jersey patients. It is used to identify the patient in the audit.", ) sex = models.IntegerField("Stated gender", choices=SEX_TYPE, blank=True, null=True) diff --git a/project/npda/models/patientsubmission.py b/project/npda/models/patientsubmission.py index 7e146f03..a4bcda17 100644 --- a/project/npda/models/patientsubmission.py +++ b/project/npda/models/patientsubmission.py @@ -1,5 +1,6 @@ from typing import Iterable from django.db import models +from django.db.models import Q from django.core.exceptions import ValidationError from django.conf import settings @@ -35,7 +36,12 @@ def save(self, *args, **kwargs): # Check for existing submissions for the same patient and audit year if ( PatientSubmission.objects.filter( - patient__nhs_number=self.patient__nhs_number, + ( + Q(patient__nhs_number=self.patient__nhs_number) + | Q( + patient__unique_reference_number=self.patient__unique_reference_number + ) + ), submission__audit_year=self.submission.audit_year, ) .exclude(pk=self.pk) diff --git a/project/npda/templates/npda/patient_form.html b/project/npda/templates/npda/patient_form.html index 1494d061..2c8e5fa9 100644 --- a/project/npda/templates/npda/patient_form.html +++ b/project/npda/templates/npda/patient_form.html @@ -9,10 +9,13 @@ {% for field in form %}
- + {% jersify pz_code field as viewable %} + {% if viewable %} + + {% endif %}
{% if field.field.widget|is_select %} @@ -27,11 +30,14 @@ {% endfor %} {% else %} - + {% jersify pz_code field as viewable %} + {% if viewable %} + + {% endif %} {% endif %} {% for error in field.errors %}
- + @@ -34,7 +35,7 @@

Seven Key Care Processes

- {{ patient.nhs_number }} + {% nhs_number_vs_urn pz_code patient %} diff --git a/project/npda/templates/partials/patient_table.html b/project/npda/templates/partials/patient_table.html index 181424b7..674a46e4 100644 --- a/project/npda/templates/partials/patient_table.html +++ b/project/npda/templates/partials/patient_table.html @@ -1,7 +1,6 @@ {% load static %} {% load npda_tags %} {% url 'patients' as patients_url %} - {% if page_obj %}
NHS Number{% nhs_number_vs_urn pz_code %} >= 12yo HbA1c BMI
@@ -40,10 +39,10 @@ @@ -52,11 +51,10 @@ {% with nhs_number_error=patient.errors|error_for_field:"nhs_number" gp_error=patient.errors|error_for_field:"gp_practice_ods_code" %} @@ -172,9 +170,8 @@
- {%if patient.is_first_valid %} - Patients where all records have been validated: {{valid_count_in_page}} + {% if patient.is_first_valid %} + Patients where all records have been validated: {{ valid_count_in_page }} {% else %} - Patients with records failing validation: {{error_count_in_page}} + Patients with records failing validation: {{ error_count_in_page }} {% endif %}
- + {% if nhs_number_error or gp_error %} @@ -66,7 +64,7 @@ {% endif %} - {{ patient.nhs_number|format_nhs_number }} + {% nhs_number_vs_urn pz_code patient %} {{ patient.latest_quarter|default:'-' }} + class="align-center flex tooltip tooltip-left {% if patient.visit_error_count < 1 %} text-rcpch_pink {% else %} text-rcpch_red {% endif %}" + data-tip="{% if patient.visit_error_count > 0 %} {{ patient.visit_error_count }} visit{{ patient.visit_error_count|pluralize }} {{ patient.visit_error_count|pluralize:'has, have' }} errors that need addressing. Items have been saved but will not be included until rectified. {% else %} View patient visits {% endif %}"> {% if patient.visit_error_count > 0 %} @@ -186,14 +183,14 @@ {% endif %} + width="24" + height="24" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + fill="none" + stroke-linecap="round" + stroke-linejoin="round"> diff --git a/project/npda/templatetags/npda_tags.py b/project/npda/templatetags/npda_tags.py index 36c3a8d1..ab2db5a9 100644 --- a/project/npda/templatetags/npda_tags.py +++ b/project/npda/templatetags/npda_tags.py @@ -339,3 +339,22 @@ def jersify(pz_code, field): return True else: return False + + +@register.simple_tag +def nhs_number_vs_urn(pz_code, patient=None): + """ + Tests to see if this field is rendered in a form with patients from Jersey + If so will return unique reference number otherwise will return a formatted nhs number + """ + if pz_code == "PZ248": + if patient and patient.unique_reference_number: + return patient.unique_reference_number + # Jersey + return "Unique Reference Number" + else: + if patient and patient.nhs_number: + if len(patient.nhs_number) >= 10: + return f"{patient.nhs_number[:3]} {patient.nhs_number[3:6]} {patient.nhs_number[6:]}" + return patient.nhs_number + return "NHS Number" From 5dcec95bf60cc3dba36330ad55168eeeebd7a179 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sat, 25 Jan 2025 10:52:43 +0000 Subject: [PATCH 06/24] add patient to visit form context to access nhs number and urn --- project/constants/csv_headings.py | 5 ++++ project/npda/forms/visit_form.py | 28 +++++++++++---------- project/npda/models/patient.py | 2 ++ project/npda/templates/npda/visit_form.html | 3 +-- project/npda/templates/visit.html | 7 ++---- project/npda/templates/visits.html | 14 +++++------ project/npda/views/visit.py | 6 ++++- 7 files changed, 37 insertions(+), 28 deletions(-) diff --git a/project/constants/csv_headings.py b/project/constants/csv_headings.py index 9f056749..356dc41d 100644 --- a/project/constants/csv_headings.py +++ b/project/constants/csv_headings.py @@ -5,6 +5,11 @@ "model_field": "nhs_number", "model": "Patient", }, + { + "heading": "Unique Reference Number", + "model_field": "unique_reference_number", + "model": "Patient", + }, { "heading": "Date of Birth", "model_field": "date_of_birth", diff --git a/project/npda/forms/visit_form.py b/project/npda/forms/visit_form.py index f751393b..8d35483b 100644 --- a/project/npda/forms/visit_form.py +++ b/project/npda/forms/visit_form.py @@ -4,7 +4,7 @@ from ...constants.styles import * from ...constants import * from ..general_functions.validate_dates import validate_date -from ..forms.external_visit_validators import validate_visit_sync +from ..forms.external_visit_validators import validate_visit_sync from ..models import Visit @@ -58,7 +58,7 @@ class Meta: "hospital_discharge_date", "hospital_admission_reason", "dka_additional_therapies", - "hospital_admission_other" + "hospital_admission_other", ] widgets = { @@ -101,7 +101,7 @@ class Meta: "hospital_discharge_date": DateInput(), "hospital_admission_reason": forms.Select(), "dka_additional_therapies": forms.Select(), - "hospital_admission_other": forms.TextInput(attrs={"class": TEXT_INPUT}) + "hospital_admission_other": forms.TextInput(attrs={"class": TEXT_INPUT}), } categories = [ @@ -393,7 +393,7 @@ def clean_systolic_blood_pressure(self): raise ValidationError( "Systolic Blood Pressure out of range. Cannot be above 240" ) - + return systolic_blood_pressure def clean_diastolic_blood_pressure(self): @@ -408,7 +408,7 @@ def clean_diastolic_blood_pressure(self): raise ValidationError( "Diastolic Blood pressure out of range. Cannot be above 120" ) - + return diastolic_blood_pressure def clean_albumin_creatinine_ratio(self): @@ -423,7 +423,7 @@ def clean_albumin_creatinine_ratio(self): raise ValidationError( "Urinary Albumin Level (ACR) out of range. Cannot be above 50" ) - + return albumin_creatinine_ratio def clean_total_cholesterol(self): @@ -438,7 +438,7 @@ def clean_total_cholesterol(self): raise ValidationError( "Total Cholesterol Level (mmol/l) out of range. Cannot be above 12" ) - + return total_cholesterol """ @@ -724,12 +724,12 @@ def handle_async_validation_errors(self): for [result_field, fields_to_attach_errors] in [ ["height_result", ["height"]], ["weight_result", ["weight"]], - ["bmi_result", ["height", "weight"]] + ["bmi_result", ["height", "weight"]], ]: result = getattr(self.async_validation_results, result_field) if result and type(result) is ValidationError: - for field in fields_to_attach_errors: + for field in fields_to_attach_errors: self.add_error(field, result) def clean(self): @@ -759,9 +759,9 @@ def round_to_one_decimal_place(value): observation_date=observation_date, height=height, weight=weight, - sex=sex + sex=sex, ) - + self.handle_async_validation_errors() # Check that the hba1c value is within the correct range @@ -794,7 +794,7 @@ def round_to_one_decimal_place(value): # Called when submitting the questionnaire. For CSV upload, instances are created directly in csv_upload to preserve # invalid data. Without overriding save here, the data from the dGC call would not be saved as the fields are not - # in the list at the top (that we expect to receive from a POST). + # in the list at the top (that we expect to receive from a POST). def save(self, commit=True): instance = super().save(commit=False) @@ -802,7 +802,9 @@ def save(self, commit=True): instance.bmi = self.async_validation_results.bmi for field_prefix in ["height", "weight", "bmi"]: - result = getattr(self.async_validation_results, f"{field_prefix}_result") + result = getattr( + self.async_validation_results, f"{field_prefix}_result" + ) if result and not type(result) is ValidationError: setattr(instance, f"{field_prefix}_centile", result.centile) diff --git a/project/npda/models/patient.py b/project/npda/models/patient.py index 3744c943..ccccd19c 100644 --- a/project/npda/models/patient.py +++ b/project/npda/models/patient.py @@ -147,6 +147,8 @@ class Meta: ] def __str__(self) -> str: + if self.unique_reference_number: + return f"ID: {self.pk}, {self.unique_reference_number}" return f"ID: {self.pk}, {self.nhs_number}" def get_absolute_url(self): diff --git a/project/npda/templates/npda/visit_form.html b/project/npda/templates/npda/visit_form.html index aa58aaa9..0f989612 100644 --- a/project/npda/templates/npda/visit_form.html +++ b/project/npda/templates/npda/visit_form.html @@ -5,7 +5,7 @@
- {{ title }} - NHS Number {{ nhs_number }} + {{ title }} - {% nhs_number_vs_urn pz_code %} {% nhs_number_vs_urn pz_code patient %}