diff --git a/manage_breast_screening/data/east_tester_today_clinic_data.yml b/manage_breast_screening/data/east_tester_today_clinic_data.yml index 7e7a000a6..462b291d5 100644 --- a/manage_breast_screening/data/east_tester_today_clinic_data.yml +++ b/manage_breast_screening/data/east_tester_today_clinic_data.yml @@ -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: diff --git a/manage_breast_screening/mammograms/forms/implanted_medical_device_history_form.py b/manage_breast_screening/mammograms/forms/implanted_medical_device_history_form.py index e9aa52a62..8be2d79dc 100644 --- a/manage_breast_screening/mammograms/forms/implanted_medical_device_history_form.py +++ b/manage_breast_screening/mammograms/forms/implanted_medical_device_history_form.py @@ -1,17 +1,12 @@ -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 ( @@ -19,22 +14,10 @@ ) -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?", @@ -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", @@ -95,9 +61,6 @@ 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( @@ -105,6 +68,7 @@ def model_values(self): 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", ""), @@ -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 + 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") diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/implanted_medical_device_history.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/implanted_medical_device_history.jinja index 8fc45a7ee..7a7aea4f5 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/implanted_medical_device_history.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/implanted_medical_device_history.jinja @@ -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() }} diff --git a/manage_breast_screening/mammograms/presenters/implanted_medical_device_history_item_presenter.py b/manage_breast_screening/mammograms/presenters/implanted_medical_device_history_item_presenter.py index d8d444691..1eca53b8f 100644 --- a/manage_breast_screening/mammograms/presenters/implanted_medical_device_history_item_presenter.py +++ b/manage_breast_screening/mammograms/presenters/implanted_medical_device_history_item_presenter.py @@ -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 @@ -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"}, diff --git a/manage_breast_screening/mammograms/tests/forms/test_implanted_medical_device_history_form.py b/manage_breast_screening/mammograms/tests/forms/test_implanted_medical_device_history_form.py index cea742c9b..1f24f8668 100644 --- a/manage_breast_screening/mammograms/tests/forms/test_implanted_medical_device_history_form.py +++ b/manage_breast_screening/mammograms/tests/forms/test_implanted_medical_device_history_form.py @@ -12,7 +12,6 @@ from ...forms.implanted_medical_device_history_form import ( ImplantedMedicalDeviceHistoryForm, - RemovalStatusChoices, ) @@ -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() @@ -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", }, ) @@ -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.", ] } @@ -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( @@ -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, }, ) @@ -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", ] } @@ -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, }, ) @@ -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 @@ -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", @@ -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, }, { @@ -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, }, ], @@ -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") + ) diff --git a/manage_breast_screening/mammograms/tests/presenters/test_implanted_medical_device_history_item_presenter.py b/manage_breast_screening/mammograms/tests/presenters/test_implanted_medical_device_history_item_presenter.py index b6092bea0..5c687898e 100644 --- a/manage_breast_screening/mammograms/tests/presenters/test_implanted_medical_device_history_item_presenter.py +++ b/manage_breast_screening/mammograms/tests/presenters/test_implanted_medical_device_history_item_presenter.py @@ -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", ) @@ -48,10 +49,10 @@ def test_single(self): }, { "key": { - "text": "Removal year", + "text": "Device has been removed", }, "value": { - "html": "2022", + "html": "Yes (2022)", }, }, { diff --git a/manage_breast_screening/participants/migrations/0047_implantedmedicaldevicehistoryitem_device_has_been_removed.py b/manage_breast_screening/participants/migrations/0047_implantedmedicaldevicehistoryitem_device_has_been_removed.py new file mode 100644 index 000000000..ca03634af --- /dev/null +++ b/manage_breast_screening/participants/migrations/0047_implantedmedicaldevicehistoryitem_device_has_been_removed.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.8 on 2025-11-25 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('participants', '0046_mastectomyorlumpectomyhistoryitem_surgery_other_reason_details_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='implantedmedicaldevicehistoryitem', + name='device_has_been_removed', + field=models.BooleanField(default=False), + preserve_default=False, + ), + ] diff --git a/manage_breast_screening/participants/migrations/max_migration.txt b/manage_breast_screening/participants/migrations/max_migration.txt index 41cc43c67..e94cf8d8e 100644 --- a/manage_breast_screening/participants/migrations/max_migration.txt +++ b/manage_breast_screening/participants/migrations/max_migration.txt @@ -1 +1 @@ -0046_mastectomyorlumpectomyhistoryitem_surgery_other_reason_details_and_more +0047_implantedmedicaldevicehistoryitem_device_has_been_removed diff --git a/manage_breast_screening/participants/models/implanted_medical_device_history_item.py b/manage_breast_screening/participants/models/implanted_medical_device_history_item.py index 15d31105f..e391e76ca 100644 --- a/manage_breast_screening/participants/models/implanted_medical_device_history_item.py +++ b/manage_breast_screening/participants/models/implanted_medical_device_history_item.py @@ -21,5 +21,6 @@ class Device(models.TextChoices): device = models.CharField(choices=Device) other_medical_device_details = models.CharField(blank=True, null=False, default="") procedure_year = models.IntegerField(null=True) + device_has_been_removed = models.BooleanField() removal_year = models.IntegerField(null=True) additional_details = models.TextField(blank=True, null=False, default="") diff --git a/manage_breast_screening/participants/tests/factories.py b/manage_breast_screening/participants/tests/factories.py index 32267ebe0..192836629 100644 --- a/manage_breast_screening/participants/tests/factories.py +++ b/manage_breast_screening/participants/tests/factories.py @@ -214,6 +214,7 @@ class Meta: appointment = SubFactory(AppointmentFactory) device = Iterator(ImplantedMedicalDeviceHistoryItem.Device) + device_has_been_removed = False class BreastAugmentationHistoryItemFactory(DjangoModelFactory): diff --git a/manage_breast_screening/tests/system/clinical/test_implanted_medical_device_history.py b/manage_breast_screening/tests/system/clinical/test_implanted_medical_device_history.py index 57a9c6eee..c5340b9d7 100644 --- a/manage_breast_screening/tests/system/clinical/test_implanted_medical_device_history.py +++ b/manage_breast_screening/tests/system/clinical/test_implanted_medical_device_history.py @@ -18,7 +18,13 @@ def test_adding_a_device(self): self.when_i_click_on_add_a_implanted_medical_device() self.then_i_see_the_add_a_implanted_medical_device_form() + self.when_i_click_save_device_without_entering_details() + self.then_i_see_validation_errors_for_missing_implanted_medical_device_details() + self.when_i_select_hickman_line() + self.and_i_enter_the_procedure_year() + self.and_i_enter_the_removal_year() + self.and_i_enter_additional_details() self.and_i_click_save_device() self.then_i_am_back_on_the_medical_information_page() self.and_the_device_is_listed() @@ -57,9 +63,31 @@ def then_i_see_the_add_a_implanted_medical_device_form(self): ).to_be_visible() self.assert_page_title_contains("Details of the implanted medical device") + def then_i_see_validation_errors_for_missing_implanted_medical_device_details(self): + self.expect_validation_error( + error_text="Select the device type", + fieldset_legend="What device does Janet have?", + field_label="Cardiac device (such as a pacemaker or ICD)", + ) + def when_i_select_hickman_line(self): self.page.get_by_label("Hickman line", exact=True).click() + def and_i_enter_the_procedure_year(self): + self.page.get_by_label("Year of procedure (optional)", exact=True).fill("2000") + + def and_i_enter_the_removal_year(self): + self.page.get_by_text("Implanted device has been removed").click() + self.page.get_by_label("Year removed (if available)", exact=True).fill("2025") + + def and_i_enter_additional_details(self): + self.page.get_by_label("Additional details (optional)", exact=True).fill( + "additional details for test of implanted medical device history" + ) + + def when_i_click_save_device_without_entering_details(self): + self.and_i_click_save_device() + def and_i_click_save_device(self): self.page.get_by_text("Save").click() @@ -73,6 +101,11 @@ def and_the_device_is_listed(self): ) row = self.page.locator(".nhsuk-summary-list__row").filter(has=key) expect(row).to_contain_text("Hickman line") + expect(row).to_contain_text("2000") + expect(row).to_contain_text("Yes (2025)") + expect(row).to_contain_text( + "additional details for test of implanted medical device history" + ) def and_the_message_says_device_added(self): alert = self.page.get_by_role("alert")