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:
Tom Byers
2020-11-10 22:25:40 +00:00
parent d648af5b76
commit 5293e3e1eb
+77 -68
View File
@@ -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