Skip to content

Commit 81a64ea

Browse files
authored
Merge pull request #739 from NHSDigital/11451-add-benign-lump
Add benign lump form
2 parents 2b67447 + d5219d0 commit 81a64ea

File tree

12 files changed

+634
-3
lines changed

12 files changed

+634
-3
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from django.forms import Textarea
2+
3+
from manage_breast_screening.core.services.auditor import Auditor
4+
from manage_breast_screening.nhsuk_forms.fields.char_field import CharField
5+
from manage_breast_screening.nhsuk_forms.fields.choice_fields import (
6+
ChoiceField,
7+
MultipleChoiceField,
8+
)
9+
from manage_breast_screening.nhsuk_forms.fields.integer_field import YearField
10+
from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
11+
from manage_breast_screening.participants.models.benign_lump_history_item import (
12+
BenignLumpHistoryItem,
13+
)
14+
15+
16+
class BenignLumpHistoryItemForm(FormWithConditionalFields):
17+
LOCATION_DETAIL_FIELDS = {
18+
BenignLumpHistoryItem.ProcedureLocation.NHS_HOSPITAL: "nhs_hospital_details",
19+
BenignLumpHistoryItem.ProcedureLocation.PRIVATE_CLINIC_UK: "private_clinic_uk_details",
20+
BenignLumpHistoryItem.ProcedureLocation.OUTSIDE_UK: "outside_uk_details",
21+
BenignLumpHistoryItem.ProcedureLocation.MULTIPLE_LOCATIONS: "multiple_locations_details",
22+
BenignLumpHistoryItem.ProcedureLocation.EXACT_LOCATION_UNKNOWN: "exact_location_unknown_details",
23+
}
24+
25+
left_breast_procedures = MultipleChoiceField(
26+
label="Left breast",
27+
label_classes="nhsuk-fieldset__legend--s",
28+
visually_hidden_label_prefix="What procedure have they had in their ",
29+
visually_hidden_label_suffix="?",
30+
choices=BenignLumpHistoryItem.Procedure,
31+
exclusive_choices={"NO_PROCEDURES"},
32+
error_messages={
33+
"required": "Select which procedures they have had in the left breast",
34+
},
35+
)
36+
right_breast_procedures = MultipleChoiceField(
37+
label="Right breast",
38+
label_classes="nhsuk-fieldset__legend--s",
39+
visually_hidden_label_prefix="What procedure have they had in their ",
40+
visually_hidden_label_suffix="?",
41+
choices=BenignLumpHistoryItem.Procedure,
42+
exclusive_choices={"NO_PROCEDURES"},
43+
error_messages={
44+
"required": "Select which procedures they have had in the right breast",
45+
},
46+
)
47+
48+
procedure_year = YearField(
49+
label="Year of procedure (optional)",
50+
label_classes="nhsuk-label--m",
51+
classes="nhsuk-input--width-4",
52+
hint="Leave blank if unknown",
53+
required=False,
54+
)
55+
56+
procedure_location = ChoiceField(
57+
label="Where were the tests and treatment done?",
58+
choices=BenignLumpHistoryItem.ProcedureLocation,
59+
error_messages={
60+
"required": "Select where the tests and treatment were done",
61+
},
62+
)
63+
nhs_hospital_details = CharField(
64+
label="Provide details",
65+
required=False,
66+
error_messages={
67+
"required": "Provide details about where the surgery and treatment took place"
68+
},
69+
)
70+
private_clinic_uk_details = CharField(
71+
label="Provide details",
72+
required=False,
73+
error_messages={
74+
"required": "Provide details about where the surgery and treatment took place"
75+
},
76+
)
77+
outside_uk_details = CharField(
78+
label="Provide details",
79+
required=False,
80+
error_messages={
81+
"required": "Provide details about where the surgery and treatment took place"
82+
},
83+
)
84+
multiple_locations_details = CharField(
85+
label="Provide details",
86+
required=False,
87+
error_messages={
88+
"required": "Provide details about where the surgery and treatment took place"
89+
},
90+
)
91+
exact_location_unknown_details = CharField(
92+
label="Provide details",
93+
required=False,
94+
error_messages={
95+
"required": "Provide details about where the surgery and treatment took place"
96+
},
97+
)
98+
99+
additional_details = CharField(
100+
label="Additional details (optional)",
101+
label_classes="nhsuk-label--m",
102+
required=False,
103+
widget=Textarea(attrs={"rows": 3}),
104+
hint="Include any other relevant information about the procedure (optional)",
105+
)
106+
107+
def __init__(self, *args, **kwargs):
108+
super().__init__(*args, **kwargs)
109+
110+
for location, detail_field in self.LOCATION_DETAIL_FIELDS.items():
111+
self.given_field_value("procedure_location", location).require_field(
112+
detail_field
113+
)
114+
115+
def create(self, appointment, request):
116+
auditor = Auditor.from_request(request)
117+
118+
benign_lump_history_item = BenignLumpHistoryItem.objects.create(
119+
appointment=appointment,
120+
left_breast_procedures=self.cleaned_data.get("left_breast_procedures", []),
121+
right_breast_procedures=self.cleaned_data.get(
122+
"right_breast_procedures", []
123+
),
124+
procedure_year=self.cleaned_data.get("procedure_year"),
125+
procedure_location=self.cleaned_data["procedure_location"],
126+
procedure_location_details=self._get_selected_location_details(),
127+
additional_details=self.cleaned_data.get("additional_details", ""),
128+
)
129+
130+
auditor.audit_create(benign_lump_history_item)
131+
132+
return benign_lump_history_item
133+
134+
@property
135+
def location_detail_fields(self):
136+
return tuple(self.LOCATION_DETAIL_FIELDS.items())
137+
138+
def _get_selected_location_details(self):
139+
location = self.cleaned_data.get("procedure_location")
140+
try:
141+
detail_field = self.LOCATION_DETAIL_FIELDS[location]
142+
except KeyError as exc:
143+
msg = f"Unsupported procedure location '{location}'"
144+
raise ValueError(msg) from exc
145+
return self.cleaned_data.get(detail_field, "")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{% extends "layout-form.jinja" %}
2+
{% from "nhsuk/components/button/macro.jinja" import button %}
3+
{% from "nhsuk/components/fieldset/macro.jinja" import fieldset %}
4+
5+
{% block form %}
6+
7+
{% do form.left_breast_procedures.add_divider_after("LUMP_REMOVED", "or") %}
8+
{% do form.right_breast_procedures.add_divider_after("LUMP_REMOVED", "or") %}
9+
10+
<h2 aria-hidden="true">
11+
What benign lump procedures has {{ participant_first_name }} had?
12+
</h2>
13+
14+
<div class="nhsuk-grid-row">
15+
<div class="nhsuk-grid-column-one-half">
16+
{{ form.right_breast_procedures.as_field_group() }}
17+
</div>
18+
<div class="nhsuk-grid-column-one-half">
19+
{{ form.left_breast_procedures.as_field_group() }}
20+
</div>
21+
</div>
22+
23+
{{ form.procedure_year.as_field_group() }}
24+
25+
{% for location_value, detail_field_name in form.location_detail_fields %}
26+
{% set detail_field = form[detail_field_name] %}
27+
{% do form.procedure_location.add_conditional_html(
28+
location_value,
29+
detail_field.as_field_group()
30+
) %}
31+
{% endfor %}
32+
{{ form.procedure_location.as_field_group() }}
33+
34+
{{ form.additional_details.as_field_group() }}
35+
36+
<div class="nhsuk-button-group">
37+
{{ button({
38+
"text": "Save"
39+
}) }}
40+
</div>
41+
42+
{% endblock %}

manage_breast_screening/mammograms/jinja2/mammograms/record_medical_information.jinja

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
{% for presented_item in presenter.benign_lump_history %}
8181
{{ summaryList(presented_item.summary_list_params) }}
8282
{% endfor %}
83+
<a href="{{ presenter.add_benign_lump_history_link.href }}" class="nhsuk-link nhsuk-link--no-visited-state">{{ presenter.add_benign_lump_history_link.text }}</a><br>
8384
{% endset %}
8485
{% set chest_procedures_link %}
8586
<a class="nhsuk-link" href="#">Enter other breast or chest procedures</a>

manage_breast_screening/mammograms/presenters/medical_information_presenter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,15 @@ def add_breast_augmentation_history_link(self):
197197
"href": url,
198198
"text": ("Add breast augmentation history"),
199199
}
200+
201+
@property
202+
def add_benign_lump_history_link(self):
203+
url = reverse(
204+
"mammograms:add_benign_lump_history_item",
205+
kwargs={"pk": self.appointment.pk},
206+
)
207+
208+
return {
209+
"href": url,
210+
"text": "Add benign lump history",
211+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import datetime
2+
from urllib.parse import urlencode
3+
4+
import pytest
5+
from django.http import QueryDict
6+
from django.test import RequestFactory
7+
8+
from manage_breast_screening.core.models import AuditLog
9+
from manage_breast_screening.mammograms.forms.benign_lump_history_item_form import (
10+
BenignLumpHistoryItemForm,
11+
)
12+
from manage_breast_screening.participants.models.benign_lump_history_item import (
13+
BenignLumpHistoryItem,
14+
)
15+
from manage_breast_screening.participants.tests.factories import AppointmentFactory
16+
17+
18+
def _form_data(data):
19+
return QueryDict(urlencode(data, doseq=True))
20+
21+
22+
@pytest.mark.django_db
23+
class TestBenignLumpHistoryItemForm:
24+
def test_missing_required_fields(self):
25+
form = BenignLumpHistoryItemForm(_form_data({}))
26+
27+
assert not form.is_valid()
28+
assert form.errors == {
29+
"left_breast_procedures": [
30+
"Select which procedures they have had in the left breast"
31+
],
32+
"right_breast_procedures": [
33+
"Select which procedures they have had in the right breast"
34+
],
35+
"procedure_location": ["Select where the tests and treatment were done"],
36+
}
37+
38+
@pytest.mark.parametrize(
39+
("location", "detail_field"),
40+
tuple(BenignLumpHistoryItemForm.LOCATION_DETAIL_FIELDS.items()),
41+
)
42+
def test_requires_location_details_for_selected_location(
43+
self, location, detail_field
44+
):
45+
form = BenignLumpHistoryItemForm(
46+
_form_data(
47+
[
48+
(
49+
"procedure_location",
50+
location,
51+
),
52+
]
53+
)
54+
)
55+
56+
assert not form.is_valid()
57+
assert form.errors.get(detail_field) == [
58+
"Provide details about where the surgery and treatment took place"
59+
]
60+
61+
@pytest.mark.parametrize(
62+
("procedure_year", "expected_message"),
63+
[
64+
(
65+
datetime.date.today().year - 80 - 1,
66+
f"Year must be {datetime.date.today().year - 80} or later",
67+
),
68+
(
69+
datetime.date.today().year + 1,
70+
f"Year must be {datetime.date.today().year} or earlier",
71+
),
72+
],
73+
)
74+
def test_procedure_year_must_be_within_range(
75+
self, procedure_year, expected_message
76+
):
77+
form = BenignLumpHistoryItemForm(
78+
_form_data(
79+
[
80+
("procedure_year", procedure_year),
81+
]
82+
)
83+
)
84+
85+
assert not form.is_valid()
86+
assert form.errors.get("procedure_year") == [expected_message]
87+
88+
def test_create_persists_data_and_audits(self, clinical_user):
89+
appointment = AppointmentFactory()
90+
request = RequestFactory().post("/test-form")
91+
request.user = clinical_user
92+
93+
data = [
94+
(
95+
"left_breast_procedures",
96+
BenignLumpHistoryItem.Procedure.NEEDLE_BIOPSY,
97+
),
98+
(
99+
"right_breast_procedures",
100+
BenignLumpHistoryItem.Procedure.NO_PROCEDURES,
101+
),
102+
("procedure_year", datetime.date.today().year - 1),
103+
(
104+
"procedure_location",
105+
BenignLumpHistoryItem.ProcedureLocation.NHS_HOSPITAL,
106+
),
107+
("nhs_hospital_details", "St Thomas' Hospital"),
108+
("additional_details", "Additional details."),
109+
]
110+
form = BenignLumpHistoryItemForm(_form_data(data))
111+
assert form.is_valid()
112+
113+
existing_log_count = AuditLog.objects.count()
114+
obj = form.create(appointment=appointment, request=request)
115+
assert AuditLog.objects.count() == existing_log_count + 1
116+
audit_log = AuditLog.objects.filter(
117+
object_id=obj.pk, operation=AuditLog.Operations.CREATE
118+
).first()
119+
assert audit_log.actor == clinical_user
120+
121+
obj.refresh_from_db()
122+
assert obj.appointment == appointment
123+
assert obj.left_breast_procedures == [
124+
BenignLumpHistoryItem.Procedure.NEEDLE_BIOPSY
125+
]
126+
assert obj.right_breast_procedures == [
127+
BenignLumpHistoryItem.Procedure.NO_PROCEDURES
128+
]
129+
assert obj.procedure_year == data[2][1]
130+
assert obj.procedure_location == (
131+
BenignLumpHistoryItem.ProcedureLocation.NHS_HOSPITAL
132+
)
133+
assert obj.procedure_location_details == "St Thomas' Hospital"
134+
assert obj.additional_details == "Additional details."

manage_breast_screening/mammograms/tests/presenters/test_medical_information_presenter.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,13 @@ def test_breast_augmentation_history_link(self):
151151
"href": f"/mammograms/{appointment.pk}/record-medical-information/breast-augmentation-history/",
152152
"text": "Add breast augmentation history",
153153
}
154+
155+
def test_add_benign_lump_history_link(self):
156+
appointment = AppointmentFactory()
157+
158+
assert MedicalInformationPresenter(
159+
appointment
160+
).add_benign_lump_history_link == {
161+
"href": f"/mammograms/{appointment.pk}/record-medical-information/benign-lump-history/",
162+
"text": "Add benign lump history",
163+
}

manage_breast_screening/mammograms/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .views import (
44
appointment_views,
5+
benign_lump_history_item_views,
56
breast_augmentation_history_view,
67
breast_cancer_history_views,
78
cyst_history_view,
@@ -143,4 +144,9 @@
143144
breast_augmentation_history_view.AddBreastAugmentationHistoryView.as_view(),
144145
name="add_breast_augmentation_history_item",
145146
),
147+
path(
148+
"<uuid:pk>/record-medical-information/benign-lump-history/",
149+
benign_lump_history_item_views.AddBenignLumpHistoryItemView.as_view(),
150+
name="add_benign_lump_history_item",
151+
),
146152
]

0 commit comments

Comments
 (0)