Skip to content

Commit fb975f0

Browse files
authored
Merge pull request #207 from NHSDigital/dtoss-9248-record-previous-mammogram-system-test
DTOSS-9248 Add system tests and fix date validation on add previous mammogram form
2 parents 79ef273 + 788c1d1 commit fb975f0

10 files changed

Lines changed: 273 additions & 37 deletions

File tree

manage_breast_screening/clinics/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ class TimeOfDay:
9797

9898
objects = ClinicQuerySet.as_manager()
9999

100+
@property
101+
def provider(self):
102+
return self.setting.provider
103+
100104
@property
101105
def current_status(self):
102106
return self.statuses.order_by("-created_at").first()
@@ -132,6 +136,10 @@ class ClinicSlot(BaseModel):
132136
starts_at = models.DateTimeField()
133137
duration_in_minutes = models.IntegerField()
134138

139+
@property
140+
def provider(self):
141+
return self.clinic.provider
142+
135143

136144
class ClinicStatus(models.Model):
137145
SCHEDULED = "SCHEDULED"

manage_breast_screening/config/jinja2_env.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,25 @@ def raise_helper(msg):
4545
raise Exception(msg)
4646

4747

48+
def autoescape(template_name):
49+
"""
50+
This is a workaround until https://nhsd-jira.digital.nhs.uk/browse/DTOSS-9978
51+
is complete.
52+
Going forwards, we want to use Django's default behaviour for autoescape.
53+
"""
54+
if template_name is None:
55+
return False
56+
elif template_name.endswith("attributes.jinja"):
57+
return False
58+
else:
59+
return template_name.endswith((".html", ".htm", ".xml", ".jinja"))
60+
61+
4862
def environment(**options):
63+
# Temporarily override autoescape for templates in nhsuk-frontend-jinja
64+
# remove after https://nhsd-jira.digital.nhs.uk/browse/DTOSS-9978 is complete
65+
options["autoescape"] = autoescape
66+
4967
env = Environment(**options, extensions=["jinja2.ext.do"])
5068
if env.loader:
5169
env.loader = ChoiceLoader(

manage_breast_screening/core/form_fields.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import datetime
22

33
from django import forms
4+
from django.core import validators
45
from django.forms import ValidationError, widgets
5-
from django.utils.timezone import now
66
from django.utils.translation import gettext
77
from django.utils.translation import gettext_lazy as _
88

9+
from manage_breast_screening.core.utils.date_formatting import format_date
10+
911

1012
class SplitDateWidget(widgets.MultiWidget):
1113
"""
@@ -63,14 +65,14 @@ class SplitDateField(forms.MultiValueField):
6365
default_error_messages = {"invalid": _("Enter a valid date.")}
6466

6567
def __init__(self, *args, **kwargs):
66-
min_year = kwargs.pop("min_year", 1900)
67-
max_year = kwargs.pop("max_year", now().year)
68+
max_value = kwargs.pop("max_value", datetime.date.today())
69+
min_value = kwargs.pop("min_value", datetime.date(1900, 1, 1))
6870

6971
day_bounds_error = gettext("Day should be between 1 and 31.")
7072
month_bounds_error = gettext("Month should be between 1 and 12.")
7173
year_bounds_error = gettext(
7274
"Year should be between %(min_year)s and %(max_year)s."
73-
) % {"min_year": min_year, "max_year": max_year}
75+
) % {"min_year": min_value.year, "max_year": max_value.year}
7476

7577
day_kwargs = {
7678
"min_value": 1,
@@ -91,8 +93,8 @@ def __init__(self, *args, **kwargs):
9193
},
9294
}
9395
year_kwargs = {
94-
"min_value": min_year,
95-
"max_value": max_year,
96+
"min_value": min_value.year,
97+
"max_value": max_value.year,
9698
"error_messages": {
9799
"min_value": year_bounds_error,
98100
"max_value": year_bounds_error,
@@ -108,6 +110,17 @@ def __init__(self, *args, **kwargs):
108110

109111
super().__init__(self.fields, *args, **kwargs)
110112

113+
self.validators.append(
114+
validators.MinValueValidator(
115+
min_value, f"Enter a date after {format_date(min_value)}"
116+
)
117+
)
118+
self.validators.append(
119+
validators.MaxValueValidator(
120+
max_value, f"Enter a date before {format_date(max_value)}"
121+
)
122+
)
123+
111124
def compress(self, data_list):
112125
if data_list:
113126
try:

manage_breast_screening/core/tests/test_form_fields.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class TestSplitDateField:
1212
def test_clean(self):
13-
f = SplitDateField(max_year=2026)
13+
f = SplitDateField(max_value=datetime.date(2026, 6, 30))
1414

1515
assert f.clean([1, 12, 2025]) == datetime.date(2025, 12, 1)
1616

@@ -25,32 +25,38 @@ def test_clean(self):
2525

2626
with pytest.raises(
2727
ValidationError,
28-
match="['Enter day as a number.', 'Enter month as a number.', 'Enter year as a number.']",
28+
match=r"\['Enter day as a number.', 'Enter month as a number.', 'Enter year as a number.'\]",
2929
):
3030
f.clean(["a", "b", "c"])
3131

3232
with pytest.raises(
3333
ValidationError,
34-
match="['Enter day as a number.', 'Enter month as a number.', 'Enter year as a number.']",
34+
match=r"\['This field is required.'\]",
3535
):
3636
f.clean(["", "", ""])
3737

3838
with pytest.raises(
3939
ValidationError,
40-
match="['Day should be between 1 and 31.', 'Month should be between 1 and 12.', 'Year should be between 1900 and 2025.']",
40+
match=r"\['Day should be between 1 and 31.', 'Month should be between 1 and 12.', 'Year should be between 1900 and 2026.']",
4141
):
4242
f.clean([0, 13, 1800])
4343

44+
with pytest.raises(
45+
ValidationError,
46+
match=r"\['Enter a date before 30 June 2026'\]",
47+
):
48+
f.clean([1, 7, 2026])
49+
4450
def test_has_changed(self):
45-
f = SplitDateField(max_year=2026)
51+
f = SplitDateField(max_value=datetime.date(2026, 12, 31))
4652
assert f.has_changed([1, 12, 2025], [2, 12, 2025])
4753
assert f.has_changed([1, 12, 2025], [1, 11, 2025])
4854
assert f.has_changed([1, 12, 2025], [1, 12, 2026])
4955
assert not f.has_changed([1, 12, 2025], [1, 12, 2025])
5056

5157
def test_default_django_render(self):
5258
class TestForm(Form):
53-
date = SplitDateField(max_year=2026)
59+
date = SplitDateField(max_value=datetime.date(2026, 12, 31))
5460

5561
f = TestForm()
5662

@@ -70,7 +76,7 @@ class TestForm(Form):
7076

7177
def test_default_django_render_in_bound_form(self):
7278
class TestForm(Form):
73-
date = SplitDateField(max_year=2026)
79+
date = SplitDateField(max_value=datetime.date(2026, 12, 31))
7480

7581
f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"})
7682

@@ -90,7 +96,7 @@ class TestForm(Form):
9096

9197
def test_form_cleaned_data(self):
9298
class TestForm(Form):
93-
date = SplitDateField(max_year=2026)
99+
date = SplitDateField(max_value=datetime.date(2026, 12, 31))
94100

95101
f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"})
96102

@@ -99,7 +105,7 @@ class TestForm(Form):
99105

100106
def test_bound_field_subwidgets(self):
101107
class TestForm(Form):
102-
date = SplitDateField(max_year=2026)
108+
date = SplitDateField(max_value=datetime.date(2026, 12, 31))
103109

104110
f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"})
105111
field = f["date"]
@@ -151,10 +157,18 @@ class TestForm(Form):
151157
"value": "2025",
152158
}
153159

154-
def test_form_errors(self):
160+
def test_subfield_errors_on_form(self):
155161
class TestForm(Form):
156-
date = SplitDateField(max_year=2026)
162+
date = SplitDateField(max_value=datetime.date(2026, 12, 31))
157163

158164
f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2027"})
159165
assert not f.is_valid()
160166
assert f.errors == {"date": ["Year should be between 1900 and 2026."]}
167+
168+
def test_same_year_but_past_max_value(self):
169+
class TestForm(Form):
170+
date = SplitDateField(max_value=datetime.date(2026, 7, 1))
171+
172+
f = TestForm({"date_0": "1", "date_1": "8", "date_2": "2026"})
173+
assert not f.is_valid()
174+
assert f.errors == {"date": ["Enter a date before 1 July 2026"]}

manage_breast_screening/core/utils/acessibility.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
AXE_VIOLATIONS_EXCLUDE_LIST = [
88
"region", # 'Some page content is not contained by landmarks' https://github.com/alphagov/govuk-frontend/issues/1604
9+
"aria-allowed-attr", # 'ARIA attribute is not allowed: aria-expanded="false"' https://github.com/alphagov/govuk-frontend/issues/979
910
]
1011

1112

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import re
2+
3+
import pytest
4+
from django.urls import reverse
5+
from playwright.sync_api import expect
6+
7+
from manage_breast_screening.core.system_test_setup import SystemTestCase
8+
from manage_breast_screening.participants.tests.factories import (
9+
AppointmentFactory,
10+
ParticipantFactory,
11+
ScreeningEpisodeFactory,
12+
)
13+
14+
15+
class TestAddingPreviousMammograms(SystemTestCase):
16+
@pytest.fixture(autouse=True)
17+
def before(self):
18+
self.participant = ParticipantFactory(first_name="Janet", last_name="Williams")
19+
self.screening_episode = ScreeningEpisodeFactory(participant=self.participant)
20+
self.appointment = AppointmentFactory(screening_episode=self.screening_episode)
21+
self.provider = self.appointment.provider
22+
23+
def test_adding_a_mammogram_at_the_same_provider(self):
24+
"""
25+
If a mammogram was taken at the same provider, but there is an error in the system, the participant can report that it was taken.
26+
"""
27+
self.given_i_am_on_the_start_screening_page()
28+
self.then_i_should_see_no_reported_mammograms()
29+
30+
self.when_i_click_on_add_mammogram()
31+
self.then_i_should_be_on_the_add_previous_mammogram_form()
32+
33+
self.when_i_select_the_same_provider()
34+
self.and_i_enter_an_exact_date()
35+
self.and_i_select_yes_same_name()
36+
self.and_i_enter_additional_information()
37+
self.and_i_click_continue()
38+
self.then_i_should_be_back_on_the_appointment()
39+
self.and_i_should_see_the_mammogram_with_the_same_provider()
40+
41+
def test_adding_a_mammogram_taken_elsewhere_with_a_different_name(self):
42+
"""
43+
If a mammogram was taken at another BSU, or elsewhere in the UK, the participant can report that it was taken
44+
If the mammogram was taken under a different name, the mammographer can record that name.
45+
"""
46+
self.given_i_am_on_the_start_screening_page()
47+
self.then_i_should_see_no_reported_mammograms()
48+
49+
self.when_i_click_on_add_mammogram()
50+
self.then_i_should_be_on_the_add_previous_mammogram_form()
51+
52+
self.when_i_enter_another_location_in_the_uk()
53+
self.and_i_enter_an_approximate_date()
54+
self.and_i_enter_a_different_name()
55+
self.and_i_enter_additional_information()
56+
self.and_i_click_continue()
57+
self.then_i_should_be_back_on_the_appointment()
58+
self.and_i_should_see_the_mammogram_with_the_other_provider_and_name()
59+
60+
def test_accessibility(self):
61+
self.given_i_am_on_the_add_previous_mammograms_page()
62+
self.then_the_accessibility_baseline_is_met()
63+
64+
def given_i_am_on_the_start_screening_page(self):
65+
self.page.goto(
66+
self.live_server_url
67+
+ reverse(
68+
"mammograms:start_screening",
69+
kwargs={"pk": self.appointment.pk},
70+
)
71+
)
72+
73+
def given_i_am_on_the_add_previous_mammograms_page(self):
74+
self.page.goto(
75+
self.live_server_url
76+
+ reverse(
77+
"participants:add_previous_mammogram",
78+
kwargs={"pk": self.participant.pk},
79+
)
80+
)
81+
82+
def then_i_should_see_no_reported_mammograms(self):
83+
expect(self.page.get_by_test_id("mammograms")).to_contain_text("Not known")
84+
85+
def when_i_click_on_add_mammogram(self):
86+
self.page.get_by_text("Add mammogram").click()
87+
88+
def then_i_should_be_on_the_add_previous_mammogram_form(self):
89+
path = reverse(
90+
"participants:add_previous_mammogram",
91+
kwargs={"pk": self.participant.pk},
92+
)
93+
expect(self.page).to_have_url(re.compile(path))
94+
95+
def then_i_should_be_back_on_the_appointment(self):
96+
path = reverse(
97+
"mammograms:start_screening",
98+
kwargs={"pk": self.appointment.pk},
99+
)
100+
expect(self.page).to_have_url(re.compile(path))
101+
102+
def when_i_select_the_same_provider(self):
103+
option = f"At {self.provider.name}"
104+
self.page.get_by_label(option).click()
105+
106+
def and_i_enter_an_exact_date(self):
107+
self.page.get_by_label("Enter an exact date").click()
108+
self.page.get_by_label("Day").fill("1")
109+
self.page.get_by_label("Month").fill("12")
110+
self.page.get_by_label("Year").fill("2023")
111+
112+
def and_i_select_yes_same_name(self):
113+
self.page.get_by_label("Yes").click()
114+
115+
def and_i_enter_additional_information(self):
116+
self.page.get_by_label("Additional information (optional)").fill("RR")
117+
118+
def and_i_click_continue(self):
119+
self.page.get_by_text("Continue").click()
120+
121+
def and_i_should_see_the_mammogram_with_the_same_provider(self):
122+
expected_inner_text = re.compile(
123+
rf"Added today\n1 December 2023 \(.* ago\)\n{self.provider.name}\nAdditional information: RR"
124+
)
125+
expect(self.page.get_by_test_id("mammograms")).to_contain_text(
126+
expected_inner_text,
127+
use_inner_text=True,
128+
)
129+
130+
def when_i_enter_another_location_in_the_uk(self):
131+
self.page.get_by_label("Somewhere in the UK").click()
132+
self.page.get_by_label("Location").first.fill("other place")
133+
134+
def and_i_enter_an_approximate_date(self):
135+
self.page.get_by_label("Enter an approximate date").and_(
136+
self.page.get_by_role("radio")
137+
).click()
138+
self.page.get_by_label("Enter an approximate date").and_(
139+
self.page.get_by_role("textbox")
140+
).fill("a year ago")
141+
142+
def and_i_enter_a_different_name(self):
143+
self.page.get_by_label("No, under a different name").click()
144+
self.page.get_by_label("Enter the previously used name").fill("Taylor Swift")
145+
146+
def and_i_should_see_the_mammogram_with_the_other_provider_and_name(self):
147+
expected_inner_text = re.compile(
148+
r"Added today\nApproximate date: a year ago\nIn the UK: other place\nPrevious name: Taylor Swift\nAdditional information: RR"
149+
)
150+
expect(self.page.get_by_test_id("mammograms")).to_contain_text(
151+
expected_inner_text,
152+
use_inner_text=True,
153+
)

0 commit comments

Comments
 (0)