diff --git a/app/assets/javascripts/modules/all.mjs b/app/assets/javascripts/modules/all.mjs
index 1d30e666d..a62daad45 100644
--- a/app/assets/javascripts/modules/all.mjs
+++ b/app/assets/javascripts/modules/all.mjs
@@ -9,6 +9,7 @@
import Header from 'govuk-frontend/components/header/header';
import Details from 'govuk-frontend/components/details/details';
import Button from 'govuk-frontend/components/button/button';
+import Radios from 'govuk-frontend/components/radios/radios';
/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
@@ -46,6 +47,11 @@ function initAll (options) {
// Find first header module to enhance.
var $toggleButton = scope.querySelector('[data-module="header"]')
new Header($toggleButton).init()
+
+ var $radios = scope.querySelectorAll('[data-module="radios"]')
+ nodeListForEach($radios, function ($radio) {
+ new Radios($radio).init()
+ })
}
// Create separate namespace for GOVUK Frontend.
diff --git a/app/main/forms.py b/app/main/forms.py
index 59097037f..e3e4b749b 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -160,7 +160,7 @@ class UKMobileNumber(TelField):
# 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):
- return govuk_field_widget(self, field, type="tel", param_extensions=param_extensions, **kwargs)
+ return govuk_text_input_field_widget(self, field, type="tel", param_extensions=param_extensions, **kwargs)
def pre_validate(self, form):
try:
@@ -179,7 +179,7 @@ class InternationalPhoneNumber(TelField):
# 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):
- return govuk_field_widget(self, field, type="tel", param_extensions=param_extensions, **kwargs)
+ return govuk_text_input_field_widget(self, field, type="tel", param_extensions=param_extensions, **kwargs)
def pre_validate(self, form):
try:
@@ -212,7 +212,7 @@ def password(label='Password'):
)
-def govuk_field_widget(self, field, type=None, param_extensions=None, **kwargs):
+def govuk_text_input_field_widget(self, field, type=None, param_extensions=None, **kwargs):
value = kwargs["value"] if kwargs.get("value") else field.data
# error messages
@@ -260,7 +260,7 @@ class GovukTextInputField(StringField):
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, **kwargs):
- return govuk_field_widget(self, field, **kwargs)
+ return govuk_text_input_field_widget(self, field, **kwargs)
class GovukPasswordField(PasswordField):
@@ -273,7 +273,7 @@ class GovukPasswordField(PasswordField):
# 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):
- return govuk_field_widget(self, field, type="password", param_extensions=param_extensions, **kwargs)
+ return govuk_text_input_field_widget(self, field, type="password", param_extensions=param_extensions, **kwargs)
class GovukEmailField(EmailField):
@@ -290,7 +290,7 @@ class GovukEmailField(EmailField):
params = {"attributes": {"spellcheck": "false"}} # email addresses don't need to be spellchecked
merge_jsonlike(params, param_extensions)
- return govuk_field_widget(self, field, type="email", param_extensions=params, **kwargs)
+ return govuk_text_input_field_widget(self, field, type="email", param_extensions=params, **kwargs)
class GovukSearchField(SearchField):
@@ -307,7 +307,7 @@ class GovukSearchField(SearchField):
params = {"classes": "govuk-!-width-full"} # email addresses don't need to be spellchecked
merge_jsonlike(params, param_extensions)
- return govuk_field_widget(self, field, type="search", param_extensions=params, **kwargs)
+ return govuk_text_input_field_widget(self, field, type="search", param_extensions=params, **kwargs)
class GovukDateField(DateField):
@@ -320,7 +320,7 @@ class GovukDateField(DateField):
# 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):
- return govuk_field_widget(self, field, param_extensions=param_extensions, **kwargs)
+ return govuk_text_input_field_widget(self, field, param_extensions=param_extensions, **kwargs)
class GovukIntegerField(IntegerField):
@@ -333,7 +333,7 @@ class GovukIntegerField(IntegerField):
# 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):
- return govuk_field_widget(self, field, param_extensions=param_extensions, **kwargs)
+ return govuk_text_input_field_widget(self, field, param_extensions=param_extensions, **kwargs)
class SMSCode(GovukTextInputField):
@@ -688,36 +688,185 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
auth_type = HiddenField('auth_type', validators=[DataRequired()])
-class govukCheckboxesMixin:
+def extend_params(params, extensions):
+ items = None
+ param_items = len(params['items']) if 'items' in params else 0
- 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']
- # split items off from params to make it a pure dict
- if 'items' in extensions:
- items = extensions['items']
- del extensions['items']
+ # merge dicts
+ merge_jsonlike(params, extensions)
- # merge dicts
- merge_jsonlike(params, 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])
+ # 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 govuk_checkbox_field_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:
+ extend_params(params, self.param_extensions)
+
+ # add any sent in though use in templates
+ if param_extensions:
+ extend_params(params, param_extensions)
+
+ return Markup(
+ render_template('forms/fields/checkboxes/macro.njk', params=params))
+
+
+def govuk_checkboxes_field_widget(self, field, wrap_in_collapsible=False, param_extensions=None, **kwargs):
+
+ def _wrap_in_collapsible(field_label, checkboxes_string):
+ # wrap the checkboxes HTML in the HTML needed by the collapisble JS
+ result = Markup(
+ f'
'
+ f' {checkboxes_string}'
+ f'
'
+ )
+
+ return result
+
+ # 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:
+ extend_params(params, self.param_extensions)
+
+ # add any sent in though use in templates
+ if param_extensions:
+ extend_params(params, param_extensions)
+
+ if wrap_in_collapsible:
+
+ # add a blank hint to act as an ARIA live-region
+ params.update(
+ {"hint": {"html": ""}})
+
+ return _wrap_in_collapsible(
+ self.field_label,
+ Markup(render_template('forms/fields/checkboxes/macro.njk', params=params))
+ )
+ else:
+ return Markup(
+ render_template('forms/fields/checkboxes/macro.njk', params=params))
+
+
+def govuk_radios_field_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"
+ }
+ },
+ 'errorMessage': error_message,
+ 'items': items
+ }
+
+ # extend default params with any sent in during instantiation
+ if self.param_extensions:
+ extend_params(params, self.param_extensions)
+
+ # add any sent in though use in templates
+ if param_extensions:
+ extend_params(params, param_extensions)
+
+ return Markup(
+ render_template('components/radios/template.njk', params=params))
+
+
+class GovukCheckboxField(BooleanField):
def __init__(self, label='', validators=None, param_extensions=None, **kwargs):
- super(govukCheckboxField, self).__init__(label, validators, false_values=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:
@@ -725,53 +874,16 @@ class govukCheckboxField(govukCheckboxesMixin, BooleanField):
# 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))
+ return govuk_checkbox_field_widget(self, field, param_extensions=param_extensions, **kwargs)
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
-class govukCheckboxesField(govukCheckboxesMixin, SelectMultipleField):
+class GovukCheckboxesField(SelectMultipleField):
render_as_list = False
def __init__(self, label='', validators=None, param_extensions=None, **kwargs):
- super(govukCheckboxesField, self).__init__(label, validators, **kwargs)
+ super(GovukCheckboxesField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
def get_item_from_option(self, option):
@@ -791,88 +903,54 @@ class govukCheckboxesField(govukCheckboxesMixin, SelectMultipleField):
# 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))
+ return govuk_checkboxes_field_widget(self, field, param_extensions=param_extensions, **kwargs)
-# Extends fields using the govukCheckboxesField interface to wrap their render in HTML needed by the collapsible JS
-class govukCollapsibleCheckboxesMixin:
+# Wraps checkboxes rendering in HTML needed by the collapsible JS
+class GovukCollapsibleCheckboxesField(GovukCheckboxesField):
def __init__(self, label='', validators=None, field_label='', param_extensions=None, **kwargs):
- super(govukCollapsibleCheckboxesMixin, self).__init__(label, validators, param_extensions, **kwargs)
+ super(GovukCollapsibleCheckboxesField, 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'
'
- )
+ return govuk_checkboxes_field_widget(self, field, wrap_in_collapsible=True, param_extensions=None, **kwargs)
-class govukCollapsibleCheckboxesField(govukCollapsibleCheckboxesMixin, govukCheckboxesField):
- pass
-
-
-# govukCollapsibleCheckboxesMixin adds an ARIA live-region to the hint and wraps the render in HTML needed by the
+# GovukCollapsibleCheckboxesField 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):
+class GovukCollapsibleNestedCheckboxesField(NestedFieldMixin, GovukCollapsibleCheckboxesField):
NONE_OPTION_VALUE = None
render_as_list = True
+class GovukRadiosField(RadioField):
+
+ def __init__(self, label='', validators=None, param_extensions=None, **kwargs):
+ super(GovukRadiosField, 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]
+
+ # 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):
+ return govuk_radios_field_widget(self, field, param_extensions=param_extensions, **kwargs)
+
+
# guard against data entries that aren't a role in permissions
def filter_by_permissions(valuelist):
if valuelist is None:
@@ -899,7 +977,7 @@ class BasePermissionsForm(StripWhitespaceForm):
(item['id'], item['name']) for item in ([{'name': 'Templates', 'id': None}] + all_template_folders)
]
- folder_permissions = govukCollapsibleNestedCheckboxesField(
+ folder_permissions = GovukCollapsibleNestedCheckboxesField(
'Folders this team member can see',
field_label='folder')
@@ -913,7 +991,7 @@ class BasePermissionsForm(StripWhitespaceForm):
validators=[DataRequired()]
)
- permissions_field = govukCheckboxesField(
+ permissions_field = GovukCheckboxesField(
'Permissions',
filters=[filter_by_permissions],
choices=[
@@ -946,7 +1024,7 @@ class PermissionsForm(BasePermissionsForm):
class BroadcastPermissionsForm(BasePermissionsForm):
- permissions_field = govukCheckboxesField(
+ permissions_field = GovukCheckboxesField(
'Permissions',
choices=[
(value, label) for value, label in broadcast_permissions
@@ -1216,14 +1294,13 @@ class BaseTemplateForm(StripWhitespaceForm):
NoCommasInPlaceHolders()
]
)
- process_type = RadioField(
+ process_type = GovukRadiosField(
"Use priority queue?",
choices=[
('priority', 'Yes'),
('normal', 'No'),
],
thing='yes or no',
- validators=[DataRequired()],
default='normal'
)
@@ -1567,7 +1644,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm):
class ServiceReplyToEmailForm(StripWhitespaceForm):
email_address = email_address(label='Reply-to email address', gov_user=False)
- is_default = govukCheckboxField("Make this email address the default")
+ is_default = GovukCheckboxField("Make this email address the default")
class ServiceSmsSenderForm(StripWhitespaceForm):
@@ -1581,11 +1658,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm):
DoesNotStartWithDoubleZero(),
]
)
- is_default = govukCheckboxField("Make this text message sender the default")
+ is_default = GovukCheckboxField("Make this text message sender the default")
class ServiceEditInboundNumberForm(StripWhitespaceForm):
- is_default = govukCheckboxField("Make this text message sender the default")
+ is_default = GovukCheckboxField("Make this text message sender the default")
class ServiceLetterContactBlockForm(StripWhitespaceForm):
@@ -1595,7 +1672,7 @@ class ServiceLetterContactBlockForm(StripWhitespaceForm):
NoCommasInPlaceHolders()
]
)
- is_default = govukCheckboxField("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')
@@ -1765,7 +1842,7 @@ class GuestList(StripWhitespaceForm):
class DateFilterForm(StripWhitespaceForm):
start_date = GovukDateField("Start Date", [validators.optional()])
end_date = GovukDateField("End Date", [validators.optional()])
- include_from_test_key = govukCheckboxField("Include test keys")
+ include_from_test_key = GovukCheckboxField("Include test keys")
class RequiredDateFilterForm(StripWhitespaceForm):
@@ -2047,7 +2124,7 @@ class TemplateFolderForm(StripWhitespaceForm):
(item.id, item.name) for item in all_service_users
]
- users_with_permission = govukCollapsibleCheckboxesField(
+ users_with_permission = GovukCollapsibleCheckboxesField(
'Team members who can see this folder',
field_label='folder')
name = GovukTextInputField('Folder name', validators=[DataRequired(message='Cannot be empty')])
@@ -2150,7 +2227,7 @@ class TemplateAndFoldersSelectionForm(Form):
return self.move_to_new_folder_name.data
return None
- templates_and_folders = govukCheckboxesField(
+ 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__
@@ -2260,7 +2337,7 @@ class AcceptAgreementForm(StripWhitespaceForm):
class BroadcastAreaForm(StripWhitespaceForm):
- areas = govukCheckboxesField('Choose areas to broadcast to')
+ areas = GovukCheckboxesField('Choose areas to broadcast to')
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -2277,7 +2354,7 @@ class BroadcastAreaForm(StripWhitespaceForm):
class BroadcastAreaFormWithSelectAll(BroadcastAreaForm):
- select_all = govukCheckboxField('Select all')
+ select_all = GovukCheckboxField('Select all')
@classmethod
def from_library(cls, library, select_all_choice):
diff --git a/app/templates/views/edit-email-template.html b/app/templates/views/edit-email-template.html
index 1f2c569d1..8e87c32d3 100644
--- a/app/templates/views/edit-email-template.html
+++ b/app/templates/views/edit-email-template.html
@@ -2,7 +2,6 @@
{% from "components/textbox.html" import textbox %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import sticky_page_footer %}
-{% from "components/radios.html" import radios %}
{% from "components/form.html" import form_wrapper %}
{% block service_page_title %}
@@ -26,7 +25,7 @@
{{ textbox(form.subject, width='1-1', highlight_placeholders=True, rows=2) }}
{{ textbox(form.template_content, highlight_placeholders=True, width='1-1', rows=8) }}
{% if current_user.platform_admin %}
- {{ radios(form.process_type) }}
+ {{ form.process_type }}
{% endif %}
{{ sticky_page_footer(
'Save'
diff --git a/app/templates/views/edit-sms-template.html b/app/templates/views/edit-sms-template.html
index faf3d25d5..7a83823b1 100644
--- a/app/templates/views/edit-sms-template.html
+++ b/app/templates/views/edit-sms-template.html
@@ -2,7 +2,6 @@
{% from "components/textbox.html" import textbox %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import sticky_page_footer %}
-{% from "components/radios.html" import radios %}
{% from "components/form.html" import form_wrapper %}
{% block service_page_title %}
@@ -27,7 +26,7 @@
{{ textbox(form.template_content, highlight_placeholders=True, width='1-1', rows=5) }}
{% if current_user.platform_admin %}
- {{ radios(form.process_type) }}
+ {{ form.process_type }}
{% endif %}
{{ sticky_page_footer(
'Save'
diff --git a/gulpfile.js b/gulpfile.js
index aef452c39..ccb3efd2d 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -71,6 +71,7 @@ const copy = {
'hint',
'label',
'checkboxes',
+ 'radios',
'input'
];
let done = 0;