diff --git a/app/assets/javascripts/collapsibleCheckboxes.js b/app/assets/javascripts/collapsibleCheckboxes.js
index 11c14a2e4..0333b8dc3 100644
--- a/app/assets/javascripts/collapsibleCheckboxes.js
+++ b/app/assets/javascripts/collapsibleCheckboxes.js
@@ -5,7 +5,7 @@
function Summary (module) {
this.module = module;
- this.$el = module.$formGroup.find('.selection-summary');
+ this.$el = module.$formGroup.find('.selection-summary').first();
this.fieldLabel = module.fieldLabel;
this.total = module.total;
this.addContent();
@@ -25,6 +25,7 @@
if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); }
this.$el.append(this.$text);
+ this.module.$formGroup.find('.govuk-hint').remove();
};
Summary.prototype.update = function(selection) {
let template;
@@ -86,12 +87,13 @@
.focus();
};
CollapsibleCheckboxes.prototype.start = function(component) {
- this.$formGroup = $(component);
- this.$fieldset = this.$formGroup.find('fieldset');
+ this.$component = $(component);
+ this.$formGroup = this.$component.find('.govuk-form-group').first();
+ this.$fieldset = this.$formGroup.find('fieldset').first();
this.$checkboxes = this.$fieldset.find('input[type=checkbox]');
- this.fieldLabel = this.$formGroup.data('fieldLabel');
+ this.fieldLabel = this.$component.data('fieldLabel');
this.total = this.$checkboxes.length;
- this.legendText = this.$fieldset.find('legend').text().trim();
+ this.legendText = this.$fieldset.find('legend').first().text().trim();
this.expanded = false;
this.addHeadingHideLegend();
@@ -113,7 +115,7 @@
};
CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; };
CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() {
- const headingLevel = this.$formGroup.data('heading-level') || '2';
+ const headingLevel = this.$component.data('heading-level') || '2';
this.$heading = $(`${this.legendText}`);
this.$fieldset.before(this.$heading);
diff --git a/app/assets/stylesheets/components/checkboxes.scss b/app/assets/stylesheets/components/checkboxes.scss
index 3986c65bf..094c37d7a 100644
--- a/app/assets/stylesheets/components/checkboxes.scss
+++ b/app/assets/stylesheets/components/checkboxes.scss
@@ -1,3 +1,7 @@
+// Taken from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/_checkboxes.scss
+$govuk-touch-target-size: 44px;
+$govuk-checkboxes-size: 40px;
+
.selection-summary {
.selection-summary__text {
@@ -65,6 +69,30 @@
}
+.govuk-form-group--nested {
+
+ $border-thickness: $govuk-touch-target-size - $govuk-checkboxes-size;
+ $border-indent: $govuk-touch-target-size / 2;
+
+ position: relative;
+
+ // To equalise the spacing between the line and the top/bottom of
+ // the radio
+ margin-top: govuk-spacing(1) + ($border-thickness / 2);
+ margin-bottom: govuk-spacing(1) * -1;
+ padding-left: govuk-spacing(2) + 2;
+
+ &:before {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: $border-indent * -1;
+ width: $border-thickness;
+ height: 100%;
+ background: $govuk-border-colour;
+ }
+}
+
.selection-content {
margin-bottom: govuk-spacing(4);
diff --git a/app/assets/stylesheets/components/message.scss b/app/assets/stylesheets/components/message.scss
index 3d63ff455..d783f7d5b 100644
--- a/app/assets/stylesheets/components/message.scss
+++ b/app/assets/stylesheets/components/message.scss
@@ -24,10 +24,13 @@
}
}
+$govuk-checkboxes-size: 40px;
+$govuk-checkboxes-label-padding-left-right: govuk-spacing(3);
+$message-type-bottom-spacing: govuk-spacing(4);
+
.message {
&-name {
- @include bold-24;
margin: 0;
a {
@@ -63,37 +66,20 @@
}
&-type {
- color: $secondary-text-colour;
- margin: 0 0 govuk-spacing(4) 0;
+ color: $govuk-secondary-text-colour;
+ margin: 0 0 $message-type-bottom-spacing 0;
+ padding-left: 0;
pointer-events: none;
}
}
-#template-list {
-
- margin-top: govuk-spacing(6);
-
- &.top-gutter-5px {
- margin-top: 5px;
- }
-
-}
-
.template-list {
&-item {
&-with-checkbox {
-
- position: relative;
- padding-left: govuk-spacing(9);
-
- .multiple-choice {
- position: absolute;
- left: 0;
- }
-
+ padding-left: $govuk-checkboxes-size + $govuk-checkboxes-label-padding-left-right;
}
&-hidden-by-default {
@@ -123,6 +109,29 @@
}
+ &-label {
+ position: absolute;
+ left: 0;
+ width: $govuk-checkboxes-size + $govuk-checkboxes-label-padding-left-right;
+ height: 100%;
+ padding: 0;
+ }
+
+ // Fix for GOVUK Frontend selector with high precendence
+ // https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/hint/_hint.scss
+ &-label:not(.govuk-label--m):not(.govuk-label--l):not(.govuk-label--xl)+.template-list-item-hint {
+ margin-bottom: $message-type-bottom-spacing;
+ }
+
+ &-hint {
+ padding-left: 0;
+ }
+
+ }
+
+ &-folder,
+ &-template {
+ @include govuk-font($size: 24, $weight: bold, $line-height: 1.25);
}
&-folder {
diff --git a/app/assets/stylesheets/govuk-frontend/_all.scss b/app/assets/stylesheets/govuk-frontend/_all.scss
index 9b8755367..6776fb95a 100644
--- a/app/assets/stylesheets/govuk-frontend/_all.scss
+++ b/app/assets/stylesheets/govuk-frontend/_all.scss
@@ -26,6 +26,7 @@ $govuk-assets-path: "/static/";
@import 'components/button/_button';
@import 'components/details/_details';
@import 'components/radios/_radios';
+@import 'components/checkboxes/_checkboxes';
@import "utilities/all";
@import "overrides/all";
diff --git a/app/main/forms.py b/app/main/forms.py
index 66c506976..569553c37 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
from itertools import chain
import pytz
-from flask import request
+from flask import Markup, render_template, request
from flask_login import current_user
from flask_wtf import FlaskForm as Form
from flask_wtf.file import FileAllowed
@@ -17,6 +17,7 @@ from notifications_utils.recipients import (
normalise_phone_number,
validate_phone_number,
)
+from werkzeug.utils import cached_property
from wtforms import (
BooleanField,
DateField,
@@ -37,7 +38,6 @@ from wtforms import (
)
from wtforms.fields.html5 import EmailField, SearchField, TelField
from wtforms.validators import URL, DataRequired, Length, Optional, Regexp
-from wtforms.widgets import CheckboxInput, ListWidget
from app import format_thousands
from app.main.validators import (
@@ -117,11 +117,6 @@ def get_next_days_until(until):
]
-class MultiCheckboxField(SelectMultipleField):
- widget = ListWidget(prefix_label=False)
- option_widget = CheckboxInput()
-
-
class RadioField(WTFormsRadioField):
def __init__(
@@ -322,13 +317,16 @@ class RadioFieldWithNoneOption(FieldWithNoneOption, RadioField):
class NestedFieldMixin:
+
def children(self):
+
# start map with root option as a single child entry
child_map = {None: [option for option in self
if option.data == self.NONE_OPTION_VALUE]}
# add entries for all other children
for option in self:
+ # assign all options with a NONE_OPTION_VALUE (not always None) to the None key
if option.data == self.NONE_OPTION_VALUE:
child_ids = [
folder['id'] for folder in self.all_template_folders
@@ -344,6 +342,47 @@ class NestedFieldMixin:
return child_map
+ # to be used as the only version of .children once radios are converted
+ @cached_property
+ def _children(self):
+ return self.children()
+
+ def get_items_from_options(self, field):
+ items = []
+
+ for option in self._children[None]:
+ item = self.get_item_from_option(option)
+ if option.data in self._children:
+ item['children'] = self.render_children(field.name, option.label.text, self._children[option.data])
+ items.append(item)
+
+ return items
+
+ def render_children(self, name, label, options):
+ params = {
+ "name": name,
+ "fieldset": {
+ "legend": {
+ "text": label,
+ "classes": "govuk-visually-hidden"
+ }
+ },
+ "formGroup": {
+ "classes": "govuk-form-group--nested"
+ },
+ "asList": True,
+ "items": []
+ }
+ for option in options:
+ item = self.get_item_from_option(option)
+
+ if len(self._children[option.data]):
+ item['children'] = self.render_children(name, option.label.text, self._children[option.data])
+
+ params['items'].append(item)
+
+ return render_template('forms/fields/checkboxes/template.njk', params=params)
+
class NestedRadioField(RadioFieldWithNoneOption, NestedFieldMixin):
pass
@@ -492,11 +531,239 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
auth_type = HiddenField('auth_type', validators=[DataRequired()])
+class govukCheckboxesMixin:
+
+ def extend_params(self, params, extensions):
+ items = None
+ param_items = len(params['items']) if 'items' in params else 0
+
+ # split items off from params to make it a pure dict
+ if 'items' in extensions:
+ items = extensions['items']
+ del extensions['items']
+
+ # merge dicts
+ params.update(extensions)
+
+ # merge items
+ if items:
+ if 'items' not in params:
+ params['items'] = items
+ else:
+ for idx, _item in enumerate(items):
+ if idx >= param_items:
+ params['items'].append(items[idx])
+ else:
+ params['items'][idx].update(items[idx])
+
+
+class govukCheckboxField(govukCheckboxesMixin, BooleanField):
+
+ def __init__(self, label='', validators=None, param_extensions=None, **kwargs):
+ super(govukCheckboxField, self).__init__(label, validators, false_values=None, **kwargs)
+ self.param_extensions = param_extensions
+
+ # self.__call__ renders the HTML for the field by:
+ # 1. delegating to self.meta.render_field which
+ # 2. calls field.widget
+ # this bypasses that by making self.widget a method with the same interface as widget.__call__
+ def widget(self, field, param_extensions=None, **kwargs):
+
+ # error messages
+ error_message = None
+ if field.errors:
+ error_message = {
+ "attributes": {
+ "data-module": "track-error",
+ "data-error-type": field.errors[0],
+ "data-error-label": field.name
+ },
+ "text": " ".join(field.errors).strip()
+ }
+
+ params = {
+ 'name': field.name,
+ 'errorMessage': error_message,
+ 'items': [
+ {
+ "name": field.name,
+ "id": field.id,
+ "text": field.label.text,
+ "value": 'y',
+ "checked": field.data
+ }
+ ]
+
+ }
+
+ # extend default params with any sent in during instantiation
+ if self.param_extensions:
+ self.extend_params(params, self.param_extensions)
+
+ # add any sent in though use in templates
+ if param_extensions:
+ self.extend_params(params, param_extensions)
+
+ return Markup(
+ render_template('forms/fields/checkboxes/macro.njk', params=params))
+
+
+# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
+class govukCheckboxesField(govukCheckboxesMixin, SelectMultipleField):
+
+ render_as_list = False
+
+ def __init__(self, label='', validators=None, param_extensions=None, **kwargs):
+ super(govukCheckboxesField, self).__init__(label, validators, **kwargs)
+ self.param_extensions = param_extensions
+
+ def get_item_from_option(self, option):
+ return {
+ "name": option.name,
+ "id": option.id,
+ "text": option.label.text,
+ "value": str(option.data), # to protect against non-string types like uuids
+ "checked": option.checked
+ }
+
+ def get_items_from_options(self, field):
+ return [self.get_item_from_option(option) for option in field]
+
+ def extend_params(self, params, extensions):
+ items = None
+ param_items = len(params['items']) if 'items' in params else 0
+
+ # split items off from params to make it a pure dict
+ if 'items' in extensions:
+ items = extensions['items']
+ del extensions['items']
+
+ # merge dicts
+ params.update(extensions)
+
+ # merge items
+ if items:
+ if 'items' not in params:
+ params['items'] = items
+ else:
+ for idx, _item in enumerate(items):
+ if idx >= param_items:
+ params['items'].append(items[idx])
+ else:
+ params['items'][idx].update(items[idx])
+
+ # self.__call__ renders the HTML for the field by:
+ # 1. delegating to self.meta.render_field which
+ # 2. calls field.widget
+ # this bypasses that by making self.widget a method with the same interface as widget.__call__
+ def widget(self, field, param_extensions=None, **kwargs):
+
+ # error messages
+ error_message = None
+ if field.errors:
+ error_message = {
+ "attributes": {
+ "data-module": "track-error",
+ "data-error-type": field.errors[0],
+ "data-error-label": field.name
+ },
+ "text": " ".join(field.errors).strip()
+ }
+
+ # returns either a list or a hierarchy of lists
+ # depending on how get_items_from_options is implemented
+ items = self.get_items_from_options(field)
+
+ params = {
+ 'name': field.name,
+ "fieldset": {
+ "attributes": {"id": field.name},
+ "legend": {
+ "text": field.label.text,
+ "classes": "govuk-fieldset__legend--s"
+ }
+ },
+ "asList": self.render_as_list,
+ 'errorMessage': error_message,
+ 'items': items
+ }
+
+ # extend default params with any sent in during instantiation
+ if self.param_extensions:
+ self.extend_params(params, self.param_extensions)
+
+ # add any sent in though use in templates
+ if param_extensions:
+ self.extend_params(params, param_extensions)
+
+ return Markup(
+ render_template('forms/fields/checkboxes/macro.njk', params=params))
+
+
+# Extends fields using the govukCheckboxesField interface to wrap their render in HTML needed by the collapsible JS
+class govukCollapsibleCheckboxesMixin:
+ def __init__(self, label='', validators=None, field_label='', param_extensions=None, **kwargs):
+
+ super(govukCollapsibleCheckboxesMixin, self).__init__(label, validators, param_extensions, **kwargs)
+ self.field_label = field_label
+
+ def widget(self, field, **kwargs):
+
+ # add a blank hint to act as an ARIA live-region
+ if self.param_extensions is not None:
+ self.param_extensions.update(
+ {"hint": {"html": "
"}})
+ else:
+ self.param_extensions = \
+ {"hint": {"html": ""}}
+
+ # wrap the checkboxes HTML in the HTML needed by the collapisble JS
+ return Markup(
+ f'
'
+ f' {super(govukCollapsibleCheckboxesMixin, self).widget(field, **kwargs)}'
+ f'
'
+ )
+
+
+class govukCollapsibleCheckboxesField(govukCollapsibleCheckboxesMixin, govukCheckboxesField):
+ pass
+
+
+# govukCollapsibleCheckboxesMixin adds an ARIA live-region to the hint and wraps the render in HTML needed by the
+# collapsible JS
+# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items
+class govukCollapsibleNestedCheckboxesField(govukCollapsibleCheckboxesMixin, NestedFieldMixin, govukCheckboxesField):
+ NONE_OPTION_VALUE = None
+ render_as_list = True
+
+
+# guard against data entries that aren't a role in permissions
+def filter_by_permissions(valuelist):
+ if valuelist is None:
+ return None
+ else:
+ return [entry for entry in valuelist if any(entry in role for role in permissions)]
+
+
+# guard against data entries that aren't a role in broadcast_permissions
+def filter_by_broadcast_permissions(valuelist):
+ if valuelist is None:
+ return None
+ else:
+ return [entry for entry in valuelist if any(entry in role for role in broadcast_permissions)]
+
+
+# Included to support both versions of how user permissions are handled in permissions forms
+# Remove when changeover to new version (permissions_field) is complete
PermissionsAbstract = type("PermissionsAbstract", (StripWhitespaceForm,), {
permission: BooleanField(label) for permission, label in permissions
})
+# Included to support both versions of how user permissions are handled in permissions forms
+# Remove when changeover to new version (permissions_field) is complete
BroadcastPermissionsAbstract = type("BroadcastPermissionsAbstract", (StripWhitespaceForm,), {
permission: BooleanField(label) for permission, label in broadcast_permissions
})
@@ -512,7 +779,9 @@ class BasePermissionsForm(StripWhitespaceForm):
(item['id'], item['name']) for item in ([{'name': 'Templates', 'id': None}] + all_template_folders)
]
- folder_permissions = NestedCheckboxesField('Folders this team member can see')
+ folder_permissions = govukCollapsibleNestedCheckboxesField(
+ 'Folders this team member can see',
+ field_label='folder')
login_authentication = RadioField(
'Sign in using',
@@ -524,10 +793,30 @@ class BasePermissionsForm(StripWhitespaceForm):
validators=[DataRequired()]
)
+ permissions_field = govukCheckboxesField(
+ 'Permssions',
+ filters=[filter_by_permissions],
+ choices=[
+ (value, label) for value, label in permissions
+ ],
+ param_extensions={
+ "hint": {"text": "All team members can see sent messages."}
+ }
+ )
+
+ # Modified to support both versions of how user permissions are handled in permissions forms
+ # Remove when changeover to new version (permissions_field) is complete
@property
def permissions(self):
- return {field.id for field in self.permissions_fields if field.data is True}
+ permissions_field_data = set(self.permissions_field.data)
+ permissions_fields_data = {field.id for field in self.permissions_fields if field.data is True}
+ if len(permissions_field_data) == 0 and len(permissions_fields_data) != 0:
+ return permissions_fields_data
+ else:
+ return permissions_field_data
+ # Included to support both versions of how user permissions are handled in permissions forms
+ # Remove when changeover to new version (permissions_field) is complete
@property
def permissions_fields(self):
return (
@@ -540,19 +829,34 @@ class BasePermissionsForm(StripWhitespaceForm):
return cls(
**kwargs,
**{
- role: user.has_permission_for_service(service_id, role)
- for role in roles.keys()
+ "permissions_field": [
+ role for role in roles.keys() if user.has_permission_for_service(service_id, role)]
},
login_authentication=user.auth_type
)
+# Included to support both versions of how user permissions are handled in permissions forms
+# Remove when changeover to new version (permissions_field) is complete
class PermissionsForm(PermissionsAbstract, BasePermissionsForm):
pass
+# Included to support both versions of how user permissions are handled in permissions forms
+# Remove when changeover to new version (permissions_field) is complete
class BroadcastPermissionsForm(BroadcastPermissionsAbstract, BasePermissionsForm):
+ permissions_field = govukCheckboxesField(
+ 'Permssions',
+ choices=[
+ (value, label) for value, label in broadcast_permissions
+ ],
+ filters=[filter_by_broadcast_permissions],
+ param_extensions={
+ "hint": {"text": "All team members can see sent messages."}
+ }
+ )
+
@property
def permissions(self):
return {'view_activity'} | super().permissions
@@ -1155,7 +1459,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm):
class ServiceReplyToEmailForm(StripWhitespaceForm):
email_address = email_address(label='Reply-to email address', gov_user=False)
- is_default = BooleanField("Make this email address the default")
+ is_default = govukCheckboxField("Make this email address the default")
class ServiceSmsSenderForm(StripWhitespaceForm):
@@ -1169,11 +1473,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm):
DoesNotStartWithDoubleZero(),
]
)
- is_default = BooleanField("Make this text message sender the default")
+ is_default = govukCheckboxField("Make this text message sender the default")
class ServiceEditInboundNumberForm(StripWhitespaceForm):
- is_default = BooleanField("Make this text message sender the default")
+ is_default = govukCheckboxField("Make this text message sender the default")
class ServiceLetterContactBlockForm(StripWhitespaceForm):
@@ -1183,7 +1487,7 @@ class ServiceLetterContactBlockForm(StripWhitespaceForm):
NoCommasInPlaceHolders()
]
)
- is_default = BooleanField("Set as your default address")
+ is_default = govukCheckboxField("Set as your default address")
def validate_letter_contact_block(self, field):
line_count = field.data.strip().count('\n')
@@ -1349,7 +1653,7 @@ class GuestList(StripWhitespaceForm):
class DateFilterForm(StripWhitespaceForm):
start_date = DateField("Start Date", [validators.optional()])
end_date = DateField("End Date", [validators.optional()])
- include_from_test_key = BooleanField("Include test keys", default="checked", false_values={"N"})
+ include_from_test_key = govukCheckboxField("Include test keys")
class RequiredDateFilterForm(StripWhitespaceForm):
@@ -1661,7 +1965,9 @@ class TemplateFolderForm(StripWhitespaceForm):
(item.id, item.name) for item in all_service_users
]
- users_with_permission = MultiCheckboxField('Team members who can see this folder')
+ users_with_permission = govukCollapsibleCheckboxesField(
+ 'Team members who can see this folder',
+ field_label='folder')
name = StringField('Folder name', validators=[DataRequired(message='Cannot be empty')])
@@ -1766,9 +2072,18 @@ class TemplateAndFoldersSelectionForm(Form):
return self.move_to_new_folder_name.data
return None
- templates_and_folders = MultiCheckboxField('Choose templates or folders', validators=[
- required_for_ops('move-to-new-folder', 'move-to-existing-folder')
- ])
+ templates_and_folders = govukCheckboxesField(
+ 'Choose templates or folders',
+ validators=[required_for_ops('move-to-new-folder', 'move-to-existing-folder')],
+ choices=[], # added to keep order of arguments, added properly in __init__
+ param_extensions={
+ "fieldset": {
+ "legend": {
+ "classes": "govuk-visually-hidden"
+ }
+ }
+ }
+ )
# if no default set, it is set to None, which process_data transforms to '__NONE__'
# this means '__NONE__' (self.ALL_TEMPLATES option) is selected when no form data has been submitted
# set default to empty string so process_data method doesn't perform any transformation
@@ -1867,11 +2182,13 @@ class AcceptAgreementForm(StripWhitespaceForm):
class BroadcastAreaForm(StripWhitespaceForm):
- areas = MultiCheckboxField('Choose areas to broadcast to')
+ areas = govukCheckboxesField('Choose areas to broadcast to')
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
self.areas.choices = choices
+ self.areas.render_as_list = True
+ self.areas.param_extensions = {'fieldset': {'legend': {'classes': 'govuk-visually-hidden'}}}
@classmethod
def from_library(cls, library):
diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py
index 53cca7132..ed85cad74 100644
--- a/app/main/views/platform_admin.py
+++ b/app/main/views/platform_admin.py
@@ -168,9 +168,11 @@ def platform_admin_services():
# Default to True if the user hasn’t done any filtering,
# otherwise respect their choice
form.include_from_test_key.data = True
+
+ include_from_test_key = form.include_from_test_key.data
api_args = {'detailed': True,
'only_active': False, # specifically DO get inactive services
- 'include_from_test_key': form.include_from_test_key.data,
+ 'include_from_test_key': include_from_test_key,
}
if form.start_date.data:
@@ -184,7 +186,7 @@ def platform_admin_services():
return render_template(
'views/platform-admin/services.html',
- include_from_test_key=form.include_from_test_key.data,
+ include_from_test_key=include_from_test_key,
form=form,
services=list(format_stats_by_service(services)),
page_title='{} services'.format(
diff --git a/app/templates/components/checkbox.html b/app/templates/components/checkbox.html
deleted file mode 100644
index 3647c4f1c..000000000
--- a/app/templates/components/checkbox.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% from "components/select-input.html" import select_nested, select %}
-
-{% macro checkbox(
- field,
- hint=False,
- width='2-3'
-) %}
-
-{% endmacro %}
diff --git a/app/templates/forms/fields/checkboxes/macro.njk b/app/templates/forms/fields/checkboxes/macro.njk
new file mode 100644
index 000000000..c91452053
--- /dev/null
+++ b/app/templates/forms/fields/checkboxes/macro.njk
@@ -0,0 +1,4 @@
+{%- macro govukCheckboxes(params) %}
+ {%- include "./template.njk" -%}
+{%- endmacro %}
+{{ govukCheckboxes(params) }}
diff --git a/app/templates/forms/fields/checkboxes/template.njk b/app/templates/forms/fields/checkboxes/template.njk
new file mode 100644
index 000000000..ddd83e88c
--- /dev/null
+++ b/app/templates/forms/fields/checkboxes/template.njk
@@ -0,0 +1,136 @@
+{% from "components/error-message/macro.njk" import govukErrorMessage -%}
+{% from "components/fieldset/macro.njk" import govukFieldset %}
+{% from "components/hint/macro.njk" import govukHint %}
+{% from "components/label/macro.njk" import govukLabel %}
+
+
+{#- Copied from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/template.njk
+ Changes:
+ - `classes` option added to `item` allow custom classes on the `.govuk-checkboxes__item` element
+ - `classes` option added to `item.hint` allow custom classes on the `.govuk-hint` element (added to GOVUK Frontend in v3.5.0 - remove when we update)
+ - `asList` option added the root `params` object to allow setting of the `.govuk-checkboxes` and `.govuk-checkboxes__item` element types
+ - `children` option added to `item` allowing the sending in of prerendered child checkboxes (allowing the creation of tree structures through recursion) -#}
+{#- If an id 'prefix' is not passed, fall back to using the name attribute
+ instead. We need this for error messages and hints as well -#}
+{% set idPrefix = params.idPrefix if params.idPrefix else params.name %}
+
+{#- a record of other elements that we need to associate with the input using
+ aria-describedby – for example hints or error messages -#}
+{% set describedBy = params.describedBy if params.describedBy else "" %}
+{% if params.fieldset.describedBy %}
+ {% set describedBy = params.fieldset.describedBy %}
+{% endif %}
+
+{#- set the types of element used for the checkboxes and their group based on
+ whether asList is set -#}
+{% if params.asList %}
+ {% set groupElement = 'ul' %}
+ {% set groupItemElement = 'li' %}
+{% else %}
+ {% set groupElement = 'div' %}
+ {% set groupItemElement = 'div' %}
+{% endif %}
+
+{% set isConditional = false %}
+{% for item in params.items %}
+ {% if item.conditional %}
+ {% set isConditional = true %}
+ {% endif %}
+{% endfor %}
+
+{#- fieldset is false by default -#}
+{% set hasFieldset = true if params.fieldset else false %}
+
+{#- Capture the HTML so we can optionally nest it in a fieldset -#}
+{% set innerHtml %}
+{% if params.hint %}
+ {% set hintId = idPrefix + '-hint' %}
+ {% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
+ {{ govukHint({
+ id: hintId,
+ classes: params.hint.classes,
+ attributes: params.hint.attributes,
+ html: params.hint.html,
+ text: params.hint.text
+ }) | indent(2) | trim }}
+{% endif %}
+{% if params.errorMessage %}
+ {% set errorId = idPrefix + '-error' %}
+ {% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
+ {{ govukErrorMessage({
+ id: errorId,
+ classes: params.errorMessage.classes,
+ attributes: params.errorMessage.attributes,
+ html: params.errorMessage.html,
+ text: params.errorMessage.text,
+ visuallyHiddenText: params.errorMessage.visuallyHiddenText
+ }) | indent(2) | trim }}
+{% endif %}
+ <{{ groupElement }} class="govuk-checkboxes {%- if params.classes %} {{ params.classes }}{% endif %}"
+ {%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}
+ {%- if isConditional %} data-module="checkboxes"{% endif -%}>
+ {% for item in params.items %}
+ {% set id = item.id if item.id else idPrefix + "-" + loop.index %}
+ {% set name = item.name if item.name else params.name %}
+ {% set conditionalId = "conditional-" + id %}
+ {% set hasHint = true if item.hint.text or item.hint.html %}
+ {% set itemHintId = id + "-item-hint" if hasHint else "" %}
+ {% set itemDescribedBy = describedBy if not hasFieldset else "" %}
+ {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %}
+ <{{ groupItemElement }} class="govuk-checkboxes__item {%- if item.classes %} {{ item.classes }}{% endif %}">
+ {%- if item.before %}{{ item.before }}{% endif -%}
+
+ {{ govukLabel({
+ html: item.html,
+ text: item.text,
+ classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes),
+ attributes: item.label.attributes,
+ for: id
+ }) | indent(6) | trim }}
+ {%- if hasHint %}
+ {{ govukHint({
+ id: itemHintId,
+ classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes),
+ attributes: item.hint.attributes,
+ html: item.hint.html,
+ text: item.hint.text
+ }) | indent(6) | trim }}
+ {%- endif %}
+ {%- if item.children %}
+ {{ item.children | safe }}
+ {%- endif %}
+ {% if params.asList and item.conditional %}
+
+ {{ item.conditional.html | safe }}
+
+ {% endif %}
+ {%- if item.after %}{{ item.after }}{% endif -%}
+ {{ groupItemElement }}>
+ {% if not params.asList and item.conditional %}
+