mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 10:28:41 -04:00
Refactor GovukCheckboxField and all variants
Aims to make the structure of their code more like GovukTextInputField so this convention can be extended for radios. It does that by: - moving the code in their widget methods out into the govuk_checkbox_field_widget helper function - moving the code that wraps the checkboxes in HTML to make them collapsible into the helper function - remove GovukCheckboxesMixin in favour of having the extend_params method as a stand-alone function* *The code is generic enough that it can be used with other fields that share the same type of data. These changes also: - don't alter the interface of any classes involved - don't break any tests
This commit is contained in:
+77
-68
@@ -688,9 +688,7 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
|
|||||||
auth_type = HiddenField('auth_type', validators=[DataRequired()])
|
auth_type = HiddenField('auth_type', validators=[DataRequired()])
|
||||||
|
|
||||||
|
|
||||||
class GovukCheckboxesMixin:
|
def extend_params(params, extensions):
|
||||||
|
|
||||||
def extend_params(self, params, extensions):
|
|
||||||
items = None
|
items = None
|
||||||
param_items = len(params['items']) if 'items' in params else 0
|
param_items = len(params['items']) if 'items' in params else 0
|
||||||
|
|
||||||
@@ -714,17 +712,7 @@ class GovukCheckboxesMixin:
|
|||||||
params['items'][idx].update(items[idx])
|
params['items'][idx].update(items[idx])
|
||||||
|
|
||||||
|
|
||||||
class GovukCheckboxField(GovukCheckboxesMixin, BooleanField):
|
def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
|
||||||
|
|
||||||
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 messages
|
||||||
error_message = None
|
error_message = None
|
||||||
@@ -755,42 +743,29 @@ class GovukCheckboxField(GovukCheckboxesMixin, BooleanField):
|
|||||||
|
|
||||||
# extend default params with any sent in during instantiation
|
# extend default params with any sent in during instantiation
|
||||||
if self.param_extensions:
|
if self.param_extensions:
|
||||||
self.extend_params(params, self.param_extensions)
|
extend_params(params, self.param_extensions)
|
||||||
|
|
||||||
# add any sent in though use in templates
|
# add any sent in though use in templates
|
||||||
if param_extensions:
|
if param_extensions:
|
||||||
self.extend_params(params, param_extensions)
|
extend_params(params, param_extensions)
|
||||||
|
|
||||||
return Markup(
|
return Markup(
|
||||||
render_template('forms/fields/checkboxes/macro.njk', params=params))
|
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
|
def govuk_checkboxes_field_widget(self, field, wrap_in_collapsible=False, param_extensions=None, **kwargs):
|
||||||
class GovukCheckboxesField(GovukCheckboxesMixin, SelectMultipleField):
|
|
||||||
|
|
||||||
render_as_list = False
|
def _wrap_in_collapsible(field_label, checkboxes_string):
|
||||||
|
# wrap the checkboxes HTML in the HTML needed by the collapisble JS
|
||||||
|
result = Markup(
|
||||||
|
f'<div class="selection-wrapper"'
|
||||||
|
f' data-module="collapsible-checkboxes"'
|
||||||
|
f' data-field-label="{field_label}">'
|
||||||
|
f' {checkboxes_string}'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, label='', validators=None, param_extensions=None, **kwargs):
|
return result
|
||||||
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]
|
|
||||||
|
|
||||||
# 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 messages
|
||||||
error_message = None
|
error_message = None
|
||||||
@@ -824,51 +799,85 @@ class GovukCheckboxesField(GovukCheckboxesMixin, SelectMultipleField):
|
|||||||
|
|
||||||
# extend default params with any sent in during instantiation
|
# extend default params with any sent in during instantiation
|
||||||
if self.param_extensions:
|
if self.param_extensions:
|
||||||
self.extend_params(params, self.param_extensions)
|
extend_params(params, self.param_extensions)
|
||||||
|
|
||||||
# add any sent in though use in templates
|
# add any sent in though use in templates
|
||||||
if param_extensions:
|
if param_extensions:
|
||||||
self.extend_params(params, 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": "<div class=\"selection-summary\" role=\"region\" aria-live=\"polite\"></div>"}})
|
||||||
|
|
||||||
|
return _wrap_in_collapsible(
|
||||||
|
self.field_label,
|
||||||
|
Markup(render_template('forms/fields/checkboxes/macro.njk', params=params))
|
||||||
|
)
|
||||||
|
else:
|
||||||
return Markup(
|
return Markup(
|
||||||
render_template('forms/fields/checkboxes/macro.njk', params=params))
|
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 GovukCheckboxField(BooleanField):
|
||||||
class GovukCollapsibleCheckboxesMixin:
|
|
||||||
|
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):
|
||||||
|
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(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]
|
||||||
|
|
||||||
|
# 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_checkboxes_field_widget(self, field, param_extensions=param_extensions, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# 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):
|
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
|
self.field_label = field_label
|
||||||
|
|
||||||
def widget(self, field, **kwargs):
|
def widget(self, field, **kwargs):
|
||||||
|
return govuk_checkboxes_field_widget(self, field, wrap_in_collapsible=True, param_extensions=None, **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": "<div class=\"selection-summary\" role=\"region\" aria-live=\"polite\"></div>"}})
|
|
||||||
else:
|
|
||||||
self.param_extensions = \
|
|
||||||
{"hint": {"html": "<div class=\"selection-summary\" role=\"region\" aria-live=\"polite\"></div>"}}
|
|
||||||
|
|
||||||
# wrap the checkboxes HTML in the HTML needed by the collapisble JS
|
|
||||||
return Markup(
|
|
||||||
f'<div class="selection-wrapper"'
|
|
||||||
f' data-module="collapsible-checkboxes"'
|
|
||||||
f' data-field-label="{self.field_label}">'
|
|
||||||
f' {super(GovukCollapsibleCheckboxesMixin, self).widget(field, **kwargs)}'
|
|
||||||
f'</div>'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GovukCollapsibleCheckboxesField(GovukCollapsibleCheckboxesMixin, GovukCheckboxesField):
|
# GovukCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# GovukCollapsibleCheckboxesMixin adds an ARIA live-region to the hint and wraps the render in HTML needed by the
|
|
||||||
# collapsible JS
|
# collapsible JS
|
||||||
# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items
|
# 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
|
NONE_OPTION_VALUE = None
|
||||||
render_as_list = True
|
render_as_list = True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user