Merge pull request #3535 from alphagov/try-introducing-govuk-checkboxes-again

Try introducing govuk checkboxes again
This commit is contained in:
Tom Byers
2020-08-04 14:39:54 +01:00
committed by GitHub
31 changed files with 1036 additions and 395 deletions
@@ -5,7 +5,7 @@
function Summary (module) { function Summary (module) {
this.module = module; this.module = module;
this.$el = module.$formGroup.find('.selection-summary'); this.$el = module.$formGroup.find('.selection-summary').first();
this.fieldLabel = module.fieldLabel; this.fieldLabel = module.fieldLabel;
this.total = module.total; this.total = module.total;
this.addContent(); this.addContent();
@@ -25,6 +25,7 @@
if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); } if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); }
this.$el.append(this.$text); this.$el.append(this.$text);
this.module.$formGroup.find('.govuk-hint').remove();
}; };
Summary.prototype.update = function(selection) { Summary.prototype.update = function(selection) {
let template; let template;
@@ -86,12 +87,13 @@
.focus(); .focus();
}; };
CollapsibleCheckboxes.prototype.start = function(component) { CollapsibleCheckboxes.prototype.start = function(component) {
this.$formGroup = $(component); this.$component = $(component);
this.$fieldset = this.$formGroup.find('fieldset'); 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.$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.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.expanded = false;
this.addHeadingHideLegend(); this.addHeadingHideLegend();
@@ -113,7 +115,7 @@
}; };
CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; }; CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; };
CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() { CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() {
const headingLevel = this.$formGroup.data('heading-level') || '2'; const headingLevel = this.$component.data('heading-level') || '2';
this.$heading = $(`<h${headingLevel} class="heading-small">${this.legendText}</h${headingLevel}>`); this.$heading = $(`<h${headingLevel} class="heading-small">${this.legendText}</h${headingLevel}>`);
this.$fieldset.before(this.$heading); this.$fieldset.before(this.$heading);
@@ -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 {
.selection-summary__text { .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 { .selection-content {
margin-bottom: govuk-spacing(4); margin-bottom: govuk-spacing(4);
+31 -22
View File
@@ -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 { .message {
&-name { &-name {
@include bold-24;
margin: 0; margin: 0;
a { a {
@@ -63,37 +66,20 @@
} }
&-type { &-type {
color: $secondary-text-colour; color: $govuk-secondary-text-colour;
margin: 0 0 govuk-spacing(4) 0; margin: 0 0 $message-type-bottom-spacing 0;
padding-left: 0;
pointer-events: none; pointer-events: none;
} }
} }
#template-list {
margin-top: govuk-spacing(6);
&.top-gutter-5px {
margin-top: 5px;
}
}
.template-list { .template-list {
&-item { &-item {
&-with-checkbox { &-with-checkbox {
padding-left: $govuk-checkboxes-size + $govuk-checkboxes-label-padding-left-right;
position: relative;
padding-left: govuk-spacing(9);
.multiple-choice {
position: absolute;
left: 0;
}
} }
&-hidden-by-default { &-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 { &-folder {
@@ -26,6 +26,7 @@ $govuk-assets-path: "/static/";
@import 'components/button/_button'; @import 'components/button/_button';
@import 'components/details/_details'; @import 'components/details/_details';
@import 'components/radios/_radios'; @import 'components/radios/_radios';
@import 'components/checkboxes/_checkboxes';
@import "utilities/all"; @import "utilities/all";
@import "overrides/all"; @import "overrides/all";
+338 -21
View File
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
from itertools import chain from itertools import chain
import pytz import pytz
from flask import request from flask import Markup, render_template, request
from flask_login import current_user from flask_login import current_user
from flask_wtf import FlaskForm as Form from flask_wtf import FlaskForm as Form
from flask_wtf.file import FileAllowed from flask_wtf.file import FileAllowed
@@ -17,6 +17,7 @@ from notifications_utils.recipients import (
normalise_phone_number, normalise_phone_number,
validate_phone_number, validate_phone_number,
) )
from werkzeug.utils import cached_property
from wtforms import ( from wtforms import (
BooleanField, BooleanField,
DateField, DateField,
@@ -37,7 +38,6 @@ from wtforms import (
) )
from wtforms.fields.html5 import EmailField, SearchField, TelField from wtforms.fields.html5 import EmailField, SearchField, TelField
from wtforms.validators import URL, DataRequired, Length, Optional, Regexp from wtforms.validators import URL, DataRequired, Length, Optional, Regexp
from wtforms.widgets import CheckboxInput, ListWidget
from app import format_thousands from app import format_thousands
from app.main.validators import ( 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): class RadioField(WTFormsRadioField):
def __init__( def __init__(
@@ -322,13 +317,16 @@ class RadioFieldWithNoneOption(FieldWithNoneOption, RadioField):
class NestedFieldMixin: class NestedFieldMixin:
def children(self): def children(self):
# start map with root option as a single child entry # start map with root option as a single child entry
child_map = {None: [option for option in self child_map = {None: [option for option in self
if option.data == self.NONE_OPTION_VALUE]} if option.data == self.NONE_OPTION_VALUE]}
# add entries for all other children # add entries for all other children
for option in self: 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: if option.data == self.NONE_OPTION_VALUE:
child_ids = [ child_ids = [
folder['id'] for folder in self.all_template_folders folder['id'] for folder in self.all_template_folders
@@ -344,6 +342,47 @@ class NestedFieldMixin:
return child_map 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): class NestedRadioField(RadioFieldWithNoneOption, NestedFieldMixin):
pass pass
@@ -492,11 +531,239 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
auth_type = HiddenField('auth_type', validators=[DataRequired()]) 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": "<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):
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,), { PermissionsAbstract = type("PermissionsAbstract", (StripWhitespaceForm,), {
permission: BooleanField(label) for permission, label in permissions 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,), { BroadcastPermissionsAbstract = type("BroadcastPermissionsAbstract", (StripWhitespaceForm,), {
permission: BooleanField(label) for permission, label in broadcast_permissions 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) (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( login_authentication = RadioField(
'Sign in using', 'Sign in using',
@@ -524,10 +793,30 @@ class BasePermissionsForm(StripWhitespaceForm):
validators=[DataRequired()] 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 @property
def permissions(self): 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 @property
def permissions_fields(self): def permissions_fields(self):
return ( return (
@@ -540,19 +829,34 @@ class BasePermissionsForm(StripWhitespaceForm):
return cls( return cls(
**kwargs, **kwargs,
**{ **{
role: user.has_permission_for_service(service_id, role) "permissions_field": [
for role in roles.keys() role for role in roles.keys() if user.has_permission_for_service(service_id, role)]
}, },
login_authentication=user.auth_type 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): class PermissionsForm(PermissionsAbstract, BasePermissionsForm):
pass 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): 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 @property
def permissions(self): def permissions(self):
return {'view_activity'} | super().permissions return {'view_activity'} | super().permissions
@@ -1155,7 +1459,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm):
class ServiceReplyToEmailForm(StripWhitespaceForm): class ServiceReplyToEmailForm(StripWhitespaceForm):
email_address = email_address(label='Reply-to email address', gov_user=False) 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): class ServiceSmsSenderForm(StripWhitespaceForm):
@@ -1169,11 +1473,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm):
DoesNotStartWithDoubleZero(), DoesNotStartWithDoubleZero(),
] ]
) )
is_default = BooleanField("Make this text message sender the default") is_default = govukCheckboxField("Make this text message sender the default")
class ServiceEditInboundNumberForm(StripWhitespaceForm): 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): class ServiceLetterContactBlockForm(StripWhitespaceForm):
@@ -1183,7 +1487,7 @@ class ServiceLetterContactBlockForm(StripWhitespaceForm):
NoCommasInPlaceHolders() NoCommasInPlaceHolders()
] ]
) )
is_default = BooleanField("Set as your default address") is_default = govukCheckboxField("Set as your default address")
def validate_letter_contact_block(self, field): def validate_letter_contact_block(self, field):
line_count = field.data.strip().count('\n') line_count = field.data.strip().count('\n')
@@ -1349,7 +1653,7 @@ class GuestList(StripWhitespaceForm):
class DateFilterForm(StripWhitespaceForm): class DateFilterForm(StripWhitespaceForm):
start_date = DateField("Start Date", [validators.optional()]) start_date = DateField("Start Date", [validators.optional()])
end_date = DateField("End 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): class RequiredDateFilterForm(StripWhitespaceForm):
@@ -1661,7 +1965,9 @@ class TemplateFolderForm(StripWhitespaceForm):
(item.id, item.name) for item in all_service_users (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')]) 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 self.move_to_new_folder_name.data
return None return None
templates_and_folders = MultiCheckboxField('Choose templates or folders', validators=[ templates_and_folders = govukCheckboxesField(
required_for_ops('move-to-new-folder', 'move-to-existing-folder') '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__' # 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 # 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 # set default to empty string so process_data method doesn't perform any transformation
@@ -1867,11 +2182,13 @@ class AcceptAgreementForm(StripWhitespaceForm):
class BroadcastAreaForm(StripWhitespaceForm): class BroadcastAreaForm(StripWhitespaceForm):
areas = MultiCheckboxField('Choose areas to broadcast to') areas = govukCheckboxesField('Choose areas to broadcast to')
def __init__(self, choices, *args, **kwargs): def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.areas.choices = choices self.areas.choices = choices
self.areas.render_as_list = True
self.areas.param_extensions = {'fieldset': {'legend': {'classes': 'govuk-visually-hidden'}}}
@classmethod @classmethod
def from_library(cls, library): def from_library(cls, library):
+4 -2
View File
@@ -168,9 +168,11 @@ def platform_admin_services():
# Default to True if the user hasnt done any filtering, # Default to True if the user hasnt done any filtering,
# otherwise respect their choice # otherwise respect their choice
form.include_from_test_key.data = True form.include_from_test_key.data = True
include_from_test_key = form.include_from_test_key.data
api_args = {'detailed': True, api_args = {'detailed': True,
'only_active': False, # specifically DO get inactive services '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: if form.start_date.data:
@@ -184,7 +186,7 @@ def platform_admin_services():
return render_template( return render_template(
'views/platform-admin/services.html', '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, form=form,
services=list(format_stats_by_service(services)), services=list(format_stats_by_service(services)),
page_title='{} services'.format( page_title='{} services'.format(
-46
View File
@@ -1,46 +0,0 @@
{% from "components/select-input.html" import select_nested, select %}
{% macro checkbox(
field,
hint=False,
width='2-3'
) %}
<div class="multiple-choice">
{{ checkbox_input(field.id, field.name, field.data) }}
<label for="{{ field.id }}">
{{ field.label.text }}
{% if hint %}
<div class="hint">
{{ hint }}
</div>
{% endif %}
</label>
</div>
{% endmacro %}
{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %}
{{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style, input="checkbox") }}
{% endmacro %}
{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}) %}
{{ select(field, hint, disable, option_hints, hide_legend, collapsible_opts, input="checkbox") }}
{% endmacro %}
{% macro checkbox_input(id, name, data=None, value="y") %}
<input
id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"
{% if data %}
checked
{% endif %}
>
{% endmacro %}
{% macro unlabelled_checkbox(id, name, data=None, value="y") %}
<div class="multiple-choice">
{{ checkbox_input(id, name, data, value) }}
<label></label>
</div>
{% endmacro %}
@@ -0,0 +1,4 @@
{%- macro govukCheckboxes(params) %}
{%- include "./template.njk" -%}
{%- endmacro %}
{{ govukCheckboxes(params) }}
@@ -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 -%}
<input class="govuk-checkboxes__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
{{-" checked" if item.checked }}
{{-" disabled" if item.disabled }}
{%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}
{%- if itemDescribedBy %} aria-describedby="{{ itemDescribedBy }}"{% endif -%}
{%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
{{ 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 %}
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
{{ item.conditional.html | safe }}
</div>
{% endif %}
{%- if item.after %}{{ item.after }}{% endif -%}
</{{ groupItemElement }}>
{% if not params.asList and item.conditional %}
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
{{ item.conditional.html | safe }}
</div>
{% endif %}
{% endfor %}
</{{ groupElement }}>
{% endset -%}
<div class="govuk-form-group {%- if params.errorMessage %} govuk-form-group--error{% endif %} {%- if params.formGroup.classes %} {{ params.formGroup.classes }}{% endif %}">
{% if params.fieldset %}
{% call govukFieldset({
describedBy: describedBy,
classes: params.fieldset.classes,
attributes: params.fieldset.attributes,
legend: params.fieldset.legend
}) %}
{{ innerHtml | trim | safe }}
{% endcall %}
{% else %}
{{ innerHtml | trim | safe }}
{% endif %}
</div>
+2 -3
View File
@@ -1,6 +1,5 @@
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import sticky_page_footer %} {% from "components/page-footer.html" import sticky_page_footer %}
{% from "components/checkbox.html" import checkboxes %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
{% from "components/live-search.html" import live_search %} {% from "components/live-search.html" import live_search %}
@@ -17,10 +16,10 @@
back_link=url_for('.choose_broadcast_library', service_id=current_service.id, broadcast_message_id=broadcast_message.id), back_link=url_for('.choose_broadcast_library', service_id=current_service.id, broadcast_message_id=broadcast_message.id),
)}} )}}
{{ live_search(target_selector='.multiple-choice', show=show_search_form, form=search_form, label='Search by name') }} {{ live_search(target_selector='.govuk-checkboxes__item', show=show_search_form, form=search_form, label='Search by name') }}
{% call form_wrapper() %} {% call form_wrapper() %}
{{ checkboxes(form.areas, hide_legend=True) }} {{ form.areas }}
{{ sticky_page_footer('Add to broadcast') }} {{ sticky_page_footer('Add to broadcast') }}
{% endcall %} {% endcall %}
-1
View File
@@ -1,5 +1,4 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
@@ -1,20 +1,9 @@
{% from "components/checkbox.html" import checkbox, checkboxes_nested %}
{% from "components/radios.html" import radio, radios, conditional_radio_panel %} {% from "components/radios.html" import radio, radios, conditional_radio_panel %}
<fieldset class="form-group"> {{ form.permissions_field }}
<legend class="form-label heading-small">
Permissions
</legend>
<span class="hint">
All team members can see sent messages.
</span>
{% for field in form.permissions_fields %}
{{ checkbox(field) }}
{% endfor %}
</fieldset>
{% if form.folder_permissions.all_template_folders %} {% if form.folder_permissions.all_template_folders %}
{{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children(), hide_legend=True, collapsible_opts={ 'field': 'folder' }) }} {{ form.folder_permissions }}
{% elif user and user.platform_admin %} {% elif user and user.platform_admin %}
<p class="bottom-gutter"> <p class="bottom-gutter">
Platform admin users can access all template folders. Platform admin users can access all template folders.
@@ -1,6 +1,5 @@
{% extends "withoutnav_template.html" %} {% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
@@ -1,6 +1,5 @@
{% extends "views/platform-admin/_base_template.html" %} {% extends "views/platform-admin/_base_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/big-number.html" import big_number, big_number_with_status %} {% from "components/big-number.html" import big_number, big_number_with_status %}
{% from "components/message-count-label.html" import message_count_label %} {% from "components/message-count-label.html" import message_count_label %}
@@ -70,8 +69,7 @@
{% call form_wrapper(method="get") %} {% call form_wrapper(method="get") %}
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }} {{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }} {{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
{{ checkbox(form.include_from_test_key) }} {{ form.include_from_test_key }}
</br>
{{ govukButton({ "text": "Filter" }) }} {{ govukButton({ "text": "Filter" }) }}
{% endcall %} {% endcall %}
{% endset %} {% endset %}
@@ -1,6 +1,5 @@
{% from "components/banner.html" import banner, banner_wrapper %} {% from "components/banner.html" import banner, banner_wrapper %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
{% from "components/button/macro.njk" import govukButton %} {% from "components/button/macro.njk" import govukButton %}
@@ -55,9 +54,7 @@
hint='This should be a shared inbox managed by your team, not your own email address' hint='This should be a shared inbox managed by your team, not your own email address'
) }} ) }}
{% if not first_email_address and not existing_is_default %} {% if not first_email_address and not existing_is_default %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{% endif %} {% endif %}
{{ page_footer('Try again') }} {{ page_footer('Try again') }}
{% endcall %} {% endcall %}
@@ -1,6 +1,5 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -24,9 +23,7 @@
safe_error_message=True safe_error_message=True
) }} ) }}
{% if not first_email_address %} {% if not first_email_address %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{% endif %} {% endif %}
{{ page_footer('Add') }} {{ page_footer('Add') }}
{% endcall %} {% endcall %}
@@ -1,7 +1,6 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %} {% from "components/banner.html" import banner_wrapper %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -29,9 +28,7 @@
</p> </p>
{{ page_footer('Save') }} {{ page_footer('Save') }}
{% else %} {% else %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{{ page_footer( {{ page_footer(
'Save', 'Save',
delete_link=url_for('.service_confirm_delete_email_reply_to', service_id=current_service.id, reply_to_email_id=reply_to_email_address_id), delete_link=url_for('.service_confirm_delete_email_reply_to', service_id=current_service.id, reply_to_email_id=reply_to_email_address_id),
@@ -1,6 +1,5 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -1,6 +1,5 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -27,9 +26,7 @@
highlight_placeholders=True highlight_placeholders=True
) }} ) }}
{% if not first_contact_block %} {% if not first_contact_block %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{% endif %} {% endif %}
{{ page_footer('Add') }} {{ page_footer('Add') }}
{% endcall %} {% endcall %}
@@ -1,6 +1,5 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -31,9 +30,7 @@
This is currently your default address for {{ current_service.name }}. This is currently your default address for {{ current_service.name }}.
</p> </p>
{% else %} {% else %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{% endif %} {% endif %}
{{ page_footer( {{ page_footer(
@@ -1,6 +1,5 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -23,9 +22,7 @@
hint='Up to 11 characters, letters, numbers and spaces only' hint='Up to 11 characters, letters, numbers and spaces only'
) }} ) }}
{% if not first_sms_sender %} {% if not first_sms_sender %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{% endif %} {% endif %}
{{ page_footer('Save') }} {{ page_footer('Save') }}
{% endcall %} {% endcall %}
@@ -1,7 +1,6 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %} {% from "components/banner.html" import banner_wrapper %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -35,9 +34,7 @@
</p> </p>
{{ page_footer('Save') }} {{ page_footer('Save') }}
{% else %} {% else %}
<div class="form-group"> {{ form.is_default }}
{{ checkbox(form.is_default) }}
</div>
{% if inbound_number %} {% if inbound_number %}
{{ page_footer('Save') }} {{ page_footer('Save') }}
{% else %} {% else %}
-1
View File
@@ -1,5 +1,4 @@
{% extends "withoutnav_template.html" %} {% extends "withoutnav_template.html" %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import sticky_page_footer %} {% from "components/page-footer.html" import sticky_page_footer %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
@@ -1,13 +1,15 @@
{% from "components/checkbox.html" import unlabelled_checkbox %}
{% from "components/message-count-label.html" import folder_contents_count, message_count_label %} {% from "components/message-count-label.html" import folder_contents_count, message_count_label %}
{% macro format_item_name(name) -%} {% macro format_item_name(name, separators=True) -%}
{%- if name is string -%} {%- if name is string -%}
{{- name -}} {{- name -}}
{%- else -%} {%- else -%}
{%- for part in name -%} {%- for part in name -%}
{{- format_item_name(part) -}} {{- format_item_name(part, separators) -}}
{%- if not loop.last %} <span class="message-name-separator"></span> {% endif -%} {%- if not loop.last -%}
{%- if separators %}
<span class="message-name-separator"></span>{%- else %} {% endif -%}
{% endif -%}
{%- endfor -%} {%- endfor -%}
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
@@ -21,37 +23,69 @@
{% endif %} {% endif %}
</p> </p>
{% else %} {% else %}
<nav id="template-list" class="{{ 'top-gutter-5px' if (not show_template_nav and not show_search_box) else '' }}"> <nav id="template-list" class="{{ 'govuk-!-margin-top-1' if (not show_template_nav and not show_search_box) else 'govuk-!margin-top-6' }}">
{% set checkboxes_data = [] %}
{% for item in template_list %} {% for item in template_list %}
<div class="template-list-item {% if current_user.has_permissions('manage_templates') %}template-list-item-with-checkbox{% endif %} {% if item.ancestors %}template-list-item-hidden-by-default{% endif %} {% if not item.ancestors %}template-list-item-without-ancestors{% endif %}">
{% if current_user.has_permissions('manage_templates') %} {% set item_link_content %}
{{ unlabelled_checkbox( {% for ancestor in item.ancestors %}
id='templates-or-folder-{}'.format(item.id), <a href="{{ url_for('.choose_template', service_id=current_service.id, template_type=template_type, template_folder_id=ancestor.id) }}" class="govuk-link govuk-link--no-visited-state template-list-folder">
name='templates_and_folders', {{- format_item_name(ancestor.name) -}}
data=templates_and_folders_form.is_selected(item.id), </a> <span class="message-name-separator"></span>
value=item.id, {% endfor %}
) }} {% if item.is_folder %}
<a href="{{ url_for('.choose_template', service_id=current_service.id, template_type=template_type, template_folder_id=item.id) }}" class="govuk-link govuk-link--no-visited-state template-list-folder">
<span class="live-search-relevant">{{- format_item_name(item.name) -}}</span>
</a>
{% else %}
<a href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.id) }}" class="govuk-link govuk-link--no-visited-state template-list-template">
<span class="live-search-relevant">{{- format_item_name(item.name) -}}</span>
</a>
{% endif %} {% endif %}
<h2 class="message-name"> {% endset %}
{% for ancestor in item.ancestors %}
<a href="{{ url_for('.choose_template', service_id=current_service.id, template_type=template_type, template_folder_id=ancestor.id) }}" class="govuk-link govuk-link--no-visited-state template-list-folder"> {% set label_content %}
{{- format_item_name(ancestor.name) -}} <span class="govuk-visually-hidden">
</a> <span class="message-name-separator"></span> {%- for ancestor in item.ancestors %}{{ format_item_name(ancestor.name, separators=False) }} {% endfor -%}
{% endfor %} {{ format_item_name(item.name, separators=False) -}}
{% if item.is_folder %} </span>
<a href="{{ url_for('.choose_template', service_id=current_service.id, template_type=template_type, template_folder_id=item.id) }}" class="govuk-link govuk-link--no-visited-state template-list-folder"> {% endset %}
<span class="live-search-relevant">{{ format_item_name(item.name) }}</span>
</a> {% set item_meta %}
{% else %} <span id="{{ item.id }}-item-hint" class="govuk-hint govuk-checkboxes__hint template-list-item-hint">
<a href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.id) }}" class="govuk-link govuk-link--no-visited-state template-list-template">
<span class="live-search-relevant">{{ format_item_name(item.name) }}</span>
</a>
{% endif %}
</h2>
<p class="message-type">
{{ item.hint }} {{ item.hint }}
</p> </span>
</div> {% endset %}
{# create the item config now to include the label content -#}
{# TODO: "attributes": { "aria-describedby": item.id ~ "-hint" } needs to be added but govuk-frontend-jinja doesn't currently support this -#}
{% set checkbox_config = {
"html": label_content,
"label": {
"classes": "template-list-item-label",
},
"id": "templates-or-folder-" ~ item.id,
"classes": "template-list-item template-list-item-with-checkbox {}".format(
"template-list-item-hidden-by-default" if item.ancestors else "template-list-item-without-ancestors"),
"after": item_link_content ~ item_meta
} %}
{% set _ = checkboxes_data.append(checkbox_config) %}
{% if not current_user.has_permissions('manage_templates') %}
<div class="template-list-item {%- if item.ancestors %} template-list-item-hidden-by-default {%- else %} template-list-item-without-ancestors{%- endif %}">
<h2 class="message-name">
{{ item_link_content }}
</h2>
<p class="message-type govuk-!-margin-bottom-4">
{{ item.hint }}
</p>
</div>
{% endif %}
{% endfor %} {% endfor %}
{% if current_user.has_permissions('manage_templates') %}
{{ templates_and_folders_form.templates_and_folders(param_extensions={ "items": checkboxes_data }) }}
{% endif %}
</nav> </nav>
{% endif %} {% endif %}
@@ -1,7 +1,6 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/folder-path.html" import folder_path, page_title_folder_path %} {% from "components/folder-path.html" import folder_path, page_title_folder_path %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox, checkboxes %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
@@ -26,7 +25,7 @@
{% call form_wrapper(action=url_for('main.manage_template_folder', service_id=current_service.id, template_folder_id=template_folder_id)) %} {% call form_wrapper(action=url_for('main.manage_template_folder', service_id=current_service.id, template_folder_id=template_folder_id)) %}
{{ textbox(form.name, width='1-1') }} {{ textbox(form.name, width='1-1') }}
{% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %} {% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %}
{{ checkboxes(form.users_with_permission, collapsible_opts={ 'field': 'team member' }) }} {{ form.users_with_permission }}
{% endif %} {% endif %}
{{ page_footer( {{ page_footer(
-1
View File
@@ -1,6 +1,5 @@
{% extends "withoutnav_template.html" %} {% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
+6 -1
View File
@@ -65,7 +65,12 @@ const copy = {
'footer', 'footer',
'back-link', 'back-link',
'details', 'details',
'button' 'button',
'error-message',
'fieldset',
'hint',
'label',
'checkboxes'
]; ];
let done = 0; let done = 0;
+131 -56
View File
@@ -288,11 +288,15 @@ def test_service_without_caseworking_doesnt_show_admin_vs_caseworker(
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
**extra_args **extra_args
) )
assert page.select('input[type=checkbox]')[0]['name'] == 'view_activity' permission_checkboxes = page.select('input[type=checkbox]')
assert page.select('input[type=checkbox]')[1]['name'] == 'send_messages'
assert page.select('input[type=checkbox]')[2]['name'] == 'manage_templates' for idx in range(len(permission_checkboxes)):
assert page.select('input[type=checkbox]')[3]['name'] == 'manage_service' assert permission_checkboxes[idx]['name'] == 'permissions_field'
assert page.select('input[type=checkbox]')[4]['name'] == 'manage_api_keys' assert permission_checkboxes[0]['value'] == 'view_activity'
assert permission_checkboxes[1]['value'] == 'send_messages'
assert permission_checkboxes[2]['value'] == 'manage_templates'
assert permission_checkboxes[3]['value'] == 'manage_service'
assert permission_checkboxes[4]['value'] == 'manage_api_keys'
@pytest.mark.parametrize('endpoint, extra_args', [ @pytest.mark.parametrize('endpoint, extra_args', [
@@ -320,11 +324,11 @@ def test_broadcast_service_only_shows_relevant_permissions(
**extra_args **extra_args
) )
assert [ assert [
field['name'] for field in page.select('input[type=checkbox]') (field['name'], field['value']) for field in page.select('input[type=checkbox]')
] == [ ] == [
'send_messages', ('permissions_field', 'send_messages'),
'manage_templates', ('permissions_field', 'manage_templates'),
'manage_service', ('permissions_field', 'manage_service'),
] ]
@@ -432,8 +436,9 @@ def test_should_show_page_for_one_user(
assert len(checkboxes) == 5 assert len(checkboxes) == 5
for index, expected in enumerate(expected_checkboxes): for index, expected in enumerate(expected_checkboxes):
expected_input_name, expected_checked = expected expected_input_value, expected_checked = expected
assert checkboxes[index]['name'] == expected_input_name assert checkboxes[index]['name'] == 'permissions_field'
assert checkboxes[index]['value'] == expected_input_value
assert checkboxes[index].has_attr('checked') == expected_checked assert checkboxes[index].has_attr('checked') == expected_checked
@@ -475,11 +480,13 @@ def test_should_not_show_page_for_non_team_member(
@pytest.mark.parametrize('submitted_permissions, permissions_sent_to_api', [ @pytest.mark.parametrize('submitted_permissions, permissions_sent_to_api', [
( (
{ {
'view_activity': 'y', 'permissions_field': [
'send_messages': 'y', 'view_activity',
'manage_templates': 'y', 'send_messages',
'manage_service': 'y', 'manage_templates',
'manage_api_keys': 'y', 'manage_service',
'manage_api_keys',
]
}, },
{ {
'view_activity', 'view_activity',
@@ -491,19 +498,38 @@ def test_should_not_show_page_for_non_team_member(
), ),
( (
{ {
'view_activity': 'y', 'permissions_field': [
'send_messages': 'y', 'view_activity',
'manage_templates': '', 'send_messages',
'manage_templates',
]
}, },
{ {
'view_activity', 'view_activity',
'send_messages', 'send_messages',
'manage_templates',
} }
), ),
( (
{}, {},
set(), set(),
), ),
( # should be able to handle permissions being sent as booleans, until changeover to a list is complete
{
'view_activity': 'y',
'send_messages': 'y',
'manage_templates': 'y',
'manage_service': 'y',
'manage_api_keys': 'y',
},
{
'view_activity',
'send_messages',
'manage_templates',
'manage_service',
'manage_api_keys',
}
),
]) ])
def test_edit_user_permissions( def test_edit_user_permissions(
client_request, client_request,
@@ -542,11 +568,12 @@ def test_edit_user_permissions(
@pytest.mark.parametrize('submitted_permissions, permissions_sent_to_api', [ @pytest.mark.parametrize('submitted_permissions, permissions_sent_to_api', [
( (
{ {
'view_activity': 'y', 'permissions_field': [
'send_messages': 'y', 'send_messages',
'manage_templates': 'y', 'manage_templates',
'manage_service': 'y', 'manage_service',
'manage_api_keys': 'y', 'manage_api_keys',
]
}, },
{ {
'view_activity', 'view_activity',
@@ -557,8 +584,9 @@ def test_edit_user_permissions(
), ),
( (
{ {
'view_activity': 'y', 'permissions_field': [
'send_messages': 'y', 'send_messages',
]
}, },
{ {
'view_activity', 'view_activity',
@@ -572,6 +600,20 @@ def test_edit_user_permissions(
'view_activity', 'view_activity',
} }
), ),
( # should be able to handle permissions being sent as booleans, until changeover to a list is complete
{
'send_messages': 'y',
'manage_templates': 'y',
'manage_service': 'y',
'manage_api_keys': 'y',
},
{
'view_activity',
'send_messages',
'manage_service',
'manage_templates',
}
),
]) ])
def test_edit_user_permissions_for_broadcast_service( def test_edit_user_permissions_for_broadcast_service(
client_request, client_request,
@@ -748,10 +790,12 @@ def test_edit_user_permissions_including_authentication_with_email_auth_service(
user_id=active_user_with_permissions['id'], user_id=active_user_with_permissions['id'],
_data={ _data={
'email_address': active_user_with_permissions['email_address'], 'email_address': active_user_with_permissions['email_address'],
'send_messages': 'y', 'permissions_field': [
'manage_templates': 'y', 'send_messages',
'manage_service': 'y', 'manage_templates',
'manage_api_keys': 'y', 'manage_service',
'manage_api_keys',
],
'login_authentication': auth_type, 'login_authentication': auth_type,
}, },
_expected_status=302, _expected_status=302,
@@ -810,13 +854,14 @@ def test_should_show_folder_permission_form_if_service_has_folder_permissions_en
assert 'Invite a team member' in page.find('h1').text.strip() assert 'Invite a team member' in page.find('h1').text.strip()
folder_checkboxes = page.find('div', class_='checkboxes-nested').find_all('li') folder_checkboxes = page.find('div', class_='selection-wrapper').find_all('li')
assert len(folder_checkboxes) == 3 assert len(folder_checkboxes) == 3
@pytest.mark.parametrize('email_address, gov_user', [ @pytest.mark.parametrize('email_address, gov_user, old_permissions', [
('test@example.gov.uk', True), ('test@example.gov.uk', True, False),
('test@example.com', False) ('test@example.com', False, False),
('test@example.gov.uk', True, True)
]) ])
def test_invite_user( def test_invite_user(
client_request, client_request,
@@ -825,6 +870,7 @@ def test_invite_user(
sample_invite, sample_invite,
email_address, email_address,
gov_user, gov_user,
old_permissions,
mock_get_template_folders, mock_get_template_folders,
mock_get_organisations, mock_get_organisations,
): ):
@@ -834,17 +880,25 @@ def test_invite_user(
mocker.patch('app.models.user.InvitedUsers.client_method', return_value=[sample_invite]) mocker.patch('app.models.user.InvitedUsers.client_method', return_value=[sample_invite])
mocker.patch('app.models.user.Users.client_method', return_value=[active_user_with_permissions]) mocker.patch('app.models.user.Users.client_method', return_value=[active_user_with_permissions])
mocker.patch('app.invite_api_client.create_invite', return_value=sample_invite) mocker.patch('app.invite_api_client.create_invite', return_value=sample_invite)
data = {'email_address': email_address}
if old_permissions:
data['view_activity'] = 'y'
data['send_messages'] = 'y'
data['manage_templates'] = 'y'
data['manage_service'] = 'y'
data['manage_api_keys'] = 'y'
else:
data['permissions_field'] = [
'view_activity',
'send_messages',
'manage_templates',
'manage_service',
'manage_api_keys',
]
page = client_request.post( page = client_request.post(
'main.invite_user', 'main.invite_user',
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
_data={ _data=data,
'email_address': email_address,
'view_activity': 'y',
'send_messages': 'y',
'manage_templates': 'y',
'manage_service': 'y',
'manage_api_keys': 'y',
},
_follow_redirects=True, _follow_redirects=True,
) )
assert page.h1.string.strip() == 'Team members' assert page.h1.string.strip() == 'Team members'
@@ -894,11 +948,13 @@ def test_invite_user_with_email_auth_service(
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
_data={ _data={
'email_address': email_address, 'email_address': email_address,
'view_activity': 'y', 'permissions_field': [
'send_messages': 'y', 'view_activity',
'manage_templates': 'y', 'send_messages',
'manage_service': 'y', 'manage_templates',
'manage_api_keys': 'y', 'manage_service',
'manage_api_keys',
],
'login_authentication': auth_type, 'login_authentication': auth_type,
}, },
_follow_redirects=True, _follow_redirects=True,
@@ -922,9 +978,11 @@ def test_invite_user_with_email_auth_service(
@pytest.mark.parametrize('post_data, expected_permissions_to_api', ( @pytest.mark.parametrize('post_data, expected_permissions_to_api', (
( (
{ {
'send_messages': 'y', 'permissions_field': [
'manage_templates': 'y', 'send_messages',
'manage_service': 'y', 'manage_templates',
'manage_service',
]
}, },
{ {
'view_activity', 'view_activity',
@@ -935,14 +993,29 @@ def test_invite_user_with_email_auth_service(
), ),
( (
{ {
'view_activity': 'y', 'permissions_field': [
'manage_api_keys': 'y', 'view_activity',
'foo': 'y', 'manage_api_keys',
'foo',
]
}, },
{ {
'view_activity', 'view_activity',
}, },
), ),
( # should be able to handle permissions being sent as booleans, until changeover to a list is complete
{
'send_messages': 'y',
'manage_templates': 'y',
'manage_service': 'y',
},
{
'view_activity',
'send_messages',
'manage_templates',
'manage_service',
},
)
)) ))
def test_invite_user_to_broadcast_service( def test_invite_user_to_broadcast_service(
client_request, client_request,
@@ -1084,9 +1157,11 @@ def test_user_cant_invite_themselves(
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
_data={ _data={
'email_address': active_user_with_permissions['email_address'], 'email_address': active_user_with_permissions['email_address'],
'send_messages': 'y', 'permissions_field': [
'manage_service': 'y', 'send_messages',
'manage_api_keys': 'y', 'manage_service',
'manage_api_keys'
]
}, },
_follow_redirects=True, _follow_redirects=True,
_expected_status=200, _expected_status=200,
+160 -93
View File
@@ -54,29 +54,43 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{}, {},
['Email', 'Text message', 'Letter'], ['Email', 'Text message', 'Letter'],
[ [
'folder_one 2 folders', 'folder_one folder_one 2 folders',
'folder_one folder_one_one 1 template, 1 folder', ('folder_one folder_one_one '
'folder_one folder_one_one folder_one_one_one 1 template', 'folder_one folder_one_one '
'folder_one folder_one_one folder_one_one_one sms_template_nested Text message template', '1 template, 1 folder'),
'folder_one folder_one_one letter_template_nested Letter template', ('folder_one folder_one_one folder_one_one_one '
'folder_one folder_one_two Empty', 'folder_one folder_one_one folder_one_one_one '
'folder_two Empty', '1 template'),
'sms_template_one Text message template', ('folder_one folder_one_one folder_one_one_one sms_template_nested '
'sms_template_two Text message template', 'folder_one folder_one_one folder_one_one_one sms_template_nested '
'email_template_one Email template', 'Text message template'),
'email_template_two Email template', ('folder_one folder_one_one letter_template_nested '
'letter_template_one Letter template', 'folder_one folder_one_one letter_template_nested '
'letter_template_two Letter template', 'Letter template'),
('folder_one folder_one_two '
'folder_one folder_one_two '
'Empty'),
'folder_two folder_two Empty',
('sms_template_one '
'sms_template_one '
'Text message template'),
('sms_template_two '
'sms_template_two '
'Text message template'),
'email_template_one email_template_one Email template',
'email_template_two email_template_two Email template',
'letter_template_one letter_template_one Letter template',
'letter_template_two letter_template_two Letter template',
], ],
[ [
'folder_one 2 folders', 'folder_one folder_one 2 folders',
'folder_two Empty', 'folder_two folder_two Empty',
'sms_template_one Text message template', 'sms_template_one sms_template_one Text message template',
'sms_template_two Text message template', 'sms_template_two sms_template_two Text message template',
'email_template_one Email template', 'email_template_one email_template_one Email template',
'email_template_two Email template', 'email_template_two email_template_two Email template',
'letter_template_one Letter template', 'letter_template_one letter_template_one Letter template',
'letter_template_two Letter template', 'letter_template_two letter_template_two Letter template',
], ],
[ [
'folder_one', 'folder_one',
@@ -102,29 +116,39 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{'template_type': 'all'}, {'template_type': 'all'},
['Email', 'Text message', 'Letter'], ['Email', 'Text message', 'Letter'],
[ [
'folder_one 2 folders', 'folder_one folder_one 2 folders',
'folder_one folder_one_one 1 template, 1 folder', ('folder_one folder_one_one '
'folder_one folder_one_one folder_one_one_one 1 template', 'folder_one folder_one_one '
'folder_one folder_one_one folder_one_one_one sms_template_nested Text message template', '1 template, 1 folder'),
'folder_one folder_one_one letter_template_nested Letter template', ('folder_one folder_one_one folder_one_one_one '
'folder_one folder_one_two Empty', 'folder_one folder_one_one folder_one_one_one '
'folder_two Empty', '1 template'),
'sms_template_one Text message template', ('folder_one folder_one_one folder_one_one_one sms_template_nested '
'sms_template_two Text message template', 'folder_one folder_one_one folder_one_one_one sms_template_nested '
'email_template_one Email template', 'Text message template'),
'email_template_two Email template', ('folder_one folder_one_one letter_template_nested '
'letter_template_one Letter template', 'folder_one folder_one_one letter_template_nested '
'letter_template_two Letter template', 'Letter template'),
('folder_one folder_one_two '
'folder_one folder_one_two '
'Empty'),
'folder_two folder_two Empty',
'sms_template_one sms_template_one Text message template',
'sms_template_two sms_template_two Text message template',
'email_template_one email_template_one Email template',
'email_template_two email_template_two Email template',
'letter_template_one letter_template_one Letter template',
'letter_template_two letter_template_two Letter template',
], ],
[ [
'folder_one 2 folders', 'folder_one folder_one 2 folders',
'folder_two Empty', 'folder_two folder_two Empty',
'sms_template_one Text message template', 'sms_template_one sms_template_one Text message template',
'sms_template_two Text message template', 'sms_template_two sms_template_two Text message template',
'email_template_one Email template', 'email_template_one email_template_one Email template',
'email_template_two Email template', 'email_template_two email_template_two Email template',
'letter_template_one Letter template', 'letter_template_one letter_template_one Letter template',
'letter_template_two Letter template', 'letter_template_two letter_template_two Letter template',
], ],
[ [
'folder_one', 'folder_one',
@@ -150,17 +174,23 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{'template_type': 'sms'}, {'template_type': 'sms'},
['All', 'Email', 'Letter'], ['All', 'Email', 'Letter'],
[ [
'folder_one 1 folder', 'folder_one folder_one 1 folder',
'folder_one folder_one_one 1 folder', ('folder_one folder_one_one '
'folder_one folder_one_one folder_one_one_one 1 template', 'folder_one folder_one_one '
'folder_one folder_one_one folder_one_one_one sms_template_nested Text message template', '1 folder'),
'sms_template_one Text message template', ('folder_one folder_one_one folder_one_one_one '
'sms_template_two Text message template', 'folder_one folder_one_one folder_one_one_one '
'1 template'),
('folder_one folder_one_one folder_one_one_one sms_template_nested '
'folder_one folder_one_one folder_one_one_one sms_template_nested '
'Text message template'),
'sms_template_one sms_template_one Text message template',
'sms_template_two sms_template_two Text message template',
], ],
[ [
'folder_one 1 folder', 'folder_one folder_one 1 folder',
'sms_template_one Text message template', 'sms_template_one sms_template_one Text message template',
'sms_template_two Text message template', 'sms_template_two sms_template_two Text message template',
], ],
[ [
'folder_one', 'folder_one',
@@ -179,15 +209,21 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{'template_folder_id': PARENT_FOLDER_ID}, {'template_folder_id': PARENT_FOLDER_ID},
['Email', 'Text message', 'Letter'], ['Email', 'Text message', 'Letter'],
[ [
'folder_one_one 1 template, 1 folder', 'folder_one_one folder_one_one 1 template, 1 folder',
'folder_one_one folder_one_one_one 1 template', ('folder_one_one folder_one_one_one '
'folder_one_one folder_one_one_one sms_template_nested Text message template', 'folder_one_one folder_one_one_one '
'folder_one_one letter_template_nested Letter template', '1 template'),
'folder_one_two Empty', ('folder_one_one folder_one_one_one sms_template_nested '
'folder_one_one folder_one_one_one sms_template_nested '
'Text message template'),
('folder_one_one letter_template_nested '
'folder_one_one letter_template_nested '
'Letter template'),
'folder_one_two folder_one_two Empty',
], ],
[ [
'folder_one_one 1 template, 1 folder', 'folder_one_one folder_one_one 1 template, 1 folder',
'folder_one_two Empty', 'folder_one_two folder_one_two Empty',
], ],
[ [
'folder_one_one', 'folder_one_one',
@@ -205,12 +241,16 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{'template_type': 'sms', 'template_folder_id': PARENT_FOLDER_ID}, {'template_type': 'sms', 'template_folder_id': PARENT_FOLDER_ID},
['All', 'Email', 'Letter'], ['All', 'Email', 'Letter'],
[ [
'folder_one_one 1 folder', 'folder_one_one folder_one_one 1 folder',
'folder_one_one folder_one_one_one 1 template', ('folder_one_one folder_one_one_one '
'folder_one_one folder_one_one_one sms_template_nested Text message template', 'folder_one_one folder_one_one_one '
'1 template'),
('folder_one_one folder_one_one_one sms_template_nested '
'folder_one_one folder_one_one_one sms_template_nested '
'Text message template'),
], ],
[ [
'folder_one_one 1 folder', 'folder_one_one folder_one_one 1 folder',
], ],
[ [
'folder_one_one', 'folder_one_one',
@@ -240,13 +280,15 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{'template_folder_id': CHILD_FOLDER_ID}, {'template_folder_id': CHILD_FOLDER_ID},
['Email', 'Text message', 'Letter'], ['Email', 'Text message', 'Letter'],
[ [
'folder_one_one_one 1 template', 'folder_one_one_one folder_one_one_one 1 template',
'folder_one_one_one sms_template_nested Text message template', ('folder_one_one_one sms_template_nested '
'letter_template_nested Letter template', 'folder_one_one_one sms_template_nested '
'Text message template'),
'letter_template_nested letter_template_nested Letter template',
], ],
[ [
'folder_one_one_one 1 template', 'folder_one_one_one folder_one_one_one 1 template',
'letter_template_nested Letter template', 'letter_template_nested letter_template_nested Letter template',
], ],
[ [
'folder_one_one_one', 'folder_one_one_one',
@@ -266,10 +308,10 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
{'template_folder_id': GRANDCHILD_FOLDER_ID}, {'template_folder_id': GRANDCHILD_FOLDER_ID},
['Email', 'Text message', 'Letter'], ['Email', 'Text message', 'Letter'],
[ [
'sms_template_nested Text message template', 'sms_template_nested sms_template_nested Text message template',
], ],
[ [
'sms_template_nested Text message template', 'sms_template_nested sms_template_nested Text message template',
], ],
[ [
'sms_template_nested', 'sms_template_nested',
@@ -395,7 +437,7 @@ def test_should_show_templates_folder_page(
assert links_in_page[index].text.strip() == expected_link assert links_in_page[index].text.strip() == expected_link
all_page_items = page.select('.template-list-item') all_page_items = page.select('.template-list-item')
all_page_items_styled_with_checkboxes = page.select('.template-list-item-with-checkbox') all_page_items_styled_with_checkboxes = page.select('.template-list-item.govuk-checkboxes__item')
assert len(all_page_items) == len(all_page_items_styled_with_checkboxes) assert len(all_page_items) == len(all_page_items_styled_with_checkboxes)
@@ -527,7 +569,7 @@ def test_get_manage_folder_viewing_permissions_for_users(
assert normalize_spaces(page.select_one('title').text) == ( assert normalize_spaces(page.select_one('title').text) == (
'folder_two Templates service one GOV.UK Notify' 'folder_two Templates service one GOV.UK Notify'
) )
form_labels = page.select('legend[class=form-label]') form_labels = page.select('legend.govuk-fieldset__legend')
assert normalize_spaces(form_labels[0].text) == "Team members who can see this folder" assert normalize_spaces(form_labels[0].text) == "Team members who can see this folder"
checkboxes = page.select('input[name=users_with_permission]') checkboxes = page.select('input[name=users_with_permission]')
@@ -538,7 +580,7 @@ def test_get_manage_folder_viewing_permissions_for_users(
assert checkboxes[1]['value'] == team_member_2['id'] assert checkboxes[1]['value'] == team_member_2['id']
assert "checked" in checkboxes[1].attrs assert "checked" in checkboxes[1].attrs
assert "Test User" in page.find_all('label', {'for': 'users_with_permission-0'})[0].text assert "Test User" in page.find_all('label', {'for': 'users_with_permission-1'})[0].text
def test_get_manage_folder_viewing_permissions_for_users_not_visible_when_no_manage_settings_permission( def test_get_manage_folder_viewing_permissions_for_users_not_visible_when_no_manage_settings_permission(
@@ -1462,45 +1504,70 @@ def test_show_custom_error_message(
( (
{}, {},
[ [
['folder_A', '1 template, 2 folders'], ['folder_A', 'folder_A', '1 template, 2 folders'],
['folder_E folder_F folder_G', '1 template'], ['folder_E folder_F folder_G',
['email_template_root', 'Email template'], 'folder_E', 'folder_F', 'folder_G',
'1 template'],
['email_template_root', 'email_template_root', 'Email template'],
], ],
[ [
['folder_A', '1 template, 2 folders'], ['folder_A', 'folder_A', '1 template, 2 folders'],
['folder_A', 'folder_C', '1 template'], ['folder_A folder_C',
['folder_A', 'folder_C', 'sms_template_C', 'Text message template'], 'folder_A', 'folder_C',
['folder_A', 'folder_D', 'Empty'], '1 template'],
['folder_A', 'sms_template_A', 'Text message template'], ['folder_A folder_C sms_template_C',
['folder_E folder_F folder_G', '1 template'], 'folder_A', 'folder_C', 'sms_template_C',
['folder_E folder_F folder_G', 'email_template_G', 'Email template'], 'Text message template'],
['email_template_root', 'Email template'], ['folder_A folder_D',
'folder_A', 'folder_D',
'Empty'],
['folder_A sms_template_A',
'folder_A', 'sms_template_A',
'Text message template'],
['folder_E folder_F folder_G',
'folder_E', 'folder_F', 'folder_G',
'1 template'],
['folder_E folder_F folder_G email_template_G',
'folder_E', 'folder_F', 'folder_G', 'email_template_G',
'Email template'],
['email_template_root', 'email_template_root', 'Email template'],
], ],
None, None,
), ),
( (
{'template_type': 'email'}, {'template_type': 'email'},
[ [
['folder_E folder_F folder_G', '1 template'], ['folder_E folder_F folder_G',
['email_template_root', 'Email template'], 'folder_E', 'folder_F', 'folder_G',
'1 template'],
['email_template_root', 'email_template_root', 'Email template'],
], ],
[ [
['folder_E folder_F folder_G', '1 template'], ['folder_E folder_F folder_G',
['folder_E folder_F folder_G', 'email_template_G', 'Email template'], 'folder_E', 'folder_F', 'folder_G',
['email_template_root', 'Email template'], '1 template'],
['folder_E folder_F folder_G email_template_G',
'folder_E', 'folder_F', 'folder_G', 'email_template_G',
'Email template'],
['email_template_root', 'email_template_root', 'Email template'],
], ],
None, None,
), ),
( (
{'template_type': 'sms'}, {'template_type': 'sms'},
[ [
['folder_A', '1 template, 1 folder'], ['folder_A', 'folder_A', '1 template, 1 folder'],
], ],
[ [
['folder_A', '1 template, 1 folder'], ['folder_A', 'folder_A', '1 template, 1 folder'],
['folder_A', 'folder_C', '1 template'], ['folder_A folder_C',
['folder_A', 'folder_C', 'sms_template_C', 'Text message template'], 'folder_A', 'folder_C',
['folder_A', 'sms_template_A', 'Text message template'], '1 template'],
['folder_A folder_C sms_template_C',
'folder_A', 'folder_C', 'sms_template_C',
'Text message template'],
['folder_A sms_template_A', 'folder_A', 'sms_template_A',
'Text message template'],
], ],
None, None,
), ),
+4 -4
View File
@@ -188,7 +188,7 @@ def test_should_show_page_for_choosing_a_template(
for index, expected_link in enumerate(expected_nav_links): for index, expected_link in enumerate(expected_nav_links):
assert links_in_page[index].text.strip() == expected_link assert links_in_page[index].text.strip() == expected_link
template_links = page.select('.message-name a') template_links = page.select('#template-list .govuk-label a, .message-name a')
assert len(template_links) == len(expected_templates) assert len(template_links) == len(expected_templates)
@@ -257,7 +257,7 @@ def test_should_show_live_search_if_list_of_templates_taller_than_screen(
assert search['data-module'] == 'live-search' assert search['data-module'] == 'live-search'
assert search['data-targets'] == '#template-list .template-list-item' assert search['data-targets'] == '#template-list .template-list-item'
assert len(page.select(search['data-targets'])) == len(page.select('.message-name')) == 14 assert len(page.select(search['data-targets'])) == len(page.select('#template-list .govuk-label')) == 14
def test_should_show_live_search_if_service_has_lots_of_folders( def test_should_show_live_search_if_service_has_lots_of_folders(
@@ -278,8 +278,8 @@ def test_should_show_live_search_if_service_has_lots_of_folders(
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
) )
count_of_templates_and_folders = len(page.select('.message-name')) count_of_templates_and_folders = len(page.select('#template-list .govuk-label'))
count_of_folders = len(page.select('.template-list-folder:first-child')) count_of_folders = len(page.select('.template-list-folder:first-of-type'))
count_of_templates = count_of_templates_and_folders - count_of_folders count_of_templates = count_of_templates_and_folders - count_of_folders
assert len(page.select('.live-search')) == 1 assert len(page.select('.live-search')) == 1
+106 -58
View File
@@ -14,49 +14,52 @@ afterAll(() => {
describe('Collapsible fieldset', () => { describe('Collapsible fieldset', () => {
const _checkboxes = (start, end) => {
result = '';
for (let num = start; num <= end; num++) {
let id = `folder-permissions-${num}`;
result += `<li class="govuk-checkboxes__item">
<input class="govuk-checkboxes__input" id="${id}" name="folder-permissions" type="checkbox" value="${id}">
<label class="govuk-label govuk-checkboxes__label" for="${id}">
Folder ${id}
</label>
</li>`;
}
return result;
};
let wrapper;
let formGroup;
let fieldset; let fieldset;
let checkboxesContainer;
let checkboxes; let checkboxes;
beforeEach(() => { beforeEach(() => {
const _checkboxes = (start, end, descendents) => {
result = '';
for (let num = start; num <= end; num++) {
let id = `folder-permissions-${num}`;
if (!descendents) { descendents = ''; }
result += `<li class="multiple-choice">
<input id="${id}" name="folder_permissions" type="checkbox" value="${id}">
<label class="block-label" for="{id}">
Folder 18
</label>
${descendents}
</li>`;
}
return result;
};
// set up DOM // set up DOM
document.body.innerHTML = document.body.innerHTML =
`<div class="form-group" data-module="collapsible-checkboxes" data-field-label="folder"> `<div class="selection-wrapper" data-module="collapsible-checkboxes" data-field-label="folder">
<div class="selection-summary"></div> <div class="govuk-form-group">
<fieldset id="folder_permissions"> <fieldset class="govuk-fieldset" id="folder_permissions">
<legend class="form-label heading-small"> <legend class="govuk-fieldset__legend govuk-fieldset__legend--s">
Folders this team member can see Folders this team member can see
</legend> <span class="govuk-hint">
<div class="checkboxes-nested"> <div class="selection-summary" role="region" aria-live="polite"></div>
<ul> </span>
</legend>
<ul class="govuk-checkboxes">
${_checkboxes(1, 10)} ${_checkboxes(1, 10)}
</ul> </ul>
</div> </fieldset>
</fieldset> </div>
</div>`; </div>`;
formGroup = document.querySelector('.form-group'); wrapper = document.querySelector('.selection-wrapper');
formGroup = wrapper.querySelector('.govuk-form-group');
fieldset = formGroup.querySelector('fieldset'); fieldset = formGroup.querySelector('fieldset');
checkboxesContainer = fieldset.querySelector('.checkboxes-nested'); checkboxesContainer = fieldset.querySelector('.govuk-checkboxes');
checkboxes = checkboxesContainer.querySelectorAll('input[type=checkbox]'); checkboxes = checkboxesContainer.querySelectorAll('input[type=checkbox]');
}); });
@@ -141,6 +144,12 @@ describe('Collapsible fieldset', () => {
}); });
test("removes the hint", () => {
expect(document.querySelector('.govuk-hint')).toBeNull();
});
}); });
test('has the right summary text when started with no checkboxes selected', () => { test('has the right summary text when started with no checkboxes selected', () => {
@@ -187,7 +196,7 @@ describe('Collapsible fieldset', () => {
test("the summary doesn't have a folder icon if fields aren't called 'folder'", () => { test("the summary doesn't have a folder icon if fields aren't called 'folder'", () => {
formGroup.dataset.fieldLabel = 'team member'; wrapper.dataset.fieldLabel = 'team member';
// start module // start module
window.GOVUK.modules.start(); window.GOVUK.modules.start();
@@ -277,35 +286,74 @@ describe('Collapsible fieldset', () => {
describe("the footer (that wraps the button)", () => { describe("the footer (that wraps the button)", () => {
beforeEach(() => { describe("is inserted", () => {
// track calls to sticky JS test("after the fieldset", () => {
window.GOVUK.stickAtBottomWhenScrolling.recalculate = jest.fn(() => {});
// start module // start module
window.GOVUK.modules.start(); window.GOVUK.modules.start();
// show the checkboxes // show the checkboxes
helpers.triggerEvent(formGroup.querySelector('.govuk-button'), 'click'); helpers.triggerEvent(formGroup.querySelector('.govuk-button'), 'click');
expect(formGroup.querySelector('.selection-footer').previousElementSibling.nodeName).toBe('FIELDSET');
});
test("after the root fieldset if the checkboxes are nested", () => {
// add a nested list of checkboxes to the first checkbox item
const nestedCheckboxes = document.createElement('div');
nestedCheckboxes.className = 'govuk-form-group govuk-form-group--nested';
nestedCheckboxes.innerHTML = _checkboxes(11, 20);
checkboxesContainer.querySelector('.govuk-checkboxes__item').appendChild(nestedCheckboxes);
// start module
window.GOVUK.modules.start();
// show the checkboxes
helpers.triggerEvent(formGroup.querySelector('.govuk-button'), 'click');
expect(formGroup.querySelector('.selection-footer').previousElementSibling.nodeName).toBe('FIELDSET');
});
}); });
test("is made sticky when the fieldset is expanded", () => { describe("its stickiness", () => {
expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(true); beforeEach(() => {
expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(1);
// track calls to sticky JS
window.GOVUK.stickAtBottomWhenScrolling.recalculate = jest.fn(() => {});
// start module
window.GOVUK.modules.start();
// show the checkboxes
helpers.triggerEvent(formGroup.querySelector('.govuk-button'), 'click');
});
test("is added when the fieldset is expanded", () => {
expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(true);
expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(1);
});
test("is removed when the fieldset is collapsed", () => {
// click the button to collapse the fieldset
helpers.triggerEvent(formGroup.querySelector('.govuk-button'), 'click');
expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(false);
expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(2);
});
}); });
test("has its stickiness removed when the fieldset is collapsed", () => {
// click the button to collapse the fieldset
helpers.triggerEvent(formGroup.querySelector('.govuk-button'), 'click');
expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(false);
expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(2);
});
}); });
describe("when the selection changes", () => { describe("when the selection changes", () => {
@@ -339,7 +387,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'folders'", () => { test("if fields are called 'folders'", () => {
formGroup.dataset.fieldLabel = 'folder'; wrapper.dataset.fieldLabel = 'folder';
checkFirstCheckbox(); checkFirstCheckbox();
@@ -359,7 +407,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'team member'", () => { test("if fields are called 'team member'", () => {
formGroup.dataset.fieldLabel = 'team member'; wrapper.dataset.fieldLabel = 'team member';
checkFirstCheckbox(); checkFirstCheckbox();
@@ -379,7 +427,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'arbitrary thing'", () => { test("if fields are called 'arbitrary thing'", () => {
formGroup.dataset.fieldLabel = 'arbitrary thing'; wrapper.dataset.fieldLabel = 'arbitrary thing';
checkFirstCheckbox(); checkFirstCheckbox();
@@ -403,7 +451,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'folder'", () => { test("if fields are called 'folder'", () => {
formGroup.dataset.fieldLabel = 'folder'; wrapper.dataset.fieldLabel = 'folder';
checkAllCheckboxes(); checkAllCheckboxes();
@@ -423,7 +471,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'team member'", () => { test("if fields are called 'team member'", () => {
formGroup.dataset.fieldLabel = 'team member'; wrapper.dataset.fieldLabel = 'team member';
checkAllCheckboxes(); checkAllCheckboxes();
@@ -447,7 +495,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'folder'", () => { test("if fields are called 'folder'", () => {
formGroup.dataset.fieldLabel = 'folder'; wrapper.dataset.fieldLabel = 'folder';
checkAllCheckboxesButTheLast(); checkAllCheckboxesButTheLast();
@@ -466,7 +514,7 @@ describe('Collapsible fieldset', () => {
test("if fields are called 'team member'", () => { test("if fields are called 'team member'", () => {
formGroup.dataset.fieldLabel = 'team member'; wrapper.dataset.fieldLabel = 'team member';
checkAllCheckboxesButTheLast(); checkAllCheckboxesButTheLast();