Skip to content

Commit 5509fc9

Browse files
committed
WIP
1 parent 5bcf176 commit 5509fc9

File tree

12 files changed

+391
-9
lines changed

12 files changed

+391
-9
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 fill_in_and_submit_participant_id(page, participant_id):
45
page.fill("input[name='participant_id']", participant_id)
@@ -84,3 +85,12 @@ def fill_in_and_submit_asbestos_exposure(page, answer):
8485
page.get_by_label(answer, exact=True).check()
8586

8687
page.click("text=Continue")
88+
89+
def fill_in_and_submit_respiratory_conditions(page, answer):
90+
expect(page.locator("legend")).to_have_text(
91+
"Have you ever been diagnosed with any of the following respiratory conditions?")
92+
93+
check_labels(page, answer)
94+
95+
page.click("text=Continue")
96+

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, "None of the above")
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: 3 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, "None of the above")
100101

101102
expect(page).to_have_url(f"{self.live_server_url}/asbestos-exposure")
102103
expect_back_link_to_have_url(page, "/respiratory-conditions")

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+
}) }}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
}
23+
)
24+
25+
# Add hints for each choice
26+
respiratory_conditions_field = self["respiratory_conditions"]
27+
respiratory_conditions_field.add_hint_for_choice(
28+
RespiratoryConditionValues.PNEUMONIA,
29+
"An infection of the lungs, usually diagnosed by a chest x-ray"
30+
)
31+
respiratory_conditions_field.add_hint_for_choice(
32+
RespiratoryConditionValues.EMPHYSEMA,
33+
"Damage to the air sacs in the lungs"
34+
)
35+
respiratory_conditions_field.add_hint_for_choice(
36+
RespiratoryConditionValues.BRONCHITIS,
37+
"An ongoing inflammation of the airways in the lungs that is usually caused by an infection"
38+
)
39+
respiratory_conditions_field.add_hint_for_choice(
40+
RespiratoryConditionValues.TUBERCULOSIS,
41+
"An infection that usually affects the lungs, but can affect any part of the body"
42+
)
43+
respiratory_conditions_field.add_hint_for_choice(
44+
RespiratoryConditionValues.COPD,
45+
"A group of lung conditions that cause breathing difficulties"
46+
)
47+
48+
# Add divider before "None of the above"
49+
respiratory_conditions_field.add_divider_after(
50+
RespiratoryConditionValues.COPD,
51+
"or"
52+
)
53+
54+
class Meta:
55+
model = ResponseSet
56+
fields = ['respiratory_conditions']
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.2.8 on 2025-11-14 11:20
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('questions', '0017_alter_responseset_asbestos_exposure'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='responseset',
16+
name='respiratory_conditions',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Chronic bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'None of the above')], max_length=1), blank=True, null=True, size=None),
18+
),
19+
]

lung_cancer_screening/questions/models/response_set.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.db import models
2+
from django.contrib.postgres.fields import ArrayField
23
from django.core.exceptions import ValidationError
34
from django.core.validators import MaxValueValidator, MinValueValidator
45
from dateutil.relativedelta import relativedelta
@@ -33,6 +34,14 @@ class EthnicityValues(models.TextChoices):
3334
OTHER = "O", "Other ethnic group"
3435
PREFER_NOT_TO_SAY = "N", "I'd prefer not to say"
3536

37+
class RespiratoryConditionValues(models.TextChoices):
38+
PNEUMONIA = "P", "Pneumonia"
39+
EMPHYSEMA = "E", "Emphysema"
40+
BRONCHITIS = "B", "Chronic bronchitis"
41+
TUBERCULOSIS = "T", "Tuberculosis (TB)"
42+
COPD = "C", "Chronic obstructive pulmonary disease (COPD)"
43+
NONE = "N", "None of the above"
44+
3645
class ResponseSet(BaseModel):
3746
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
3847

@@ -95,6 +104,12 @@ class ResponseSet(BaseModel):
95104
blank=True
96105
)
97106

107+
respiratory_conditions = ArrayField(
108+
models.CharField(max_length=1, choices=RespiratoryConditionValues.choices),
109+
null=True,
110+
blank=True
111+
)
112+
98113
asbestos_exposure = models.BooleanField(
99114
null=True,
100115
blank=True
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from django.test import TestCase
2+
3+
from ....models.participant import Participant
4+
from ....forms.respiratory_conditions_form import RespiratoryConditionsForm
5+
6+
7+
class TestRespiratoryConditionsForm(TestCase):
8+
def setUp(self):
9+
self.participant = Participant.objects.create(unique_id="1234567890")
10+
11+
def test_is_valid_with_single_condition(self):
12+
form = RespiratoryConditionsForm(
13+
participant=self.participant,
14+
data={
15+
"respiratory_conditions": ["P"]
16+
}
17+
)
18+
self.assertTrue(form.is_valid())
19+
self.assertEqual(
20+
form.cleaned_data["respiratory_conditions"],
21+
["P"]
22+
)
23+
24+
def test_is_valid_with_multiple_conditions(self):
25+
form = RespiratoryConditionsForm(
26+
participant=self.participant,
27+
data={
28+
"respiratory_conditions": ["P", "E", "C"]
29+
}
30+
)
31+
self.assertTrue(form.is_valid())
32+
self.assertEqual(
33+
form.cleaned_data["respiratory_conditions"],
34+
["P", "E", "C"]
35+
)
36+
37+
def test_is_valid_with_none_of_the_above(self):
38+
form = RespiratoryConditionsForm(
39+
participant=self.participant,
40+
data={
41+
"respiratory_conditions": ["N"]
42+
}
43+
)
44+
self.assertTrue(form.is_valid())
45+
self.assertEqual(
46+
form.cleaned_data["respiratory_conditions"],
47+
["N"]
48+
)
49+
50+
def test_is_invalid_with_an_invalid_value(self):
51+
form = RespiratoryConditionsForm(
52+
participant=self.participant,
53+
data={
54+
"respiratory_conditions": ["INVALID"]
55+
}
56+
)
57+
self.assertFalse(form.is_valid())
58+
self.assertEqual(
59+
form.errors["respiratory_conditions"],
60+
["Select a valid choice. INVALID is not one of the available choices."]
61+
)
62+
63+
def test_is_invalid_when_no_option_is_selected(self):
64+
form = RespiratoryConditionsForm(
65+
participant=self.participant,
66+
data={
67+
"respiratory_conditions": []
68+
}
69+
)
70+
self.assertFalse(form.is_valid())
71+
self.assertEqual(
72+
form.errors["respiratory_conditions"],
73+
["Select if you have had any respiratory conditions"]
74+
)
75+
76+
# TODO: Add test for when none of the above is selected and other options are selected
77+
# def test_is_invalid_with_none_of_the_above_selected_and_other_options_selected(self):
78+
# form = RespiratoryConditionsForm(
79+
# participant=self.participant,
80+
# data={
81+
# "respiratory_conditions": ["N", "P", "E", "C"]
82+
# }
83+
# )
84+
# self.assertFalse(form.is_valid())
85+
# self.assertEqual(
86+
# form.errors["respiratory_conditions"],
87+
# ["Select if you have had any respiratory conditions"]
88+
# )
89+
90+

0 commit comments

Comments
 (0)