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;