From 194faa5ff1b9007b60b6b461d619aadb2ce285d1 Mon Sep 17 00:00:00 2001 From: Steve Webber <198724792+swebberuk@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:21:50 +0100 Subject: [PATCH 1/3] Allow symptoms to be highlighted to image readers Add option for breast pain and 'other' symptoms to be highlighted to readers Display info text for symptoms that are always highlighted to readers Add highlight_to_readers column to participants_symptom Use 'Highlight to image readers' tag on medical info page Add heading_description to skin and nipple change templates --- .../mammograms/forms/symptom_forms.py | 18 ++++++ .../symptoms/forms/breast_pain.jinja | 2 + .../symptoms/forms/nipple_change.jinja | 6 ++ .../symptoms/forms/other.jinja | 2 + .../symptoms/forms/skin_change.jinja | 6 ++ .../presenters/symptom_presenter.py | 13 ++++- .../tests/forms/test_symptom_forms.py | 13 +++++ .../test_medical_information_presenter.py | 55 +++++++++++++++++- .../presenters/test_symptom_presenter.py | 39 ++++++++++++- .../tests/views/test_symptom_views.py | 57 +++++++++++++++++++ .../mammograms/views/symptom_views.py | 23 +++----- .../0068_symptom_highlight_to_readers.py | 18 ++++++ .../participants/migrations/max_migration.txt | 2 +- .../participants/models/symptom.py | 7 +++ .../participants/tests/factories.py | 3 + .../clinical/test_recording_symptoms.py | 8 +-- 16 files changed, 246 insertions(+), 26 deletions(-) create mode 100644 manage_breast_screening/participants/migrations/0068_symptom_highlight_to_readers.py diff --git a/manage_breast_screening/mammograms/forms/symptom_forms.py b/manage_breast_screening/mammograms/forms/symptom_forms.py index d5ffbbaa0..1b51ff36d 100644 --- a/manage_breast_screening/mammograms/forms/symptom_forms.py +++ b/manage_breast_screening/mammograms/forms/symptom_forms.py @@ -19,6 +19,7 @@ from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields from manage_breast_screening.nhsuk_forms.utils import YesNo, yes_no, yes_no_field from manage_breast_screening.participants.models.symptom import ( + HighlightToReaderChoices, NippleChangeChoices, RelativeDateChoices, SkinChangeChoices, @@ -80,6 +81,13 @@ class CommonFields: widget=Textarea(attrs={"rows": 5}), error_messages={"required": "Enter details of any investigations"}, ) + highlight_to_readers = ChoiceField( + choices=HighlightToReaderChoices, + label="Highlight this symptom to readers?", + error_messages={ + "required": "Select whether this symptom should be highlighted to image readers" + }, + ) additional_information = CharField( required=False, label="Additional info (optional)", @@ -151,6 +159,9 @@ def initial_values(self, instance): "when_resolved": instance.when_resolved, "investigated": yes_no(instance.investigated), "investigation_details": instance.investigation_details, + "highlight_to_readers": HighlightToReaderChoices.YES + if instance.highlight_to_readers + else HighlightToReaderChoices.NO, "additional_information": instance.additional_information, } @@ -184,6 +195,10 @@ def model_values(self): intermittent = self.cleaned_data.get("intermittent", False) recently_resolved = self.cleaned_data.get("recently_resolved", False) when_resolved = self.cleaned_data.get("when_resolved", "") + highlight_to_readers = ( + self.cleaned_data.get("highlight_to_readers", HighlightToReaderChoices.YES) + == HighlightToReaderChoices.YES + ) additional_information = self.cleaned_data.get("additional_information", "") return dict( @@ -199,6 +214,7 @@ def model_values(self): intermittent=intermittent, recently_resolved=recently_resolved, when_resolved=when_resolved, + highlight_to_readers=highlight_to_readers, additional_information=additional_information, ) @@ -470,6 +486,7 @@ class OtherSymptomForm(SymptomForm): when_resolved = CommonFields.when_resolved investigated = CommonFields.investigated investigation_details = CommonFields.investigation_details + highlight_to_readers = CommonFields.highlight_to_readers additional_information = CommonFields.additional_information def __init__(self, instance=None, **kwargs): @@ -514,6 +531,7 @@ class BreastPainForm(SymptomForm): when_resolved = CommonFields.when_resolved investigated = CommonFields.investigated investigation_details = CommonFields.investigation_details + highlight_to_readers = CommonFields.highlight_to_readers additional_information = CommonFields.additional_information def __init__(self, instance=None, **kwargs): diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/breast_pain.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/breast_pain.jinja index ccc32eddd..4ab81904d 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/breast_pain.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/breast_pain.jinja @@ -30,6 +30,8 @@ {{ form.investigated.as_field_group() }} + {{ form.highlight_to_readers.as_field_group() }} + {{ form.additional_information.as_field_group() }}
diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/nipple_change.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/nipple_change.jinja index e5b6616eb..79d1065a4 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/nipple_change.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/nipple_change.jinja @@ -3,6 +3,12 @@ {% from "nhsuk/components/fieldset/macro.jinja" import fieldset %} {% block form %} + {% if heading_description %} +
+

{{ heading_description }}

+
+ {% endif %} + {% do form.symptom_sub_type.add_conditional_html('NIPPLE_CHANGE_OTHER', form.symptom_sub_type_details.as_field_group()) %} {% do form.when_started.add_divider_after("OVER_THREE_YEARS", "or") %} diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/other.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/other.jinja index c4e781c46..bd68cd981 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/other.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/other.jinja @@ -32,6 +32,8 @@ {{ form.investigated.as_field_group() }} + {{ form.highlight_to_readers.as_field_group() }} + {{ form.additional_information.as_field_group() }}
diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/skin_change.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/skin_change.jinja index 9f8c9d502..526696de9 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/skin_change.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/forms/skin_change.jinja @@ -3,6 +3,12 @@ {% from "nhsuk/components/fieldset/macro.jinja" import fieldset %} {% block form %} + {% if heading_description %} +
+

{{ heading_description }}

+
+ {% endif %} + {% include "mammograms/medical_information/symptoms/forms/inline_area_radios.jinja" %} {% do form.symptom_sub_type.add_conditional_html('SKIN_CHANGE_OTHER', form.symptom_sub_type_details.as_field_group()) %} diff --git a/manage_breast_screening/mammograms/presenters/symptom_presenter.py b/manage_breast_screening/mammograms/presenters/symptom_presenter.py index 4ac80c590..0db9e1d9a 100644 --- a/manage_breast_screening/mammograms/presenters/symptom_presenter.py +++ b/manage_breast_screening/mammograms/presenters/symptom_presenter.py @@ -1,5 +1,5 @@ from django.urls import reverse -from django.utils.html import escape +from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from manage_breast_screening.core.template_helpers import ( @@ -139,8 +139,17 @@ def build_summary_list_row(self, include_actions=True): ] ) + if self._symptom.highlight_to_readers: + key = { + "html": format_html( + '{}
Highlight to image readers', + self._symptom.symptom_type.name, + ) + } + else: + key = {"text": self._symptom.symptom_type.name} result = { - "key": {"text": self._symptom.symptom_type.name}, + "key": key, "value": {"html": html}, } diff --git a/manage_breast_screening/mammograms/tests/forms/test_symptom_forms.py b/manage_breast_screening/mammograms/tests/forms/test_symptom_forms.py index 71a228e02..8f7d2f462 100644 --- a/manage_breast_screening/mammograms/tests/forms/test_symptom_forms.py +++ b/manage_breast_screening/mammograms/tests/forms/test_symptom_forms.py @@ -19,6 +19,7 @@ ) from manage_breast_screening.nhsuk_forms.choices import YesNo from manage_breast_screening.participants.models.symptom import ( + HighlightToReaderChoices, NippleChangeChoices, SkinChangeChoices, SymptomAreas, @@ -588,6 +589,7 @@ def test_valid_form(self): "symptom_sub_type": SkinChangeChoices.COLOUR_CHANGE, "when_started": RelativeDateChoices.LESS_THAN_THREE_MONTHS, "investigated": YesNo.NO, + "highlight_to_readers": HighlightToReaderChoices.YES, } ) ) @@ -603,6 +605,9 @@ def test_missing_required_fields(self): "symptom_sub_type_details": ["Enter a description of the symptom"], "investigated": ["Select whether the symptom has been investigated or not"], "area": ["Select the location of the symptom"], + "highlight_to_readers": [ + "Select whether this symptom should be highlighted to image readers" + ], } def test_missing_conditionally_required_fields(self): @@ -614,6 +619,7 @@ def test_missing_conditionally_required_fields(self): "symptom_sub_type_details": "abc symptom", "when_started": RelativeDateChoices.SINCE_A_SPECIFIC_DATE, "investigated": YesNo.YES, + "highlight_to_readers": HighlightToReaderChoices.YES, } ) ) @@ -641,6 +647,7 @@ def test_valid_form_with_conditionally_required_fields(self): "specific_date_0": "2", "specific_date_1": "2025", "investigation_details": "def", + "highlight_to_readers": HighlightToReaderChoices.YES, } ) ) @@ -658,6 +665,7 @@ def test_valid_form(self): "area_description_left_breast": "uoq", "when_started": RelativeDateChoices.LESS_THAN_THREE_MONTHS, "investigated": YesNo.NO, + "highlight_to_readers": HighlightToReaderChoices.YES, } ) ) @@ -672,6 +680,9 @@ def test_missing_required_fields(self): "when_started": ["Select how long the symptom has existed"], "investigated": ["Select whether the symptom has been investigated or not"], "area": ["Select the location of the pain"], + "highlight_to_readers": [ + "Select whether this symptom should be highlighted to image readers" + ], } def test_missing_conditionally_required_fields(self): @@ -683,6 +694,7 @@ def test_missing_conditionally_required_fields(self): "when_started": RelativeDateChoices.SINCE_A_SPECIFIC_DATE, "investigated": YesNo.YES, "recently_resolved": True, + "highlight_to_readers": HighlightToReaderChoices.YES, } ) ) @@ -712,6 +724,7 @@ def test_valid_form_with_conditionally_required_fields(self): "investigation_details": "def", "recently_resolved": True, "when_resolved": "3 months ago", + "highlight_to_readers": HighlightToReaderChoices.YES, } ) ) 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 0dce526cc..e0b2a12ed 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 @@ -51,6 +51,23 @@ def test_formats_symptoms_summary_list(self): area=SymptomAreas.BOTH_BREASTS, ) + symptom3 = SymptomFactory.create( + other=True, + appointment=appointment, + when_started=RelativeDateChoices.LESS_THAN_THREE_MONTHS, + area=SymptomAreas.RIGHT_BREAST, + symptom_sub_type_details="abc", + ) + + symptom4 = SymptomFactory.create( + other=True, + appointment=appointment, + when_started=RelativeDateChoices.LESS_THAN_THREE_MONTHS, + area=SymptomAreas.LEFT_BREAST, + highlight_to_readers=False, + symptom_sub_type_details="xyz", + ) + presenter = MedicalInformationPresenter(appointment) assert presenter.symptom_rows == [ @@ -66,12 +83,46 @@ def test_formats_symptoms_summary_list(self): ], }, "key": { - "text": "Lump", + "html": 'Lump
Highlight to image readers', }, "value": { "html": "Left breast
Not sure
Symptom is intermittent
Stopped: resolved date
Not investigated
Additional information: abc", }, }, + { + "actions": { + "items": [ + { + "text": "Change", + "classes": "nhsuk-link--no-visited-state", + "visuallyHiddenText": "other", + "href": f"/mammograms/{appointment.id}/record-medical-information/other/{symptom3.id}/", + }, + ], + }, + "key": { + "html": 'Other
Highlight to image readers' + }, + "value": { + "html": "Description: abc
Right breast
Less than 3 months ago
Not investigated" + }, + }, + { + "actions": { + "items": [ + { + "text": "Change", + "classes": "nhsuk-link--no-visited-state", + "visuallyHiddenText": "other", + "href": f"/mammograms/{appointment.id}/record-medical-information/other/{symptom4.id}/", + } + ] + }, + "key": {"text": "Other"}, + "value": { + "html": "Description: xyz
Left breast
Less than 3 months ago
Not investigated" + }, + }, { "actions": { "items": [ @@ -84,7 +135,7 @@ def test_formats_symptoms_summary_list(self): ], }, "key": { - "text": "Swelling or shape change", + "html": 'Swelling or shape change
Highlight to image readers', }, "value": { "html": "Both breasts
Less than 3 months ago
Not investigated", diff --git a/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py b/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py index 85a61aad7..46fde1eff 100644 --- a/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py +++ b/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py @@ -135,7 +135,7 @@ def test_formats_intermittent_stopped_and_additional_information(self): assert presenter.intermittent_line == "Symptom is intermittent" assert presenter.additional_information_line == "Additional information: abc" - def test_formats_for_summary_list(self): + def test_formats_lump_for_summary_list(self): symptom = SymptomFactory.create( lump=True, when_started=RelativeDateChoices.NOT_SURE, @@ -159,13 +159,48 @@ def test_formats_for_summary_list(self): ], }, "key": { - "text": "Lump", + "html": 'Lump
Highlight to image readers', }, "value": { "html": "Left breast
Not sure
Symptom is intermittent
Stopped: resolved date
Not investigated
Additional information: abc", }, } + @pytest.mark.parametrize("highlight_to_readers", [True, False]) + def test_formats_other_for_summary_list(self, highlight_to_readers): + symptom = SymptomFactory.create( + breast_pain=True, + when_started=RelativeDateChoices.LESS_THAN_THREE_MONTHS, + area=SymptomAreas.LEFT_BREAST, + highlight_to_readers=highlight_to_readers, + ) + + presenter = SymptomPresenter(symptom) + + if highlight_to_readers: + expected_key = { + "html": 'Breast pain
Highlight to image readers', + } + else: + expected_key = {"text": "Breast pain"} + + assert presenter.summary_list_row == { + "actions": { + "items": [ + { + "text": "Change", + "classes": "nhsuk-link--no-visited-state", + "visuallyHiddenText": "breast pain", + "href": f"/mammograms/{symptom.appointment_id}/record-medical-information/breast-pain/{symptom.id}/", + } + ] + }, + "key": expected_key, + "value": { + "html": "Left breast
Less than 3 months ago
Not investigated" + }, + } + def test_delete_message_html(self): lump = SymptomFactory.create(lump=True) presenter = SymptomPresenter(lump) diff --git a/manage_breast_screening/mammograms/tests/views/test_symptom_views.py b/manage_breast_screening/mammograms/tests/views/test_symptom_views.py index e6ed3608e..9a5f43942 100644 --- a/manage_breast_screening/mammograms/tests/views/test_symptom_views.py +++ b/manage_breast_screening/mammograms/tests/views/test_symptom_views.py @@ -5,6 +5,7 @@ from manage_breast_screening.nhsuk_forms.choices import YesNo from manage_breast_screening.participants.models.symptom import ( + HighlightToReaderChoices, NippleChangeChoices, RelativeDateChoices, SkinChangeChoices, @@ -34,6 +35,10 @@ def test_renders_response( ) ) assert response.status_code == 200 + assert ( + "Lumps are a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + in response.content.decode() + ) def test_valid_post_redirects_to_appointment( self, clinical_user_client, confirmed_identity_appointment @@ -112,6 +117,10 @@ def test_renders_response(self, clinical_user_client, lump): ) ) assert response.status_code == 200 + assert ( + "Lumps are a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + in response.content.decode() + ) def test_non_existant_or_deleted_symptom_id_is_a_404( self, clinical_user_client, confirmed_identity_appointment @@ -232,6 +241,10 @@ def test_renders_response( ) ) assert response.status_code == 200 + assert ( + "Skin change is a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + in response.content.decode() + ) def test_valid_post_redirects_to_appointment( self, clinical_user_client, confirmed_identity_appointment @@ -277,6 +290,10 @@ def test_renders_response(self, clinical_user_client, colour_change): ) ) assert response.status_code == 200 + assert ( + "Skin change is a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + in response.content.decode() + ) def test_valid_post_redirects_to_appointment( self, clinical_user_client, colour_change @@ -318,6 +335,10 @@ def test_renders_response( ) ) assert response.status_code == 200 + assert ( + "Nipple change is a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + in response.content.decode() + ) def test_valid_post_redirects_to_appointment( self, clinical_user_client, confirmed_identity_appointment @@ -359,6 +380,10 @@ def test_renders_response(self, clinical_user_client, inversion): ) ) assert response.status_code == 200 + assert ( + "Nipple change is a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + in response.content.decode() + ) def test_valid_post_redirects_to_appointment(self, clinical_user_client, inversion): response = clinical_user_client.http.post( @@ -395,6 +420,13 @@ def test_renders_response( ) assert response.status_code == 200 + content = response.content.decode() + assert ( + "Information recorded here will be highlighted during image reading." + not in content + ) + assert "Highlight this symptom to readers?" in content + def test_valid_post_redirects_to_appointment( self, clinical_user_client, confirmed_identity_appointment ): @@ -409,6 +441,7 @@ def test_valid_post_redirects_to_appointment( "symptom_sub_type_details": "abc", "when_started": RelativeDateChoices.LESS_THAN_THREE_MONTHS.value, "investigated": YesNo.NO.value, + "highlight_to_readers": HighlightToReaderChoices.YES.value, }, ) assertRedirects( @@ -440,6 +473,13 @@ def test_renders_response(self, clinical_user_client, other_symptom): ) assert response.status_code == 200 + content = response.content.decode() + assert ( + "Information recorded here will be highlighted during image reading." + not in content + ) + assert "Highlight this symptom to readers?" in content + def test_valid_post_redirects_to_appointment( self, clinical_user_client, other_symptom ): @@ -457,6 +497,7 @@ def test_valid_post_redirects_to_appointment( "symptom_sub_type_details": "abc", "when_started": RelativeDateChoices.LESS_THAN_THREE_MONTHS.value, "investigated": YesNo.NO.value, + "highlight_to_readers": HighlightToReaderChoices.YES.value, }, ) assertRedirects( @@ -481,6 +522,13 @@ def test_renders_response( ) assert response.status_code == 200 + content = response.content.decode() + assert ( + "Information recorded here will be highlighted during image reading." + not in content + ) + assert "Highlight this symptom to readers?" in content + def test_valid_post_redirects_to_appointment( self, clinical_user_client, confirmed_identity_appointment ): @@ -494,6 +542,7 @@ def test_valid_post_redirects_to_appointment( "area_description_right_breast": "uiq", "when_started": RelativeDateChoices.LESS_THAN_THREE_MONTHS.value, "investigated": YesNo.NO.value, + "highlight_to_readers": HighlightToReaderChoices.YES.value, }, ) assertRedirects( @@ -526,6 +575,13 @@ def test_renders_response(self, clinical_user_client, breast_pain): ) assert response.status_code == 200 + content = response.content.decode() + assert ( + "Information recorded here will be highlighted during image reading." + not in content + ) + assert "Highlight this symptom to readers?" in content + def test_valid_post_redirects_to_appointment( self, clinical_user_client, breast_pain ): @@ -542,6 +598,7 @@ def test_valid_post_redirects_to_appointment( "area_description_right_breast": "uiq", "when_started": RelativeDateChoices.LESS_THAN_THREE_MONTHS.value, "investigated": YesNo.NO.value, + "highlight_to_readers": HighlightToReaderChoices.YES.value, }, ) assertRedirects( diff --git a/manage_breast_screening/mammograms/views/symptom_views.py b/manage_breast_screening/mammograms/views/symptom_views.py index 3e69aa461..711ea9983 100644 --- a/manage_breast_screening/mammograms/views/symptom_views.py +++ b/manage_breast_screening/mammograms/views/symptom_views.py @@ -48,7 +48,7 @@ def get_back_link_params(self): } def get_context_data(self, **kwargs): - context = super().get_context_data() + context = super().get_context_data(**kwargs) participant = self.appointment.participant @@ -58,11 +58,18 @@ def get_context_data(self, **kwargs): "caption": participant.full_name, "heading": f"Details of the {self.symptom_type_name.lower()}", "page_title": f"Details of the {self.symptom_type_name.lower()}", + "heading_description": self._get_heading_description(), }, ) return context + def _get_heading_description(self): + suffix = " a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + if self.symptom_type_name == "lump": + return "Lumps are" + suffix + return f"{self.symptom_type_name.capitalize()} is" + suffix + class AddSymptomView(BaseSymptomFormView): """ @@ -137,13 +144,6 @@ class AddSymptomLumpView(AddSymptomView): form_class = LumpForm template_name = "mammograms/medical_information/symptoms/forms/simple_symptom.jinja" - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["heading_description"] = ( - "Lumps are a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." - ) - return context - class AddSymptomSwellingOrShapeChangeView(AddSymptomView): """ @@ -212,13 +212,6 @@ class UpdateSymptomLumpView(UpdateSymptomView): def extra_filters(self): return {"symptom_type_id": SymptomType.LUMP} - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["heading_description"] = ( - "Record whether the lump is in the left breast, right breast or both." - ) - return context - class UpdateSymptomSwellingOrShapeChangeView(UpdateSymptomView): """ diff --git a/manage_breast_screening/participants/migrations/0068_symptom_highlight_to_readers.py b/manage_breast_screening/participants/migrations/0068_symptom_highlight_to_readers.py new file mode 100644 index 000000000..28872ee37 --- /dev/null +++ b/manage_breast_screening/participants/migrations/0068_symptom_highlight_to_readers.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.4 on 2026-04-28 11:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('participants', '0001_squashed_0067_participantreportedmammogram_created_by_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='symptom', + name='highlight_to_readers', + field=models.BooleanField(default=True), + ), + ] diff --git a/manage_breast_screening/participants/migrations/max_migration.txt b/manage_breast_screening/participants/migrations/max_migration.txt index bb7026a49..8cf7e2d60 100644 --- a/manage_breast_screening/participants/migrations/max_migration.txt +++ b/manage_breast_screening/participants/migrations/max_migration.txt @@ -1 +1 @@ -0001_squashed_0067_participantreportedmammogram_created_by_and_more +0068_symptom_highlight_to_readers diff --git a/manage_breast_screening/participants/models/symptom.py b/manage_breast_screening/participants/models/symptom.py index d69a0b732..937dae35a 100644 --- a/manage_breast_screening/participants/models/symptom.py +++ b/manage_breast_screening/participants/models/symptom.py @@ -58,6 +58,11 @@ class RelativeDateChoices(models.TextChoices): NOT_SURE = "NOT_SURE", "Not sure" +class HighlightToReaderChoices(models.TextChoices): + YES = "YES", "Yes, highlight to image readers" + NO = "NO", "No, just record information" + + class Symptom(BaseModel): symptom_type = models.ForeignKey(SymptomType, on_delete=models.PROTECT) symptom_sub_type = models.ForeignKey( @@ -89,6 +94,8 @@ class Symptom(BaseModel): recently_resolved = models.BooleanField(null=False, default=False) when_resolved = models.CharField(blank=True, null=False) + highlight_to_readers = models.BooleanField(null=False, default=True) + additional_information = models.CharField(blank=True, null=False) @property diff --git a/manage_breast_screening/participants/tests/factories.py b/manage_breast_screening/participants/tests/factories.py index 7783182e7..fb222e85b 100644 --- a/manage_breast_screening/participants/tests/factories.py +++ b/manage_breast_screening/participants/tests/factories.py @@ -292,6 +292,7 @@ class Meta: intermittent = False investigated = False recently_resolved = False + highlight_to_readers = True appointment = SubFactory(AppointmentFactory) area_description = LazyAttribute( lambda o: "" if o.area == models.SymptomAreas.BOTH_BREASTS else "abc" @@ -329,6 +330,8 @@ class Params: symptom_type_id=models.SymptomType.OTHER, symptom_sub_type_details="abc" ) + breast_pain = Trait(symptom_type_id=models.SymptomType.BREAST_PAIN) + class HormoneReplacementTherapyFactory(DjangoModelFactory): class Meta: diff --git a/manage_breast_screening/tests/system/clinical/test_recording_symptoms.py b/manage_breast_screening/tests/system/clinical/test_recording_symptoms.py index 6aae12aa2..8cf414855 100644 --- a/manage_breast_screening/tests/system/clinical/test_recording_symptoms.py +++ b/manage_breast_screening/tests/system/clinical/test_recording_symptoms.py @@ -173,7 +173,7 @@ def then_i_am_back_on_the_medical_information_page(self): def and_the_lump_on_the_right_breast_is_listed(self): key = self.page.locator( - ".nhsuk-summary-list__key", has=self.page.get_by_text("Lump", exact=True) + ".nhsuk-summary-list__key", has=self.page.get_by_text("Lump") ) row = self.page.locator(".nhsuk-summary-list__row").filter(has=key) expect(row).to_contain_text("Right breast") @@ -181,7 +181,7 @@ def and_the_lump_on_the_right_breast_is_listed(self): def when_i_click_on_change(self): key = self.page.locator( ".nhsuk-summary-list__key", - has=self.page.get_by_text("Swelling or shape change", exact=True), + has=self.page.get_by_text("Swelling or shape change"), ) row = self.page.locator(".nhsuk-summary-list__row").filter(has=key) row.locator(".nhsuk-summary-list__actions").get_by_text( @@ -194,7 +194,7 @@ def then_less_than_three_months_should_be_selected(self): def and_i_see_three_months_to_a_year(self): key = self.page.locator( ".nhsuk-summary-list__key", - has=self.page.get_by_text("Swelling or shape change", exact=True), + has=self.page.get_by_text("Swelling or shape change"), ) row = self.page.locator(".nhsuk-summary-list__row").filter(has=key) expect(row).to_contain_text("3 months to a year") @@ -207,7 +207,7 @@ def and_i_confirm_i_want_to_delete_the_symptom(self): def and_the_lump_is_no_longer_listed(self): locator = self.page.locator( - ".nhsuk-summary-list__key", has=self.page.get_by_text("Lump", exact=True) + ".nhsuk-summary-list__key", has=self.page.get_by_text("Lump") ) expect(locator).not_to_be_attached() From addf2444daae0d194cd87755eb4dfc120f7087aa Mon Sep 17 00:00:00 2001 From: Steve Webber <198724792+swebberuk@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:40:51 +0100 Subject: [PATCH 2/3] Only set heading_description for symptoms that use it --- .../mammograms/views/symptom_views.py | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/manage_breast_screening/mammograms/views/symptom_views.py b/manage_breast_screening/mammograms/views/symptom_views.py index 711ea9983..f9a7ed23c 100644 --- a/manage_breast_screening/mammograms/views/symptom_views.py +++ b/manage_breast_screening/mammograms/views/symptom_views.py @@ -22,6 +22,9 @@ ) from .mixins import InProgressAppointmentMixin, MedicalInformationMixin +HEADING_DESCRIPTION_KEY = "heading_description" +HEADING_DESCRIPTION_SUFFIX = " a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." + class BaseSymptomFormView(InProgressAppointmentMixin, FormView): """ @@ -58,18 +61,11 @@ def get_context_data(self, **kwargs): "caption": participant.full_name, "heading": f"Details of the {self.symptom_type_name.lower()}", "page_title": f"Details of the {self.symptom_type_name.lower()}", - "heading_description": self._get_heading_description(), }, ) return context - def _get_heading_description(self): - suffix = " a recognised symptom of breast cancer. Information recorded here will be highlighted during image reading." - if self.symptom_type_name == "lump": - return "Lumps are" + suffix - return f"{self.symptom_type_name.capitalize()} is" + suffix - class AddSymptomView(BaseSymptomFormView): """ @@ -144,6 +140,11 @@ class AddSymptomLumpView(AddSymptomView): form_class = LumpForm template_name = "mammograms/medical_information/symptoms/forms/simple_symptom.jinja" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = "Lumps are" + HEADING_DESCRIPTION_SUFFIX + return context + class AddSymptomSwellingOrShapeChangeView(AddSymptomView): """ @@ -154,6 +155,13 @@ class AddSymptomSwellingOrShapeChangeView(AddSymptomView): form_class = SwellingOrShapeChangeForm template_name = "mammograms/medical_information/symptoms/forms/simple_symptom.jinja" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = ( + "Swelling or shape change is" + HEADING_DESCRIPTION_SUFFIX + ) + return context + class AddSymptomSkinChangeView(AddSymptomView): """ @@ -164,6 +172,11 @@ class AddSymptomSkinChangeView(AddSymptomView): form_class = SkinChangeForm template_name = "mammograms/medical_information/symptoms/forms/skin_change.jinja" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = "Skin change is" + HEADING_DESCRIPTION_SUFFIX + return context + class AddSymptomNippleChangeView(AddSymptomView): """ @@ -174,6 +187,13 @@ class AddSymptomNippleChangeView(AddSymptomView): form_class = NippleChangeForm template_name = "mammograms/medical_information/symptoms/forms/nipple_change.jinja" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = ( + "Nipple change is" + HEADING_DESCRIPTION_SUFFIX + ) + return context + class AddOtherSymptomView(AddSymptomView): """ @@ -185,7 +205,7 @@ class AddOtherSymptomView(AddSymptomView): template_name = "mammograms/medical_information/symptoms/forms/other.jinja" def get_context_data(self, **kwargs): - context = super().get_context_data() + context = super().get_context_data(**kwargs) context["heading"] = "Symptom details" return context @@ -212,6 +232,11 @@ class UpdateSymptomLumpView(UpdateSymptomView): def extra_filters(self): return {"symptom_type_id": SymptomType.LUMP} + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = "Lumps are" + HEADING_DESCRIPTION_SUFFIX + return context + class UpdateSymptomSwellingOrShapeChangeView(UpdateSymptomView): """ @@ -225,6 +250,13 @@ class UpdateSymptomSwellingOrShapeChangeView(UpdateSymptomView): def extra_filters(self): return {"symptom_type_id": SymptomType.SWELLING_OR_SHAPE_CHANGE} + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = ( + "Swelling or shape change is" + HEADING_DESCRIPTION_SUFFIX + ) + return context + class UpdateSymptomSkinChangeView(UpdateSymptomView): """ @@ -238,6 +270,11 @@ class UpdateSymptomSkinChangeView(UpdateSymptomView): def extra_filters(self): return {"symptom_type_id": SymptomType.SKIN_CHANGE} + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = "Skin change is" + HEADING_DESCRIPTION_SUFFIX + return context + class UpdateSymptomNippleChangeView(UpdateSymptomView): """ @@ -251,6 +288,13 @@ class UpdateSymptomNippleChangeView(UpdateSymptomView): def extra_filters(self): return {"symptom_type_id": SymptomType.NIPPLE_CHANGE} + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[HEADING_DESCRIPTION_KEY] = ( + "Nipple change is" + HEADING_DESCRIPTION_SUFFIX + ) + return context + class UpdateOtherSymptomView(UpdateSymptomView): """ @@ -265,7 +309,7 @@ def extra_filters(self): return {"symptom_type_id": SymptomType.OTHER} def get_context_data(self, **kwargs): - context = super().get_context_data() + context = super().get_context_data(**kwargs) context["heading"] = "Symptom details" return context From e19314620990fe49de206e4c749615611c638c41 Mon Sep 17 00:00:00 2001 From: Steve Webber <198724792+swebberuk@users.noreply.github.com> Date: Fri, 1 May 2026 13:23:14 +0100 Subject: [PATCH 3/3] Add symptoms_summary for displaying symptoms Adds 'Highlight to readers' tag if required --- .../medical_information/section_cards.jinja | 22 ++++++++++-- .../symptoms/confirm_delete_lump.jinja | 7 ++-- .../presenters/symptom_presenter.py | 16 +++------ .../test_medical_information_presenter.py | 34 +++++++------------ .../presenters/test_symptom_presenter.py | 23 ++++--------- 5 files changed, 43 insertions(+), 59 deletions(-) diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/section_cards.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/section_cards.jinja index 59f567617..d76814abc 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/section_cards.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/section_cards.jinja @@ -97,9 +97,7 @@ {% set symptom_rows = presented_medical_info.read_only_symptom_rows if read_only else presented_medical_info.symptom_rows %}

Any problems or symptoms, including lumps, swelling, rashes or nipple changes

{% if symptom_rows %} - {{ summaryList({ - "rows": symptom_rows - }) }} + {{ symptoms_summary(symptom_rows) }} {% else %} {% set insetHtml %}

No symptoms have been recorded for this participant.

@@ -244,3 +242,21 @@ ] }) }} {% endmacro %} + +{% macro symptoms_summary(symptom_rows, classes='') %} + {% set rows = [] %} + {% for row in symptom_rows %} + {% set key_html %} + {{ row.symptom_name }} + {% if row.highlight_to_readers %} +
Highlight to image readers + {% endif %} + {% endset %} + {% set _ = rows.append({ + "key": {"html": key_html}, + "value": {"html": row.html}, + "actions": row.actions if row.actions else {"items": []} + }) %} + {% endfor %} + {{ summaryList({"rows": rows, "classes": classes}) }} +{% endmacro %} diff --git a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/confirm_delete_lump.jinja b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/confirm_delete_lump.jinja index 80a1d0bd1..1222afbb2 100644 --- a/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/confirm_delete_lump.jinja +++ b/manage_breast_screening/mammograms/jinja2/mammograms/medical_information/symptoms/confirm_delete_lump.jinja @@ -1,12 +1,9 @@ {% extends "layout-confirmation.jinja" %} -{% from 'nhsuk/components/summary-list/macro.jinja' import summaryList %} +{% from "mammograms/medical_information/section_cards.jinja" import symptoms_summary %} {% from 'nhsuk/components/inset-text/macro.jinja' import insetText %} {% block details %} - {{ summaryList({ - "rows": [summary_list_row], - "classes": "nhsuk-summary-list--no-border" - }) }} + {{ symptoms_summary([summary_list_row], classes="nhsuk-summary-list--no-border") }} {% set insetTextHtml %}

This action is final and cannot be undone.

diff --git a/manage_breast_screening/mammograms/presenters/symptom_presenter.py b/manage_breast_screening/mammograms/presenters/symptom_presenter.py index 0db9e1d9a..404df87ef 100644 --- a/manage_breast_screening/mammograms/presenters/symptom_presenter.py +++ b/manage_breast_screening/mammograms/presenters/symptom_presenter.py @@ -1,5 +1,5 @@ from django.urls import reverse -from django.utils.html import escape, format_html +from django.utils.html import escape from django.utils.safestring import mark_safe from manage_breast_screening.core.template_helpers import ( @@ -139,18 +139,10 @@ def build_summary_list_row(self, include_actions=True): ] ) - if self._symptom.highlight_to_readers: - key = { - "html": format_html( - '{}
Highlight to image readers', - self._symptom.symptom_type.name, - ) - } - else: - key = {"text": self._symptom.symptom_type.name} result = { - "key": key, - "value": {"html": html}, + "symptom_name": self._symptom.symptom_type.name, + "highlight_to_readers": self._symptom.highlight_to_readers, + "html": html, } if include_actions: 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 e0b2a12ed..289a88bd9 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 @@ -82,12 +82,9 @@ def test_formats_symptoms_summary_list(self): }, ], }, - "key": { - "html": 'Lump
Highlight to image readers', - }, - "value": { - "html": "Left breast
Not sure
Symptom is intermittent
Stopped: resolved date
Not investigated
Additional information: abc", - }, + "symptom_name": "Lump", + "highlight_to_readers": True, + "html": "Left breast
Not sure
Symptom is intermittent
Stopped: resolved date
Not investigated
Additional information: abc", }, { "actions": { @@ -100,12 +97,9 @@ def test_formats_symptoms_summary_list(self): }, ], }, - "key": { - "html": 'Other
Highlight to image readers' - }, - "value": { - "html": "Description: abc
Right breast
Less than 3 months ago
Not investigated" - }, + "symptom_name": "Other", + "highlight_to_readers": True, + "html": "Description: abc
Right breast
Less than 3 months ago
Not investigated", }, { "actions": { @@ -118,10 +112,9 @@ def test_formats_symptoms_summary_list(self): } ] }, - "key": {"text": "Other"}, - "value": { - "html": "Description: xyz
Left breast
Less than 3 months ago
Not investigated" - }, + "symptom_name": "Other", + "highlight_to_readers": False, + "html": "Description: xyz
Left breast
Less than 3 months ago
Not investigated", }, { "actions": { @@ -134,12 +127,9 @@ def test_formats_symptoms_summary_list(self): }, ], }, - "key": { - "html": 'Swelling or shape change
Highlight to image readers', - }, - "value": { - "html": "Both breasts
Less than 3 months ago
Not investigated", - }, + "symptom_name": "Swelling or shape change", + "highlight_to_readers": True, + "html": "Both breasts
Less than 3 months ago
Not investigated", }, ] diff --git a/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py b/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py index 46fde1eff..aa61b9e4b 100644 --- a/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py +++ b/manage_breast_screening/mammograms/tests/presenters/test_symptom_presenter.py @@ -158,12 +158,9 @@ def test_formats_lump_for_summary_list(self): }, ], }, - "key": { - "html": 'Lump
Highlight to image readers', - }, - "value": { - "html": "Left breast
Not sure
Symptom is intermittent
Stopped: resolved date
Not investigated
Additional information: abc", - }, + "symptom_name": "Lump", + "highlight_to_readers": True, + "html": "Left breast
Not sure
Symptom is intermittent
Stopped: resolved date
Not investigated
Additional information: abc", } @pytest.mark.parametrize("highlight_to_readers", [True, False]) @@ -177,13 +174,6 @@ def test_formats_other_for_summary_list(self, highlight_to_readers): presenter = SymptomPresenter(symptom) - if highlight_to_readers: - expected_key = { - "html": 'Breast pain
Highlight to image readers', - } - else: - expected_key = {"text": "Breast pain"} - assert presenter.summary_list_row == { "actions": { "items": [ @@ -195,10 +185,9 @@ def test_formats_other_for_summary_list(self, highlight_to_readers): } ] }, - "key": expected_key, - "value": { - "html": "Left breast
Less than 3 months ago
Not investigated" - }, + "symptom_name": "Breast pain", + "highlight_to_readers": highlight_to_readers, + "html": "Left breast
Less than 3 months ago
Not investigated", } def test_delete_message_html(self):