Skip to content

Commit 5d92c90

Browse files
committed
Move boolean choice to a Form object
1 parent 9878891 commit 5d92c90

File tree

9 files changed

+297
-29
lines changed

9 files changed

+297
-29
lines changed

lung_cancer_screening/core/form_fields.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,46 @@ def _template_name(widget):
278278
return "forms/select.jinja"
279279

280280

281+
class TypedChoiceField(forms.TypedChoiceField):
282+
"""
283+
A TypedChoiceField that renders using NHS.UK design system radios/select
284+
components.
285+
"""
286+
287+
widget = widgets.RadioSelect
288+
bound_field_class = BoundChoiceField
289+
290+
def __init__(
291+
self,
292+
*args,
293+
hint=None,
294+
label_classes=None,
295+
classes=None,
296+
**kwargs,
297+
):
298+
kwargs["template_name"] = TypedChoiceField._template_name(
299+
kwargs.get("widget", self.widget)
300+
)
301+
302+
self.hint = hint
303+
self.classes = classes
304+
self.label_classes = label_classes
305+
306+
super().__init__(*args, **kwargs)
307+
308+
@staticmethod
309+
def _template_name(widget):
310+
if (isinstance(widget, type) and widget is widgets.RadioSelect) or isinstance(
311+
widget, widgets.RadioSelect
312+
):
313+
return "forms/radios.jinja"
314+
elif (isinstance(widget, type) and widget is widgets.Select) or isinstance(
315+
widget, widgets.Select
316+
):
317+
return "forms/select.jinja"
318+
319+
320+
281321
class MultipleChoiceField(forms.MultipleChoiceField):
282322
"""
283323
A MultipleChoiceField that renders using the NHS.UK design system checkboxes
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{% from 'components/radios/macro.jinja' import radios %}
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 conditional_html = field.conditional_html(value) %}
9+
{% set ns.items = ns.items + [{
10+
"id": field.auto_id if loop.first,
11+
"value": value,
12+
"text": text,
13+
"checked": field.value() == value,
14+
"conditional": {
15+
"html": conditional_html
16+
} if conditional_html 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+
{{ radios({
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+
},
32+
"errorMessage": error_message,
33+
"hint": {
34+
"html": unbound_field.hint|e
35+
} if unbound_field.hint,
36+
"items": ns.items
37+
}) }}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import os
2+
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
3+
from playwright.sync_api import sync_playwright, expect
4+
5+
class TestQuestionnaireValidationErrors(StaticLiveServerTestCase):
6+
7+
@classmethod
8+
def setUpClass(cls):
9+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
10+
super().setUpClass()
11+
cls.playwright = sync_playwright().start()
12+
cls.browser = cls.playwright.chromium.launch()
13+
14+
@classmethod
15+
def tearDownClass(cls):
16+
super().tearDownClass()
17+
cls.browser.close()
18+
cls.playwright.stop()
19+
20+
def test_full_questionaire_user_journey_with_validation_errors(self):
21+
participant_id = '123'
22+
23+
page = self.browser.new_page()
24+
page.goto(f"{self.live_server_url}/start")
25+
26+
page.fill("input[name='participant_id']", participant_id)
27+
28+
page.click('text=Start now')
29+
30+
expect(page).to_have_url(
31+
f"{self.live_server_url}/have-you-ever-smoked")
32+
33+
expect(page.locator("legend")).to_have_text(
34+
"Have you ever smoked?")
35+
36+
page.get_by_label('Yes').check()
37+
38+
page.click("text=Continue")
39+
40+
expect(page).to_have_url(f"{self.live_server_url}/date-of-birth")
41+
42+
expect(page.locator("legend")).to_have_text(
43+
"What is your date of birth?")
44+
45+
page.get_by_label("Day").fill("100")
46+
page.get_by_label("Month").fill("100")
47+
page.get_by_label("Year").fill("some string")
48+
49+
page.click("text=Continue")
50+
51+
expect(page).to_have_url(f"{self.live_server_url}/date-of-birth")
52+
53+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
54+
"Day should be between 1 and 31"
55+
)

lung_cancer_screening/core/tests/unit/test_form_fields.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
IntegerField,
1919
MultipleChoiceField,
2020
SplitDateField,
21+
TypedChoiceField,
2122
)
2223

2324

@@ -541,6 +542,116 @@ def test_adding_dividers_via_boundfield(self, form_class):
541542
assert bound_field.get_divider_after("a") == "or"
542543

543544

545+
class TestTypedChoiceField:
546+
@pytest.fixture
547+
def form_class(self):
548+
class TestForm(Form):
549+
field = TypedChoiceField(
550+
label="Abc",
551+
label_classes="app-abc",
552+
choices=(("a", "A"), ("b", "B")),
553+
hint="Pick either one",
554+
)
555+
select_field = TypedChoiceField(
556+
label="Select",
557+
label_classes="app-select",
558+
choices=(("a", "A"), ("b", "B")),
559+
hint="Pick either one",
560+
widget=Select,
561+
)
562+
details = CharField(label="Abc", initial="")
563+
564+
return TestForm
565+
566+
def test_renders_nhs_radios(self, form_class):
567+
assertHTMLEqual(
568+
form_class()["field"].as_field_group(),
569+
"""
570+
<div class="nhsuk-form-group">
571+
<fieldset aria-describedby="id_field-hint" class="nhsuk-fieldset">
572+
<legend class="nhsuk-fieldset__legend app-abc">
573+
Abc
574+
</legend>
575+
<div class="nhsuk-hint" id="id_field-hint">
576+
Pick either one
577+
</div>
578+
<div class="nhsuk-radios" data-module="nhsuk-radios">
579+
<div class="nhsuk-radios__item">
580+
<input class="nhsuk-radios__input" id="id_field" name="field" type="radio" value="a">
581+
<label class="nhsuk-label nhsuk-radios__label" for="id_field">A</label>
582+
</div>
583+
<div class="nhsuk-radios__item">
584+
<input class="nhsuk-radios__input" id="id_field-2" name="field" type="radio" value="b">
585+
<label class="nhsuk-label nhsuk-radios__label" for="id_field-2">B</label>
586+
</div>
587+
</div>
588+
</fieldset>
589+
</div>
590+
""",
591+
)
592+
593+
def test_renders_radios_with_conditional_html(self, form_class):
594+
form = form_class()
595+
form["field"].add_conditional_html("b", "<p>Hello</p>")
596+
597+
assertHTMLEqual(
598+
form["field"].as_field_group(),
599+
"""
600+
<div class="nhsuk-form-group">
601+
<fieldset aria-describedby="id_field-hint" class="nhsuk-fieldset">
602+
<legend class="nhsuk-fieldset__legend app-abc">
603+
Abc
604+
</legend>
605+
<div class="nhsuk-hint" id="id_field-hint">
606+
Pick either one
607+
</div>
608+
<div class="nhsuk-radios nhsuk-radios--conditional" data-module="nhsuk-radios">
609+
<div class="nhsuk-radios__item">
610+
<input class="nhsuk-radios__input" id="id_field" name="field" type="radio" value="a">
611+
<label class="nhsuk-label nhsuk-radios__label" for="id_field">A</label>
612+
</div>
613+
<div class="nhsuk-radios__item">
614+
<input aria-controls="conditional-id_field-2" aria-expanded="false" class="nhsuk-radios__input" id="id_field-2" name="field" type="radio" value="b">
615+
<label class="nhsuk-label nhsuk-radios__label" for="id_field-2">B</label>
616+
</div>
617+
<div class="nhsuk-radios__conditional nhsuk-radios__conditional--hidden" id="conditional-id_field-2">
618+
<p>Hello</p>
619+
</div>
620+
</div>
621+
</fieldset>
622+
</div>
623+
""",
624+
)
625+
626+
def test_renders_nhs_select(self, form_class):
627+
assertHTMLEqual(
628+
form_class()["select_field"].as_field_group(),
629+
"""
630+
<div class="nhsuk-form-group">
631+
<label class="nhsuk-label app-select" for="id_select_field">Select</label>
632+
<div class="nhsuk-hint" id="id_select_field-hint">
633+
Pick either one
634+
</div>
635+
<select aria-describedby="id_select_field-hint" class="nhsuk-select" id="id_select_field" name="select_field">
636+
<option value="a">A</option>
637+
<option value="b">B</option>
638+
</select>
639+
</div>
640+
""",
641+
)
642+
643+
def test_renders_select_with_conditional_html(self, form_class):
644+
form = form_class()
645+
646+
with pytest.raises(ValueError):
647+
form["select_field"].add_conditional_html("b", "<p>Hello</p>")
648+
649+
def test_adding_dividers_via_boundfield(self, form_class):
650+
bound_field = form_class()["field"]
651+
bound_field.add_divider_after("a", "or")
652+
assert bound_field.get_divider_after("a") == "or"
653+
654+
544655
class TestMultipleChoiceField:
545656
@pytest.fixture
546657
def form_class(self):
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django import forms
2+
3+
from lung_cancer_screening.core.form_fields import TypedChoiceField
4+
from ..models.boolean_response import BooleanResponse
5+
6+
class BooleanResponseForm(forms.ModelForm):
7+
CHOICES = [
8+
(True, "Yes"),
9+
(False, "No")
10+
]
11+
12+
def __init__(self, *args, **kwargs):
13+
super().__init__(*args, **kwargs)
14+
15+
self.fields["value"] = TypedChoiceField(
16+
choices=self.CHOICES,
17+
widget=forms.RadioSelect,
18+
coerce=lambda x: x == 'True',
19+
label="Have you ever smoked?",
20+
label_classes="nhsuk-fieldset__legend--m",
21+
)
22+
23+
class Meta:
24+
model = BooleanResponse
25+
fields = ['value']

lung_cancer_screening/questions/jinja2/have_you_ever_smoked.jinja

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,8 @@
88
<form action="{{ request.path }}" method="POST">
99
{{ csrf_input }}
1010

11-
{{ radios({
12-
"name": "value",
13-
"fieldset": {
14-
"legend": {
15-
"text": "Have you ever smoked?",
16-
"classes": "nhsuk-fieldset__legend--l",
17-
"isPageHeading": true
18-
}
19-
},
20-
"items": [
21-
{
22-
"value": "1",
23-
"text": "Yes"
24-
},
25-
{
26-
"value": "0",
27-
"text": "No"
28-
}
29-
]
30-
}) }}
11+
{{ form.value.as_field_group() }}
12+
3113

3214
{{ button({
3315
"text": "Continue"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.test import TestCase
2+
from ....forms.boolean_response_form import BooleanResponseForm
3+
4+
class TestBooleanResponseForm(TestCase):
5+
def test_is_valid(self):
6+
form = BooleanResponseForm(data={"value": True})
7+
self.assertTrue(form.is_valid())
8+
9+
def test_is_invalid(self):
10+
form = BooleanResponseForm(data={"value": "invalid"})
11+
self.assertFalse(form.is_valid())
12+
13+
def test_returns_a_boolean_type(self):
14+
form = BooleanResponseForm(data={"value": True})
15+
form.is_valid()
16+
self.assertIsInstance(form.cleaned_data["value"], bool)

lung_cancer_screening/questions/tests/unit/views/test_have_you_ever_smoked.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class TestHaveYouEverSmoked(TestCase):
88
def setUp(self):
99
self.participant = Participant.objects.create(unique_id="12345")
10-
self.valid_params = { "value": 1 }
10+
self.valid_params = { "value": True }
1111

1212
session = self.client.session
1313
session['participant_id'] = self.participant.unique_id
@@ -87,7 +87,7 @@ def test_post_does_not_create_a_date_response_if_the_user_is_not_a_smoker(self):
8787
def test_post_redirects_if_the_user_not_a_smoker(self):
8888
response = self.client.post(
8989
reverse("questions:have_you_ever_smoked"),
90-
{"value": 0}
90+
{"value": False }
9191
)
9292

9393
self.assertRedirects(response, reverse("questions:non_smoker_exit"))

0 commit comments

Comments
 (0)