Skip to content

Commit 64f7af8

Browse files
authored
Merge pull request #143 from NHSDigital/PPHA-268-respiratory-condition-page
PPHA-268: Add respiratory conditions
2 parents 502d2bc + b5dae6a commit 64f7af8

16 files changed

+557
-12
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
def check_labels(page, answers):
2+
if answers is None:
3+
return
4+
5+
if isinstance(answers, str):
6+
page.get_by_label(answers, exact=True).check()
7+
return
8+
9+
if isinstance(answers, (list, tuple)):
10+
for answer in answers:
11+
page.get_by_label(answer, exact=True).check()
12+
return
13+
14+
raise TypeError("answers must be a string, list, or tuple")
15+

lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from playwright.sync_api import expect
2+
from .test_helpers import check_labels
23

34
def setup_participant(page, live_server_url):
45
participant_id = 'abc123'
@@ -89,3 +90,12 @@ def fill_in_and_submit_asbestos_exposure(page, answer):
8990
page.get_by_label(answer, exact=True).check()
9091

9192
page.click("text=Continue")
93+
94+
def fill_in_and_submit_respiratory_conditions(page, answer):
95+
expect(page.locator("legend")).to_have_text(
96+
"Have you ever been diagnosed with any of the following respiratory conditions?")
97+
98+
check_labels(page, answer)
99+
100+
page.click("text=Continue")
101+

lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
fill_in_and_submit_sex_at_birth,
1414
fill_in_and_submit_gender,
1515
fill_in_and_submit_ethnicity,
16-
fill_in_and_submit_asbestos_exposure
16+
fill_in_and_submit_asbestos_exposure,
17+
fill_in_and_submit_respiratory_conditions
1718
)
1819

1920
class TestQuestionnaire(StaticLiveServerTestCase):
@@ -48,7 +49,7 @@ def test_cannot_change_responses_once_checked_and_submitted(self):
4849
fill_in_and_submit_gender(page, "Male")
4950
fill_in_and_submit_ethnicity(page, "White")
5051
page.click("text=Continue") # education
51-
page.click("text=Continue") # respiratory conditions
52+
fill_in_and_submit_respiratory_conditions(page, "No, I have not had any of these respiratory conditions")
5253
fill_in_and_submit_asbestos_exposure(page, "No")
5354
page.click("text=Continue") # cancer diagnosis
5455
page.click("text=Continue") # family history

lung_cancer_screening/core/tests/acceptance/test_questionnaire.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
fill_in_and_submit_sex_at_birth,
1616
fill_in_and_submit_gender,
1717
fill_in_and_submit_ethnicity,
18-
fill_in_and_submit_asbestos_exposure
18+
fill_in_and_submit_asbestos_exposure,
19+
fill_in_and_submit_respiratory_conditions
1920
)
2021

2122
from .helpers.assertion_helpers import expect_back_link_to_have_url
@@ -96,7 +97,7 @@ def test_full_questionnaire_user_journey(self):
9697

9798
expect(page).to_have_url(f"{self.live_server_url}/respiratory-conditions")
9899
expect_back_link_to_have_url(page, "/education")
99-
page.click("text=Continue")
100+
fill_in_and_submit_respiratory_conditions(page, "No, I have not had any of these respiratory conditions")
100101

101102
expect(page).to_have_url(f"{self.live_server_url}/asbestos-exposure")
102103
expect_back_link_to_have_url(page, "/respiratory-conditions")
@@ -122,7 +123,40 @@ def test_full_questionnaire_user_journey(self):
122123
expect(responses).to_contain_text("Which of these best describes you? Male")
123124
expect(responses).to_contain_text("What is your ethnic background? White")
124125
expect(responses).to_contain_text("Have you ever worked in a job where you might have been exposed to asbestos? No")
126+
expect(responses).to_contain_text("Have you ever been diagnosed with any of the following respiratory conditions? No, I have not had any of these respiratory conditions")
125127

126128
page.click("text=Submit")
127129

128130
expect(page).to_have_url(f"{self.live_server_url}/your-results")
131+
132+
def test_can_select_multiple_respiratory_conditions(self):
133+
"""Test that users can select multiple respiratory conditions in the UI"""
134+
participant_id = '456'
135+
smoking_status = 'Yes, I currently smoke'
136+
age = datetime.now() - relativedelta(years=60)
137+
138+
page = self.browser.new_page()
139+
page.goto(f"{self.live_server_url}/start")
140+
141+
fill_in_and_submit_participant_id(page, participant_id)
142+
fill_in_and_submit_smoking_eligibility(page, smoking_status)
143+
fill_in_and_submit_date_of_birth(page, age)
144+
fill_in_and_submit_height_metric(page, "170")
145+
fill_in_and_submit_weight_metric(page, "70")
146+
fill_in_and_submit_sex_at_birth(page, "Female")
147+
fill_in_and_submit_gender(page, "Female")
148+
fill_in_and_submit_ethnicity(page, "White")
149+
page.click("text=Continue") # education
150+
151+
# Select multiple respiratory conditions
152+
expect(page).to_have_url(f"{self.live_server_url}/respiratory-conditions")
153+
fill_in_and_submit_respiratory_conditions(page, ["Pneumonia", "Emphysema"])
154+
155+
fill_in_and_submit_asbestos_exposure(page, "No")
156+
page.click("text=Continue") # cancer diagnosis
157+
page.click("text=Continue") # family history
158+
159+
# Verify both conditions appear on the responses page
160+
expect(page).to_have_url(f"{self.live_server_url}/responses")
161+
responses = page.locator(".responses")
162+
expect(responses).to_contain_text("Have you ever been diagnosed with any of the following respiratory conditions? Pneumonia, Emphysema")

lung_cancer_screening/core/tests/acceptance/test_questionnaire_validation_errors.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,37 @@ def test_ethnicity_validation_errors(self):
115115
expect(page.locator(".nhsuk-error-message")).to_contain_text(
116116
"Select your ethnic background."
117117
)
118+
119+
def test_respiratory_conditions_validation_errors(self):
120+
participant_id = '123'
121+
122+
page = self.browser.new_page()
123+
page.goto(f"{self.live_server_url}/start")
124+
fill_in_and_submit_participant_id(page, participant_id)
125+
page.goto(f"{self.live_server_url}/respiratory-conditions")
126+
127+
page.click("text=Continue")
128+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
129+
"Select if you have had any respiratory conditions"
130+
)
131+
132+
# Select one respiratory condition
133+
page.get_by_label("Bronchitis").click()
134+
135+
# Select None option
136+
page.get_by_label("No, I have not had any of these respiratory conditions").click()
137+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
138+
"Select if you have had any respiratory conditions"
139+
)
140+
141+
# Continue
142+
page.click("text=Continue")
143+
144+
# Assert error is shown
145+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
146+
"Select if you have had any respiratory conditions, or select 'No, I have not had any of these respiratory conditions'"
147+
)
148+
149+
expect(page).to_have_url(f"{self.live_server_url}/respiratory-conditions")
150+
151+

lung_cancer_screening/nhsuk_forms/choice_field.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def __init__(self, form: forms.Form, field: "ChoiceField", name: str):
2020

2121
self._conditional_html = {}
2222
self.dividers = {}
23+
self.choice_hints = {}
2324

2425
def add_conditional_html(self, value, html):
2526
if isinstance(self.field.widget, widgets.Select):
@@ -36,6 +37,12 @@ def add_divider_after(self, previous, divider):
3637
def get_divider_after(self, previous):
3738
return self.dividers.get(previous)
3839

40+
def add_hint_for_choice(self, value, hint):
41+
self.choice_hints[value] = hint
42+
43+
def get_hint_for_choice(self, value):
44+
return self.choice_hints.get(value)
45+
3946

4047
class ChoiceField(forms.ChoiceField):
4148
"""
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% from 'nhsuk/components/checkboxes/macro.jinja' import checkboxes %}
2+
{% set unbound_field = field.field %}
3+
{% if field.errors %}
4+
{% set error_message = {"text": field.errors | first} %}
5+
{% endif %}
6+
{% set ns = namespace(items=[]) %}
7+
{% for value, text in unbound_field.choices %}
8+
{% set hint_text = field.get_hint_for_choice(value) %}
9+
{% set ns.items = ns.items + [{
10+
"id": field.auto_id ~ '_' ~ loop.index0,
11+
"value": value,
12+
"text": text,
13+
"checked": value in (field.value() or []),
14+
"hint": {
15+
"text": hint_text
16+
} if hint_text else undefined
17+
}] %}
18+
{% set divider = field.get_divider_after(value) %}
19+
{% if divider %}
20+
{% set ns.items = ns.items + [{"divider": divider}] %}
21+
{% endif %}
22+
{% endfor %}
23+
{{ checkboxes({
24+
"name": field.html_name,
25+
"idPrefix": field.auto_id,
26+
"fieldset": {
27+
"legend": {
28+
"text": field.label,
29+
"classes": unbound_field.label_classes
30+
}
31+
} if field.use_fieldset else none,
32+
"errorMessage": error_message,
33+
"classes": unbound_field.classes if unbound_field.classes,
34+
"hint": {
35+
"html": unbound_field.hint|e
36+
} if unbound_field.hint,
37+
"items": ns.items
38+
}) }}

lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
{% set ns = namespace(items=[]) %}
77
{% for value, text in unbound_field.choices %}
88
{% set conditional_html = field.conditional_html(value) %}
9+
{% set hint_text = field.get_hint_for_choice(value) %}
910
{% set ns.items = ns.items + [{
1011
"id": field.auto_id if loop.first,
1112
"value": value,
1213
"text": text,
1314
"checked": field.value() == value,
15+
"hint": {
16+
"text": hint_text
17+
} if hint_text else undefined,
1418
"conditional": {
1519
"html": conditional_html
1620
} if conditional_html else undefined

lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.test import TestCase
22
from django.forms import Form
33

4-
from ...choice_field import ChoiceField
4+
from ...choice_field import ChoiceField, MultipleChoiceField
55

66
class TestForm(Form):
77
field = ChoiceField(
@@ -11,6 +11,13 @@ class TestForm(Form):
1111
hint="Pick either one",
1212
)
1313

14+
class TestMultipleChoiceForm(Form):
15+
field = MultipleChoiceField(
16+
label="Select options",
17+
choices=(("a", "Option A"), ("b", "Option B"), ("c", "Option C")),
18+
hint="Select all that apply",
19+
)
20+
1421
class TestChoiceField(TestCase):
1522
def test_renders_nhs_radios(self):
1623
self.assertHTMLEqual(
@@ -74,3 +81,72 @@ class TestForm(Form):
7481
</div>
7582
""",
7683
)
84+
85+
def test_checkbox_field_with_choice_hints(self):
86+
"""Test that choice hints are rendered correctly for checkbox fields"""
87+
form = TestMultipleChoiceForm()
88+
bound_field = form["field"]
89+
90+
# Add hints for specific choices
91+
bound_field.add_hint_for_choice("a", "This is hint for option A")
92+
bound_field.add_hint_for_choice("b", "This is hint for option B")
93+
94+
rendered_html = bound_field.as_field_group()
95+
96+
# Verify the hints are rendered
97+
self.assertIn('This is hint for option A', rendered_html)
98+
self.assertIn('This is hint for option B', rendered_html)
99+
self.assertIn('aria-describedby="id_field_0-item-hint"', rendered_html)
100+
self.assertIn('aria-describedby="id_field_1-item-hint"', rendered_html)
101+
102+
def test_get_hint_for_choice_returns_correct_hint(self):
103+
"""Test that get_hint_for_choice returns the correct hint text"""
104+
form = TestMultipleChoiceForm()
105+
bound_field = form["field"]
106+
107+
# Add a hint
108+
bound_field.add_hint_for_choice("a", "Hint for A")
109+
110+
# Verify the hint can be retrieved
111+
self.assertEqual(bound_field.get_hint_for_choice("a"), "Hint for A")
112+
113+
def test_get_hint_for_choice_returns_none_when_no_hint(self):
114+
"""Test that get_hint_for_choice returns None when no hint is set"""
115+
form = TestMultipleChoiceForm()
116+
bound_field = form["field"]
117+
118+
# No hint added, should return None
119+
self.assertIsNone(bound_field.get_hint_for_choice("a"))
120+
121+
def test_checkbox_field_renders_without_hints_when_none_added(self):
122+
"""Test that checkbox field renders correctly when no hints are added"""
123+
form = TestMultipleChoiceForm()
124+
rendered_html = form["field"].as_field_group()
125+
126+
# Should render without hint elements
127+
self.assertNotIn('nhsuk-checkboxes__hint', rendered_html)
128+
129+
def test_radio_field_with_choice_hints(self):
130+
"""Test that choice hints are rendered correctly for radio fields"""
131+
form = TestForm()
132+
bound_field = form["field"]
133+
134+
# Add hints for specific choices
135+
bound_field.add_hint_for_choice("a", "This is hint for option A")
136+
bound_field.add_hint_for_choice("b", "This is hint for option B")
137+
138+
rendered_html = bound_field.as_field_group()
139+
140+
# Verify the hints are rendered
141+
self.assertIn('This is hint for option A', rendered_html)
142+
self.assertIn('This is hint for option B', rendered_html)
143+
self.assertIn('aria-describedby="id_field-item-hint"', rendered_html)
144+
self.assertIn('aria-describedby="id_field-2-item-hint"', rendered_html)
145+
146+
def test_radio_field_renders_without_hints_when_none_added(self):
147+
"""Test that radio field renders correctly when no hints are added"""
148+
form = TestForm()
149+
rendered_html = form["field"].as_field_group()
150+
151+
# Should render without hint elements for individual items
152+
self.assertNotIn('nhsuk-radios__hint', rendered_html)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django import forms
2+
3+
from ...nhsuk_forms.choice_field import MultipleChoiceField
4+
from ..models.response_set import ResponseSet, RespiratoryConditionValues
5+
6+
7+
class RespiratoryConditionsForm(forms.ModelForm):
8+
9+
def __init__(self, *args, **kwargs):
10+
self.participant = kwargs.pop('participant')
11+
super().__init__(*args, **kwargs)
12+
self.instance.participant = self.participant
13+
14+
self.fields["respiratory_conditions"] = MultipleChoiceField(
15+
choices=RespiratoryConditionValues.choices,
16+
widget=forms.CheckboxSelectMultiple,
17+
label="Have you ever been diagnosed with any of the following respiratory conditions?",
18+
label_classes="nhsuk-fieldset__legend--l",
19+
hint="Select all that apply",
20+
error_messages={
21+
'required': 'Select if you have had any respiratory conditions',
22+
'singleton_option': 'Select if you have had any respiratory conditions, or select \'No, I have not had any of these respiratory conditions\''
23+
}
24+
)
25+
26+
# Add hints for each choice
27+
respiratory_conditions_field = self["respiratory_conditions"]
28+
respiratory_conditions_field.add_hint_for_choice(
29+
RespiratoryConditionValues.PNEUMONIA,
30+
"An infection of the lungs, usually diagnosed by a chest x-ray"
31+
)
32+
respiratory_conditions_field.add_hint_for_choice(
33+
RespiratoryConditionValues.EMPHYSEMA,
34+
"Damage to the air sacs in the lungs"
35+
)
36+
respiratory_conditions_field.add_hint_for_choice(
37+
RespiratoryConditionValues.BRONCHITIS,
38+
"An ongoing inflammation of the airways in the lungs that is usually caused by an infection"
39+
)
40+
respiratory_conditions_field.add_hint_for_choice(
41+
RespiratoryConditionValues.TUBERCULOSIS,
42+
"An infection that usually affects the lungs, but can affect any part of the body"
43+
)
44+
respiratory_conditions_field.add_hint_for_choice(
45+
RespiratoryConditionValues.COPD,
46+
"A group of lung conditions that cause breathing difficulties"
47+
)
48+
49+
# Add divider before "None of the above"
50+
respiratory_conditions_field.add_divider_after(
51+
RespiratoryConditionValues.COPD,
52+
"or"
53+
)
54+
55+
class Meta:
56+
model = ResponseSet
57+
fields = ['respiratory_conditions']

0 commit comments

Comments
 (0)