Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ clinic:
device: OTHER_MEDICAL_DEVICE
other_medical_device_details: Lorem ipsum dolor sit amet
procedure_year: 2005
device_has_been_removed: true
removal_year: 2010
additional_details: Recalled device, removed in 2010
other_procedure_history_items:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
import datetime

from django import forms
from django.db.models import TextChoices
from django.forms.widgets import Textarea

from manage_breast_screening.core.services.auditor import Auditor
from manage_breast_screening.nhsuk_forms.fields import (
BooleanField,
CharField,
ChoiceField,
IntegerField,
)
from manage_breast_screening.nhsuk_forms.fields.choice_fields import (
MultipleChoiceField,
YearField,
)
from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
from manage_breast_screening.participants.models.implanted_medical_device_history_item import (
ImplantedMedicalDeviceHistoryItem,
)


class RemovalStatusChoices(TextChoices):
HAS_BEEN_REMOVED = "HAS_BEEN_REMOVED", "Implanted device has been removed"


class ImplantedMedicalDeviceHistoryForm(FormWithConditionalFields):
def __init__(self, *args, participant, **kwargs):
super().__init__(*args, **kwargs)

# if entered, years should be between 80 years ago and this year
max_year = datetime.date.today().year
min_year = max_year - 80
year_outside_range_error_message = (
f"Year should be between {min_year} and {max_year}."
)
year_invalid_format_error_message = "Enter year as a number."

self.fields["device"] = ChoiceField(
choices=ImplantedMedicalDeviceHistoryItem.Device,
label=f"What device does {participant.first_name} have?",
Expand All @@ -46,39 +29,22 @@ def __init__(self, *args, participant, **kwargs):
error_messages={"required": "Provide details of the device"},
classes="nhsuk-u-width-two-thirds",
)
self.fields["procedure_year"] = IntegerField(
self.fields["procedure_year"] = YearField(
hint="Leave blank if unknown",
required=False,
label="Year of procedure (optional)",
label_classes="nhsuk-label--m",
classes="nhsuk-input--width-4",
min_value=min_year,
max_value=max_year,
error_messages={
"min_value": year_outside_range_error_message,
"max_value": year_outside_range_error_message,
"invalid": year_invalid_format_error_message,
},
)
self.fields["removal_status"] = MultipleChoiceField(
self.fields["device_has_been_removed"] = BooleanField(
required=False,
choices=RemovalStatusChoices,
widget=forms.CheckboxSelectMultiple,
label="Removed implants",
label="Implanted device has been removed",
classes="app-checkboxes",
)
self.fields["removal_year"] = IntegerField(
self.fields["removal_year"] = YearField(
required=False,
label="Year removed",
label="Year removed (if available)",
classes="nhsuk-input--width-4",
min_value=min_year,
max_value=max_year,
error_messages={
"required": "Enter the year the device was removed",
"min_value": year_outside_range_error_message,
"max_value": year_outside_range_error_message,
"invalid": year_invalid_format_error_message,
},
)
self.fields["additional_details"] = CharField(
hint="Include any other relevant information about the device or procedure",
Expand All @@ -95,16 +61,14 @@ def __init__(self, *args, participant, **kwargs):
self.given_field_value(
"device", ImplantedMedicalDeviceHistoryItem.Device.OTHER_MEDICAL_DEVICE
).require_field("other_medical_device_details")
self.given_field_value(
"removal_status", RemovalStatusChoices.HAS_BEEN_REMOVED
).require_field("removal_year")

def model_values(self):
return dict(
device=self.cleaned_data.get("device", ""),
other_medical_device_details=self.cleaned_data.get(
"other_medical_device_details", ""
),
device_has_been_removed=self.cleaned_data.get("device_has_been_removed"),
removal_year=self.cleaned_data.get("removal_year", ""),
procedure_year=self.cleaned_data.get("procedure_year", ""),
additional_details=self.cleaned_data.get("additional_details", ""),
Expand All @@ -125,6 +89,19 @@ def create(self, appointment, request):

return implanted_medical_device_history

def full_clean(self):
# if a removal_year is provided then remove it if device_has_been_removed is False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if FormWithConditionalFields should have a way of handling this use case too (i.e. declaring that a field should only be parsed in a certain case, even if it is not required to pass a value) 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that could be useful. Should I create a ticket in the backlog to investigate that? I don't want it to be a requirement of merging this PR, as these changes are required for DTOSS-11528 (updating implanted medical device history).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that would be great, don't let it block this work 👍🏻

if self.data.get("removal_year") and not self.data.get(
"device_has_been_removed"
):
# makes QueryDict mutable
self.data = self.data.copy()
self.data["removal_year"] = None
if hasattr(self.data, "_mutable"):
self.data._mutable = False

super().full_clean()

def clean(self):
cleaned_data = super().clean()
procedure_year = cleaned_data.get("procedure_year")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
{% from "nhsuk/components/fieldset/macro.jinja" import fieldset %}

{% block form %}
{% do form.device.add_conditional_html('OTHER_MEDICAL_DEVICE', form.other_medical_device_details.as_field_group()) %}

{% do form.removal_status.add_conditional_html('HAS_BEEN_REMOVED', form.removal_year.as_field_group()) %}

{{ form.device.as_field_group() }}

{{ form.procedure_year.as_field_group() }}

{{ form.removal_status.as_field_group() }}
{% do form.device_has_been_removed.add_conditional_html('true', form.removal_year.as_field_group()) %}
{% call fieldset({
"legend": {
"text": "Removed implants",
"classes": "nhsuk-fieldset__legend--m"
}
}) %}
{{ form.device_has_been_removed.as_field_group() }}
{% endcall %}

{{ form.additional_details.as_field_group() }}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ def __init__(self, implanted_medical_device_history_item):
self._item.other_medical_device_details or "N/A"
)
self.procedure_year = str(self._item.procedure_year)
self.removal_year = str(self._item.removal_year) or "N/A"
self.device_has_been_removed = (
"Yes" if self._item.device_has_been_removed else "No"
)
if self._item.device_has_been_removed and self._item.removal_year:
self.device_has_been_removed += f" ({self._item.removal_year})"
self.additional_details = nl2br(self._item.additional_details)

@property
Expand All @@ -33,8 +37,10 @@ def summary_list_params(self):
"value": {"html": self.procedure_year},
},
{
"key": {"text": "Removal year"},
"value": {"html": self.removal_year},
"key": {"text": "Device has been removed"},
"value": {
"html": self.device_has_been_removed,
},
},
{
"key": {"text": "Additional details"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from ...forms.implanted_medical_device_history_form import (
ImplantedMedicalDeviceHistoryForm,
RemovalStatusChoices,
)


Expand Down Expand Up @@ -69,7 +68,7 @@ def test_procedure_year_invalid_format(self, clinical_user):
)

assert not form.is_valid()
assert form.errors == {"procedure_year": ["Enter year as a number."]}
assert form.errors == {"procedure_year": ["Enter a whole number."]}

def test_removal_year_invalid_format(self, clinical_user):
appointment = AppointmentFactory()
Expand All @@ -81,7 +80,7 @@ def test_removal_year_invalid_format(self, clinical_user):
urlencode(
{
"device": ImplantedMedicalDeviceHistoryItem.Device.HICKMAN_LINE,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"device_has_been_removed": True,
"removal_year": "qwerty",
},
)
Expand All @@ -92,8 +91,7 @@ def test_removal_year_invalid_format(self, clinical_user):
assert not form.is_valid()
assert form.errors == {
"removal_year": [
"Enter year as a number.",
"Enter the year the device was removed",
"Enter a whole number.",
]
}

Expand All @@ -112,10 +110,8 @@ def test_procedure_year_outside_range(self, clinical_user, procedure_year):
request = RequestFactory().get("/test-form")
request.user = clinical_user

max_year = datetime.date.today().year
min_year = max_year - 80
year_outside_range_error_message = (
f"Year should be between {min_year} and {max_year}."
self.create_year_outside_range_error_messsage(procedure_year)
)
form = ImplantedMedicalDeviceHistoryForm(
QueryDict(
Expand Down Expand Up @@ -147,17 +143,15 @@ def test_removal_year_outside_range(self, clinical_user, removal_year):
request = RequestFactory().get("/test-form")
request.user = clinical_user

max_year = datetime.date.today().year
min_year = max_year - 80
year_outside_range_error_message = (
f"Year should be between {min_year} and {max_year}."
self.create_year_outside_range_error_messsage(removal_year)
)
form = ImplantedMedicalDeviceHistoryForm(
QueryDict(
urlencode(
{
"device": ImplantedMedicalDeviceHistoryItem.Device.HICKMAN_LINE,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"device_has_been_removed": True,
"removal_year": removal_year,
},
)
Expand All @@ -169,7 +163,6 @@ def test_removal_year_outside_range(self, clinical_user, removal_year):
assert form.errors == {
"removal_year": [
year_outside_range_error_message,
"Enter the year the device was removed",
]
}

Expand All @@ -194,7 +187,7 @@ def test_removal_year_before_procedure_year(
{
"device": ImplantedMedicalDeviceHistoryItem.Device.HICKMAN_LINE,
"procedure_year": procedure_year,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"device_has_been_removed": True,
"removal_year": removal_year,
},
)
Expand All @@ -207,7 +200,7 @@ def test_removal_year_before_procedure_year(
"removal_year": ["Year removed cannot be before year of procedure"]
}

def test_has_been_removed_without_removal_date(self, clinical_user):
def test_removal_year_when_not_removed(self, clinical_user):
appointment = AppointmentFactory()
request = RequestFactory().get("/test-form")
request.user = clinical_user
Expand All @@ -217,17 +210,37 @@ def test_has_been_removed_without_removal_date(self, clinical_user):
urlencode(
{
"device": ImplantedMedicalDeviceHistoryItem.Device.HICKMAN_LINE,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"procedure_year": 2010,
"removal_year": 1900,
"additional_details": "removal_year provided but not device_has_been_removed",
},
doseq=True,
)
),
participant=appointment.participant,
)

assert not form.is_valid()
assert form.errors == {
"removal_year": ["Enter the year the device was removed"]
}
# confirm full_clean removes removal_year but keeps procedure_year
assert form.data["removal_year"] == "1900"
form.full_clean()
assert form.data["removal_year"] is None
assert form.cleaned_data["removal_year"] is None
assert form.cleaned_data["procedure_year"] == 2010

assert form.is_valid()

obj = form.create(appointment=appointment, request=request)

obj.refresh_from_db()
assert obj.appointment == appointment
assert obj.device == ImplantedMedicalDeviceHistoryItem.Device.HICKMAN_LINE
assert obj.procedure_year == 2010
assert not obj.device_has_been_removed
assert obj.removal_year is None
assert (
obj.additional_details
== "removal_year provided but not device_has_been_removed"
)

@pytest.mark.parametrize(
"data",
Expand All @@ -239,14 +252,14 @@ def test_has_been_removed_without_removal_date(self, clinical_user):
"device": ImplantedMedicalDeviceHistoryItem.Device.OTHER_MEDICAL_DEVICE,
"other_medical_device_details": "Some details about the device",
"procedure_year": 2010,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"device_has_been_removed": True,
"removal_year": 2015,
"additional_details": "Some additional details",
},
{
"device": ImplantedMedicalDeviceHistoryItem.Device.CARDIAC_DEVICE,
"procedure_year": 2015,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"device_has_been_removed": True,
"removal_year": 2015,
},
{
Expand All @@ -255,7 +268,7 @@ def test_has_been_removed_without_removal_date(self, clinical_user):
},
{
"device": ImplantedMedicalDeviceHistoryItem.Device.HICKMAN_LINE,
"removal_status": RemovalStatusChoices.HAS_BEEN_REMOVED,
"device_has_been_removed": True,
"removal_year": 2015,
},
],
Expand All @@ -282,3 +295,12 @@ def test_success(self, clinical_user, data):
assert obj.procedure_year == data.get("procedure_year", None)
assert obj.removal_year == data.get("removal_year", None)
assert obj.additional_details == data.get("additional_details", "")

def create_year_outside_range_error_messsage(self, request_year):
max_year = datetime.date.today().year
min_year = max_year - 80
return (
(f"Year must be {max_year} or earlier")
if request_year > max_year
else (f"Year must be {min_year} or later")
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def test_single(self):
device=ImplantedMedicalDeviceHistoryItem.Device.OTHER_MEDICAL_DEVICE,
other_medical_device_details="Test Device",
procedure_year=2020,
device_has_been_removed=True,
removal_year=2022,
additional_details="Some additional details",
)
Expand Down Expand Up @@ -48,10 +49,10 @@ def test_single(self):
},
{
"key": {
"text": "Removal year",
"text": "Device has been removed",
},
"value": {
"html": "2022",
"html": "Yes (2022)",
},
},
{
Expand Down
Loading