Skip to content

Commit

Permalink
Merge pull request #552 from rcpch/486-transfer-patient
Browse files Browse the repository at this point in the history
486-transfer-patient
  • Loading branch information
eatyourpeas authored Feb 4, 2025
2 parents 60bc45b + 4f52735 commit 961518b
Show file tree
Hide file tree
Showing 14 changed files with 497 additions and 222 deletions.
85 changes: 82 additions & 3 deletions project/npda/forms/patient_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
# django imports
from django.apps import apps
from django.core.exceptions import ValidationError
from httpx import HTTPError

from project.constants.leave_pdu_reasons import LEAVE_PDU_REASONS

from ...constants.styles.form_styles import *
from ..models import Patient
from ..models import Patient, Transfer
from ..validators import not_in_the_future_validator
from .external_patient_validators import validate_patient_sync

Expand Down Expand Up @@ -50,6 +51,11 @@ def to_python(self, value):

class PatientForm(forms.ModelForm):

date_leaving_service = forms.DateField(required=False, widget=DateInput())
reason_leaving_service = forms.ChoiceField(
required=False, choices=LEAVE_PDU_REASONS
)

class Meta:
model = Patient
fields = [
Expand Down Expand Up @@ -84,6 +90,20 @@ class Meta:
"gp_practice_postcode": forms.TextInput(attrs={"class": TEXT_INPUT}),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
try:
patient_transfer = Transfer.objects.filter(patient=self.instance).get()
self.fields["date_leaving_service"].initial = (
patient_transfer.date_leaving_service
)
self.fields["reason_leaving_service"].initial = (
patient_transfer.reason_leaving_service
)
except Transfer.DoesNotExist:
pass

def clean_date_of_birth(self):
date_of_birth = self.cleaned_data["date_of_birth"]

Expand Down Expand Up @@ -113,6 +133,20 @@ def clean_death_date(self):

return death_date

def clean_date_leaving_service(self):
date_leaving_service = self.cleaned_data["date_leaving_service"]
if date_leaving_service == "":
return None
elif date_leaving_service is not None:
not_in_the_future_validator(date_leaving_service)
return date_leaving_service

def clean_reason_leaving_service(self):
reason_leaving_service = self.cleaned_data["reason_leaving_service"]
if reason_leaving_service == "":
return None
return reason_leaving_service

def handle_async_validation_result(self, key):
value = getattr(self.async_validation_results, key)

Expand All @@ -123,13 +157,46 @@ def handle_async_validation_result(self, key):

def clean(self):
cleaned_data = self.cleaned_data

date_of_birth = cleaned_data.get("date_of_birth")
diagnosis_date = cleaned_data.get("diagnosis_date")
death_date = cleaned_data.get("death_date")
gp_practice_ods_code = cleaned_data.get("gp_practice_ods_code")
gp_practice_postcode = cleaned_data.get("gp_practice_postcode")

reason_leaving_service = cleaned_data.get("reason_leaving_service")
date_leaving_service = cleaned_data.get("date_leaving_service")
if date_leaving_service and not reason_leaving_service:
self.add_error(
"reason_leaving_service",
ValidationError(
"You must provide a reason for leaving the Paediatric Diabetes Unit"
),
)
if reason_leaving_service and not date_leaving_service:
self.add_error(
"date_leaving_service",
ValidationError(
"You must provide a date for leaving the Paediatric Diabetes Unit"
),
)
if date_leaving_service is not None and date_of_birth is not None:
if date_leaving_service < date_of_birth:
self.add_error(
"date_leaving_service",
ValidationError(
"'Date Leaving Service' cannot be before 'Date of Birth'"
),
)

if date_leaving_service is not None and diagnosis_date is not None:
if date_leaving_service < diagnosis_date:
self.add_error(
"date_leaving_service",
ValidationError(
"'Date Leaving Service' cannot be before 'Date of Diabetes Diagnosis'"
),
)

if diagnosis_date is not None and date_of_birth is not None:
if diagnosis_date < date_of_birth:
self.add_error(
Expand Down Expand Up @@ -195,5 +262,17 @@ def save(self, commit=True):

if commit:
self.instance.save()
if Transfer.objects.filter(patient=self.instance).exists():
patient_transfer = Transfer.objects.get(patient=self.instance)
patient_transfer.date_leaving_service = self.cleaned_data[
"date_leaving_service"
]
patient_transfer.reason_leaving_service = self.cleaned_data[
"reason_leaving_service"
]
patient_transfer.previous_pz_code = (
patient_transfer.paediatric_diabetes_unit.pz_code
) # set previous_pz_code to the current PZ code
patient_transfer.save()

return self.instance
1 change: 1 addition & 0 deletions project/npda/forms/visit_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def __init__(self, *args, **kwargs):
super(VisitForm, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
model_field = Visit._meta.get_field(field_name)

if hasattr(model_field, "category"):
field.category = model_field.category

Expand Down
17 changes: 17 additions & 0 deletions project/npda/models/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,20 @@ def age(self, today_date=None):
if today_date is None:
today_date = self.get_todays_date()
return stringify_time_elapsed(self.date_of_birth, today_date)

def is_in_transfer_in_the_last_year(self):
"""
Returns True if the patient is in transfer
"""
current_audit_year = date.today().year
if date.today().month < 4:
current_audit_year -= 1

audit_year_start = date(current_audit_year, 4, 1)
audit_year_end = date(current_audit_year + 1, 3, 31)

if self.paediatric_diabetes_units.filter(
date_leaving_service__range=(audit_year_start, audit_year_end),
).exists():
return True
return False
131 changes: 91 additions & 40 deletions project/npda/templates/npda/patient_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,110 @@
<form id="update-form" method="post" action="">
{% csrf_token %}
{% for field in form %}
<div class="md:flex md:items-center mb-6">
<div class="md:w-1/3">
<label for="{{ field.id_for_label }}"
class="block text-gray-700 font-bold md:text-center mb-1 md:mb-0 pr-4">
<small>{{ field.label }}</small>
</label>
{% if field|field_is_not_related_to_transfer %}
<div class="md:flex md:items-center mb-6">
<div class="md:w-1/3">
<label for="{{ field.id_for_label }}"
class="block text-gray-700 font-bold md:text-center mb-1 md:mb-0 pr-4">
<small>{{ field.label }}</small>
</label>
</div>
<div class="md:w-2/3">
{% if field.field.widget|is_select and field.id_for_label != 'id_reason_leaving_service' %}
<select id="{{ field.id_for_label }}"
name="{{ field.html_name }}"
class="select rcpch-select {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}">
{% for choice in field.field.choices %}
<option value="{{ choice.0 }}"
{% if field.value|stringformat:'s' == choice.0|stringformat:'s' %} selected="true" {% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</select>
{% else %}
<input id="{{ field.id_for_label }}"
name="{{ field.html_name }}"
class="input rcpch-input-text {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}"
{% if field.field.widget|is_dateinput %} type="date" {% else %} type="text" {% endif %}
{% if field.value %}value="{{ field.value|stringformat:'s' }}"{% endif %}>
{% endif %}
{% for error in field.errors %}
<div role="alert" class="alert alert-error py-1 my-0 rounded-none">
<svg xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ error|escape }}</span>
</div>
{% endfor %}
</div>
</div>
<div class="md:w-2/3">
{% if field.field.widget|is_select %}
<select id="{{ field.id_for_label }}"
name="{{ field.html_name }}"
class="select rcpch-select {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}">
{% for choice in field.field.choices %}
<option value="{{ choice.0 }}"
{% if field.value|stringformat:'s' == choice.0|stringformat:'s' %} selected="true" {% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</select>
{% else %}
<input id="{{ field.id_for_label }}"
name="{{ field.html_name }}"
class="input rcpch-input-text {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}"
{% if field.field.widget|is_dateinput %} type="date" {% else %} type="text" {% endif %}
{% if field.value %}value="{{ field.value|stringformat:'s' }}"{% endif %}>
{% endif %}
{% for error in field.errors %}
<div role="alert" class="alert alert-error py-1 my-0 rounded-none">
<svg xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ error|escape }}</span>
</div>
{% endif %}
{% endfor %}
<div class="collapse collapse-plus bg-base-200 py-2 mb-2">
<input type="checkbox"
{% if form.date_leaving_service.value or form.reason_leaving_service.value %}checked{% endif %} />
<div class="collapse-title text-xl font-medium">
Transfer Patient to Another PDU
</div>
<div class="collapse-content">
<select id="{{ form.reason_leaving_service.id_for_label }}"
name="{{ form.reason_leaving_service.html_name }}"
class="select rcpch-select mb-2 {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}">
<option value="">Select a reason</option>
{% for choice in form.reason_leaving_service.field.choices %}
<option value={{ choice.0 }} {% if form.reason_leaving_service.value == choice.0 %} selected="true" {% endif %}>
{{ choice.1 }}
</option>
{% endfor %}
</div>
</select>
{% for error in form.reason_leaving_service.errors %}
<div role="alert" class="alert alert-error py-1 my-0 rounded-none">
<svg xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ error|escape }}</span>
</div>
{% endfor %}
<input id="{{ form.date_leaving_service.id_for_label }}"
name="{{ form.date_leaving_service.html_name }}"
class="input rcpch-input-text mb-2 {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}"
type="date"
{% if form.date_leaving_service.value %}value="{{ form.date_leaving_service.value|stringformat:'s' }}"{% endif %}>
{% for error in form.date_leaving_service.errors %}
<div role="alert" class="alert alert-error py-1 my-0 rounded-none">
<svg xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ error|escape }}</span>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="flex justify-end">
<a role="button"
class="btn rcpch-light-blue-btn"
href='{% url 'patients' %}'>Back to list</a>
<button type="submit"
value="Submit"
value="submit"
name="action"
class="btn rcpch-light-blue-btn {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}">
{{ button_title }}
</button>
</div>
{% if form_method == 'update' and perms.npda.delete_patient %}
<a class="bg-rcpch_red text-white font-semibold hover:text-white py-2.5 px-3 mt-20 border border-rcpch_red hover:bg-rcpch_red_dark_tint hover:border-rcpch_red_dark_tint {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}"
href="{% url 'patient-delete' patient_id %}">Delete</a>
<button type="submit"
name="action"
class="rcpch-danger-btn {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}"
href="{% url 'patient-delete' patient_id %}">Delete</button>
{% endif %}
</form>
</div>
Expand Down
14 changes: 6 additions & 8 deletions project/npda/templates/npda/visit_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
{% block content %}
<div class="flex justify-center bg-white py-8">
<div class="max-w-full lg:max-w-[75%] px-2 bg-white font-montserrat">
<strong>
{{ title }} - NHS Number {{ nhs_number }}
</strong>
<strong>{{ title }} - NHS Number {{ nhs_number }}</strong>
<form id="update-form"
method="post"
{% if form_method == "create" %} action="{% url 'visit-create' patient_id %}" {% else %} action="{% url 'visit-update' patient_id=patient_id pk=visit.pk %}" {% endif %}>
Expand All @@ -23,10 +21,10 @@
</div>
<div class="flex space-between md:w-2/3">
{% if field.id_for_label == "id_visit_date" %}
<input type="date" id="{{ field.id_for_label }}" name="{{ field.html_name }}" class="input rcpch-input-text {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}" {% if field.value %}value={{ field.value|stringformat:'s' }}{% endif %}>
<input type="date" id="{{ field.id_for_label }}" name="{{ field.html_name }}" class="input rcpch-input-text {% if not can_alter_this_audit_year_submission or not can_use_questionnaire or form.patient.is_in_transfer_in_the_last_year %} opacity-40 pointer-events-none {% endif %}" {% if field.value %}value={{ field.value|stringformat:'s' }}{% endif %}>
<button type='button'
_="on click set #{{ field.id_for_label }}'s value to '{% today_date %}'"
class="btn rcpch-btn bg-rcpch_light_blue border border-rcpch_light_blue hover:bg-rcpch_strong_blue hover:border-rcpch_strong_blue {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}">
class="btn rcpch-btn bg-rcpch_light_blue border border-rcpch_light_blue hover:bg-rcpch_strong_blue hover:border-rcpch_strong_blue {% if not can_alter_this_audit_year_submission or not can_use_questionnaire or form.patient.is_in_transfer_in_the_last_year %}opacity-40 pointer-events-none{% endif %}">
Today
</button>
{% endif %}
Expand All @@ -47,13 +45,13 @@
<div class="flex justify-end">
<button type="submit"
value="Submit"
class="btn rcpch-btn bg-rcpch_light_blue border border-rcpch_light_blue hover:bg-rcpch_strong_blue hover:border-rcpch_strong_blue {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}">
class="btn rcpch-btn bg-rcpch_light_blue border border-rcpch_light_blue hover:bg-rcpch_strong_blue hover:border-rcpch_strong_blue {% if not can_alter_this_audit_year_submission or not can_use_questionnaire or form.patient.is_in_transfer_in_the_last_year %}opacity-40 pointer-events-none{% endif %}">
{{ button_title }}
</button>
<a class="btn rcpch-btn bg-rcpch_yellow_light_tint1 border border-rcpch_yellow_light_tint1 hover:bg-rcpch_yellow hover:border-rcpch_yellow"
href="{% url 'patient_visits' patient_id=patient_id %}">Cancel</a>
{% if form_method == 'update' and perms.npda.delete_visit %}
<a class="btn rcpch-btn bg-rcpch_red text-white font-semibold hover:text-white py-2.5 px-3 border border-rcpch_red hover:bg-rcpch_red_dark_tint hover:border-rcpch_red_dark_tint {% if not can_alter_this_audit_year_submission or not can_use_questionnaire %}opacity-40 pointer-events-none{% endif %}"
<a class="btn rcpch-btn bg-rcpch_red text-white font-semibold hover:text-white py-2.5 px-3 border border-rcpch_red hover:bg-rcpch_red_dark_tint hover:border-rcpch_red_dark_tint {% if not can_alter_this_audit_year_submission or not can_use_questionnaire or form.patient.is_in_transfer_in_the_last_year %}opacity-40 pointer-events-none{% endif %}"
href="{% url 'visit-delete' patient_id visit.pk %}">Delete</a>
{% endif %}
</div>
Expand Down Expand Up @@ -82,7 +80,7 @@
} else {
tab.checked = false;
}
}
}
} else {
anchor = document.querySelector('a[data-has-errors]');
}
Expand Down
Loading

0 comments on commit 961518b

Please sign in to comment.