Skip to content

Commit 1d3e841

Browse files
authored
Merge pull request #116 from NHSDigital/test-custom-form-fields
Migrate custom form fields into nhsuk_forms
2 parents 8bcb602 + d2c241d commit 1d3e841

31 files changed

+915
-1257
lines changed

lung_cancer_screening/core/form_fields.py

Lines changed: 0 additions & 523 deletions
This file was deleted.

lung_cancer_screening/core/tests/unit/not_test_form_fields.py

Lines changed: 0 additions & 728 deletions
This file was deleted.
File renamed without changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django import forms
2+
from django.forms import widgets
3+
4+
from lung_cancer_screening.nhsuk_forms import choice_field
5+
6+
class BoundChoiceField(forms.BoundField):
7+
"""
8+
Specialisation of BoundField that can deal with conditionally shown fields,
9+
and divider content between choices.
10+
This can be used to render a set of radios or checkboxes with text boxes to capture
11+
more details.
12+
"""
13+
14+
def __init__(self, form: forms.Form, field: "choice_field", name: str):
15+
super().__init__(form, field, name)
16+
17+
self._conditional_html = {}
18+
self.dividers = {}
19+
20+
def add_conditional_html(self, value, html):
21+
if isinstance(self.field.widget, widgets.Select):
22+
raise ValueError(
23+
"select component does not support conditional fields")
24+
25+
self._conditional_html[value] = html
26+
27+
def conditional_html(self, value):
28+
return self._conditional_html.get(value)
29+
30+
def add_divider_after(self, previous, divider):
31+
self.dividers[previous] = divider
32+
33+
def get_divider_after(self, previous):
34+
return self.dividers.get(previous)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from django import forms
2+
from django.forms import widgets
3+
4+
from .bound_choice_field import BoundChoiceField
5+
6+
class ChoiceField(forms.ChoiceField):
7+
"""
8+
A ChoiceField that renders using NHS.UK design system radios/select
9+
components.
10+
"""
11+
12+
widget = widgets.RadioSelect
13+
bound_field_class = BoundChoiceField
14+
15+
def __init__(
16+
self,
17+
*args,
18+
hint=None,
19+
label_classes=None,
20+
classes=None,
21+
**kwargs,
22+
):
23+
kwargs["template_name"] = ChoiceField._template_name(
24+
kwargs.get("widget", self.widget)
25+
)
26+
27+
self.hint = hint
28+
self.classes = classes
29+
self.label_classes = label_classes
30+
31+
super().__init__(*args, **kwargs)
32+
33+
@staticmethod
34+
def _template_name(widget):
35+
return "radios.jinja"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from django import forms
2+
3+
class DecimalField(forms.DecimalField):
4+
def __init__(
5+
self,
6+
*args,
7+
hint=None,
8+
label_classes=None,
9+
classes=None,
10+
**kwargs,
11+
):
12+
kwargs["template_name"] = "input.jinja"
13+
14+
self.hint = hint
15+
self.classes = classes
16+
self.label_classes = label_classes
17+
18+
super().__init__(*args, **kwargs)
19+
20+
def widget_attrs(self, widget):
21+
attrs = super().widget_attrs(widget)
22+
23+
# Don't use min/max/step attributes.
24+
attrs.pop("min", None)
25+
attrs.pop("max", None)
26+
attrs.pop("step", None)
27+
28+
return attrs
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from django import forms
2+
from django.forms import widgets
3+
4+
from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField
5+
6+
7+
class ImperialHeightWidget(widgets.MultiWidget):
8+
"""
9+
A widget that splits height into feet and inches inputs.
10+
"""
11+
12+
def __init__(self, attrs=None):
13+
height_widgets = (
14+
widgets.NumberInput(attrs=attrs),
15+
widgets.NumberInput(attrs=attrs),
16+
)
17+
super().__init__(height_widgets, attrs)
18+
19+
def decompress(self, value):
20+
"""
21+
Convert total inches back to feet and inches for display.
22+
"""
23+
if value:
24+
feet = int(value // 12)
25+
inches = value % 12
26+
return [feet, inches]
27+
return [None, None]
28+
29+
def subwidgets(self, name, value, attrs=None):
30+
"""
31+
Expose data for each subwidget, so that we can render them separately in the template.
32+
"""
33+
context = self.get_context(name, value, attrs)
34+
for subwidget in context["widget"]["subwidgets"]:
35+
yield subwidget
36+
37+
38+
class ImperialHeightField(forms.MultiValueField):
39+
"""
40+
A field that combines feet and inches into a single height value in cm.
41+
"""
42+
43+
widget = ImperialHeightWidget
44+
45+
def __init__(self, *args, **kwargs):
46+
error_messages = kwargs.get("error_messages", {})
47+
48+
feet_kwargs = {
49+
"error_messages": {
50+
'invalid': 'Feet must be in whole numbers.',
51+
**error_messages,
52+
},
53+
}
54+
inches_kwargs = {
55+
"error_messages": {
56+
'invalid': 'Inches must be in whole numbers.',
57+
**error_messages,
58+
},
59+
}
60+
fields = (
61+
IntegerField(**feet_kwargs),
62+
IntegerField(**inches_kwargs),
63+
)
64+
kwargs["template_name"] = "imperial-height-input.jinja"
65+
66+
super().__init__(fields, *args, **kwargs)
67+
68+
def compress(self, data_list):
69+
"""
70+
Convert feet and inches to total inches.
71+
"""
72+
if data_list and all(data_list):
73+
feet, inches = data_list
74+
total_inches = feet * 12 + inches
75+
return int(total_inches)
76+
return None
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from django import forms
2+
from django.forms import widgets
3+
4+
from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField
5+
6+
class ImperialWeightWidget(widgets.MultiWidget):
7+
"""
8+
A widget that splits weight into stone and pounds inputs.
9+
"""
10+
11+
def __init__(self, attrs=None):
12+
weight_widgets = (
13+
widgets.NumberInput(attrs=attrs),
14+
widgets.NumberInput(attrs=attrs),
15+
)
16+
super().__init__(weight_widgets, attrs)
17+
18+
def decompress(self, value):
19+
"""
20+
Convert total pounds back to stone and pounds for display.
21+
"""
22+
if value:
23+
stone = int(value // 14)
24+
pounds = value % 14
25+
return [stone, pounds]
26+
return [None, None]
27+
28+
def subwidgets(self, name, value, attrs=None):
29+
"""
30+
Expose data for each subwidget, so that we can render them separately in the template.
31+
"""
32+
context = self.get_context(name, value, attrs)
33+
for subwidget in context["widget"]["subwidgets"]:
34+
yield subwidget
35+
36+
37+
class ImperialWeightField(forms.MultiValueField):
38+
"""
39+
A field that combines stone and pounds into a single weight value in total pounds.
40+
"""
41+
42+
widget = ImperialWeightWidget
43+
44+
def __init__(self, *args, **kwargs):
45+
error_messages = kwargs.get("error_messages", {})
46+
47+
stone_kwargs = {
48+
"min_value": 0,
49+
"max_value": 50,
50+
"error_messages": {
51+
'invalid': 'Stone must be in whole numbers.',
52+
'min_value': 'Weight must be between 4 stone and 50 stone.',
53+
'max_value': 'Weight must be between 4 stone and 50 stone.',
54+
**error_messages,
55+
},
56+
}
57+
pounds_kwargs = {
58+
"min_value": 0,
59+
"max_value": 13,
60+
"error_messages": {
61+
'invalid': 'Pounds must be in whole numbers.',
62+
'min_value': 'Pounds must be between 0 and 13.',
63+
'max_value': 'Pounds must be between 0 and 13.',
64+
**error_messages,
65+
},
66+
}
67+
fields = (
68+
IntegerField(**stone_kwargs),
69+
IntegerField(**pounds_kwargs),
70+
)
71+
kwargs["template_name"] = "imperial-weight-input.jinja"
72+
73+
super().__init__(fields, *args, **kwargs)
74+
75+
def compress(self, data_list):
76+
"""
77+
Convert stone and pounds to total pounds.
78+
"""
79+
if data_list and all(item is not None for item in data_list):
80+
stone, pounds = data_list
81+
total_pounds = stone * 14 + pounds
82+
return int(total_pounds)
83+
return None
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django import forms
2+
3+
4+
class IntegerField(forms.IntegerField):
5+
def __init__(
6+
self,
7+
*args,
8+
hint=None,
9+
label_classes=None,
10+
classes=None,
11+
**kwargs,
12+
):
13+
kwargs["template_name"] = "input.jinja"
14+
15+
self.hint = hint
16+
self.classes = classes
17+
self.label_classes = label_classes
18+
19+
super().__init__(*args, **kwargs)
20+
21+
def widget_attrs(self, widget):
22+
attrs = super().widget_attrs(widget)
23+
24+
# Don't use min/max/step attributes.
25+
attrs.pop("min", None)
26+
attrs.pop("max", None)
27+
attrs.pop("step", None)
28+
29+
return attrs

lung_cancer_screening/core/jinja2/forms/date-input.jinja renamed to lung_cancer_screening/nhsuk_forms/jinja2/date-input.jinja

File renamed without changes.

0 commit comments

Comments
 (0)