mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-24 00:07:02 -04:00
Merge pull request #2819 from alphagov/edit-folder-permissions
Edit user folder permissions
This commit is contained in:
44
app/assets/stylesheets/components/checkboxes.scss
Normal file
44
app/assets/stylesheets/components/checkboxes.scss
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
.checkboxes-nested {
|
||||||
|
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.multiple-choice {
|
||||||
|
|
||||||
|
$circle-diameter: 39px;
|
||||||
|
$border-thickness: 4px;
|
||||||
|
$border-indent: ($circle-diameter / 2) - ($border-thickness / 2);
|
||||||
|
$border-colour: $border-colour;
|
||||||
|
|
||||||
|
float: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: $border-indent;
|
||||||
|
width: $border-thickness;
|
||||||
|
height: 100%;
|
||||||
|
background: $border-colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=checkbox]+label::before {
|
||||||
|
// To overlap the grey inset line
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
// To equalise the spacing between the line and the top/bottom of
|
||||||
|
// the radio
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ $path: '/static/images/';
|
|||||||
@import 'components/api-key';
|
@import 'components/api-key';
|
||||||
@import 'components/vendor/previous-next-navigation';
|
@import 'components/vendor/previous-next-navigation';
|
||||||
@import 'components/radios';
|
@import 'components/radios';
|
||||||
|
@import 'components/checkboxes';
|
||||||
@import 'components/pill';
|
@import 'components/pill';
|
||||||
@import 'components/secondary-button';
|
@import 'components/secondary-button';
|
||||||
@import 'components/show-more';
|
@import 'components/show-more';
|
||||||
|
|||||||
@@ -255,6 +255,78 @@ def organisation_type():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldWithNoneOption():
|
||||||
|
|
||||||
|
# This is a special value that is specific to our forms. This is
|
||||||
|
# more expicit than casting `None` to a string `'None'` which can
|
||||||
|
# have unexpected edge cases
|
||||||
|
NONE_OPTION_VALUE = '__NONE__'
|
||||||
|
|
||||||
|
# When receiving Python data, eg when instantiating the form object
|
||||||
|
# we want to convert that data to our special value, so that it gets
|
||||||
|
# recognised as being one of the valid choices
|
||||||
|
def process_data(self, value):
|
||||||
|
self.data = self.NONE_OPTION_VALUE if value is None else value
|
||||||
|
|
||||||
|
# After validation we want to convert it back to a Python `None` for
|
||||||
|
# use elsewhere, eg posting to the API
|
||||||
|
def post_validate(self, form, validation_stopped):
|
||||||
|
if self.data == self.NONE_OPTION_VALUE and not validation_stopped:
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
|
||||||
|
class RadioFieldWithNoneOption(FieldWithNoneOption, RadioField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NestedFieldMixin:
|
||||||
|
def children(self):
|
||||||
|
# start map with root option as a single child entry
|
||||||
|
child_map = {None: [option for option in self
|
||||||
|
if option.data == self.NONE_OPTION_VALUE]}
|
||||||
|
|
||||||
|
# add entries for all other children
|
||||||
|
for option in self:
|
||||||
|
if option.data == self.NONE_OPTION_VALUE:
|
||||||
|
child_ids = [
|
||||||
|
folder['id'] for folder in self.all_template_folders
|
||||||
|
if folder['parent_id'] is None]
|
||||||
|
key = self.NONE_OPTION_VALUE
|
||||||
|
else:
|
||||||
|
child_ids = [
|
||||||
|
folder['id'] for folder in self.all_template_folders
|
||||||
|
if folder['parent_id'] == option.data]
|
||||||
|
key = option.data
|
||||||
|
|
||||||
|
child_map[key] = [option for option in self if option.data in child_ids]
|
||||||
|
|
||||||
|
return child_map
|
||||||
|
|
||||||
|
|
||||||
|
class NestedRadioField(RadioFieldWithNoneOption, NestedFieldMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NestedCheckboxesField(SelectMultipleField, NestedFieldMixin):
|
||||||
|
NONE_OPTION_VALUE = None
|
||||||
|
|
||||||
|
|
||||||
|
class HiddenFieldWithNoneOption(FieldWithNoneOption, HiddenField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RadioFieldWithRequiredMessage(RadioField):
|
||||||
|
def __init__(self, *args, required_message='Not a valid choice', **kwargs):
|
||||||
|
self.required_message = required_message
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def pre_validate(self, form):
|
||||||
|
try:
|
||||||
|
return super().pre_validate(form)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(self.required_message)
|
||||||
|
|
||||||
|
|
||||||
class StripWhitespaceForm(Form):
|
class StripWhitespaceForm(Form):
|
||||||
class Meta:
|
class Meta:
|
||||||
def bind_field(self, form, unbound_field, options):
|
def bind_field(self, form, unbound_field, options):
|
||||||
@@ -346,6 +418,15 @@ PermissionsAbstract = type("PermissionsAbstract", (StripWhitespaceForm,), {
|
|||||||
|
|
||||||
|
|
||||||
class PermissionsForm(PermissionsAbstract):
|
class PermissionsForm(PermissionsAbstract):
|
||||||
|
def __init__(self, all_template_folders=None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if all_template_folders is not None:
|
||||||
|
self.folder_permissions.all_template_folders = all_template_folders
|
||||||
|
self.folder_permissions.choices = [
|
||||||
|
(item['id'], item['name']) for item in ([{'name': 'Templates', 'id': None}] + all_template_folders)
|
||||||
|
]
|
||||||
|
|
||||||
|
folder_permissions = NestedCheckboxesField('Folders this team member can see')
|
||||||
|
|
||||||
login_authentication = RadioField(
|
login_authentication = RadioField(
|
||||||
'Sign in using',
|
'Sign in using',
|
||||||
@@ -365,8 +446,9 @@ class PermissionsForm(PermissionsAbstract):
|
|||||||
return (getattr(self, permission) for permission, _ in permissions)
|
return (getattr(self, permission) for permission, _ in permissions)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_user(cls, user, service_id):
|
def from_user(cls, user, service_id, **kwargs):
|
||||||
return cls(
|
return cls(
|
||||||
|
**kwargs,
|
||||||
**{
|
**{
|
||||||
role: user.has_permission_for_service(service_id, role)
|
role: user.has_permission_for_service(service_id, role)
|
||||||
for role in roles.keys()
|
for role in roles.keys()
|
||||||
@@ -813,73 +895,6 @@ class ServiceSwitchChannelForm(ServiceOnOffSettingForm):
|
|||||||
super().__init__(name, *args, **kwargs)
|
super().__init__(name, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class FieldWithNoneOption():
|
|
||||||
|
|
||||||
# This is a special value that is specific to our forms. This is
|
|
||||||
# more expicit than casting `None` to a string `'None'` which can
|
|
||||||
# have unexpected edge cases
|
|
||||||
NONE_OPTION_VALUE = '__NONE__'
|
|
||||||
|
|
||||||
# When receiving Python data, eg when instantiating the form object
|
|
||||||
# we want to convert that data to our special value, so that it gets
|
|
||||||
# recognised as being one of the valid choices
|
|
||||||
def process_data(self, value):
|
|
||||||
self.data = self.NONE_OPTION_VALUE if value is None else value
|
|
||||||
|
|
||||||
# After validation we want to convert it back to a Python `None` for
|
|
||||||
# use elsewhere, eg posting to the API
|
|
||||||
def post_validate(self, form, validation_stopped):
|
|
||||||
if self.data == self.NONE_OPTION_VALUE and not validation_stopped:
|
|
||||||
self.data = None
|
|
||||||
|
|
||||||
|
|
||||||
class RadioFieldWithNoneOption(FieldWithNoneOption, RadioField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NestedRadioField(RadioFieldWithNoneOption):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def children(self):
|
|
||||||
# start map with root option as a single child entry
|
|
||||||
child_map = {None: [option for option in self
|
|
||||||
if option.data == self.NONE_OPTION_VALUE]}
|
|
||||||
|
|
||||||
# add entries for all other children
|
|
||||||
for option in self:
|
|
||||||
if option.data == self.NONE_OPTION_VALUE:
|
|
||||||
child_ids = [
|
|
||||||
folder['id'] for folder in self.all_template_folders
|
|
||||||
if folder['parent_id'] is None]
|
|
||||||
key = self.NONE_OPTION_VALUE
|
|
||||||
else:
|
|
||||||
child_ids = [
|
|
||||||
folder['id'] for folder in self.all_template_folders
|
|
||||||
if folder['parent_id'] == option.data]
|
|
||||||
key = option.data
|
|
||||||
|
|
||||||
child_map[key] = [option for option in self if option.data in child_ids]
|
|
||||||
|
|
||||||
return child_map
|
|
||||||
|
|
||||||
|
|
||||||
class HiddenFieldWithNoneOption(FieldWithNoneOption, HiddenField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RadioFieldWithRequiredMessage(RadioField):
|
|
||||||
def __init__(self, *args, required_message='Not a valid choice', **kwargs):
|
|
||||||
self.required_message = required_message
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def pre_validate(self, form):
|
|
||||||
try:
|
|
||||||
return super().pre_validate(form)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(self.required_message)
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceSetEmailBranding(StripWhitespaceForm):
|
class ServiceSetEmailBranding(StripWhitespaceForm):
|
||||||
|
|
||||||
branding_style = RadioFieldWithNoneOption(
|
branding_style = RadioFieldWithNoneOption(
|
||||||
|
|||||||
@@ -84,12 +84,24 @@ def edit_user_permissions(service_id, user_id):
|
|||||||
if user.mobile_number:
|
if user.mobile_number:
|
||||||
mobile_number = redact_mobile_number(user.mobile_number, " ")
|
mobile_number = redact_mobile_number(user.mobile_number, " ")
|
||||||
|
|
||||||
form = PermissionsForm.from_user(user, service_id)
|
form = PermissionsForm.from_user(
|
||||||
|
user,
|
||||||
|
service_id,
|
||||||
|
folder_permissions=[
|
||||||
|
f['id'] for f in current_service.all_template_folders
|
||||||
|
if user_id in f.get('users_with_permission', [])
|
||||||
|
],
|
||||||
|
all_template_folders=current_service.all_template_folders
|
||||||
|
)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user_api_client.set_user_permissions(
|
user_api_client.set_user_permissions(
|
||||||
user_id, service_id,
|
user_id, service_id,
|
||||||
permissions=form.permissions,
|
permissions=form.permissions,
|
||||||
|
folder_permissions=(
|
||||||
|
form.folder_permissions.data
|
||||||
|
if current_service.has_permission('edit_folder_permissions') else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if service_has_email_auth:
|
if service_has_email_auth:
|
||||||
user_api_client.update_user_attribute(user_id, auth_type=form.login_authentication.data)
|
user_api_client.update_user_attribute(user_id, auth_type=form.login_authentication.data)
|
||||||
|
|||||||
@@ -161,13 +161,17 @@ class UserApiClient(NotifyAdminAPIClient):
|
|||||||
resp = self.post('/organisations/{}/users/{}'.format(org_id, user_id), data={})
|
resp = self.post('/organisations/{}/users/{}'.format(org_id, user_id), data={})
|
||||||
return User(resp['data'], max_failed_login_count=self.max_failed_login_count)
|
return User(resp['data'], max_failed_login_count=self.max_failed_login_count)
|
||||||
|
|
||||||
|
@cache.delete('service-{service_id}-template-folders')
|
||||||
@cache.delete('user-{user_id}')
|
@cache.delete('user-{user_id}')
|
||||||
def set_user_permissions(self, user_id, service_id, permissions):
|
def set_user_permissions(self, user_id, service_id, permissions, folder_permissions=None):
|
||||||
# permissions passed in are the combined admin roles, not db permissions
|
# permissions passed in are the combined admin roles, not db permissions
|
||||||
data = {
|
data = {
|
||||||
'permissions': [{'permission': x} for x in translate_permissions_from_admin_roles_to_db(permissions)]
|
'permissions': [{'permission': x} for x in translate_permissions_from_admin_roles_to_db(permissions)],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if folder_permissions is not None:
|
||||||
|
data['folder_permissions'] = folder_permissions
|
||||||
|
|
||||||
endpoint = '/user/{}/service/{}/permission'.format(user_id, service_id)
|
endpoint = '/user/{}/service/{}/permission'.format(user_id, service_id)
|
||||||
self.post(endpoint, data=data)
|
self.post(endpoint, data=data)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
{% from "components/select-input.html" import select_nested %}
|
||||||
|
|
||||||
{% macro checkbox(
|
{% macro checkbox(
|
||||||
field,
|
field,
|
||||||
hint=False,
|
hint=False,
|
||||||
@@ -17,6 +19,11 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
||||||
|
{{ select_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, input="checkbox") }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro checkbox_input(id, name, data=None, value="y") %}
|
{% macro checkbox_input(id, name, data=None, value="y") %}
|
||||||
<input
|
<input
|
||||||
id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"
|
id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"
|
||||||
|
|||||||
@@ -1,111 +1,22 @@
|
|||||||
{% macro radios(
|
{% from "components/select-input.html" import select, select_list, select_nested, select_wrapper, select_input %}
|
||||||
field,
|
|
||||||
hint=None,
|
{% macro radios(field, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
||||||
disable=[],
|
{{ select(field, hint, disable, option_hints, hide_legend, input="radio") }}
|
||||||
option_hints={},
|
|
||||||
hide_legend=False
|
|
||||||
) %}
|
|
||||||
{% call radios_wrapper(
|
|
||||||
field, hint, disable, option_hints, hide_legend
|
|
||||||
) %}
|
|
||||||
{% for option in field %}
|
|
||||||
{{ radio(option, disable, option_hints) }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endcall %}
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro radio_list(
|
{% macro radio_list(options, child_map, disable=[], option_hints={}) %}
|
||||||
options,
|
{{ select_list(options, child_map, disable, option_hints, input="radio") }}
|
||||||
child_map,
|
|
||||||
disable=[],
|
|
||||||
option_hints={}
|
|
||||||
) %}
|
|
||||||
<ul>
|
|
||||||
{% for option in options %}
|
|
||||||
{% if child_map[option.data] %}
|
|
||||||
{% call radio(option, disable, option_hints, as_list_item=True) %}
|
|
||||||
{{ radio_list(child_map[option.data], child_map, disable, option_hints) }}
|
|
||||||
{% endcall %}
|
|
||||||
{% else %}
|
|
||||||
{{ radio(option, disable, option_hints, as_list_item=True) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro radios_nested(
|
{% macro radios_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
||||||
field,
|
{{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, input="radio") }}
|
||||||
child_map,
|
|
||||||
hint=None,
|
|
||||||
disable=[],
|
|
||||||
option_hints={},
|
|
||||||
hide_legend=False
|
|
||||||
) %}
|
|
||||||
{% call radios_wrapper(
|
|
||||||
field, hint, disable, option_hints, hide_legend
|
|
||||||
) %}
|
|
||||||
<div class="radios-nested">
|
|
||||||
{{ radio_list(child_map[None], child_map, disable, option_hints) }}
|
|
||||||
</div>
|
|
||||||
{% endcall %}
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro radios_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
|
||||||
<div class="form-group {% if field.errors %} form-group-error{% endif %}">
|
|
||||||
<fieldset>
|
|
||||||
<legend class="{{ 'form-label' if not hide_legend else '' }}">
|
|
||||||
{% if hide_legend %}<span class="visually-hidden">{% endif %}
|
|
||||||
{{ field.label.text|safe }}
|
|
||||||
{% if hide_legend %}</span>{% endif %}
|
|
||||||
{% if hint %}
|
|
||||||
<span class="form-hint">
|
|
||||||
{{ hint }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if field.errors %}
|
|
||||||
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
|
||||||
{{ field.errors[0] }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</legend>
|
|
||||||
{{ caller() }}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro radio(option, disable=[], option_hints={}, data_target=None, as_list_item=False) %}
|
{% macro radio(option, disable=[], option_hints={}, data_target=None, as_list_item=False) %}
|
||||||
{% if as_list_item %}
|
{{ select_input(option, disable, option_hints, data_target, as_list_item, input="radio") }}
|
||||||
<li class="multiple-choice" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
|
|
||||||
{% else %}
|
|
||||||
<div class="multiple-choice" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
|
|
||||||
{% endif %}
|
|
||||||
<input
|
|
||||||
id="{{ option.id }}" name="{{ option.name }}" type="radio" value="{{ option.data }}"
|
|
||||||
{% if option.data in disable %}
|
|
||||||
disabled
|
|
||||||
{% endif %}
|
|
||||||
{% if option.checked %}
|
|
||||||
checked
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
<label class="block-label" for="{{ option.id }}">
|
|
||||||
{{ option.label.text }}
|
|
||||||
{% if option_hints[option.data] %}
|
|
||||||
<div class="block-label-hint">
|
|
||||||
{{ option_hints[option.data] }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</label>
|
|
||||||
{% if caller %}
|
|
||||||
{{ caller() }}
|
|
||||||
{% endif %}
|
|
||||||
{% if as_list_item %}
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
92
app/templates/components/select-input.html
Normal file
92
app/templates/components/select-input.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{% macro select(field, hint=None, disable=[], option_hints={}, hide_legend=False, input="radio") %}
|
||||||
|
{% call select_wrapper(
|
||||||
|
field, hint, disable, option_hints, hide_legend
|
||||||
|
) %}
|
||||||
|
{% for option in field %}
|
||||||
|
{{ select_input(option, disable, option_hints, input=input) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro select_list(options, child_map, disable=[], option_hints={}, input="radio") %}
|
||||||
|
<ul>
|
||||||
|
{% for option in options %}
|
||||||
|
{% if child_map[option.data] %}
|
||||||
|
{% call select_input(option, disable, option_hints, as_list_item=True, input=input) %}
|
||||||
|
{{ select_list(child_map[option.data], child_map, disable, option_hints, input=input) }}
|
||||||
|
{% endcall %}
|
||||||
|
{% else %}
|
||||||
|
{{ select_input(option, disable, option_hints, as_list_item=True, input=input) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro select_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, input="radio") %}
|
||||||
|
{% call select_wrapper(
|
||||||
|
field, hint, disable, option_hints, hide_legend
|
||||||
|
) %}
|
||||||
|
<div class="{{ "radios" if input == "radio" else "checkboxes" }}-nested">
|
||||||
|
{{ select_list(child_map[None], child_map, disable, option_hints, input=input) }}
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro select_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
||||||
|
<div class="form-group {% if field.errors %} form-group-error{% endif %}">
|
||||||
|
<fieldset>
|
||||||
|
<legend class="{{ 'form-label' if not hide_legend else '' }}">
|
||||||
|
{% if hide_legend %}<span class="visually-hidden">{% endif %}
|
||||||
|
{{ field.label.text|safe }}
|
||||||
|
{% if hide_legend %}</span>{% endif %}
|
||||||
|
{% if hint %}
|
||||||
|
<span class="form-hint">
|
||||||
|
{{ hint }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||||
|
{{ field.errors[0] }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</legend>
|
||||||
|
{{ caller() }}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro select_input(option, disable=[], option_hints={}, data_target=None, as_list_item=False, input="radio") %}
|
||||||
|
{% if as_list_item %}
|
||||||
|
<li class="multiple-choice" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
|
||||||
|
{% else %}
|
||||||
|
<div class="multiple-choice" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
|
||||||
|
{% endif %}
|
||||||
|
<input
|
||||||
|
id="{{ option.id }}" name="{{ option.name }}" type="{{ input }}" value="{{ option.data }}"
|
||||||
|
{% if option.data in disable %}
|
||||||
|
disabled
|
||||||
|
{% endif %}
|
||||||
|
{% if option.checked %}
|
||||||
|
checked
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
<label class="block-label" for="{{ option.id }}">
|
||||||
|
{{ option.label.text }}
|
||||||
|
{% if option_hints[option.data] %}
|
||||||
|
<div class="block-label-hint">
|
||||||
|
{{ option_hints[option.data] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
{% if caller %}
|
||||||
|
{{ caller() }}
|
||||||
|
{% endif %}
|
||||||
|
{% if as_list_item %}
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% from "components/checkbox.html" import checkbox %}
|
{% from "components/checkbox.html" import checkbox, checkboxes_nested %}
|
||||||
{% from "components/radios.html" import radio, radios, radios_wrapper, conditional_radio_panel %}
|
{% from "components/radios.html" import radio, radios, conditional_radio_panel %}
|
||||||
|
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<legend class="form-label">
|
<legend class="form-label">
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
All team members can see sent messages.
|
All team members can see sent messages.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{% if current_service.has_permission("edit_folder_permissions") and form.folder_permissions.all_template_folders %}
|
||||||
|
{{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children()) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if service_has_email_auth %}
|
{% if service_has_email_auth %}
|
||||||
{% if not mobile_number %}
|
{% if not mobile_number %}
|
||||||
{{ radios(
|
{{ radios(
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{% extends "withnav_template.html" %}
|
{% extends "withnav_template.html" %}
|
||||||
{% from "components/textbox.html" import textbox %}
|
{% from "components/textbox.html" import textbox %}
|
||||||
{% from "components/page-footer.html" import page_footer %}
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
{% from "components/radios.html" import radio, radios_wrapper %}
|
{% from "components/radios.html" import radio %}
|
||||||
|
{% from "components/select-input.html" import select_wrapper %}
|
||||||
{% from "components/form.html" import form_wrapper %}
|
{% from "components/form.html" import form_wrapper %}
|
||||||
|
|
||||||
{% block service_page_title %}
|
{% block service_page_title %}
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{% call form_wrapper() %}
|
{% call form_wrapper() %}
|
||||||
|
|
||||||
{% call radios_wrapper(form.contact_details_type, hide_legend=true) %}
|
{% call select_wrapper(form.contact_details_type, hide_legend=true) %}
|
||||||
{% for option in form.contact_details_type %}
|
{% for option in form.contact_details_type %}
|
||||||
{% set data_target = option.data.replace('_', '-') ~ "-type" %}
|
{% set data_target = option.data.replace('_', '-') ~ "-type" %}
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ def test_service_with_no_email_auth_hides_auth_type_options(
|
|||||||
auth_options_hidden,
|
auth_options_hidden,
|
||||||
service_one,
|
service_one,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
|
mock_get_template_folders
|
||||||
):
|
):
|
||||||
if service_has_email_auth:
|
if service_has_email_auth:
|
||||||
service_one['permissions'].append('email_auth')
|
service_one['permissions'].append('email_auth')
|
||||||
@@ -249,6 +250,7 @@ def test_service_with_no_email_auth_hides_auth_type_options(
|
|||||||
def test_service_without_caseworking_doesnt_show_admin_vs_caseworker(
|
def test_service_without_caseworking_doesnt_show_admin_vs_caseworker(
|
||||||
client_request,
|
client_request,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
|
mock_get_template_folders,
|
||||||
endpoint,
|
endpoint,
|
||||||
service_has_caseworking,
|
service_has_caseworking,
|
||||||
extra_args,
|
extra_args,
|
||||||
@@ -304,6 +306,7 @@ def test_manage_users_page_shows_member_auth_type_if_service_has_email_auth_acti
|
|||||||
def test_user_with_no_mobile_number_cant_be_set_to_sms_auth(
|
def test_user_with_no_mobile_number_cant_be_set_to_sms_auth(
|
||||||
client_request,
|
client_request,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
|
mock_get_template_folders,
|
||||||
user,
|
user,
|
||||||
sms_option_disabled,
|
sms_option_disabled,
|
||||||
expected_label,
|
expected_label,
|
||||||
@@ -353,6 +356,7 @@ def test_user_with_no_mobile_number_cant_be_set_to_sms_auth(
|
|||||||
def test_should_show_page_for_one_user(
|
def test_should_show_page_for_one_user(
|
||||||
client_request,
|
client_request,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
|
mock_get_template_folders,
|
||||||
endpoint,
|
endpoint,
|
||||||
extra_args,
|
extra_args,
|
||||||
expected_checkboxes,
|
expected_checkboxes,
|
||||||
@@ -419,6 +423,7 @@ def test_edit_user_permissions(
|
|||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
mock_get_invites_for_service,
|
mock_get_invites_for_service,
|
||||||
mock_set_user_permissions,
|
mock_set_user_permissions,
|
||||||
|
mock_get_template_folders,
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
submitted_permissions,
|
submitted_permissions,
|
||||||
permissions_sent_to_api,
|
permissions_sent_to_api,
|
||||||
@@ -442,6 +447,45 @@ def test_edit_user_permissions(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
SERVICE_ONE_ID,
|
SERVICE_ONE_ID,
|
||||||
permissions=permissions_sent_to_api,
|
permissions=permissions_sent_to_api,
|
||||||
|
folder_permissions=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_user_folder_permissions(
|
||||||
|
client_request,
|
||||||
|
mocker,
|
||||||
|
service_one,
|
||||||
|
mock_get_users_by_service,
|
||||||
|
mock_get_invites_for_service,
|
||||||
|
mock_set_user_permissions,
|
||||||
|
mock_get_template_folders,
|
||||||
|
fake_uuid,
|
||||||
|
):
|
||||||
|
service_one['permissions'] = ['edit_folder_permissions']
|
||||||
|
mock_get_template_folders.return_value = [
|
||||||
|
{'id': 'folder-id-1', 'name': 'folder_one', 'parent_id': None, 'users_with_permission': []},
|
||||||
|
{'id': 'folder-id-2', 'name': 'folder_one', 'parent_id': None, 'users_with_permission': []},
|
||||||
|
{'id': 'folder-id-3', 'name': 'folder_one', 'parent_id': 'folder-id-1', 'users_with_permission': []},
|
||||||
|
]
|
||||||
|
client_request.post(
|
||||||
|
'main.edit_user_permissions',
|
||||||
|
service_id=SERVICE_ONE_ID,
|
||||||
|
user_id=fake_uuid,
|
||||||
|
_data=dict(
|
||||||
|
folder_permissions=['folder-id-1', 'folder-id-3']
|
||||||
|
),
|
||||||
|
_expected_status=302,
|
||||||
|
_expected_redirect=url_for(
|
||||||
|
'main.manage_users',
|
||||||
|
service_id=SERVICE_ONE_ID,
|
||||||
|
_external=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mock_set_user_permissions.assert_called_with(
|
||||||
|
fake_uuid,
|
||||||
|
SERVICE_ONE_ID,
|
||||||
|
permissions=set(),
|
||||||
|
folder_permissions=['folder-id-1', 'folder-id-3']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -474,7 +518,8 @@ def test_edit_user_permissions_including_authentication_with_email_auth_service(
|
|||||||
mock_set_user_permissions,
|
mock_set_user_permissions,
|
||||||
mock_update_user_attribute,
|
mock_update_user_attribute,
|
||||||
service_one,
|
service_one,
|
||||||
auth_type
|
auth_type,
|
||||||
|
mock_get_template_folders
|
||||||
):
|
):
|
||||||
service_one['permissions'].append('email_auth')
|
service_one['permissions'].append('email_auth')
|
||||||
|
|
||||||
@@ -502,7 +547,8 @@ def test_edit_user_permissions_including_authentication_with_email_auth_service(
|
|||||||
'manage_templates',
|
'manage_templates',
|
||||||
'manage_service',
|
'manage_service',
|
||||||
'manage_api_keys',
|
'manage_api_keys',
|
||||||
}
|
},
|
||||||
|
folder_permissions=None
|
||||||
)
|
)
|
||||||
mock_update_user_attribute.assert_called_with(
|
mock_update_user_attribute.assert_called_with(
|
||||||
str(active_user_with_permissions.id),
|
str(active_user_with_permissions.id),
|
||||||
@@ -1015,6 +1061,7 @@ def test_edit_user_permissions_page_displays_redacted_mobile_number_and_change_l
|
|||||||
client_request,
|
client_request,
|
||||||
active_user_with_permissions,
|
active_user_with_permissions,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
|
mock_get_template_folders,
|
||||||
service_one,
|
service_one,
|
||||||
mocker
|
mocker
|
||||||
):
|
):
|
||||||
|
|||||||
Reference in New Issue
Block a user