From 7db1706f4cc22dfaaace2c71f9dc53f841dbfe71 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:39:42 +0100 Subject: [PATCH 1/9] Move IntegerField to nhsuk_forms module and add tests --- lung_cancer_screening/core/form_fields.py | 65 +----- .../core/tests/unit/not_test_form_fields.py | 195 ------------------ .../{core/utils => nhsuk_forms}/__init__.py | 0 .../nhsuk_forms/integer_field.py | 30 +++ .../nhsuk_forms/tests/__init__.py | 0 .../nhsuk_forms/tests/unit/__init__.py | 0 .../tests/unit/test_integer_field.py | 21 ++ .../nhsuk_forms/utils/__init__.py | 0 .../utils/date_formatting.py | 0 lung_cancer_screening/settings.py | 1 + 10 files changed, 54 insertions(+), 258 deletions(-) rename lung_cancer_screening/{core/utils => nhsuk_forms}/__init__.py (100%) create mode 100644 lung_cancer_screening/nhsuk_forms/integer_field.py create mode 100644 lung_cancer_screening/nhsuk_forms/tests/__init__.py create mode 100644 lung_cancer_screening/nhsuk_forms/tests/unit/__init__.py create mode 100644 lung_cancer_screening/nhsuk_forms/tests/unit/test_integer_field.py create mode 100644 lung_cancer_screening/nhsuk_forms/utils/__init__.py rename lung_cancer_screening/{core => nhsuk_forms}/utils/date_formatting.py (100%) diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/core/form_fields.py index 2ff0dfc1..b80fbe1f 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/core/form_fields.py @@ -6,8 +6,8 @@ from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ -from .utils.date_formatting import format_date - +from lung_cancer_screening.nhsuk_forms.utils.date_formatting import format_date +from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField class SplitDateWidget(widgets.MultiWidget): """ @@ -146,67 +146,6 @@ def widget_attrs(self, widget): subwidget.attrs["max"] = subfield.max_value return attrs - -class CharField(forms.CharField): - def __init__( - self, - *args, - hint=None, - label_classes=None, - classes=None, - **kwargs, - ): - widget = kwargs.get("widget") - if (isinstance(widget, type) and widget is Textarea) or isinstance( - widget, Textarea - ): - kwargs["template_name"] = "forms/textarea.jinja" - else: - kwargs["template_name"] = "forms/input.jinja" - - self.hint = hint - self.classes = classes - self.label_classes = label_classes - - super().__init__(*args, **kwargs) - - def widget_attrs(self, widget): - attrs = super().widget_attrs(widget) - - # Don't use maxlength even if there is a max length validator. - # This attribute prevents the user from seeing errors, so we don't use it - attrs.pop("maxlength", None) - - return attrs - - -class IntegerField(forms.IntegerField): - def __init__( - self, - *args, - hint=None, - label_classes=None, - classes=None, - **kwargs, - ): - kwargs["template_name"] = "forms/input.jinja" - - self.hint = hint - self.classes = classes - self.label_classes = label_classes - - super().__init__(*args, **kwargs) - - def widget_attrs(self, widget): - attrs = super().widget_attrs(widget) - - # Don't use min/max/step attributes. - attrs.pop("min", None) - attrs.pop("max", None) - attrs.pop("step", None) - - return attrs - class DecimalField(forms.DecimalField): def __init__( self, diff --git a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py index f36b0040..b0aa022f 100644 --- a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py +++ b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py @@ -237,201 +237,6 @@ class TestForm(Form): assert not f.is_valid() assert f.errors == {"date": ["Enter a date before 1 July 2026"]} - -class TestCharField: - @pytest.fixture - def form_class(self): - class TestForm(Form): - field = CharField(label="Abc", initial="somevalue", max_length=10) - field_with_visually_hidden_label = CharField( - label="Abc", - initial="somevalue", - label_classes="nhsuk-u-visually-hidden", - ) - field_with_hint = CharField( - label="With hint", initial="", hint="ALL UPPERCASE" - ) - field_with_classes = CharField( - label="With classes", initial="", classes="nhsuk-u-width-two-thirds" - ) - field_with_extra_attrs = CharField( - label="Extra", - widget=TextInput( - attrs=dict( - autocomplete="off", - inputmode="numeric", - spellcheck="false", - autocapitalize="none", - pattern=r"\d{3}", - ) - ), - ) - telephone_field = CharField(label="Ring ring", widget=TelInput) - textfield = CharField( - label="Text", - widget=Textarea( - attrs={ - "rows": "3", - "autocomplete": "autocomplete", - "spellcheck": "true", - } - ), - ) - textfield_simple = CharField(label="Text", widget=Textarea) - - return TestForm - - def test_renders_nhs_input(self, form_class): - assertHTMLEqual( - form_class()["field"].as_field_group(), - """ -
- -
- """, - ) - - def test_renders_nhs_input_with_visually_hidden_label(self, form_class): - assertHTMLEqual( - form_class()["field_with_visually_hidden_label"].as_field_group(), - """ -
- -
- """, - ) - - def test_renders_nhs_input_with_hint(self, form_class): - assertHTMLEqual( - form_class()["field_with_hint"].as_field_group(), - """ -
- -
ALL UPPERCASE
- -
- """, - ) - - def test_renders_nhs_input_with_classes(self, form_class): - assertHTMLEqual( - form_class()["field_with_classes"].as_field_group(), - """ -
- - -
- """, - ) - - def test_renders_nhs_input_with_extra_attrs(self, form_class): - assertHTMLEqual( - form_class()["field_with_extra_attrs"].as_field_group(), - """ -
- - -
- """, - ) - - def test_bound_value_reflected_in_html_value(self, form_class): - assertHTMLEqual( - form_class({"field": "othervalue"})["field"].as_field_group(), - """ -
- -
- """, - ) - - def test_invalid_value_renders_validation_error(self, form_class): - assertHTMLEqual( - form_class({"field": "reallylongvalue"})["field"].as_field_group(), - """ -
- - - Error: Ensure this value has at most 10 characters (it has 15). - -
- """, - ) - - def test_telinput_renders_input_with_type_tel(self, form_class): - assertHTMLEqual( - form_class()["telephone_field"].as_field_group(), - """ -
- -
- """, - ) - - def test_textarea_renders_textarea(self, form_class): - assertHTMLEqual( - form_class()["textfield"].as_field_group(), - """ -
- - -
- """, - ) - - def test_textarea_class_renders_textarea(self, form_class): - assertHTMLEqual( - form_class()["textfield_simple"].as_field_group(), - """ -
- - -
- """, - ) - - -class TestIntegerField: - @pytest.fixture - def form_class(self): - class TestForm(Form): - field = IntegerField(label="Abc", initial=1, max_value=10) - - return TestForm - - def test_renders_nhs_input(self, form_class): - assertHTMLEqual( - form_class()["field"].as_field_group(), - """ -
- -
- """, - ) - - class TestChoiceField: @pytest.fixture def form_class(self): diff --git a/lung_cancer_screening/core/utils/__init__.py b/lung_cancer_screening/nhsuk_forms/__init__.py similarity index 100% rename from lung_cancer_screening/core/utils/__init__.py rename to lung_cancer_screening/nhsuk_forms/__init__.py diff --git a/lung_cancer_screening/nhsuk_forms/integer_field.py b/lung_cancer_screening/nhsuk_forms/integer_field.py new file mode 100644 index 00000000..bed29586 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/integer_field.py @@ -0,0 +1,30 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class IntegerField(forms.IntegerField): + def __init__( + self, + *args, + hint=None, + label_classes=None, + classes=None, + **kwargs, + ): + kwargs["template_name"] = "forms/input.jinja" + + self.hint = hint + self.classes = classes + self.label_classes = label_classes + + super().__init__(*args, **kwargs) + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + + # Don't use min/max/step attributes. + attrs.pop("min", None) + attrs.pop("max", None) + attrs.pop("step", None) + + return attrs diff --git a/lung_cancer_screening/nhsuk_forms/tests/__init__.py b/lung_cancer_screening/nhsuk_forms/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/__init__.py b/lung_cancer_screening/nhsuk_forms/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/test_integer_field.py b/lung_cancer_screening/nhsuk_forms/tests/unit/test_integer_field.py new file mode 100644 index 00000000..795f4a7f --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/tests/unit/test_integer_field.py @@ -0,0 +1,21 @@ +from django.test import TestCase +from django.forms import Form +from ...integer_field import IntegerField + + +class TestForm(Form): + field = IntegerField(label="Abc", initial=1, max_value=10) + +class TestIntegerField(TestCase): + def test_renders_nhs_input(self): + self.assertHTMLEqual( + TestForm()["field"].as_field_group(), + """ +
+ +
+ """, + ) + diff --git a/lung_cancer_screening/nhsuk_forms/utils/__init__.py b/lung_cancer_screening/nhsuk_forms/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lung_cancer_screening/core/utils/date_formatting.py b/lung_cancer_screening/nhsuk_forms/utils/date_formatting.py similarity index 100% rename from lung_cancer_screening/core/utils/date_formatting.py rename to lung_cancer_screening/nhsuk_forms/utils/date_formatting.py diff --git a/lung_cancer_screening/settings.py b/lung_cancer_screening/settings.py index 9f367c8e..663f605d 100644 --- a/lung_cancer_screening/settings.py +++ b/lung_cancer_screening/settings.py @@ -50,6 +50,7 @@ def list_env(key): 'django.contrib.messages', 'django.contrib.staticfiles', 'lung_cancer_screening.core', + 'lung_cancer_screening.nhsuk_forms', 'lung_cancer_screening.questions' ] From f7401886aadfbdf127f84bb8ffdc2b611828ca98 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:58:38 +0100 Subject: [PATCH 2/9] Move SplitDateWidget to nhsuk_forms and add tests --- lung_cancer_screening/core/form_fields.py | 143 +--------- .../core/tests/unit/not_test_form_fields.py | 224 --------------- .../nhsuk_forms/split_date_field.py | 148 ++++++++++ .../tests/unit/test_split_date_field.py | 261 ++++++++++++++++++ .../questions/forms/date_of_birth_form.py | 2 +- 5 files changed, 411 insertions(+), 367 deletions(-) create mode 100644 lung_cancer_screening/nhsuk_forms/split_date_field.py create mode 100644 lung_cancer_screening/nhsuk_forms/tests/unit/test_split_date_field.py diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/core/form_fields.py index b80fbe1f..5a83ad1c 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/core/form_fields.py @@ -1,151 +1,10 @@ -import datetime - from django import forms -from django.core import validators -from django.forms import Textarea, ValidationError, widgets -from django.utils.translation import gettext +from django.forms import widgets from django.utils.translation import gettext_lazy as _ from lung_cancer_screening.nhsuk_forms.utils.date_formatting import format_date from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField -class SplitDateWidget(widgets.MultiWidget): - """ - A widget that splits a date into 3 number inputs. - Adapted from https://github.com/ministryofjustice/django-govuk-forms/blob/master/govuk_forms/widgets.py - """ - - def __init__(self, attrs=None): - date_widgets = ( - widgets.NumberInput(attrs=attrs), - widgets.NumberInput(attrs=attrs), - widgets.NumberInput(attrs=attrs), - ) - super().__init__(date_widgets, attrs) - - def decompress(self, value): - if value: - return [value.day, value.month, value.year] - return [None, None, None] - - def subwidgets(self, name, value, attrs=None): - """ - Expose data for each subwidget, so that we can render them separately in the template. - - For some reason, as of Django 5.2, `MultiWidget` does not actually override the default - implementation provided by `Widget`, which means you can't call `form.date.0` `form.date.1` - to access the individual parts. - (see https://stackoverflow.com/questions/24866936/render-only-one-part-of-a-multiwidget-in-django) - """ - context = self.get_context(name, value, attrs) - for subwidget in context["widget"]["subwidgets"]: - yield subwidget - - -class SplitHiddenDateWidget(SplitDateWidget): - """ - A widget that splits a date into 3 number inputs (hidden variant) - Adapted from https://github.com/ministryofjustice/django-govuk-forms/blob/master/govuk_forms/widgets.py - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for widget in self.widgets: - widget.input_type = "hidden" - - -class SplitDateField(forms.MultiValueField): - """ - A form field that can be rendered as 3 inputs using the dateInput component in the design system. - Adapted from https://github.com/ministryofjustice/django-govuk-forms/blob/master/govuk_forms/fields.py - """ - - widget = SplitDateWidget - hidden_widget = SplitHiddenDateWidget - default_error_messages = {"invalid": _("Enter a valid date.")} - - def __init__(self, *args, **kwargs): - max_value = kwargs.pop("max_value", datetime.date.today()) - min_value = kwargs.pop("min_value", datetime.date(1900, 1, 1)) - self.hint = kwargs.pop("hint", None) - - day_bounds_error = gettext("Day should be between 1 and 31.") - month_bounds_error = gettext("Month should be between 1 and 12.") - year_bounds_error = gettext( - "Year should be between %(min_year)s and %(max_year)s." - ) % {"min_year": min_value.year, "max_year": max_value.year} - - day_kwargs = { - "min_value": 1, - "max_value": 31, - "error_messages": { - "min_value": day_bounds_error, - "max_value": day_bounds_error, - "invalid": gettext("Enter day as a number."), - }, - } - month_kwargs = { - "min_value": 1, - "max_value": 12, - "error_messages": { - "min_value": month_bounds_error, - "max_value": month_bounds_error, - "invalid": gettext("Enter month as a number."), - }, - } - year_kwargs = { - "min_value": min_value.year, - "max_value": max_value.year, - "error_messages": { - "min_value": year_bounds_error, - "max_value": year_bounds_error, - "invalid": gettext("Enter year as a number."), - }, - } - - self.fields = [ - IntegerField(**day_kwargs), - IntegerField(**month_kwargs), - IntegerField(**year_kwargs), - ] - - kwargs["template_name"] = "forms/date-input.jinja" - - super().__init__(self.fields, *args, **kwargs) - - self.validators.append( - validators.MinValueValidator( - min_value, f"Enter a date after {format_date(min_value)}" - ) - ) - self.validators.append( - validators.MaxValueValidator( - max_value, f"Enter a date before {format_date(max_value)}" - ) - ) - - def compress(self, data_list): - if data_list: - try: - if any(item in self.empty_values for item in data_list): - raise ValueError - return datetime.date(data_list[2], data_list[1], data_list[0]) - except ValueError: - raise ValidationError( - self.error_messages["invalid"], code="invalid") - return None - - def widget_attrs(self, widget): - attrs = super().widget_attrs(widget) - if not isinstance(widget, SplitDateWidget): - return attrs - for subfield, subwidget in zip(self.fields, widget.widgets): - if subfield.min_value is not None: - subwidget.attrs["min"] = subfield.min_value - if subfield.max_value is not None: - subwidget.attrs["max"] = subfield.max_value - return attrs - class DecimalField(forms.DecimalField): def __init__( self, diff --git a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py index b0aa022f..eb90b1f1 100644 --- a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py +++ b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py @@ -1,242 +1,18 @@ -import datetime - import pytest -from django.core.exceptions import ValidationError from django.forms import Form from django.forms.widgets import ( CheckboxSelectMultiple, Select, - TelInput, - Textarea, - TextInput, ) from pytest_django.asserts import assertHTMLEqual from ..form_fields import ( CharField, ChoiceField, - IntegerField, MultipleChoiceField, - SplitDateField, TypedChoiceField, ) - -class TestSplitDateField: - def test_clean(self): - f = SplitDateField(max_value=datetime.date(2026, 6, 30)) - - assert f.clean([1, 12, 2025]) == datetime.date(2025, 12, 1) - - with pytest.raises(ValidationError, match="This field is required."): - f.clean(None) - - with pytest.raises(ValidationError, match="This field is required."): - f.clean("") - - with pytest.raises(ValidationError, match="Enter a valid date."): - f.clean("hello") - - with pytest.raises( - ValidationError, - match=r"\['Enter day as a number.', 'Enter month as a number.', 'Enter year as a number.'\]", - ): - f.clean(["a", "b", "c"]) - - with pytest.raises( - ValidationError, - match=r"\['This field is required.'\]", - ): - f.clean(["", "", ""]) - - with pytest.raises( - ValidationError, - match=r"\['Day should be between 1 and 31.', 'Month should be between 1 and 12.', 'Year should be between 1900 and 2026.']", - ): - f.clean([0, 13, 1800]) - - with pytest.raises( - ValidationError, - match=r"\['Enter a date before 30 June 2026'\]", - ): - f.clean([1, 7, 2026]) - - def test_has_changed(self): - f = SplitDateField(max_value=datetime.date(2026, 12, 31)) - assert f.has_changed([1, 12, 2025], [2, 12, 2025]) - assert f.has_changed([1, 12, 2025], [1, 11, 2025]) - assert f.has_changed([1, 12, 2025], [1, 12, 2026]) - assert not f.has_changed([1, 12, 2025], [1, 12, 2025]) - - def test_default_django_render(self): - class TestForm(Form): - date = SplitDateField(max_value=datetime.date(2026, 12, 31)) - - f = TestForm() - - assertHTMLEqual( - str(f), - """
-
-
- - Date - -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- """, - ) - - def test_default_django_render_in_bound_form(self): - class TestForm(Form): - date = SplitDateField(max_value=datetime.date(2026, 12, 31)) - - f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"}) - - assertHTMLEqual( - str(f), - """
-
-
- - Date - -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- """, - ) - - def test_form_cleaned_data(self): - class TestForm(Form): - date = SplitDateField(max_value=datetime.date(2026, 12, 31)) - - f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"}) - - assert f.is_valid() - assert f.cleaned_data["date"] == datetime.date(2025, 12, 1) - - def test_bound_field_subwidgets(self): - class TestForm(Form): - date = SplitDateField(max_value=datetime.date(2026, 12, 31)) - - f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"}) - field = f["date"] - - assert len(field.subwidgets) == 3 - - assert field.subwidgets[0].data == { - "attrs": { - "id": "id_date_0", - "max": 31, - "min": 1, - "required": True, - }, - "is_hidden": False, - "name": "date_0", - "required": False, - "template_name": "django/forms/widgets/number.html", - "type": "number", - "value": "1", - } - - assert field.subwidgets[1].data == { - "attrs": { - "id": "id_date_1", - "max": 12, - "min": 1, - "required": True, - }, - "is_hidden": False, - "name": "date_1", - "required": False, - "template_name": "django/forms/widgets/number.html", - "type": "number", - "value": "12", - } - - assert field.subwidgets[2].data == { - "attrs": { - "id": "id_date_2", - "max": 2026, - "min": 1900, - "required": True, - }, - "is_hidden": False, - "name": "date_2", - "required": False, - "template_name": "django/forms/widgets/number.html", - "type": "number", - "value": "2025", - } - - def test_subfield_errors_on_form(self): - class TestForm(Form): - date = SplitDateField(max_value=datetime.date(2026, 12, 31)) - - f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2027"}) - assert not f.is_valid() - assert f.errors == {"date": ["Year should be between 1900 and 2026."]} - - def test_same_year_but_past_max_value(self): - class TestForm(Form): - date = SplitDateField(max_value=datetime.date(2026, 7, 1)) - - f = TestForm({"date_0": "1", "date_1": "8", "date_2": "2026"}) - assert not f.is_valid() - assert f.errors == {"date": ["Enter a date before 1 July 2026"]} - class TestChoiceField: @pytest.fixture def form_class(self): diff --git a/lung_cancer_screening/nhsuk_forms/split_date_field.py b/lung_cancer_screening/nhsuk_forms/split_date_field.py new file mode 100644 index 00000000..17653587 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/split_date_field.py @@ -0,0 +1,148 @@ +import datetime + +from django import forms +from django.core import validators +from django.forms import ValidationError, widgets +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ + +from lung_cancer_screening.nhsuk_forms.utils.date_formatting import format_date +from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField + + +class SplitDateWidget(widgets.MultiWidget): + """ + A widget that splits a date into 3 number inputs. + Adapted from https://github.com/ministryofjustice/django-govuk-forms/blob/master/govuk_forms/widgets.py + """ + + def __init__(self, attrs=None): + date_widgets = ( + widgets.NumberInput(attrs=attrs), + widgets.NumberInput(attrs=attrs), + widgets.NumberInput(attrs=attrs), + ) + super().__init__(date_widgets, attrs) + + def decompress(self, value): + if value: + return [value.day, value.month, value.year] + return [None, None, None] + + def subwidgets(self, name, value, attrs=None): + """ + Expose data for each subwidget, so that we can render them separately in the template. + + For some reason, as of Django 5.2, `MultiWidget` does not actually override the default + implementation provided by `Widget`, which means you can't call `form.date.0` `form.date.1` + to access the individual parts. + (see https://stackoverflow.com/questions/24866936/render-only-one-part-of-a-multiwidget-in-django) + """ + context = self.get_context(name, value, attrs) + for subwidget in context["widget"]["subwidgets"]: + yield subwidget + + +class SplitHiddenDateWidget(SplitDateWidget): + """ + A widget that splits a date into 3 number inputs (hidden variant) + Adapted from https://github.com/ministryofjustice/django-govuk-forms/blob/master/govuk_forms/widgets.py + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for widget in self.widgets: + widget.input_type = "hidden" + + +class SplitDateField(forms.MultiValueField): + """ + A form field that can be rendered as 3 inputs using the dateInput component in the design system. + Adapted from https://github.com/ministryofjustice/django-govuk-forms/blob/master/govuk_forms/fields.py + """ + + widget = SplitDateWidget + hidden_widget = SplitHiddenDateWidget + default_error_messages = {"invalid": _("Enter a valid date.")} + + def __init__(self, *args, **kwargs): + max_value = kwargs.pop("max_value", datetime.date.today()) + min_value = kwargs.pop("min_value", datetime.date(1900, 1, 1)) + self.hint = kwargs.pop("hint", None) + + day_bounds_error = gettext("Day should be between 1 and 31.") + month_bounds_error = gettext("Month should be between 1 and 12.") + year_bounds_error = gettext( + "Year should be between %(min_year)s and %(max_year)s." + ) % {"min_year": min_value.year, "max_year": max_value.year} + + day_kwargs = { + "min_value": 1, + "max_value": 31, + "error_messages": { + "min_value": day_bounds_error, + "max_value": day_bounds_error, + "invalid": gettext("Enter day as a number."), + }, + } + month_kwargs = { + "min_value": 1, + "max_value": 12, + "error_messages": { + "min_value": month_bounds_error, + "max_value": month_bounds_error, + "invalid": gettext("Enter month as a number."), + }, + } + year_kwargs = { + "min_value": min_value.year, + "max_value": max_value.year, + "error_messages": { + "min_value": year_bounds_error, + "max_value": year_bounds_error, + "invalid": gettext("Enter year as a number."), + }, + } + + self.fields = [ + IntegerField(**day_kwargs), + IntegerField(**month_kwargs), + IntegerField(**year_kwargs), + ] + + kwargs["template_name"] = "forms/date-input.jinja" + + super().__init__(self.fields, *args, **kwargs) + + self.validators.append( + validators.MinValueValidator( + min_value, f"Enter a date after {format_date(min_value)}." + ) + ) + self.validators.append( + validators.MaxValueValidator( + max_value, f"Enter a date before {format_date(max_value)}." + ) + ) + + def compress(self, data_list): + if data_list: + try: + if any(item in self.empty_values for item in data_list): + raise ValueError + return datetime.date(data_list[2], data_list[1], data_list[0]) + except ValueError: + raise ValidationError( + self.error_messages["invalid"], code="invalid") + return None + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + if not isinstance(widget, SplitDateWidget): + return attrs + for subfield, subwidget in zip(self.fields, widget.widgets): + if subfield.min_value is not None: + subwidget.attrs["min"] = subfield.min_value + if subfield.max_value is not None: + subwidget.attrs["max"] = subfield.max_value + return attrs diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/test_split_date_field.py b/lung_cancer_screening/nhsuk_forms/tests/unit/test_split_date_field.py new file mode 100644 index 00000000..9c0ea2f9 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/tests/unit/test_split_date_field.py @@ -0,0 +1,261 @@ +import datetime + +from django.test import TestCase +from django.core.exceptions import ValidationError +from django.forms import Form + +from ...split_date_field import SplitDateField + +class TestSplitDateField(TestCase): + def test_clean(self): + f = SplitDateField(max_value=datetime.date(2026, 6, 30)) + + assert f.clean([1, 12, 2025]) == datetime.date(2025, 12, 1) + + with self.assertRaises(ValidationError) as context: + f.clean(None) + + self.assertEqual( + context.exception.messages[0], + "This field is required." + ) + + with self.assertRaises(ValidationError) as context: + f.clean("") + + self.assertEqual( + context.exception.messages[0], + "This field is required." + ) + + with self.assertRaises(ValidationError) as context: + f.clean("hello") + + self.assertEqual( + context.exception.messages[0], + "Enter a valid date." + ) + + with self.assertRaises(ValidationError) as context: + f.clean(["a", "b", "c"]) + + self.assertIn( + "Enter day as a number.", + context.exception.messages + ) + self.assertIn( + "Enter month as a number.", + context.exception.messages + ) + self.assertIn( + "Enter year as a number.", + context.exception.messages + ) + + with self.assertRaises(ValidationError) as context: + f.clean(["", "", ""]) + + self.assertEqual( + context.exception.messages[0], + "This field is required." + ) + + with self.assertRaises(ValidationError) as context: + f.clean([0, 13, 1800]) + + self.assertIn( + "Day should be between 1 and 31.", + context.exception.messages + ) + self.assertIn( + "Month should be between 1 and 12.", + context.exception.messages + ) + self.assertIn( + "Year should be between 1900 and 2026.", + context.exception.messages + ) + + with self.assertRaises(ValidationError) as context: + f.clean([1, 7, 2026]) + + self.assertIn( + "Enter a date before 30 June 2026.", + context.exception.messages + ) + + def test_has_changed(self): + f = SplitDateField(max_value=datetime.date(2026, 12, 31)) + assert f.has_changed([1, 12, 2025], [2, 12, 2025]) + assert f.has_changed([1, 12, 2025], [1, 11, 2025]) + assert f.has_changed([1, 12, 2025], [1, 12, 2026]) + assert not f.has_changed([1, 12, 2025], [1, 12, 2025]) + + def test_default_django_render(self): + class TestForm(Form): + date = SplitDateField(max_value=datetime.date(2026, 12, 31)) + + f = TestForm() + + self.assertHTMLEqual( + str(f), + """
+
+
+ + Date + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ """, + ) + + def test_default_django_render_in_bound_form(self): + class TestForm(Form): + date = SplitDateField(max_value=datetime.date(2026, 12, 31)) + + f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"}) + + self.assertHTMLEqual( + str(f), + """
+
+
+ + Date + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ """, + ) + + def test_form_cleaned_data(self): + class TestForm(Form): + date = SplitDateField(max_value=datetime.date(2026, 12, 31)) + + f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"}) + + assert f.is_valid() + assert f.cleaned_data["date"] == datetime.date(2025, 12, 1) + + def test_bound_field_subwidgets(self): + class TestForm(Form): + date = SplitDateField(max_value=datetime.date(2026, 12, 31)) + + f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2025"}) + field = f["date"] + + assert len(field.subwidgets) == 3 + + assert field.subwidgets[0].data == { + "attrs": { + "id": "id_date_0", + "max": 31, + "min": 1, + "required": True, + }, + "is_hidden": False, + "name": "date_0", + "required": False, + "template_name": "django/forms/widgets/number.html", + "type": "number", + "value": "1", + } + + assert field.subwidgets[1].data == { + "attrs": { + "id": "id_date_1", + "max": 12, + "min": 1, + "required": True, + }, + "is_hidden": False, + "name": "date_1", + "required": False, + "template_name": "django/forms/widgets/number.html", + "type": "number", + "value": "12", + } + + assert field.subwidgets[2].data == { + "attrs": { + "id": "id_date_2", + "max": 2026, + "min": 1900, + "required": True, + }, + "is_hidden": False, + "name": "date_2", + "required": False, + "template_name": "django/forms/widgets/number.html", + "type": "number", + "value": "2025", + } + + def test_subfield_errors_on_form(self): + class TestForm(Form): + date = SplitDateField(max_value=datetime.date(2026, 12, 31)) + + f = TestForm({"date_0": "1", "date_1": "12", "date_2": "2027"}) + assert not f.is_valid() + assert f.errors == {"date": ["Year should be between 1900 and 2026."]} + + def test_same_year_but_past_max_value(self): + class TestForm(Form): + date = SplitDateField(max_value=datetime.date(2026, 7, 1)) + + f = TestForm({"date_0": "1", "date_1": "8", "date_2": "2026"}) + assert not f.is_valid() + assert f.errors == {"date": ["Enter a date before 1 July 2026."]} diff --git a/lung_cancer_screening/questions/forms/date_of_birth_form.py b/lung_cancer_screening/questions/forms/date_of_birth_form.py index affa0951..3556ab4c 100644 --- a/lung_cancer_screening/questions/forms/date_of_birth_form.py +++ b/lung_cancer_screening/questions/forms/date_of_birth_form.py @@ -2,7 +2,7 @@ from datetime import date from ..models.response_set import ResponseSet -from lung_cancer_screening.core.form_fields import SplitDateField +from ...nhsuk_forms.split_date_field import SplitDateField class DateOfBirthForm(forms.ModelForm): def __init__(self, *args, **kwargs): From 27c1e21788b463b40388f7f7c3812a7d85a5ab76 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:17:26 +0100 Subject: [PATCH 3/9] Move TypedFormField to nhsuk_forms and add tests --- lung_cancer_screening/core/form_fields.py | 40 ------- .../core/tests/unit/not_test_form_fields.py | 110 ------------------ .../tests/unit/test_typed_choice_field.py | 79 +++++++++++++ .../nhsuk_forms/typed_choice_field.py | 44 +++++++ .../forms/have_you_ever_smoked_form.py | 2 +- 5 files changed, 124 insertions(+), 151 deletions(-) create mode 100644 lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py create mode 100644 lung_cancer_screening/nhsuk_forms/typed_choice_field.py diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/core/form_fields.py index 5a83ad1c..7b22c89b 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/core/form_fields.py @@ -103,46 +103,6 @@ def _template_name(widget): return "forms/select.jinja" -class TypedChoiceField(forms.TypedChoiceField): - """ - A TypedChoiceField that renders using NHS.UK design system radios/select - components. - """ - - widget = widgets.RadioSelect - bound_field_class = BoundChoiceField - - def __init__( - self, - *args, - hint=None, - label_classes=None, - classes=None, - **kwargs, - ): - kwargs["template_name"] = TypedChoiceField._template_name( - kwargs.get("widget", self.widget) - ) - - self.hint = hint - self.classes = classes - self.label_classes = label_classes - - super().__init__(*args, **kwargs) - - @staticmethod - def _template_name(widget): - if (isinstance(widget, type) and widget is widgets.RadioSelect) or isinstance( - widget, widgets.RadioSelect - ): - return "forms/radios.jinja" - elif (isinstance(widget, type) and widget is widgets.Select) or isinstance( - widget, widgets.Select - ): - return "forms/select.jinja" - - - class MultipleChoiceField(forms.MultipleChoiceField): """ A MultipleChoiceField that renders using the NHS.UK design system checkboxes diff --git a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py index eb90b1f1..449441da 100644 --- a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py +++ b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py @@ -123,116 +123,6 @@ def test_adding_dividers_via_boundfield(self, form_class): assert bound_field.get_divider_after("a") == "or" -class TestTypedChoiceField: - @pytest.fixture - def form_class(self): - class TestForm(Form): - field = TypedChoiceField( - label="Abc", - label_classes="app-abc", - choices=(("a", "A"), ("b", "B")), - hint="Pick either one", - ) - select_field = TypedChoiceField( - label="Select", - label_classes="app-select", - choices=(("a", "A"), ("b", "B")), - hint="Pick either one", - widget=Select, - ) - details = CharField(label="Abc", initial="") - - return TestForm - - def test_renders_nhs_radios(self, form_class): - assertHTMLEqual( - form_class()["field"].as_field_group(), - """ -
-
- - Abc - -
- Pick either one -
-
-
- - -
-
- - -
-
-
-
- """, - ) - - def test_renders_radios_with_conditional_html(self, form_class): - form = form_class() - form["field"].add_conditional_html("b", "

Hello

") - - assertHTMLEqual( - form["field"].as_field_group(), - """ -
-
- - Abc - -
- Pick either one -
-
-
- - -
-
- - -
-
-

Hello

-
-
-
-
- """, - ) - - def test_renders_nhs_select(self, form_class): - assertHTMLEqual( - form_class()["select_field"].as_field_group(), - """ -
- -
- Pick either one -
- -
- """, - ) - - def test_renders_select_with_conditional_html(self, form_class): - form = form_class() - - with pytest.raises(ValueError): - form["select_field"].add_conditional_html("b", "

Hello

") - - def test_adding_dividers_via_boundfield(self, form_class): - bound_field = form_class()["field"] - bound_field.add_divider_after("a", "or") - assert bound_field.get_divider_after("a") == "or" - - class TestMultipleChoiceField: @pytest.fixture def form_class(self): diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py new file mode 100644 index 00000000..433eb1be --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/tests/unit/test_typed_choice_field.py @@ -0,0 +1,79 @@ +from django.test import TestCase +from django.forms import Form + +from ...typed_choice_field import TypedChoiceField + + +class TestForm(Form): + field = TypedChoiceField( + label="Abc", + label_classes="app-abc", + choices=(("a", "A"), ("b", "B")), + hint="Pick either one", + ) + +class TestTypedChoiceField(TestCase): + def test_renders_nhs_radios(self): + self.assertHTMLEqual( + TestForm()["field"].as_field_group(), + """ +
+
+ + Abc + +
+ Pick either one +
+
+
+ + +
+
+ + +
+
+
+
+ """, + ) + + def test_renders_radios_with_conditional_html(self): + form = TestForm() + form["field"].add_conditional_html("b", "

Hello

") + + self.assertHTMLEqual( + form["field"].as_field_group(), + """ +
+
+ + Abc + +
+ Pick either one +
+
+
+ + +
+
+ + +
+
+

Hello

+
+
+
+
+ """, + ) + + def test_adding_dividers_via_boundfield(self): + bound_field = TestForm()["field"] + bound_field.add_divider_after("a", "or") + assert bound_field.get_divider_after("a") == "or" diff --git a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py new file mode 100644 index 00000000..fe7dd3aa --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py @@ -0,0 +1,44 @@ + +from django import forms +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ + +from lung_cancer_screening.core.form_fields import BoundChoiceField + +class TypedChoiceField(forms.TypedChoiceField): + """ + A TypedChoiceField that renders using NHS.UK design system radios/select + components. + """ + + widget = widgets.RadioSelect + bound_field_class = BoundChoiceField + + def __init__( + self, + *args, + hint=None, + label_classes=None, + classes=None, + **kwargs, + ): + kwargs["template_name"] = TypedChoiceField._template_name( + kwargs.get("widget", self.widget) + ) + + self.hint = hint + self.classes = classes + self.label_classes = label_classes + + super().__init__(*args, **kwargs) + + @staticmethod + def _template_name(widget): + if (isinstance(widget, type) and widget is widgets.RadioSelect) or isinstance( + widget, widgets.RadioSelect + ): + return "forms/radios.jinja" + elif (isinstance(widget, type) and widget is widgets.Select) or isinstance( + widget, widgets.Select + ): + return "forms/select.jinja" diff --git a/lung_cancer_screening/questions/forms/have_you_ever_smoked_form.py b/lung_cancer_screening/questions/forms/have_you_ever_smoked_form.py index 8bb008cf..e0ac4bdc 100644 --- a/lung_cancer_screening/questions/forms/have_you_ever_smoked_form.py +++ b/lung_cancer_screening/questions/forms/have_you_ever_smoked_form.py @@ -1,6 +1,6 @@ from django import forms -from lung_cancer_screening.core.form_fields import TypedChoiceField +from ...nhsuk_forms.typed_choice_field import TypedChoiceField from ..models.response_set import ResponseSet, HaveYouEverSmokedValues class HaveYouEverSmokedForm(forms.ModelForm): From 86cc03cc0553fc1860664d9893493b7eba9dd057 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:27:11 +0100 Subject: [PATCH 4/9] Move ChoiceField to nhsuk_forms module and add tests --- .../core/tests/unit/not_test_form_fields.py | 114 ------------------ .../nhsuk_forms/choice_field.py | 36 ++++++ .../tests/unit/test_choice_field.py | 78 ++++++++++++ .../nhsuk_forms/typed_choice_field.py | 9 +- 4 files changed, 115 insertions(+), 122 deletions(-) create mode 100644 lung_cancer_screening/nhsuk_forms/choice_field.py create mode 100644 lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py diff --git a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py index 449441da..0c032662 100644 --- a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py +++ b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py @@ -2,127 +2,13 @@ from django.forms import Form from django.forms.widgets import ( CheckboxSelectMultiple, - Select, ) from pytest_django.asserts import assertHTMLEqual from ..form_fields import ( CharField, - ChoiceField, MultipleChoiceField, - TypedChoiceField, ) - -class TestChoiceField: - @pytest.fixture - def form_class(self): - class TestForm(Form): - field = ChoiceField( - label="Abc", - label_classes="app-abc", - choices=(("a", "A"), ("b", "B")), - hint="Pick either one", - ) - select_field = ChoiceField( - label="Select", - label_classes="app-select", - choices=(("a", "A"), ("b", "B")), - hint="Pick either one", - widget=Select, - ) - details = CharField(label="Abc", initial="") - - return TestForm - - def test_renders_nhs_radios(self, form_class): - assertHTMLEqual( - form_class()["field"].as_field_group(), - """ -
-
- - Abc - -
- Pick either one -
-
-
- - -
-
- - -
-
-
-
- """, - ) - - def test_renders_radios_with_conditional_html(self, form_class): - form = form_class() - form["field"].add_conditional_html("b", "

Hello

") - - assertHTMLEqual( - form["field"].as_field_group(), - """ -
-
- - Abc - -
- Pick either one -
-
-
- - -
-
- - -
-
-

Hello

-
-
-
-
- """, - ) - - def test_renders_nhs_select(self, form_class): - assertHTMLEqual( - form_class()["select_field"].as_field_group(), - """ -
- -
- Pick either one -
- -
- """, - ) - - def test_renders_select_with_conditional_html(self, form_class): - form = form_class() - - with pytest.raises(ValueError): - form["select_field"].add_conditional_html("b", "

Hello

") - - def test_adding_dividers_via_boundfield(self, form_class): - bound_field = form_class()["field"] - bound_field.add_divider_after("a", "or") - assert bound_field.get_divider_after("a") == "or" - - class TestMultipleChoiceField: @pytest.fixture def form_class(self): diff --git a/lung_cancer_screening/nhsuk_forms/choice_field.py b/lung_cancer_screening/nhsuk_forms/choice_field.py new file mode 100644 index 00000000..b306b965 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/choice_field.py @@ -0,0 +1,36 @@ +from django import forms +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ + +from lung_cancer_screening.core.form_fields import BoundChoiceField + +class ChoiceField(forms.ChoiceField): + """ + A ChoiceField that renders using NHS.UK design system radios/select + components. + """ + + widget = widgets.RadioSelect + bound_field_class = BoundChoiceField + + def __init__( + self, + *args, + hint=None, + label_classes=None, + classes=None, + **kwargs, + ): + kwargs["template_name"] = ChoiceField._template_name( + kwargs.get("widget", self.widget) + ) + + self.hint = hint + self.classes = classes + self.label_classes = label_classes + + super().__init__(*args, **kwargs) + + @staticmethod + def _template_name(widget): + return "forms/radios.jinja" diff --git a/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py b/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py new file mode 100644 index 00000000..d8ae7f62 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/tests/unit/test_choice_field.py @@ -0,0 +1,78 @@ +from django.test import TestCase +from django.forms import Form + +from ...choice_field import ChoiceField + +class TestForm(Form): + field = ChoiceField( + label="Abc", + label_classes="app-abc", + choices=(("a", "A"), ("b", "B")), + hint="Pick either one", + ) + +class TestChoiceField(TestCase): + def test_renders_nhs_radios(self): + self.assertHTMLEqual( + TestForm()["field"].as_field_group(), + """ +
+
+ + Abc + +
+ Pick either one +
+
+
+ + +
+
+ + +
+
+
+
+ """, + ) + + def test_renders_radios_with_conditional_html(self): + form = TestForm() + form["field"].add_conditional_html("b", "

Hello

") + + self.assertHTMLEqual( + form["field"].as_field_group(), + """ +
+
+ + Abc + +
+ Pick either one +
+
+
+ + +
+
+ + +
+
+

Hello

+
+
+
+
+ """, + ) + + def test_adding_dividers_via_boundfield(self): + bound_field = TestForm()["field"] + bound_field.add_divider_after("a", "or") + assert bound_field.get_divider_after("a") == "or" diff --git a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py index fe7dd3aa..11907980 100644 --- a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py @@ -34,11 +34,4 @@ def __init__( @staticmethod def _template_name(widget): - if (isinstance(widget, type) and widget is widgets.RadioSelect) or isinstance( - widget, widgets.RadioSelect - ): - return "forms/radios.jinja" - elif (isinstance(widget, type) and widget is widgets.Select) or isinstance( - widget, widgets.Select - ): - return "forms/select.jinja" + return "forms/radios.jinja" From 844fc011181f906ebd2c8ec65ba78c3c873fd047 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:32:33 +0100 Subject: [PATCH 5/9] Move BoundFormField to nhsuk_forms --- lung_cancer_screening/core/form_fields.py | 96 ------------------- .../core/tests/unit/not_test_form_fields.py | 85 ---------------- .../nhsuk_forms/bound_choice_field.py | 34 +++++++ .../nhsuk_forms/choice_field.py | 2 +- .../nhsuk_forms/typed_choice_field.py | 2 +- 5 files changed, 36 insertions(+), 183 deletions(-) delete mode 100644 lung_cancer_screening/core/tests/unit/not_test_form_fields.py create mode 100644 lung_cancer_screening/nhsuk_forms/bound_choice_field.py diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/core/form_fields.py index 7b22c89b..14efc55a 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/core/form_fields.py @@ -33,102 +33,6 @@ def widget_attrs(self, widget): return attrs -class BoundChoiceField(forms.BoundField): - """ - Specialisation of BoundField that can deal with conditionally shown fields, - and divider content between choices. - This can be used to render a set of radios or checkboxes with text boxes to capture - more details. - """ - - def __init__(self, form: forms.Form, field: "ChoiceField", name: str): - super().__init__(form, field, name) - - self._conditional_html = {} - self.dividers = {} - - def add_conditional_html(self, value, html): - if isinstance(self.field.widget, widgets.Select): - raise ValueError( - "select component does not support conditional fields") - - self._conditional_html[value] = html - - def conditional_html(self, value): - return self._conditional_html.get(value) - - def add_divider_after(self, previous, divider): - self.dividers[previous] = divider - - def get_divider_after(self, previous): - return self.dividers.get(previous) - - -class ChoiceField(forms.ChoiceField): - """ - A ChoiceField that renders using NHS.UK design system radios/select - components. - """ - - widget = widgets.RadioSelect - bound_field_class = BoundChoiceField - - def __init__( - self, - *args, - hint=None, - label_classes=None, - classes=None, - **kwargs, - ): - kwargs["template_name"] = ChoiceField._template_name( - kwargs.get("widget", self.widget) - ) - - self.hint = hint - self.classes = classes - self.label_classes = label_classes - - super().__init__(*args, **kwargs) - - @staticmethod - def _template_name(widget): - if (isinstance(widget, type) and widget is widgets.RadioSelect) or isinstance( - widget, widgets.RadioSelect - ): - return "forms/radios.jinja" - elif (isinstance(widget, type) and widget is widgets.Select) or isinstance( - widget, widgets.Select - ): - return "forms/select.jinja" - - -class MultipleChoiceField(forms.MultipleChoiceField): - """ - A MultipleChoiceField that renders using the NHS.UK design system checkboxes - component. - """ - - widget = widgets.CheckboxSelectMultiple - bound_field_class = BoundChoiceField - - def __init__( - self, - *args, - hint=None, - label_classes=None, - classes=None, - **kwargs, - ): - kwargs["template_name"] = "forms/checkboxes.jinja" - - self.hint = hint - self.classes = classes - self.label_classes = label_classes - - super().__init__(*args, **kwargs) - - class ImperialHeightWidget(widgets.MultiWidget): """ A widget that splits height into feet and inches inputs. diff --git a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py b/lung_cancer_screening/core/tests/unit/not_test_form_fields.py deleted file mode 100644 index 0c032662..00000000 --- a/lung_cancer_screening/core/tests/unit/not_test_form_fields.py +++ /dev/null @@ -1,85 +0,0 @@ -import pytest -from django.forms import Form -from django.forms.widgets import ( - CheckboxSelectMultiple, -) -from pytest_django.asserts import assertHTMLEqual - -from ..form_fields import ( - CharField, - MultipleChoiceField, -) -class TestMultipleChoiceField: - @pytest.fixture - def form_class(self): - class TestForm(Form): - checkbox_field = MultipleChoiceField( - label="Def", - label_classes="app-def", - choices=(("a", "A"), ("b", "B")), - hint="Pick any number", - widget=CheckboxSelectMultiple, - ) - details = CharField(label="Abc", initial="") - - return TestForm - - def test_renders_nhs_checkboxes(self, form_class): - assertHTMLEqual( - form_class()["checkbox_field"].as_field_group(), - """ -
-
- - Def - -
- Pick any number -
-
-
- - -
-
- - -
-
-
-
- """, - ) - - def test_renders_nhs_checkboxes_with_conditional_html(self, form_class): - form = form_class() - form["checkbox_field"].add_conditional_html("b", "

Hello

") - - assertHTMLEqual( - form["checkbox_field"].as_field_group(), - """ -
-
- - Def - -
- Pick any number -
-
-
- - -
-
- - -
-
-

Hello

-
-
-
-
- """, - ) diff --git a/lung_cancer_screening/nhsuk_forms/bound_choice_field.py b/lung_cancer_screening/nhsuk_forms/bound_choice_field.py new file mode 100644 index 00000000..77f0dfb9 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/bound_choice_field.py @@ -0,0 +1,34 @@ +from django import forms +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ + + +class BoundChoiceField(forms.BoundField): + """ + Specialisation of BoundField that can deal with conditionally shown fields, + and divider content between choices. + This can be used to render a set of radios or checkboxes with text boxes to capture + more details. + """ + + def __init__(self, form: forms.Form, field: "ChoiceField", name: str): + super().__init__(form, field, name) + + self._conditional_html = {} + self.dividers = {} + + def add_conditional_html(self, value, html): + if isinstance(self.field.widget, widgets.Select): + raise ValueError( + "select component does not support conditional fields") + + self._conditional_html[value] = html + + def conditional_html(self, value): + return self._conditional_html.get(value) + + def add_divider_after(self, previous, divider): + self.dividers[previous] = divider + + def get_divider_after(self, previous): + return self.dividers.get(previous) diff --git a/lung_cancer_screening/nhsuk_forms/choice_field.py b/lung_cancer_screening/nhsuk_forms/choice_field.py index b306b965..0d9b4713 100644 --- a/lung_cancer_screening/nhsuk_forms/choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/choice_field.py @@ -2,7 +2,7 @@ from django.forms import widgets from django.utils.translation import gettext_lazy as _ -from lung_cancer_screening.core.form_fields import BoundChoiceField +from .bound_choice_field import BoundChoiceField class ChoiceField(forms.ChoiceField): """ diff --git a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py index 11907980..69878426 100644 --- a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py @@ -3,7 +3,7 @@ from django.forms import widgets from django.utils.translation import gettext_lazy as _ -from lung_cancer_screening.core.form_fields import BoundChoiceField +from .bound_choice_field import BoundChoiceField class TypedChoiceField(forms.TypedChoiceField): """ From 28414d63128a2048d0e346f27ab0852370446a5c Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:01:56 +0100 Subject: [PATCH 6/9] Fix linting issues after form fields migration --- lung_cancer_screening/core/form_fields.py | 2 -- lung_cancer_screening/nhsuk_forms/bound_choice_field.py | 4 ++-- lung_cancer_screening/nhsuk_forms/choice_field.py | 1 - lung_cancer_screening/nhsuk_forms/integer_field.py | 1 - lung_cancer_screening/nhsuk_forms/typed_choice_field.py | 1 - 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/core/form_fields.py index 14efc55a..5d41ffcb 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/core/form_fields.py @@ -1,8 +1,6 @@ from django import forms from django.forms import widgets -from django.utils.translation import gettext_lazy as _ -from lung_cancer_screening.nhsuk_forms.utils.date_formatting import format_date from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField class DecimalField(forms.DecimalField): diff --git a/lung_cancer_screening/nhsuk_forms/bound_choice_field.py b/lung_cancer_screening/nhsuk_forms/bound_choice_field.py index 77f0dfb9..c93f93a6 100644 --- a/lung_cancer_screening/nhsuk_forms/bound_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/bound_choice_field.py @@ -1,7 +1,7 @@ from django import forms from django.forms import widgets -from django.utils.translation import gettext_lazy as _ +from lung_cancer_screening.nhsuk_forms import choice_field class BoundChoiceField(forms.BoundField): """ @@ -11,7 +11,7 @@ class BoundChoiceField(forms.BoundField): more details. """ - def __init__(self, form: forms.Form, field: "ChoiceField", name: str): + def __init__(self, form: forms.Form, field: "choice_field", name: str): super().__init__(form, field, name) self._conditional_html = {} diff --git a/lung_cancer_screening/nhsuk_forms/choice_field.py b/lung_cancer_screening/nhsuk_forms/choice_field.py index 0d9b4713..e23ee9ef 100644 --- a/lung_cancer_screening/nhsuk_forms/choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/choice_field.py @@ -1,6 +1,5 @@ from django import forms from django.forms import widgets -from django.utils.translation import gettext_lazy as _ from .bound_choice_field import BoundChoiceField diff --git a/lung_cancer_screening/nhsuk_forms/integer_field.py b/lung_cancer_screening/nhsuk_forms/integer_field.py index bed29586..5fc954fd 100644 --- a/lung_cancer_screening/nhsuk_forms/integer_field.py +++ b/lung_cancer_screening/nhsuk_forms/integer_field.py @@ -1,5 +1,4 @@ from django import forms -from django.utils.translation import gettext_lazy as _ class IntegerField(forms.IntegerField): diff --git a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py index 69878426..f2bef19e 100644 --- a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py @@ -1,7 +1,6 @@ from django import forms from django.forms import widgets -from django.utils.translation import gettext_lazy as _ from .bound_choice_field import BoundChoiceField From 409c417a07c826d5ae9422968844f3ba11078659 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:58:37 +0000 Subject: [PATCH 7/9] Add DecimalField to nhsuk_forms --- lung_cancer_screening/core/form_fields.py | 27 ------------------ .../nhsuk_forms/decimal_field.py | 28 +++++++++++++++++++ .../questions/forms/metric_height_form.py | 2 +- .../questions/forms/metric_weight_form.py | 2 +- 4 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 lung_cancer_screening/nhsuk_forms/decimal_field.py diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/core/form_fields.py index 5d41ffcb..cca2ecdf 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/core/form_fields.py @@ -3,33 +3,6 @@ from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField -class DecimalField(forms.DecimalField): - def __init__( - self, - *args, - hint=None, - label_classes=None, - classes=None, - **kwargs, - ): - kwargs["template_name"] = "forms/input.jinja" - - self.hint = hint - self.classes = classes - self.label_classes = label_classes - - super().__init__(*args, **kwargs) - - def widget_attrs(self, widget): - attrs = super().widget_attrs(widget) - - # Don't use min/max/step attributes. - attrs.pop("min", None) - attrs.pop("max", None) - attrs.pop("step", None) - - return attrs - class ImperialHeightWidget(widgets.MultiWidget): """ diff --git a/lung_cancer_screening/nhsuk_forms/decimal_field.py b/lung_cancer_screening/nhsuk_forms/decimal_field.py new file mode 100644 index 00000000..f0f41f52 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/decimal_field.py @@ -0,0 +1,28 @@ +from django import forms + +class DecimalField(forms.DecimalField): + def __init__( + self, + *args, + hint=None, + label_classes=None, + classes=None, + **kwargs, + ): + kwargs["template_name"] = "forms/input.jinja" + + self.hint = hint + self.classes = classes + self.label_classes = label_classes + + super().__init__(*args, **kwargs) + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + + # Don't use min/max/step attributes. + attrs.pop("min", None) + attrs.pop("max", None) + attrs.pop("step", None) + + return attrs diff --git a/lung_cancer_screening/questions/forms/metric_height_form.py b/lung_cancer_screening/questions/forms/metric_height_form.py index 0ebecd9e..73669a8e 100644 --- a/lung_cancer_screening/questions/forms/metric_height_form.py +++ b/lung_cancer_screening/questions/forms/metric_height_form.py @@ -1,6 +1,6 @@ from django import forms -from lung_cancer_screening.core.form_fields import DecimalField +from ...nhsuk_forms.decimal_field import DecimalField from ..models.response_set import ResponseSet class MetricHeightForm(forms.ModelForm): diff --git a/lung_cancer_screening/questions/forms/metric_weight_form.py b/lung_cancer_screening/questions/forms/metric_weight_form.py index 4e9c3b14..338a68aa 100644 --- a/lung_cancer_screening/questions/forms/metric_weight_form.py +++ b/lung_cancer_screening/questions/forms/metric_weight_form.py @@ -1,6 +1,6 @@ from django import forms -from lung_cancer_screening.core.form_fields import DecimalField +from ...nhsuk_forms.decimal_field import DecimalField from ..models.response_set import ResponseSet class MetricWeightForm(forms.ModelForm): From 3f708bd50744d77b57f7c585a0d65818f1e38184 Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:02:24 +0000 Subject: [PATCH 8/9] Move Imperial height and weight form fields to nhsuk_forms --- .../core/tests/unit/__init__.py | 0 .../nhsuk_forms/imperial_height_form.py | 76 +++++++++++++++++++ .../imperial_weight_form.py} | 73 ------------------ .../questions/forms/imperial_height_form.py | 2 +- .../questions/forms/imperial_weight_form.py | 2 +- 5 files changed, 78 insertions(+), 75 deletions(-) delete mode 100644 lung_cancer_screening/core/tests/unit/__init__.py create mode 100644 lung_cancer_screening/nhsuk_forms/imperial_height_form.py rename lung_cancer_screening/{core/form_fields.py => nhsuk_forms/imperial_weight_form.py} (56%) diff --git a/lung_cancer_screening/core/tests/unit/__init__.py b/lung_cancer_screening/core/tests/unit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lung_cancer_screening/nhsuk_forms/imperial_height_form.py b/lung_cancer_screening/nhsuk_forms/imperial_height_form.py new file mode 100644 index 00000000..504c23df --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/imperial_height_form.py @@ -0,0 +1,76 @@ +from django import forms +from django.forms import widgets + +from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField + + +class ImperialHeightWidget(widgets.MultiWidget): + """ + A widget that splits height into feet and inches inputs. + """ + + def __init__(self, attrs=None): + height_widgets = ( + widgets.NumberInput(attrs=attrs), + widgets.NumberInput(attrs=attrs), + ) + super().__init__(height_widgets, attrs) + + def decompress(self, value): + """ + Convert total inches back to feet and inches for display. + """ + if value: + feet = int(value // 12) + inches = value % 12 + return [feet, inches] + return [None, None] + + def subwidgets(self, name, value, attrs=None): + """ + Expose data for each subwidget, so that we can render them separately in the template. + """ + context = self.get_context(name, value, attrs) + for subwidget in context["widget"]["subwidgets"]: + yield subwidget + + +class ImperialHeightField(forms.MultiValueField): + """ + A field that combines feet and inches into a single height value in cm. + """ + + widget = ImperialHeightWidget + + def __init__(self, *args, **kwargs): + error_messages = kwargs.get("error_messages", {}) + + feet_kwargs = { + "error_messages": { + 'invalid': 'Feet must be in whole numbers.', + **error_messages, + }, + } + inches_kwargs = { + "error_messages": { + 'invalid': 'Inches must be in whole numbers.', + **error_messages, + }, + } + fields = ( + IntegerField(**feet_kwargs), + IntegerField(**inches_kwargs), + ) + kwargs["template_name"] = "forms/imperial-height-input.jinja" + + super().__init__(fields, *args, **kwargs) + + def compress(self, data_list): + """ + Convert feet and inches to total inches. + """ + if data_list and all(data_list): + feet, inches = data_list + total_inches = feet * 12 + inches + return int(total_inches) + return None diff --git a/lung_cancer_screening/core/form_fields.py b/lung_cancer_screening/nhsuk_forms/imperial_weight_form.py similarity index 56% rename from lung_cancer_screening/core/form_fields.py rename to lung_cancer_screening/nhsuk_forms/imperial_weight_form.py index cca2ecdf..bab9355e 100644 --- a/lung_cancer_screening/core/form_fields.py +++ b/lung_cancer_screening/nhsuk_forms/imperial_weight_form.py @@ -3,79 +3,6 @@ from lung_cancer_screening.nhsuk_forms.integer_field import IntegerField - -class ImperialHeightWidget(widgets.MultiWidget): - """ - A widget that splits height into feet and inches inputs. - """ - - def __init__(self, attrs=None): - height_widgets = ( - widgets.NumberInput(attrs=attrs), - widgets.NumberInput(attrs=attrs), - ) - super().__init__(height_widgets, attrs) - - def decompress(self, value): - """ - Convert total inches back to feet and inches for display. - """ - if value: - feet = int(value // 12) - inches = value % 12 - return [feet, inches] - return [None, None] - - def subwidgets(self, name, value, attrs=None): - """ - Expose data for each subwidget, so that we can render them separately in the template. - """ - context = self.get_context(name, value, attrs) - for subwidget in context["widget"]["subwidgets"]: - yield subwidget - - -class ImperialHeightField(forms.MultiValueField): - """ - A field that combines feet and inches into a single height value in cm. - """ - - widget = ImperialHeightWidget - - def __init__(self, *args, **kwargs): - error_messages = kwargs.get("error_messages", {}) - - feet_kwargs = { - "error_messages": { - 'invalid': 'Feet must be in whole numbers.', - **error_messages, - }, - } - inches_kwargs = { - "error_messages": { - 'invalid': 'Inches must be in whole numbers.', - **error_messages, - }, - } - fields = ( - IntegerField(**feet_kwargs), - IntegerField(**inches_kwargs), - ) - kwargs["template_name"] = "forms/imperial-height-input.jinja" - - super().__init__(fields, *args, **kwargs) - - def compress(self, data_list): - """ - Convert feet and inches to total inches. - """ - if data_list and all(data_list): - feet, inches = data_list - total_inches = feet * 12 + inches - return int(total_inches) - return None - - class ImperialWeightWidget(widgets.MultiWidget): """ A widget that splits weight into stone and pounds inputs. diff --git a/lung_cancer_screening/questions/forms/imperial_height_form.py b/lung_cancer_screening/questions/forms/imperial_height_form.py index d75cd293..f9ec5794 100644 --- a/lung_cancer_screening/questions/forms/imperial_height_form.py +++ b/lung_cancer_screening/questions/forms/imperial_height_form.py @@ -1,6 +1,6 @@ from django import forms -from lung_cancer_screening.core.form_fields import ImperialHeightField +from ...nhsuk_forms.imperial_height_form import ImperialHeightField from ..models.response_set import ResponseSet class ImperialHeightForm(forms.ModelForm): diff --git a/lung_cancer_screening/questions/forms/imperial_weight_form.py b/lung_cancer_screening/questions/forms/imperial_weight_form.py index 69066891..2420c6ce 100644 --- a/lung_cancer_screening/questions/forms/imperial_weight_form.py +++ b/lung_cancer_screening/questions/forms/imperial_weight_form.py @@ -1,6 +1,6 @@ from django import forms -from lung_cancer_screening.core.form_fields import ImperialWeightField +from ...nhsuk_forms.imperial_weight_form import ImperialWeightField from ..models.response_set import ResponseSet class ImperialWeightForm(forms.ModelForm): From d2c241d144ed27842244ef352fc2545c5664a88d Mon Sep 17 00:00:00 2001 From: Andy Mitchell <326561+Themitchell@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:05:17 +0000 Subject: [PATCH 9/9] Move form jinja templates to nhsuk_forms --- lung_cancer_screening/nhsuk_forms/choice_field.py | 2 +- lung_cancer_screening/nhsuk_forms/decimal_field.py | 2 +- lung_cancer_screening/nhsuk_forms/imperial_height_form.py | 2 +- lung_cancer_screening/nhsuk_forms/imperial_weight_form.py | 2 +- lung_cancer_screening/nhsuk_forms/integer_field.py | 2 +- .../{core/jinja2/forms => nhsuk_forms/jinja2}/date-input.jinja | 0 .../forms => nhsuk_forms/jinja2}/imperial-height-input.jinja | 0 .../forms => nhsuk_forms/jinja2}/imperial-weight-input.jinja | 0 .../{core/jinja2/forms => nhsuk_forms/jinja2}/input.jinja | 0 .../{core/jinja2/forms => nhsuk_forms/jinja2}/radios.jinja | 0 lung_cancer_screening/nhsuk_forms/split_date_field.py | 2 +- lung_cancer_screening/nhsuk_forms/typed_choice_field.py | 2 +- 12 files changed, 7 insertions(+), 7 deletions(-) rename lung_cancer_screening/{core/jinja2/forms => nhsuk_forms/jinja2}/date-input.jinja (100%) rename lung_cancer_screening/{core/jinja2/forms => nhsuk_forms/jinja2}/imperial-height-input.jinja (100%) rename lung_cancer_screening/{core/jinja2/forms => nhsuk_forms/jinja2}/imperial-weight-input.jinja (100%) rename lung_cancer_screening/{core/jinja2/forms => nhsuk_forms/jinja2}/input.jinja (100%) rename lung_cancer_screening/{core/jinja2/forms => nhsuk_forms/jinja2}/radios.jinja (100%) diff --git a/lung_cancer_screening/nhsuk_forms/choice_field.py b/lung_cancer_screening/nhsuk_forms/choice_field.py index e23ee9ef..45db783a 100644 --- a/lung_cancer_screening/nhsuk_forms/choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/choice_field.py @@ -32,4 +32,4 @@ def __init__( @staticmethod def _template_name(widget): - return "forms/radios.jinja" + return "radios.jinja" diff --git a/lung_cancer_screening/nhsuk_forms/decimal_field.py b/lung_cancer_screening/nhsuk_forms/decimal_field.py index f0f41f52..0a7520d3 100644 --- a/lung_cancer_screening/nhsuk_forms/decimal_field.py +++ b/lung_cancer_screening/nhsuk_forms/decimal_field.py @@ -9,7 +9,7 @@ def __init__( classes=None, **kwargs, ): - kwargs["template_name"] = "forms/input.jinja" + kwargs["template_name"] = "input.jinja" self.hint = hint self.classes = classes diff --git a/lung_cancer_screening/nhsuk_forms/imperial_height_form.py b/lung_cancer_screening/nhsuk_forms/imperial_height_form.py index 504c23df..655f74bd 100644 --- a/lung_cancer_screening/nhsuk_forms/imperial_height_form.py +++ b/lung_cancer_screening/nhsuk_forms/imperial_height_form.py @@ -61,7 +61,7 @@ def __init__(self, *args, **kwargs): IntegerField(**feet_kwargs), IntegerField(**inches_kwargs), ) - kwargs["template_name"] = "forms/imperial-height-input.jinja" + kwargs["template_name"] = "imperial-height-input.jinja" super().__init__(fields, *args, **kwargs) diff --git a/lung_cancer_screening/nhsuk_forms/imperial_weight_form.py b/lung_cancer_screening/nhsuk_forms/imperial_weight_form.py index bab9355e..0e323701 100644 --- a/lung_cancer_screening/nhsuk_forms/imperial_weight_form.py +++ b/lung_cancer_screening/nhsuk_forms/imperial_weight_form.py @@ -68,7 +68,7 @@ def __init__(self, *args, **kwargs): IntegerField(**stone_kwargs), IntegerField(**pounds_kwargs), ) - kwargs["template_name"] = "forms/imperial-weight-input.jinja" + kwargs["template_name"] = "imperial-weight-input.jinja" super().__init__(fields, *args, **kwargs) diff --git a/lung_cancer_screening/nhsuk_forms/integer_field.py b/lung_cancer_screening/nhsuk_forms/integer_field.py index 5fc954fd..8e513dd2 100644 --- a/lung_cancer_screening/nhsuk_forms/integer_field.py +++ b/lung_cancer_screening/nhsuk_forms/integer_field.py @@ -10,7 +10,7 @@ def __init__( classes=None, **kwargs, ): - kwargs["template_name"] = "forms/input.jinja" + kwargs["template_name"] = "input.jinja" self.hint = hint self.classes = classes diff --git a/lung_cancer_screening/core/jinja2/forms/date-input.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/date-input.jinja similarity index 100% rename from lung_cancer_screening/core/jinja2/forms/date-input.jinja rename to lung_cancer_screening/nhsuk_forms/jinja2/date-input.jinja diff --git a/lung_cancer_screening/core/jinja2/forms/imperial-height-input.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/imperial-height-input.jinja similarity index 100% rename from lung_cancer_screening/core/jinja2/forms/imperial-height-input.jinja rename to lung_cancer_screening/nhsuk_forms/jinja2/imperial-height-input.jinja diff --git a/lung_cancer_screening/core/jinja2/forms/imperial-weight-input.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/imperial-weight-input.jinja similarity index 100% rename from lung_cancer_screening/core/jinja2/forms/imperial-weight-input.jinja rename to lung_cancer_screening/nhsuk_forms/jinja2/imperial-weight-input.jinja diff --git a/lung_cancer_screening/core/jinja2/forms/input.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/input.jinja similarity index 100% rename from lung_cancer_screening/core/jinja2/forms/input.jinja rename to lung_cancer_screening/nhsuk_forms/jinja2/input.jinja diff --git a/lung_cancer_screening/core/jinja2/forms/radios.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja similarity index 100% rename from lung_cancer_screening/core/jinja2/forms/radios.jinja rename to lung_cancer_screening/nhsuk_forms/jinja2/radios.jinja diff --git a/lung_cancer_screening/nhsuk_forms/split_date_field.py b/lung_cancer_screening/nhsuk_forms/split_date_field.py index 17653587..59569757 100644 --- a/lung_cancer_screening/nhsuk_forms/split_date_field.py +++ b/lung_cancer_screening/nhsuk_forms/split_date_field.py @@ -110,7 +110,7 @@ def __init__(self, *args, **kwargs): IntegerField(**year_kwargs), ] - kwargs["template_name"] = "forms/date-input.jinja" + kwargs["template_name"] = "date-input.jinja" super().__init__(self.fields, *args, **kwargs) diff --git a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py index f2bef19e..0a35aae2 100644 --- a/lung_cancer_screening/nhsuk_forms/typed_choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/typed_choice_field.py @@ -33,4 +33,4 @@ def __init__( @staticmethod def _template_name(widget): - return "forms/radios.jinja" + return "radios.jinja"