diff --git a/manage_breast_screening/mammograms/forms/breast_cancer_history_form.py b/manage_breast_screening/mammograms/forms/breast_cancer_history_form.py
new file mode 100644
index 000000000..bb2ee0ea1
--- /dev/null
+++ b/manage_breast_screening/mammograms/forms/breast_cancer_history_form.py
@@ -0,0 +1,206 @@
+from django.db.models import TextChoices
+from django.forms import Textarea
+
+from manage_breast_screening.core.services.auditor import Auditor
+from manage_breast_screening.nhsuk_forms.fields.char_field import CharField
+from manage_breast_screening.nhsuk_forms.fields.choice_fields import (
+ ChoiceField,
+ MultipleChoiceField,
+)
+from manage_breast_screening.nhsuk_forms.fields.integer_field import IntegerField
+from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
+from manage_breast_screening.participants.models.breast_cancer_history_item import (
+ BreastCancerHistoryItem,
+)
+
+
+class BreastCancerHistoryForm(FormWithConditionalFields):
+ class DiagnosisLocationChoices(TextChoices):
+ RIGHT_BREAST = "RIGHT_BREAST", "Right breast"
+ LEFT_BREAST = "LEFT_BREAST", "Left breast"
+ DONT_KNOW = "DONT_KNOW", "Don't know"
+
+ diagnosis_location = MultipleChoiceField(
+ label="In which breasts was cancer diagnosed?",
+ choices=DiagnosisLocationChoices,
+ error_messages={"required": "Select which breasts cancer was diagnosed in"},
+ exclusive_choices={DiagnosisLocationChoices.DONT_KNOW},
+ )
+ # todo: constrain min/max
+ diagnosis_year = IntegerField(
+ label="Year of diagnosis (optional)",
+ label_classes="nhsuk-label--m",
+ classes="nhsuk-input--width-4",
+ hint="Leave blank if unknown",
+ required=False,
+ )
+
+ right_breast_procedure = ChoiceField(
+ label="Right breast (or axilla)",
+ visually_hidden_label_prefix="What procedure have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.Procedure,
+ error_messages={
+ "required": "Select which procedure they have had in the right breast"
+ },
+ )
+ left_breast_procedure = ChoiceField(
+ label="Left breast (or axilla)",
+ visually_hidden_label_prefix="What procedure have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.Procedure,
+ error_messages={
+ "required": "Select which procedure they have had in the left breast"
+ },
+ )
+
+ right_breast_other_surgery = MultipleChoiceField(
+ label="Right breast (or axilla)",
+ visually_hidden_label_prefix="What other surgery have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.Surgery,
+ exclusive_choices={BreastCancerHistoryItem.Surgery.NO_SURGERY},
+ error_messages={
+ "required": "Select any other surgery they have had in the right breast"
+ },
+ )
+ left_breast_other_surgery = MultipleChoiceField(
+ label="Left breast (or axilla)",
+ visually_hidden_label_prefix="What other surgery have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.Surgery,
+ exclusive_choices={BreastCancerHistoryItem.Surgery.NO_SURGERY},
+ error_messages={
+ "required": "Select any other surgery they have had in the left breast"
+ },
+ )
+
+ right_breast_treatment = MultipleChoiceField(
+ label="Right breast (or axilla)",
+ visually_hidden_label_prefix="What treatment have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.Treatment,
+ exclusive_choices={BreastCancerHistoryItem.Treatment.NO_RADIOTHERAPY},
+ error_messages={
+ "required": "Select what treatment they have had in the right breast"
+ },
+ )
+ left_breast_treatment = MultipleChoiceField(
+ label="Left breast (or axilla)",
+ visually_hidden_label_prefix="What treatment have they had in their ",
+ visually_hidden_label_suffix="?",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.Treatment,
+ exclusive_choices={BreastCancerHistoryItem.Treatment.NO_RADIOTHERAPY},
+ error_messages={
+ "required": "Select what treatment they have had in the left breast"
+ },
+ )
+
+ systemic_treatments = MultipleChoiceField(
+ visually_hidden_label_prefix="What treatment have they had that are ",
+ visually_hidden_label_suffix="?",
+ label="Systemic treatments",
+ label_classes="nhsuk-fieldset__legend--s",
+ choices=BreastCancerHistoryItem.SystemicTreatment,
+ exclusive_choices={
+ BreastCancerHistoryItem.SystemicTreatment.NO_SYSTEMIC_TREATMENTS
+ },
+ error_messages={"required": "Select what systemic treatments they have had"},
+ )
+ systemic_treatments_other_treatment_details = CharField(
+ label="Provide details",
+ required=False,
+ error_messages={"required": "Provide details of the other systemic treatment"},
+ )
+
+ intervention_location = ChoiceField(
+ label="Where did surgery and treatment take place?",
+ choices=BreastCancerHistoryItem.InterventionLocation,
+ error_messages={"required": "Select where surgery and treatment took place"},
+ )
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ self.given_field_value(
+ "systemic_treatments", BreastCancerHistoryItem.SystemicTreatment.OTHER
+ ).require_field("systemic_treatments_other_treatment_details")
+
+ for location_value in BreastCancerHistoryItem.InterventionLocation:
+ self.fields[f"intervention_location_details_{location_value.lower()}"] = (
+ CharField(
+ label="Provide details",
+ required=False,
+ error_messages={
+ "required": "Provide details about where the surgery and treatment took place"
+ },
+ )
+ )
+
+ self.fields["additional_details"] = CharField(
+ label="Additional details (optional)",
+ label_classes="nhsuk-label--m",
+ hint="Include any other relevant information about the treatment",
+ required=False,
+ widget=Textarea({"rows": 3}),
+ )
+
+ self.given_field("intervention_location").require_field_with_prefix(
+ "intervention_location_details"
+ )
+
+ def model_values(self):
+ match self.cleaned_data.get("diagnosis_location", []):
+ case [
+ self.DiagnosisLocationChoices.RIGHT_BREAST,
+ self.DiagnosisLocationChoices.LEFT_BREAST,
+ ]:
+ diagnosis_location = (
+ BreastCancerHistoryItem.DiagnosisLocationChoices.BOTH_BREASTS.value
+ )
+ case [other]:
+ diagnosis_location = other
+
+ location_value = self.cleaned_data["intervention_location"]
+
+ return dict(
+ diagnosis_location=diagnosis_location,
+ diagnosis_year=self.cleaned_data.get("diagnosis_year"),
+ right_breast_procedure=self.cleaned_data.get("right_breast_procedure"),
+ left_breast_procedure=self.cleaned_data.get("left_breast_procedure"),
+ right_breast_other_surgery=self.cleaned_data.get(
+ "right_breast_other_surgery"
+ ),
+ left_breast_other_surgery=self.cleaned_data.get(
+ "left_breast_other_surgery"
+ ),
+ right_breast_treatment=self.cleaned_data.get("right_breast_treatment"),
+ left_breast_treatment=self.cleaned_data.get("left_breast_treatment"),
+ systemic_treatments=self.cleaned_data.get("systemic_treatments"),
+ systemic_treatments_other_treatment_details=self.cleaned_data.get(
+ "systemic_treatments_other_treatment_details"
+ ),
+ intervention_location=location_value,
+ intervention_location_details=self.cleaned_data.get(
+ f"intervention_location_details_{location_value.lower()}"
+ ),
+ additional_details=self.cleaned_data.get("additional_details"),
+ )
+
+ def create(self, appointment, request):
+ auditor = Auditor.from_request(request)
+ field_values = self.model_values()
+
+ instance = appointment.breast_cancer_history_items.create(
+ **field_values,
+ )
+
+ auditor.audit_create(instance)
+
+ return instance
diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/breast_cancer_history_item_form.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/breast_cancer_history_item_form.jinja
new file mode 100644
index 000000000..23b5dd38b
--- /dev/null
+++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/breast_cancer_history_item_form.jinja
@@ -0,0 +1,65 @@
+{% extends "layout-form.jinja" %}
+{% from "nhsuk/components/button/macro.jinja" import button %}
+{% from "mammograms/medical_information/medical_history/forms/macros.jinja" import diagnosis_location_field %}
+
+{% block form %}
+
+ {{ diagnosis_location_field(form.diagnosis_location) }}
+
+ {{ form.diagnosis_year.as_field_group() }}
+
+ {# H2 hidden from AT as it's included in the fieldset legend of each set of radios #}
+
What procedure have they 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() }}
+
+
+
+ {# H2 hidden from AT as it's included in the fieldset legend of each set of checkboxes #}
+ What other surgery have they 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() }}
+
+
+
+ {# H2 hidden from AT as it's included in the fieldset legend of each set of checkboxes #}
+ What treatment have they had?
+
+
+
+ {% do form.right_breast_treatment.add_divider_after("LYMPH_NODE_RADIOTHERAPY", "or") %}
+ {{ form.right_breast_treatment.as_field_group() }}
+
+
+ {% do form.left_breast_treatment.add_divider_after("LYMPH_NODE_RADIOTHERAPY", "or") %}
+ {{ form.left_breast_treatment.as_field_group() }}
+
+
+
+ {% do form.systemic_treatments.add_divider_after("OTHER", "or") %}
+ {{ form.systemic_treatments.as_field_group() }}
+
+ {{ form.intervention_location.as_field_group() }}
+
+ {{ form.additional_details.as_field_group() }}
+
+
+ {{ button({
+ "text": "Save"
+ }) }}
+
+
+{% endblock %}
diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/macros.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/macros.jinja
new file mode 100644
index 000000000..a34204051
--- /dev/null
+++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/medical_history/forms/macros.jinja
@@ -0,0 +1,74 @@
+{% from "nhsuk/components/checkboxes/macro.jinja" import checkboxes %}
+{% from "nhsuk/components/fieldset/macro.jinja" import fieldset %}
+
+{# A single field with its options arranged on a grid. #}
+{# Taborder is left to right, top to bottom. #}
+{# Error messages belong to the field as a whole so are displayed above the grid. #}
+{% macro diagnosis_location_field(form_field) %}
+
+{% endmacro %}
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 4123f4904..0de679ada 100644
--- a/manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja
+++ b/manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja
@@ -37,6 +37,7 @@
{% for presented_item in presenter.breast_cancer_history %}
{{ summaryList(presented_item.summary_list_params) }}
{% endfor %}
+ {{ presenter.add_breast_cancer_history_link.text }}
{% endset %}
{% set mastectomy_or_lumpectomy_history_html %}
diff --git a/manage_breast_screening/mammograms/presenters/medical_information_presenter.py b/manage_breast_screening/mammograms/presenters/medical_information_presenter.py
index 32eaa2a70..5f954722b 100644
--- a/manage_breast_screening/mammograms/presenters/medical_information_presenter.py
+++ b/manage_breast_screening/mammograms/presenters/medical_information_presenter.py
@@ -150,6 +150,18 @@ def add_other_symptom_link(self):
),
}
+ @property
+ def add_breast_cancer_history_link(self):
+ url = reverse(
+ "mammograms:add_breast_cancer_history_item",
+ kwargs={"pk": self.appointment.pk},
+ )
+
+ return {
+ "href": url,
+ "text": ("Add breast cancer history"),
+ }
+
@property
def add_implanted_medical_device_history_link(self):
url = reverse(
diff --git a/manage_breast_screening/mammograms/tests/forms/test_breast_cancer_history_form.py b/manage_breast_screening/mammograms/tests/forms/test_breast_cancer_history_form.py
new file mode 100644
index 000000000..d6e467fde
--- /dev/null
+++ b/manage_breast_screening/mammograms/tests/forms/test_breast_cancer_history_form.py
@@ -0,0 +1,177 @@
+from urllib.parse import urlencode
+
+import pytest
+from django.forms import model_to_dict
+from django.http import QueryDict
+from django.test import RequestFactory
+
+from manage_breast_screening.mammograms.forms.breast_cancer_history_form import (
+ BreastCancerHistoryForm,
+)
+from manage_breast_screening.participants.tests.factories import AppointmentFactory
+
+
+@pytest.fixture
+def appointment():
+ return AppointmentFactory()
+
+
+@pytest.fixture
+def incoming_request(clinical_user):
+ request = RequestFactory().get("/test-form")
+ request.user = clinical_user
+ return request
+
+
+class TestBreastCancerHistoryForm:
+ def test_no_data_not_valid(self):
+ form = BreastCancerHistoryForm(data=QueryDict())
+ assert not form.is_valid()
+ assert form.errors == {
+ "diagnosis_location": ["Select which breasts cancer was diagnosed in"],
+ "intervention_location": ["Select where surgery and treatment took place"],
+ "left_breast_other_surgery": [
+ "Select any other surgery they have had in the left breast"
+ ],
+ "left_breast_procedure": [
+ "Select which procedure they have had in the left breast"
+ ],
+ "left_breast_treatment": [
+ "Select what treatment they have had in the left breast"
+ ],
+ "right_breast_other_surgery": [
+ "Select any other surgery they have had in the right breast"
+ ],
+ "right_breast_procedure": [
+ "Select which procedure they have had in the right breast"
+ ],
+ "right_breast_treatment": [
+ "Select what treatment they have had in the right breast"
+ ],
+ "systemic_treatments": ["Select what systemic treatments they have had"],
+ }
+
+ def test_valid_form(self):
+ form = BreastCancerHistoryForm(
+ data=QueryDict(
+ urlencode(
+ {
+ "diagnosis_location": "RIGHT_BREAST",
+ "intervention_location": "NHS_HOSPITAL",
+ "intervention_location_details_nhs_hospital": "abc",
+ "left_breast_other_surgery": "NO_SURGERY",
+ "left_breast_procedure": "NO_PROCEDURE",
+ "left_breast_treatment": "NO_RADIOTHERAPY",
+ "right_breast_other_surgery": "LYMPH_NODE_SURGERY",
+ "right_breast_procedure": "LUMPECTOMY",
+ "right_breast_treatment": "BREAST_RADIOTHERAPY",
+ "systemic_treatments": "NO_SYSTEMIC_TREATMENTS",
+ }
+ )
+ )
+ )
+
+ assert form.is_valid(), form.errors
+
+ def test_missing_intervention_location_details(self):
+ form = BreastCancerHistoryForm(
+ data=QueryDict(
+ urlencode(
+ {
+ "diagnosis_location": "RIGHT_BREAST",
+ "intervention_location": "NHS_HOSPITAL",
+ "left_breast_other_surgery": "NO_SURGERY",
+ "left_breast_procedure": "NO_PROCEDURE",
+ "left_breast_treatment": "NO_RADIOTHERAPY",
+ "right_breast_other_surgery": "LYMPH_NODE_SURGERY",
+ "right_breast_procedure": "LUMPECTOMY",
+ "right_breast_treatment": "BREAST_RADIOTHERAPY",
+ "systemic_treatments": "NO_SYSTEMIC_TREATMENTS",
+ }
+ )
+ )
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "intervention_location_details_nhs_hospital": [
+ "Provide details about where the surgery and treatment took place"
+ ],
+ }
+
+ def test_missing_systemic_treatments_other_treatment_details(self):
+ form = BreastCancerHistoryForm(
+ data=QueryDict(
+ urlencode(
+ {
+ "diagnosis_location": "RIGHT_BREAST",
+ "intervention_location": "NHS_HOSPITAL",
+ "intervention_location_details_nhs_hospital": "abc",
+ "left_breast_other_surgery": "NO_SURGERY",
+ "left_breast_procedure": "NO_PROCEDURE",
+ "left_breast_treatment": "NO_RADIOTHERAPY",
+ "right_breast_other_surgery": "LYMPH_NODE_SURGERY",
+ "right_breast_procedure": "LUMPECTOMY",
+ "right_breast_treatment": "BREAST_RADIOTHERAPY",
+ "systemic_treatments": "OTHER",
+ }
+ )
+ )
+ )
+
+ assert not form.is_valid()
+ assert form.errors == {
+ "systemic_treatments_other_treatment_details": [
+ "Provide details of the other systemic treatment"
+ ]
+ }
+
+ @pytest.mark.django_db
+ def test_create(self, appointment, incoming_request):
+ form = BreastCancerHistoryForm(
+ data=QueryDict(
+ urlencode(
+ {
+ "diagnosis_location": "RIGHT_BREAST",
+ "intervention_location": "NHS_HOSPITAL",
+ "intervention_location_details_nhs_hospital": "abc",
+ "left_breast_other_surgery": "NO_SURGERY",
+ "left_breast_procedure": "NO_PROCEDURE",
+ "left_breast_treatment": "NO_RADIOTHERAPY",
+ "right_breast_other_surgery": "LYMPH_NODE_SURGERY",
+ "right_breast_procedure": "LUMPECTOMY",
+ "right_breast_treatment": "BREAST_RADIOTHERAPY",
+ "systemic_treatments": "NO_SYSTEMIC_TREATMENTS",
+ }
+ )
+ )
+ )
+ assert form.is_valid()
+ instance = form.create(appointment, incoming_request)
+
+ assert model_to_dict(instance) == {
+ "additional_details": "",
+ "appointment": appointment.pk,
+ "diagnosis_location": "RIGHT_BREAST",
+ "diagnosis_year": None,
+ "intervention_location": "NHS_HOSPITAL",
+ "intervention_location_details": "abc",
+ "left_breast_other_surgery": [
+ "NO_SURGERY",
+ ],
+ "left_breast_procedure": "NO_PROCEDURE",
+ "left_breast_treatment": [
+ "NO_RADIOTHERAPY",
+ ],
+ "right_breast_other_surgery": [
+ "LYMPH_NODE_SURGERY",
+ ],
+ "right_breast_procedure": "LUMPECTOMY",
+ "right_breast_treatment": [
+ "BREAST_RADIOTHERAPY",
+ ],
+ "systemic_treatments": [
+ "NO_SYSTEMIC_TREATMENTS",
+ ],
+ "systemic_treatments_other_treatment_details": "",
+ }
diff --git a/manage_breast_screening/mammograms/tests/presenters/test_breast_cancer_history_item_presenter.py b/manage_breast_screening/mammograms/tests/presenters/test_breast_cancer_history_item_presenter.py
index 05de1d485..797d5bfc1 100644
--- a/manage_breast_screening/mammograms/tests/presenters/test_breast_cancer_history_item_presenter.py
+++ b/manage_breast_screening/mammograms/tests/presenters/test_breast_cancer_history_item_presenter.py
@@ -44,7 +44,7 @@ def test_single(self):
"text": "Other surgery",
},
"value": {
- "html": "Right breast: No surgery
Left breast: No surgery",
+ "html": "Right breast: No other surgery
Left breast: No other surgery",
},
},
{
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 fef0481c1..a55c2eed1 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
@@ -114,6 +114,16 @@ def test_add_nipple_change_link(self):
"text": "Add another nipple change",
}
+ def test_add_breast_cancer_history_link(self):
+ appointment = AppointmentFactory()
+
+ assert MedicalInformationPresenter(
+ appointment
+ ).add_breast_cancer_history_link == {
+ "href": f"/mammograms/{appointment.pk}/record-medical-information/breast-cancer-history/",
+ "text": "Add breast cancer history",
+ }
+
def test_implanted_medical_device_history_link(self):
appointment = AppointmentFactory()
diff --git a/manage_breast_screening/mammograms/tests/views/test_breast_cancer_history_views.py b/manage_breast_screening/mammograms/tests/views/test_breast_cancer_history_views.py
new file mode 100644
index 000000000..6f4a8811b
--- /dev/null
+++ b/manage_breast_screening/mammograms/tests/views/test_breast_cancer_history_views.py
@@ -0,0 +1,92 @@
+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.tests.factories import AppointmentFactory
+
+
+@pytest.mark.django_db
+class TestBreastCancerHistoryViews:
+ 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_breast_cancer_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_breast_cancer_history_item",
+ kwargs={"pk": appointment.pk},
+ ),
+ {
+ "diagnosis_location": "RIGHT_BREAST",
+ "intervention_location": "NHS_HOSPITAL",
+ "intervention_location_details_nhs_hospital": "abc",
+ "left_breast_other_surgery": "NO_SURGERY",
+ "left_breast_procedure": "NO_PROCEDURE",
+ "left_breast_treatment": "NO_RADIOTHERAPY",
+ "right_breast_other_surgery": "LYMPH_NODE_SURGERY",
+ "right_breast_procedure": "LUMPECTOMY",
+ "right_breast_treatment": "BREAST_RADIOTHERAPY",
+ "systemic_treatments": "NO_SYSTEMIC_TREATMENTS",
+ },
+ )
+
+ assertRedirects(
+ response,
+ reverse(
+ "mammograms:record_medical_information",
+ kwargs={"pk": appointment.pk},
+ ),
+ )
+ assertMessages(
+ response,
+ [
+ messages.Message(
+ level=messages.SUCCESS,
+ message="Breast cancer history 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_breast_cancer_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 d7460b7d2..0388a4574 100644
--- a/manage_breast_screening/mammograms/urls.py
+++ b/manage_breast_screening/mammograms/urls.py
@@ -3,6 +3,7 @@
from .views import (
appointment_views,
breast_augmentation_history_view,
+ breast_cancer_history_views,
cyst_history_view,
implanted_medical_device_history_view,
special_appointment_views,
@@ -122,6 +123,11 @@
symptom_views.DeleteSymptomView.as_view(),
name="delete_symptom",
),
+ path(
+ "/record-medical-information/breast-cancer-history/",
+ breast_cancer_history_views.AddBreastCancerHistoryView.as_view(),
+ name="add_breast_cancer_history_item",
+ ),
path(
"/record-medical-information/implanted-medical-device-history/",
implanted_medical_device_history_view.AddImplantedMedicalDeviceHistoryView.as_view(),
diff --git a/manage_breast_screening/mammograms/views/breast_cancer_history_views.py b/manage_breast_screening/mammograms/views/breast_cancer_history_views.py
new file mode 100644
index 000000000..9841efbbc
--- /dev/null
+++ b/manage_breast_screening/mammograms/views/breast_cancer_history_views.py
@@ -0,0 +1,55 @@
+from django.contrib import messages
+from django.urls import reverse
+from django.views.generic import FormView
+
+from manage_breast_screening.mammograms.forms.breast_cancer_history_form import (
+ BreastCancerHistoryForm,
+)
+
+from .mixins import InProgressAppointmentMixin
+
+
+class AddBreastCancerHistoryView(InProgressAppointmentMixin, FormView):
+ form_class = BreastCancerHistoryForm
+ template_name = "mammograms/medical_information/medical_history/forms/breast_cancer_history_item_form.jinja"
+
+ def form_valid(self, form):
+ form.create(appointment=self.appointment, request=self.request)
+
+ messages.add_message(
+ self.request,
+ messages.SUCCESS,
+ "Breast cancer history added",
+ )
+
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse(
+ "mammograms:record_medical_information", kwargs={"pk": self.appointment.pk}
+ )
+
+ def get_back_link_params(self):
+ return {
+ "href": reverse(
+ "mammograms:record_medical_information",
+ kwargs={"pk": self.appointment_pk},
+ ),
+ "text": "Back",
+ }
+
+ 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,
+ "heading": "Add details of breast cancer",
+ "page_title": "Add details of breast cancer",
+ },
+ )
+
+ return context
diff --git a/manage_breast_screening/nhsuk_forms/fields/choice_fields.py b/manage_breast_screening/nhsuk_forms/fields/choice_fields.py
index 04474529f..78841fcb6 100644
--- a/manage_breast_screening/nhsuk_forms/fields/choice_fields.py
+++ b/manage_breast_screening/nhsuk_forms/fields/choice_fields.py
@@ -1,6 +1,7 @@
from django import forms
from django.forms import widgets
+from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
from manage_breast_screening.nhsuk_forms.validators import ExcludesOtherOptionsValidator
@@ -34,7 +35,14 @@ def add_conditional_html(self, value, html):
self._conditional_html[value] = html
def conditional_html(self, value):
- return self._conditional_html.get(value)
+ explicitly_set_html = self._conditional_html.get(value)
+ if explicitly_set_html:
+ return explicitly_set_html
+
+ if isinstance(self.form, FormWithConditionalFields):
+ return self.form.conditionally_shown_html(self.name, value)
+
+ return None
def add_divider_after(self, previous, divider):
self.dividers[previous] = divider
diff --git a/manage_breast_screening/nhsuk_forms/forms.py b/manage_breast_screening/nhsuk_forms/forms.py
index 0c6b2d863..6ba83b2bb 100644
--- a/manage_breast_screening/nhsuk_forms/forms.py
+++ b/manage_breast_screening/nhsuk_forms/forms.py
@@ -46,7 +46,7 @@ def require_field(self, conditionally_required_field):
)
-class ConditionalFieldValidator:
+class ConditionalFieldDeclarations:
"""
Helper class to perform the conditional validation for the FormWithConditionalFields
"""
@@ -103,6 +103,15 @@ def clean_conditional_fields(self):
),
)
+ def conditionally_required_fields(self, predicate_field, predicate_field_value):
+ matches = [
+ requirement
+ for requirement in self.conditional_requirements
+ if requirement.predicate_field == predicate_field
+ and requirement.predicate_field_value == predicate_field_value
+ ]
+ return matches
+
class FormWithConditionalFields(Form):
"""
@@ -116,10 +125,29 @@ class FormWithConditionalFields(Form):
"""
def __init__(self, *args, **kwargs):
- self.conditional_field_validator = ConditionalFieldValidator(self)
+ self.conditional_field_declarations = ConditionalFieldDeclarations(self)
super().__init__(*args, **kwargs)
+ def conditionally_shown_html(self, field, value):
+ """
+ If a single field is conditionally shown when `field` is set to `value`,
+ then return the HTML for that field.
+
+ If there is no conditional logic, return None.
+
+ If there are multiple conditional fields that get shown, also return None,
+ (it is up to the template to decide how to combine them).
+ """
+ conditional_fields = (
+ self.conditional_field_declarations.conditionally_required_fields(
+ field, value
+ )
+ )
+ if len(conditional_fields) == 1:
+ field_name = conditional_fields[0].conditionally_required_field
+ return self[field_name].as_field_group()
+
def given_field_value(self, field, field_value):
"""
Mini-DSL to declare conditional field relationships
@@ -127,7 +155,7 @@ def given_field_value(self, field, field_value):
e.g. self.given_field_value('foo', 'choice1').require_field('other_details')
"""
return FieldValuePredicate(
- self.conditional_field_validator, field=field, field_value=field_value
+ self.conditional_field_declarations, field=field, field_value=field_value
)
def given_field(self, predicate_field):
@@ -137,7 +165,7 @@ def given_field(self, predicate_field):
e.g. self.given_field('foo').require_field_with_prefix('other')
"""
return FieldPredicate(
- self.conditional_field_validator,
+ self.conditional_field_declarations,
field_name=predicate_field,
field_choices=self.fields[predicate_field].choices,
)
@@ -148,10 +176,10 @@ def clean_conditional_fields(self):
This can happen when the user selects one option, fills out the conditional field, and then changes
to a different option.
"""
- return self.conditional_field_validator.clean_conditional_fields()
+ return self.conditional_field_declarations.clean_conditional_fields()
def full_clean(self):
- for requirement in self.conditional_field_validator.conditional_requirements:
+ for requirement in self.conditional_field_declarations.conditional_requirements:
field_name = requirement.conditionally_required_field
predicate_field_values = self.data.getlist(requirement.predicate_field)
if not predicate_field_values:
diff --git a/manage_breast_screening/nhsuk_forms/tests/fields/test_choice_fields.py b/manage_breast_screening/nhsuk_forms/tests/fields/test_choice_fields.py
index 629c33d59..7d5689870 100644
--- a/manage_breast_screening/nhsuk_forms/tests/fields/test_choice_fields.py
+++ b/manage_breast_screening/nhsuk_forms/tests/fields/test_choice_fields.py
@@ -8,6 +8,7 @@
CheckboxSelectMultipleWithoutFieldset,
RadioSelectWithoutFieldset,
)
+from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
from ...fields import CharField, ChoiceField, MultipleChoiceField
@@ -40,6 +41,23 @@ class TestForm(Form):
return TestForm
+ @pytest.fixture
+ def conditional_form_class(self):
+ class TestForm(FormWithConditionalFields):
+ predicate = ChoiceField(
+ label="Abc",
+ label_classes="app-abc",
+ choices=(("a", "A"), ("b", "B")),
+ hint="Pick either one",
+ )
+ details = CharField()
+
+ def __init__(self):
+ super().__init__()
+ self.given_field_value("predicate", "b").require_field("details")
+
+ return TestForm
+
def test_renders_nhs_radios(self, form_class):
assertHTMLEqual(
form_class()["field"].as_field_group(),
@@ -67,7 +85,37 @@ def test_renders_nhs_radios(self, form_class):
""",
)
- def test_renders_radios_with_conditional_html(self, form_class):
+ def test_renders_radios_with_conditional_field(self, conditional_form_class):
+ form = conditional_form_class()
+
+ assertHTMLEqual(
+ form["predicate"].as_field_group(),
+ """
+