diff --git a/manage_breast_screening/data/west_tester_today_clinic_data.yml b/manage_breast_screening/data/west_tester_today_clinic_data.yml
index 9049f79a6..dd545f19d 100644
--- a/manage_breast_screening/data/west_tester_today_clinic_data.yml
+++ b/manage_breast_screening/data/west_tester_today_clinic_data.yml
@@ -175,7 +175,7 @@ clinic:
right_breast_other_surgery:
- RECONSTRUCTION
left_breast_other_surgery:
- - NO_SURGERY
+ - NO_OTHER_SURGERY
year_of_surgery: 2018
surgery_reason: RISK_REDUCTION
additional_details: Right mastectomy with reconstruction following genetic testing
diff --git a/manage_breast_screening/mammograms/forms/__init__.py b/manage_breast_screening/mammograms/forms/__init__.py
index b12a1ed3e..50aa348db 100644
--- a/manage_breast_screening/mammograms/forms/__init__.py
+++ b/manage_breast_screening/mammograms/forms/__init__.py
@@ -3,6 +3,7 @@
from .breast_augmentation_history_form import BreastAugmentationHistoryForm
from .cyst_history_form import CystHistoryForm
from .implanted_medical_device_history_form import ImplantedMedicalDeviceHistoryForm
+from .mastectomy_or_lumpectomy_history_form import MastectomyOrLumpectomyHistoryForm
from .record_medical_information_form import RecordMedicalInformationForm
from .screening_appointment_form import ScreeningAppointmentForm
from .special_appointment_forms import (
@@ -19,5 +20,6 @@
"ScreeningAppointmentForm",
"ProvideSpecialAppointmentDetailsForm",
"MarkReasonsTemporaryForm",
+ "MastectomyOrLumpectomyHistoryForm",
"ImplantedMedicalDeviceHistoryForm",
]
diff --git a/manage_breast_screening/mammograms/forms/mastectomy_or_lumpectomy_history_form.py b/manage_breast_screening/mammograms/forms/mastectomy_or_lumpectomy_history_form.py
new file mode 100644
index 000000000..fe5d312b1
--- /dev/null
+++ b/manage_breast_screening/mammograms/forms/mastectomy_or_lumpectomy_history_form.py
@@ -0,0 +1,134 @@
+from django.forms import widgets
+from django.forms.widgets import RadioSelect, Textarea
+
+from manage_breast_screening.core.services.auditor import Auditor
+from manage_breast_screening.nhsuk_forms.fields import (
+ CharField,
+ ChoiceField,
+ YearField,
+)
+from manage_breast_screening.nhsuk_forms.fields.choice_fields import (
+ MultipleChoiceField,
+)
+from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
+from manage_breast_screening.participants.models.mastectomy_or_lumpectomy_history_item import (
+ MastectomyOrLumpectomyHistoryItem,
+)
+
+
+class MastectomyOrLumpectomyHistoryForm(FormWithConditionalFields):
+ right_breast_procedure = ChoiceField(
+ label="Right breast",
+ visually_hidden_label_prefix="What procedure have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ widget=RadioSelect,
+ choices=MastectomyOrLumpectomyHistoryItem.Procedure,
+ error_messages={
+ "required": "Select which procedure they have had in the right breast",
+ },
+ )
+ left_breast_procedure = ChoiceField(
+ label="Left breast",
+ visually_hidden_label_prefix="What procedure have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ widget=RadioSelect,
+ choices=MastectomyOrLumpectomyHistoryItem.Procedure,
+ error_messages={
+ "required": "Select which procedure they have had in the left breast",
+ },
+ )
+ right_breast_other_surgery = MultipleChoiceField(
+ label="Right breast",
+ visually_hidden_label_prefix="What other surgery have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=MastectomyOrLumpectomyHistoryItem.Surgery,
+ error_messages={
+ "required": "Select any other surgery they have had in the right breast",
+ },
+ exclusive_choices={"NO_OTHER_SURGERY"},
+ )
+ left_breast_other_surgery = MultipleChoiceField(
+ label="Left breast",
+ visually_hidden_label_prefix="What other surgery have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=MastectomyOrLumpectomyHistoryItem.Surgery,
+ error_messages={
+ "required": "Select any other surgery they have had in the left breast",
+ },
+ exclusive_choices={"NO_OTHER_SURGERY"},
+ )
+ year_of_surgery = YearField(
+ hint="Leave blank if unknown",
+ required=False,
+ label="Year of surgery (optional)",
+ label_classes="nhsuk-label--m",
+ classes="nhsuk-input--width-4",
+ )
+ surgery_reason = ChoiceField(
+ choices=MastectomyOrLumpectomyHistoryItem.SurgeryReason,
+ label="Why was this surgery done?",
+ widget=widgets.RadioSelect,
+ error_messages={"required": "Select the reason for surgery"},
+ )
+ surgery_other_reason_details = CharField(
+ required=False,
+ label="Provide details",
+ error_messages={"required": "Provide details of the surgery"},
+ classes="nhsuk-u-width-two-thirds",
+ )
+ additional_details = CharField(
+ hint="Include any other relevant information about the surgery",
+ required=False,
+ label="Additional details (optional)",
+ label_classes="nhsuk-label--m",
+ widget=Textarea(attrs={"rows": 4}),
+ max_words=500,
+ error_messages={"max_words": "Additional details must be 500 words or less"},
+ )
+
+ def __init__(self, *args, participant, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.given_field_value(
+ "surgery_reason",
+ MastectomyOrLumpectomyHistoryItem.SurgeryReason.OTHER_REASON,
+ ).require_field("surgery_other_reason_details")
+
+ def model_values(self):
+ return dict(
+ left_breast_procedure=self.cleaned_data.get("left_breast_procedure", None),
+ right_breast_procedure=self.cleaned_data.get(
+ "right_breast_procedure", None
+ ),
+ left_breast_other_surgery=self.cleaned_data.get(
+ "left_breast_other_surgery", []
+ ),
+ right_breast_other_surgery=self.cleaned_data.get(
+ "right_breast_other_surgery", []
+ ),
+ year_of_surgery=self.cleaned_data.get("year_of_surgery", None),
+ surgery_reason=self.cleaned_data.get("surgery_reason", None),
+ surgery_other_reason_details=self.cleaned_data.get(
+ "surgery_other_reason_details", ""
+ ),
+ additional_details=self.cleaned_data.get("additional_details", ""),
+ )
+
+ def create(self, appointment, request):
+ auditor = Auditor.from_request(request)
+ field_values = self.model_values()
+
+ mastectomy_or_lumpectomy_history = (
+ appointment.mastectomy_or_lumpectomy_history_items.create(
+ appointment=appointment,
+ **field_values,
+ )
+ )
+
+ auditor.audit_create(mastectomy_or_lumpectomy_history)
+
+ return mastectomy_or_lumpectomy_history
diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/mastectomy_or_lumpectomy_history.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/mastectomy_or_lumpectomy_history.jinja
new file mode 100644
index 000000000..9891c13bd
--- /dev/null
+++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/mastectomy_or_lumpectomy_history.jinja
@@ -0,0 +1,51 @@
+{% extends "layout-form.jinja" %}
+{% from "nhsuk/components/button/macro.jinja" import button %}
+{% from "nhsuk/components/fieldset/macro.jinja" import fieldset %}
+
+
+{% block form %}
+
+ This is designed to capture information on prophylactic or elective surgery.
+
+ Add breast cancer details
+
+ if that was the reason for this surgery.
+
+
+ What procedure has {{ participant_first_name }} had?
+
+
+ {% do form.right_breast_procedure.add_divider_after("MASTECTOMY_NO_TISSUE_REMAINING", "or") %}
+ {{ form.right_breast_procedure.as_field_group() }}
+
+
+ {% do form.left_breast_procedure.add_divider_after("MASTECTOMY_NO_TISSUE_REMAINING", "or") %}
+ {{ form.left_breast_procedure.as_field_group() }}
+
+
+
+ What other surgery has {{ participant_first_name }} had?
+
+
+ {% do form.right_breast_other_surgery.add_divider_after("SYMMETRISATION", "or") %}
+ {{ form.right_breast_other_surgery.as_field_group() }}
+
+
+ {% do form.left_breast_other_surgery.add_divider_after("SYMMETRISATION", "or") %}
+ {{ form.left_breast_other_surgery.as_field_group() }}
+
+
+
+ {{ form.year_of_surgery.as_field_group() }}
+
+ {{ form.surgery_reason.as_field_group() }}
+
+ {{ form.additional_details.as_field_group() }}
+
+
+ {{ button({
+ "text": "Save"
+ }) }}
+
+{% endblock %}
+
diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja
index 3f101df5d..1717e481e 100644
--- a/manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja
+++ b/manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja
@@ -44,6 +44,7 @@
{% for presented_item in presenter.mastectomy_or_lumpectomy_history %}
{{ summaryList(presented_item.summary_list_params) }}
{% endfor %}
+ {{ presenter.add_mastectomy_or_lumpectomy_history_link.text }}
{% endset %}
{% set implanted_medical_device_history_html %}
diff --git a/manage_breast_screening/mammograms/presenters/mastectomy_or_lumpectomy_history_item_presenter.py b/manage_breast_screening/mammograms/presenters/mastectomy_or_lumpectomy_history_item_presenter.py
index 279996561..aac1954b4 100644
--- a/manage_breast_screening/mammograms/presenters/mastectomy_or_lumpectomy_history_item_presenter.py
+++ b/manage_breast_screening/mammograms/presenters/mastectomy_or_lumpectomy_history_item_presenter.py
@@ -25,6 +25,7 @@ def __init__(self, mastectomy_or_lumpectomy_history_item):
else "Not specified"
)
self.surgery_reason = self._item.get_surgery_reason_display()
+ self.surgery_other_reason_details = self._item.surgery_other_reason_details
self.additional_details = nl2br(self._item.additional_details)
@property
@@ -61,6 +62,10 @@ def summary_list_params(self):
"key": {"text": "Surgery reason"},
"value": {"html": self.surgery_reason},
},
+ {
+ "key": {"text": "Surgery other reason details"},
+ "value": {"text": self.surgery_other_reason_details},
+ },
{
"key": {"text": "Additional details"},
"value": {"html": self.additional_details},
diff --git a/manage_breast_screening/mammograms/presenters/medical_information_presenter.py b/manage_breast_screening/mammograms/presenters/medical_information_presenter.py
index 464b72d13..c70520bd1 100644
--- a/manage_breast_screening/mammograms/presenters/medical_information_presenter.py
+++ b/manage_breast_screening/mammograms/presenters/medical_information_presenter.py
@@ -209,3 +209,15 @@ def add_benign_lump_history_link(self):
"href": url,
"text": "Add benign lump history",
}
+
+ @property
+ def add_mastectomy_or_lumpectomy_history_link(self):
+ url = reverse(
+ "mammograms:add_mastectomy_or_lumpectomy_history_item",
+ kwargs={"pk": self.appointment.pk},
+ )
+
+ return {
+ "href": url,
+ "text": "Add mastectomy or lumpectomy history",
+ }
diff --git a/manage_breast_screening/mammograms/tests/forms/test_mastectomy_or_lumpectomy_history_form.py b/manage_breast_screening/mammograms/tests/forms/test_mastectomy_or_lumpectomy_history_form.py
new file mode 100644
index 000000000..55dd2aad1
--- /dev/null
+++ b/manage_breast_screening/mammograms/tests/forms/test_mastectomy_or_lumpectomy_history_form.py
@@ -0,0 +1,358 @@
+import datetime
+from urllib.parse import urlencode
+
+import pytest
+from django.http import QueryDict
+from django.test import RequestFactory
+
+from manage_breast_screening.core.models import AuditLog
+from manage_breast_screening.participants.models.mastectomy_or_lumpectomy_history_item import (
+ MastectomyOrLumpectomyHistoryItem,
+)
+from manage_breast_screening.participants.tests.factories import AppointmentFactory
+
+from ...forms.mastectomy_or_lumpectomy_history_form import (
+ MastectomyOrLumpectomyHistoryForm,
+)
+
+
+@pytest.mark.django_db
+class TestMastectomyOrLumpectomyHistoryItemForm:
+ def test_missing_required_fields(self, clinical_user):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(), participant=appointment.participant
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "right_breast_procedure": [
+ "Select which procedure they have had in the right breast",
+ ],
+ "left_breast_procedure": [
+ "Select which procedure they have had in the left breast",
+ ],
+ "right_breast_other_surgery": [
+ "Select any other surgery they have had in the right breast",
+ ],
+ "left_breast_other_surgery": [
+ "Select any other surgery they have had in the left breast",
+ ],
+ "surgery_reason": ["Select the reason for surgery"],
+ }
+
+ def test_right_breast_other_surgery_no_other_surgery_and_others(
+ self, clinical_user
+ ):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(
+ urlencode(
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ MastectomyOrLumpectomyHistoryItem.Surgery.SYMMETRISATION,
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY,
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION,
+ },
+ doseq=True,
+ ),
+ ),
+ participant=appointment.participant,
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "right_breast_other_surgery": [
+ 'Unselect "No other surgery" in order to select other options'
+ ],
+ }
+
+ def test_left_breast_other_surgery_no_other_surgery_and_others(self, clinical_user):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(
+ urlencode(
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY,
+ ],
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION,
+ },
+ doseq=True,
+ ),
+ ),
+ participant=appointment.participant,
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "left_breast_other_surgery": [
+ 'Unselect "No other surgery" in order to select other options'
+ ],
+ }
+
+ @pytest.mark.parametrize(
+ "year_of_surgery",
+ [
+ -1,
+ 1900,
+ datetime.date.today().year - 81,
+ datetime.date.today().year + 1,
+ 3000,
+ ],
+ )
+ def test_year_of_surgery_outside_range(self, clinical_user, year_of_surgery):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(
+ urlencode(
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "year_of_surgery": year_of_surgery,
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION,
+ },
+ doseq=True,
+ ),
+ ),
+ participant=appointment.participant,
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "year_of_surgery": [
+ self.create_year_outside_range_error_messsage(year_of_surgery)
+ ],
+ }
+
+ def test_year_of_surgery_invalid(self, clinical_user):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(
+ urlencode(
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "year_of_surgery": "invalid value for year_of_surgery",
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION,
+ },
+ doseq=True,
+ ),
+ ),
+ participant=appointment.participant,
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "year_of_surgery": ["Enter a whole number."],
+ }
+
+ def test_other_reason_without_details(self, clinical_user):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(
+ urlencode(
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.OTHER_REASON,
+ },
+ doseq=True,
+ ),
+ ),
+ participant=appointment.participant,
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "surgery_other_reason_details": ["Provide details of the surgery"],
+ }
+
+ def test_other_reason_details_when_not_other_reason(self, clinical_user):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(
+ urlencode(
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.MASTECTOMY_TISSUE_REMAINING,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.SYMMETRISATION,
+ ],
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION,
+ "surgery_other_reason_details": "surgery other details",
+ },
+ doseq=True,
+ ),
+ ),
+ participant=appointment.participant,
+ )
+
+ assert form.is_valid()
+
+ obj = form.create(appointment=appointment, request=request)
+ obj.refresh_from_db()
+
+ assert obj.appointment == appointment
+ assert (
+ obj.right_breast_procedure
+ == MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY
+ )
+ assert (
+ obj.left_breast_procedure
+ == MastectomyOrLumpectomyHistoryItem.Procedure.MASTECTOMY_TISSUE_REMAINING
+ )
+ assert obj.right_breast_other_surgery == [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ ]
+ assert obj.left_breast_other_surgery == [
+ MastectomyOrLumpectomyHistoryItem.Surgery.SYMMETRISATION,
+ ]
+ assert obj.year_of_surgery is None
+ assert (
+ obj.surgery_reason
+ == MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION
+ )
+ assert obj.surgery_other_reason_details == ""
+ assert obj.additional_details == ""
+
+ @pytest.mark.parametrize(
+ "data",
+ [
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.MASTECTOMY_TISSUE_REMAINING,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.MASTECTOMY_NO_TISSUE_REMAINING,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ MastectomyOrLumpectomyHistoryItem.Surgery.SYMMETRISATION,
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY,
+ ],
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.RISK_REDUCTION,
+ },
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.NO_PROCEDURE,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION,
+ MastectomyOrLumpectomyHistoryItem.Surgery.SYMMETRISATION,
+ ],
+ "year_of_surgery": datetime.date.today().year - 80,
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION,
+ },
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "year_of_surgery": datetime.date.today().year,
+ "surgery_reason": MastectomyOrLumpectomyHistoryItem.SurgeryReason.OTHER_REASON,
+ "surgery_other_reason_details": "surgery other details",
+ "additional_details": "additional details",
+ },
+ ],
+ )
+ def test_success(self, clinical_user, data):
+ appointment = AppointmentFactory()
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+
+ form = MastectomyOrLumpectomyHistoryForm(
+ QueryDict(urlencode(data, doseq=True)),
+ participant=appointment.participant,
+ )
+
+ assert form.is_valid()
+
+ existing_log_count = AuditLog.objects.count()
+ obj = form.create(appointment=appointment, request=request)
+ assert AuditLog.objects.count() == existing_log_count + 1
+ audit_log = AuditLog.objects.filter(
+ object_id=obj.pk, operation=AuditLog.Operations.CREATE
+ ).first()
+ assert audit_log.actor == clinical_user
+
+ obj.refresh_from_db()
+ assert obj.appointment == appointment
+ assert obj.right_breast_procedure == data.get("right_breast_procedure")
+ assert obj.left_breast_procedure == data.get("left_breast_procedure")
+ assert obj.right_breast_other_surgery == data.get("right_breast_other_surgery")
+ assert obj.left_breast_other_surgery == data.get("left_breast_other_surgery")
+ assert obj.year_of_surgery == data.get("year_of_surgery", None)
+ assert obj.surgery_reason == data.get("surgery_reason", "")
+ assert obj.surgery_other_reason_details == data.get(
+ "surgery_other_reason_details", ""
+ )
+ 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_mastectomy_or_lumpectomy_history_item_presenter.py b/manage_breast_screening/mammograms/tests/presenters/test_mastectomy_or_lumpectomy_history_item_presenter.py
index 4a0121c52..65783a21a 100644
--- a/manage_breast_screening/mammograms/tests/presenters/test_mastectomy_or_lumpectomy_history_item_presenter.py
+++ b/manage_breast_screening/mammograms/tests/presenters/test_mastectomy_or_lumpectomy_history_item_presenter.py
@@ -18,7 +18,7 @@ def test_single(self):
MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
],
left_breast_other_surgery=[
- MastectomyOrLumpectomyHistoryItem.Surgery.NO_SURGERY
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY
],
year_of_surgery=2018,
surgery_reason=MastectomyOrLumpectomyHistoryItem.SurgeryReason.RISK_REDUCTION,
@@ -42,7 +42,7 @@ def test_single(self):
"text": "Other surgery",
},
"value": {
- "html": "Right breast: Reconstruction
Left breast: No surgery",
+ "html": "Right breast: Reconstruction
Left breast: No other surgery",
},
},
{
@@ -61,6 +61,14 @@ def test_single(self):
"html": "Risk reduction",
},
},
+ {
+ "key": {
+ "text": "Surgery other reason details",
+ },
+ "value": {
+ "text": "",
+ },
+ },
{
"key": {
"text": "Additional details",
diff --git a/manage_breast_screening/mammograms/tests/presenters/test_medical_information_presenter.py b/manage_breast_screening/mammograms/tests/presenters/test_medical_information_presenter.py
index 5fc6db8c8..124c38cdd 100644
--- a/manage_breast_screening/mammograms/tests/presenters/test_medical_information_presenter.py
+++ b/manage_breast_screening/mammograms/tests/presenters/test_medical_information_presenter.py
@@ -161,3 +161,13 @@ def test_add_benign_lump_history_link(self):
"href": f"/mammograms/{appointment.pk}/record-medical-information/benign-lump-history/",
"text": "Add benign lump history",
}
+
+ def test_mastectomy_or_lumpectomy_history_link(self):
+ appointment = AppointmentFactory()
+
+ assert MedicalInformationPresenter(
+ appointment
+ ).add_mastectomy_or_lumpectomy_history_link == {
+ "href": f"/mammograms/{appointment.pk}/record-medical-information/mastectomy-or-lumpectomy-history/",
+ "text": "Add mastectomy or lumpectomy history",
+ }
diff --git a/manage_breast_screening/mammograms/tests/views/test_mastectomy_or_lumpectomy_history_views.py b/manage_breast_screening/mammograms/tests/views/test_mastectomy_or_lumpectomy_history_views.py
new file mode 100644
index 000000000..24ca28908
--- /dev/null
+++ b/manage_breast_screening/mammograms/tests/views/test_mastectomy_or_lumpectomy_history_views.py
@@ -0,0 +1,91 @@
+import pytest
+from django.contrib import messages
+from django.urls import reverse
+from pytest_django.asserts import assertInHTML, assertMessages, assertRedirects
+
+from manage_breast_screening.participants.models.mastectomy_or_lumpectomy_history_item import (
+ MastectomyOrLumpectomyHistoryItem,
+)
+from manage_breast_screening.participants.tests.factories import (
+ AppointmentFactory,
+)
+
+
+@pytest.mark.django_db
+class TestAddMastectomyOrLumpectomyHistoryView:
+ def test_renders_response(self, clinical_user_client):
+ appointment = AppointmentFactory.create(
+ clinic_slot__clinic__setting__provider=clinical_user_client.current_provider
+ )
+ response = clinical_user_client.http.get(
+ reverse(
+ "mammograms:add_mastectomy_or_lumpectomy_history_item",
+ kwargs={"pk": appointment.pk},
+ )
+ )
+ assert response.status_code == 200
+
+ def test_valid_post_redirects_to_appointment(self, clinical_user_client):
+ appointment = AppointmentFactory.create(
+ clinic_slot__clinic__setting__provider=clinical_user_client.current_provider
+ )
+ response = clinical_user_client.http.post(
+ reverse(
+ "mammograms:add_mastectomy_or_lumpectomy_history_item",
+ kwargs={"pk": appointment.pk},
+ ),
+ {
+ "right_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "left_breast_procedure": MastectomyOrLumpectomyHistoryItem.Procedure.LUMPECTOMY,
+ "right_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "left_breast_other_surgery": [
+ MastectomyOrLumpectomyHistoryItem.Surgery.RECONSTRUCTION
+ ],
+ "surgery_reason": [
+ MastectomyOrLumpectomyHistoryItem.SurgeryReason.GENDER_AFFIRMATION
+ ],
+ },
+ )
+ assertRedirects(
+ response,
+ reverse(
+ "mammograms:record_medical_information",
+ kwargs={"pk": appointment.pk},
+ ),
+ )
+ assertMessages(
+ response,
+ [
+ messages.Message(
+ level=messages.SUCCESS,
+ message="Details of mastectomy or lumpectomy added",
+ )
+ ],
+ )
+
+ def test_invalid_post_renders_response_with_errors(self, clinical_user_client):
+ appointment = AppointmentFactory.create(
+ clinic_slot__clinic__setting__provider=clinical_user_client.current_provider
+ )
+ response = clinical_user_client.http.post(
+ reverse(
+ "mammograms:add_mastectomy_or_lumpectomy_history_item",
+ kwargs={"pk": appointment.pk},
+ ),
+ {},
+ )
+ assert response.status_code == 200
+ assertInHTML(
+ """
+
+ """,
+ response.text,
+ )
diff --git a/manage_breast_screening/mammograms/urls.py b/manage_breast_screening/mammograms/urls.py
index 014284ffb..121624002 100644
--- a/manage_breast_screening/mammograms/urls.py
+++ b/manage_breast_screening/mammograms/urls.py
@@ -7,6 +7,7 @@
breast_cancer_history_views,
cyst_history_view,
implanted_medical_device_history_view,
+ mastectomy_or_lumpectomy_history_view,
special_appointment_views,
symptom_views,
)
@@ -149,4 +150,9 @@
benign_lump_history_item_views.AddBenignLumpHistoryItemView.as_view(),
name="add_benign_lump_history_item",
),
+ path(
+ "/record-medical-information/mastectomy-or-lumpectomy-history/",
+ mastectomy_or_lumpectomy_history_view.AddMastectomyOrLumpectomyHistoryView.as_view(),
+ name="add_mastectomy_or_lumpectomy_history_item",
+ ),
]
diff --git a/manage_breast_screening/mammograms/views/mastectomy_or_lumpectomy_history_view.py b/manage_breast_screening/mammograms/views/mastectomy_or_lumpectomy_history_view.py
new file mode 100644
index 000000000..eef6180c7
--- /dev/null
+++ b/manage_breast_screening/mammograms/views/mastectomy_or_lumpectomy_history_view.py
@@ -0,0 +1,71 @@
+from functools import cached_property
+
+from django.contrib import messages
+from django.urls import reverse
+from django.views.generic import FormView
+
+from manage_breast_screening.mammograms.presenters.medical_information_presenter import (
+ MedicalInformationPresenter,
+)
+
+from ..forms.mastectomy_or_lumpectomy_history_form import (
+ MastectomyOrLumpectomyHistoryForm,
+)
+from .mixins import InProgressAppointmentMixin
+
+
+class AddMastectomyOrLumpectomyHistoryView(InProgressAppointmentMixin, FormView):
+ form_class = MastectomyOrLumpectomyHistoryForm
+ template_name = "mammograms/medical_information/medical_history/forms/mastectomy_or_lumpectomy_history.jinja"
+
+ def get_success_url(self):
+ return reverse(
+ "mammograms:record_medical_information", kwargs={"pk": self.appointment.pk}
+ )
+
+ def form_valid(self, form):
+ form.create(appointment=self.appointment, request=self.request)
+
+ messages.add_message(
+ self.request,
+ messages.SUCCESS,
+ "Details of mastectomy or lumpectomy added",
+ )
+
+ return super().form_valid(form)
+
+ def get_back_link_params(self):
+ return {
+ "href": reverse(
+ "mammograms:record_medical_information",
+ kwargs={"pk": self.appointment_pk},
+ ),
+ "text": "Back to appointment",
+ }
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data()
+
+ participant = self.appointment.participant
+
+ context.update(
+ {
+ "back_link_params": self.get_back_link_params(),
+ "caption": participant.full_name,
+ "participant_first_name": participant.first_name,
+ "heading": "Add details of mastectomy or lumpectomy",
+ "page_title": "Add details of mastectomy or lumpectomy",
+ "presenter": MedicalInformationPresenter(self.appointment),
+ },
+ )
+
+ return context
+
+ @cached_property
+ def participant(self):
+ return self.appointment.participant
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["participant"] = self.participant
+ return kwargs
diff --git a/manage_breast_screening/participants/migrations/0046_mastectomyorlumpectomyhistoryitem_surgery_other_reason_details_and_more.py b/manage_breast_screening/participants/migrations/0046_mastectomyorlumpectomyhistoryitem_surgery_other_reason_details_and_more.py
new file mode 100644
index 000000000..0957e0903
--- /dev/null
+++ b/manage_breast_screening/participants/migrations/0046_mastectomyorlumpectomyhistoryitem_surgery_other_reason_details_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.2.8 on 2025-11-24 14:34
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('participants', '0045_alter_breastcancerhistoryitem_left_breast_other_surgery_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='mastectomyorlumpectomyhistoryitem',
+ name='surgery_other_reason_details',
+ field=models.CharField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='mastectomyorlumpectomyhistoryitem',
+ name='left_breast_other_surgery',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('RECONSTRUCTION', 'Reconstruction'), ('SYMMETRISATION', 'Symmetrisation'), ('NO_OTHER_SURGERY', 'No other surgery')]), default=list, size=None),
+ ),
+ migrations.AlterField(
+ model_name='mastectomyorlumpectomyhistoryitem',
+ name='right_breast_other_surgery',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('RECONSTRUCTION', 'Reconstruction'), ('SYMMETRISATION', 'Symmetrisation'), ('NO_OTHER_SURGERY', 'No other surgery')]), default=list, size=None),
+ ),
+ ]
diff --git a/manage_breast_screening/participants/migrations/max_migration.txt b/manage_breast_screening/participants/migrations/max_migration.txt
index 3d9b37a95..41cc43c67 100644
--- a/manage_breast_screening/participants/migrations/max_migration.txt
+++ b/manage_breast_screening/participants/migrations/max_migration.txt
@@ -1 +1 @@
-0045_alter_breastcancerhistoryitem_left_breast_other_surgery_and_more
+0046_mastectomyorlumpectomyhistoryitem_surgery_other_reason_details_and_more
diff --git a/manage_breast_screening/participants/models/mastectomy_or_lumpectomy_history_item.py b/manage_breast_screening/participants/models/mastectomy_or_lumpectomy_history_item.py
index cd6f410b3..91d0b3c9b 100644
--- a/manage_breast_screening/participants/models/mastectomy_or_lumpectomy_history_item.py
+++ b/manage_breast_screening/participants/models/mastectomy_or_lumpectomy_history_item.py
@@ -21,7 +21,7 @@ class Procedure(models.TextChoices):
class Surgery(models.TextChoices):
RECONSTRUCTION = "RECONSTRUCTION", "Reconstruction"
SYMMETRISATION = "SYMMETRISATION", "Symmetrisation"
- NO_SURGERY = "NO_SURGERY", "No surgery"
+ NO_OTHER_SURGERY = "NO_OTHER_SURGERY", "No other surgery"
class SurgeryReason(models.TextChoices):
RISK_REDUCTION = "RISK_REDUCTION", "Risk reduction"
@@ -43,6 +43,9 @@ class SurgeryReason(models.TextChoices):
base_field=models.CharField(choices=Surgery),
default=list,
)
+ # TODO should we rename year_of_surgery to surgery_year
+ # to be more consitent with names used elsewhere. e.g. procedure_year
year_of_surgery = models.IntegerField(null=True, blank=True)
surgery_reason = models.CharField(choices=SurgeryReason)
+ surgery_other_reason_details = models.CharField(blank=True, null=False, default="")
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 fc1d8ef5d..32267ebe0 100644
--- a/manage_breast_screening/participants/tests/factories.py
+++ b/manage_breast_screening/participants/tests/factories.py
@@ -189,8 +189,12 @@ class Meta:
appointment = SubFactory(AppointmentFactory)
right_breast_procedure = MastectomyOrLumpectomyHistoryItem.Procedure.NO_PROCEDURE
left_breast_procedure = MastectomyOrLumpectomyHistoryItem.Procedure.NO_PROCEDURE
- right_breast_other_surgery = [MastectomyOrLumpectomyHistoryItem.Surgery.NO_SURGERY]
- left_breast_other_surgery = [MastectomyOrLumpectomyHistoryItem.Surgery.NO_SURGERY]
+ right_breast_other_surgery = [
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY
+ ]
+ left_breast_other_surgery = [
+ MastectomyOrLumpectomyHistoryItem.Surgery.NO_OTHER_SURGERY
+ ]
year_of_surgery = None
surgery_reason = MastectomyOrLumpectomyHistoryItem.SurgeryReason.OTHER_REASON
additional_details = ""
diff --git a/manage_breast_screening/tests/system/clinical/test_mastectomy_or_lumpectomy_history.py b/manage_breast_screening/tests/system/clinical/test_mastectomy_or_lumpectomy_history.py
new file mode 100644
index 000000000..ae25710ba
--- /dev/null
+++ b/manage_breast_screening/tests/system/clinical/test_mastectomy_or_lumpectomy_history.py
@@ -0,0 +1,159 @@
+from django.urls import reverse
+from playwright.sync_api import expect
+
+from manage_breast_screening.participants.tests.factories import (
+ AppointmentFactory,
+ ParticipantFactory,
+ ScreeningEpisodeFactory,
+)
+
+from ..system_test_setup import SystemTestCase
+
+
+class TestRecordingMastectomyOrLumpectomy(SystemTestCase):
+ def test_adding_mastectomy_or_lumpectomy(self):
+ self.given_i_am_logged_in_as_a_clinical_user()
+ self.and_there_is_an_appointment()
+ self.and_i_am_on_the_record_medical_information_page()
+ self.when_i_click_on_add_mastectomy_or_lumpectomy()
+ self.then_i_see_the_add_mastectomy_or_lumpectomy_form()
+
+ self.when_i_click_save()
+ self.then_i_see_validation_errors_for_missing_mastectomy_or_lumpectomy_details()
+
+ self.when_i_provide_mastectomy_or_lumpectomy_details()
+ self.and_i_click_save()
+ self.then_i_am_back_on_the_medical_information_page()
+ self.and_the_mastectomy_or_lumpectomy_is_listed()
+ self.and_the_message_says_mastectomy_or_lumpectomy_added()
+
+ def test_accessibility(self):
+ self.given_i_am_logged_in_as_a_clinical_user()
+ self.and_there_is_an_appointment()
+ self.and_i_am_on_the_record_medical_information_page()
+ self.when_i_click_on_add_mastectomy_or_lumpectomy()
+ self.then_the_accessibility_baseline_is_met()
+
+ def and_there_is_an_appointment(self):
+ self.participant = ParticipantFactory(first_name="Janet", last_name="Williams")
+ self.screening_episode = ScreeningEpisodeFactory(participant=self.participant)
+ self.appointment = AppointmentFactory(
+ screening_episode=self.screening_episode,
+ clinic_slot__clinic__setting__provider=self.current_provider,
+ )
+
+ def and_i_am_on_the_record_medical_information_page(self):
+ self.page.goto(
+ self.live_server_url
+ + reverse(
+ "mammograms:record_medical_information",
+ kwargs={"pk": self.appointment.pk},
+ )
+ )
+
+ def when_i_click_on_add_mastectomy_or_lumpectomy(self):
+ self.page.get_by_text("Add mastectomy or lumpectomy history").click()
+
+ def then_i_see_the_add_mastectomy_or_lumpectomy_form(self):
+ expect(
+ self.page.get_by_text("Add details of mastectomy or lumpectomy")
+ ).to_be_visible()
+ expect(
+ self.page.get_by_text(
+ "Add breast cancer details if that was the reason for this surgery."
+ )
+ ).to_be_visible()
+ link = self.page.get_by_role(
+ "link", name="Add breast cancer details"
+ ).get_attribute("href")
+ assert link == reverse(
+ "mammograms:add_breast_cancer_history_item",
+ kwargs={"pk": self.appointment.pk},
+ )
+
+ def then_i_see_validation_errors_for_missing_mastectomy_or_lumpectomy_details(self):
+ self.expect_validation_error(
+ error_text="Select which procedure they have had in the right breast",
+ fieldset_legend="Right breast",
+ field_label="Lumpectomy",
+ )
+ self.expect_validation_error(
+ error_text="Select which procedure they have had in the left breast",
+ fieldset_legend="Left breast",
+ field_label="Lumpectomy",
+ )
+ self.expect_validation_error(
+ error_text="Select any other surgery they have had in the right breast",
+ fieldset_legend="Right breast",
+ field_label="Reconstruction",
+ )
+ self.expect_validation_error(
+ error_text="Select any other surgery they have had in the left breast",
+ fieldset_legend="Left breast",
+ field_label="Reconstruction",
+ )
+ self.expect_validation_error(
+ error_text="Select the reason for surgery",
+ fieldset_legend="Why was this surgery done?",
+ field_label="Risk reduction",
+ )
+
+ def when_i_provide_mastectomy_or_lumpectomy_details(self):
+ self.page.locator(
+ "input[name='right_breast_procedure'] + label", has_text="No procedure"
+ ).click()
+ self.page.locator(
+ "input[name='left_breast_procedure'] + label",
+ has_text="Mastectomy (no tissue remaining)",
+ ).click()
+ self.page.locator(
+ "input[name='right_breast_other_surgery'] + label",
+ has_text="Reconstruction",
+ ).click()
+ self.page.locator(
+ "input[name='right_breast_other_surgery'] + label",
+ has_text="Symmetrisation",
+ ).click()
+ self.page.locator(
+ "input[name='left_breast_other_surgery'] + label",
+ has_text="No other surgery",
+ ).click()
+ self.page.get_by_label("Year of surgery (optional)", exact=True).fill("2000")
+ self.page.get_by_label("Other reason", exact=True).click()
+ self.page.get_by_label("Provide details", exact=True).fill(
+ "Other reason for surgery text"
+ )
+ self.page.get_by_label("Additional details (optional)", exact=True).fill(
+ "additional details for test of mastectomy or lumpectomy details"
+ )
+
+ def and_i_click_save(self):
+ self.page.get_by_text("Save").click()
+
+ when_i_click_save = and_i_click_save
+
+ def then_i_am_back_on_the_medical_information_page(self):
+ self.expect_url("mammograms:record_medical_information", pk=self.appointment.pk)
+
+ def and_the_mastectomy_or_lumpectomy_is_listed(self):
+ key = self.page.locator(
+ ".nhsuk-summary-list__key",
+ has=self.page.get_by_text("Mastectomy or lumpectomy history", exact=True),
+ )
+ row = self.page.locator(".nhsuk-summary-list__row").filter(has=key)
+ expect(row).to_contain_text("Right breast: No procedure")
+ expect(row).to_contain_text("Left breast: Mastectomy (no tissue remaining)")
+ expect(row).to_contain_text("Right breast: Reconstruction, Symmetrisation")
+ expect(row).to_contain_text("Left breast: No other surgery")
+ expect(row).to_contain_text("2000")
+ expect(row).to_contain_text("Other reason")
+ expect(row).to_contain_text("Other reason for surgery text")
+ expect(row).to_contain_text(
+ "additional details for test of mastectomy or lumpectomy details"
+ )
+
+ def and_the_message_says_mastectomy_or_lumpectomy_added(self):
+ alert = self.page.get_by_role("alert")
+
+ expect(alert).to_contain_text("Success")
+ expect(alert).to_contain_text("Details of mastectomy or lumpectomy added")