Skip to content

Commit

Permalink
Improve Usability of Permission Dropdown
Browse files Browse the repository at this point in the history
* Add a select all and deselect all
* use a multi-checkbox field for BS4 implementation with fallback to MultiSelect

Co-authored-by: Nick Zaccardi <mail@nickzaccardi.com>
  • Loading branch information
Nick Zaccardi and nZac committed Feb 22, 2020
2 parents 00ea957 + 23bebb7 commit 479e985
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 10 deletions.
69 changes: 64 additions & 5 deletions keg_auth/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import flask
from keg_elements.forms import Form, ModelForm, FieldMeta
from keg_elements.forms import Form, ModelForm, FieldMeta, MultiCheckboxField
from keg_elements.forms.validators import ValidateUnique
from sqlalchemy.sql.functions import coalesce
from sqlalchemy_utils import EmailType
from webhelpers2.html.tags import link_to
from wtforms.fields import (
Expand Down Expand Up @@ -51,7 +52,10 @@ class SetPassword(Form):

def get_permission_options():
perm_cls = flask.current_app.auth_manager.entity_registry.permission_cls
return [(str(perm.id), perm.description) for perm in perm_cls.query.order_by('description')]
query = perm_cls.query.with_entities(
perm_cls.id, coalesce(perm_cls.description, perm_cls.token).label('desc')
).order_by('desc').all()
return [(str(perm.id), perm.desc) for perm in query]


def get_bundle_options():
Expand Down Expand Up @@ -85,7 +89,18 @@ def get_selected_groups(self):


class PermissionsMixin(object):
permission_ids = SelectMultipleField('Permissions')
select_deselect_all = MultiCheckboxField(
'Bulk Permission Action (takes effect after submit)',
choices=(
('select_all', 'Select All'),
('deselect_all', 'Deselect All')
),
render_kw={'class': 'list-unstyled', 'style': 'margin-bottom:0;'},
)
permission_ids = MultiCheckboxField(
'Permissions',
render_kw={'class': 'list-unstyled'},
)

def after_init(self, args, kwargs):
self.permission_ids.choices = get_permission_options()
Expand All @@ -94,8 +109,13 @@ def after_init(self, args, kwargs):
super().after_init(args, kwargs)

def get_selected_permissions(self):
selected_ids = self.permission_ids.data
if 'select_all' in self.select_deselect_all.data:
selected_ids = [choice[0] for choice in self.permission_ids.choices]
elif 'deselect_all' in self.select_deselect_all.data:
selected_ids = []
return entities_from_ids(flask.current_app.auth_manager.entity_registry.permission_cls,
self.permission_ids.data)
selected_ids)


class BundlesMixin(object):
Expand Down Expand Up @@ -150,7 +170,8 @@ class FieldsMeta:
is_superuser = FieldMeta('Superuser')
__default__ = FieldMeta

field_order = tuple(_fields + ['group_ids', 'bundle_ids', 'permission_ids'])
field_order = tuple(_fields + ['group_ids', 'bundle_ids', 'select_deselect_all',
'permission_ids'])

setattr(FieldsMeta, username_key, FieldMeta(
extra_validators=[validators.data_required(),
Expand All @@ -176,6 +197,18 @@ def get_object_by_field(self, field):
def obj(self):
return self._obj

def validate(self):
if not ModelForm.validate(self):
return False

if 'select_all' in self.select_deselect_all.data and 'deselect_all' in self.select_deselect_all.data: # noqa
error_list = list(self.select_deselect_all.errors)
error_list.append('You may not select all and deselect all. Please choose one.')
self.select_deselect_all.errors = tuple(error_list)
return False

return True

def __iter__(self):
order = ('csrf_token', ) + self.field_order
return (getattr(self, field_id) for field_id in order)
Expand All @@ -191,6 +224,8 @@ def html_link(obj):
return link_to(obj.name, flask.url_for(endpoint, objid=obj.id))

class Group(PermissionsMixin, BundlesMixin, ModelForm):
_field_order = ('name', 'bundle_ids', 'select_deselect_all', 'permission_ids',)

class Meta:
model = group_cls

Expand All @@ -204,6 +239,18 @@ def get_object_by_field(self, field):
def obj(self):
return self._obj

def validate(self):
if not ModelForm.validate(self):
return False

if 'select_all' in self.select_deselect_all.data and 'deselect_all' in self.select_deselect_all.data: # noqa
error_list = list(self.select_deselect_all.errors)
error_list.append('You may not select all and deselect all. Please choose one.')
self.select_deselect_all.errors = tuple(error_list)
return False

return True

return Group


Expand All @@ -228,4 +275,16 @@ def get_object_by_field(self, field):
def obj(self):
return self._obj

def validate(self):
if not ModelForm.validate(self):
return False

if 'select_all' in self.select_deselect_all.data and 'deselect_all' in self.select_deselect_all.data: # noqa
error_list = list(self.select_deselect_all.errors)
error_list.append('You may not select all and deselect all. Please choose one.')
self.select_deselect_all.errors = tuple(error_list)
return False

return True

return Bundle
171 changes: 167 additions & 4 deletions keg_auth/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Using unicode_literals instead of adding 'u' prefix to all stings that go to SA.
#s Using unicode_literals instead of adding 'u' prefix to all stings that go to SA.
from __future__ import unicode_literals
import flask
import freezegun
Expand Down Expand Up @@ -556,6 +556,53 @@ def test_add(self):
assert user.groups == [group_approve]
assert user.bundles == [bundle_approve]

def test_add_select_all_permissions(self):
for i in range(3):
ents.Permission.testing_create()

resp = self.client.get('/users/add')

resp.form['email'] = 'select_all@test.com'
resp.form['select_deselect_all'] = ['select_all']

resp = resp.form.submit()
# import pdb; pdb.set_trace()
assert resp.status_code == 302
assert resp.location.endswith('/users')
assert resp.flashes == [('success', 'Successfully created User')]

user = ents.User.get_by(email='select_all@test.com')
assert user.permissions == ents.Permission.query.all()

def test_add_deselect_all_permissions(self):
p1 = ents.Permission.testing_create()
p2 = ents.Permission.testing_create()

resp = self.client.get('/users/add')

resp.form['email'] = 'deselect_all@test.com'
resp.form['select_deselect_all'] = ['deselect_all']
resp.form['permission_ids'] = [p1.id, p2.id]

resp = resp.form.submit()
assert resp.status_code == 302
assert resp.location.endswith('/users')
assert resp.flashes == [('success', 'Successfully created User')]

user = ents.User.get_by(email='deselect_all@test.com')
assert user.permissions == []

def test_select_and_deselect_throws_error(self):
resp = self.client.get('/users/add')

resp.form['email'] = 'select_and_deselect_all@test.com'
resp.form['select_deselect_all'] = ['select_all', 'deselect_all']

resp = resp.form.submit()
assert resp.flashes == [('error', 'Form errors detected. Please see below for details.')]
assert resp.pyquery('#select_deselect_all').siblings('.help-block').text() ==\
'You may not select all and deselect all. Please choose one.'

def test_resend_verification(self):
self.current_user.is_verified = True
self.current_user.permissions = ents.Permission.query.filter_by(token='auth-manage').all()
Expand Down Expand Up @@ -719,7 +766,15 @@ def test_edit(self):
assert resp.form['email'].value == user_edit.email
assert resp.form['group_ids'].value == [str(obj.id) for obj in user_edit.groups]
assert resp.form['bundle_ids'].value == [str(obj.id) for obj in user_edit.bundles]
assert resp.form['permission_ids'].value == [str(obj.id) for obj in user_edit.permissions]
all_permissions = [p.description or p.token for p in ents.Permission.query.all()]
user_permissions = [p.description or p.token for p in user_edit.permissions]
listed_permissions = resp.pyquery('#permission_ids')('option')
assert len(listed_permissions) == len(all_permissions)
for permission_list_item in listed_permissions:
assert permission_list_item.text in all_permissions
if permission_list_item.text in user_permissions:
assert 'selected' in permission_list_item.attrib
assert resp.pyquery('#select_deselect_all')
resp.form['email'] = 'foo@bar.baz'
resp = resp.form.submit()

Expand Down Expand Up @@ -906,14 +961,68 @@ def test_add(self):
assert group.permissions == [perm_approve]
assert group.bundles == [bundle_approve]

def test_add_select_all_permissions(self):
for i in range(3):
ents.Permission.testing_create()

resp = self.client.get('/groups/add')

resp.form['name'] = 'test adding a group with select all permissions'
resp.form['select_deselect_all'] = ['select_all']

resp = resp.form.submit()
assert resp.status_code == 302
assert resp.location.endswith('/groups')
assert resp.flashes == [('success', 'Successfully created Group')]

group = ents.Group.get_by(name='test adding a group with select all permissions')
assert group.permissions == ents.Permission.query.all()

def test_add_deselect_all_permissions(self):
p1 = ents.Permission.testing_create()
p2 = ents.Permission.testing_create()

resp = self.client.get('/groups/add')

resp.form['name'] = 'test adding a group with deselect all permissions'
resp.form['select_deselect_all'] = ['deselect_all']
resp.form['permission_ids'] = [p1.id, p2.id]

resp = resp.form.submit()
assert resp.status_code == 302
assert resp.location.endswith('/groups')
assert resp.flashes == [('success', 'Successfully created Group')]

group = ents.Group.get_by(name='test adding a group with deselect all permissions')
assert group.permissions == []

def test_select_and_deselect_throws_error(self):
resp = self.client.get('/groups/add')

resp.form['name'] = 'test adding a group with select and deselect all permissions'
resp.form['select_deselect_all'] = ['select_all', 'deselect_all']

resp = resp.form.submit()
assert resp.flashes == [('error', 'Form errors detected. Please see below for details.')]
assert resp.pyquery('#select_deselect_all').siblings('.help-block').text() ==\
'You may not select all and deselect all. Please choose one.'

def test_edit(self):
group_edit = ents.Group.testing_create(bundles=[ents.Bundle.testing_create()],
permissions=[ents.Permission.testing_create()])

resp = self.client.get('/groups/{}/edit'.format(group_edit.id))
assert resp.form['name'].value == group_edit.name
assert resp.form['bundle_ids'].value == [str(obj.id) for obj in group_edit.bundles]
assert resp.form['permission_ids'].value == [str(obj.id) for obj in group_edit.permissions]
all_permissions = [p.description or p.token for p in ents.Permission.query.all()]
user_permissions = [p.description or p.token for p in group_edit.permissions]
listed_permissions = resp.pyquery('#permission_ids')('option')
assert len(listed_permissions) == len(all_permissions)
for permission_list_item in listed_permissions:
assert permission_list_item.text in all_permissions
if permission_list_item.text in user_permissions:
assert 'selected' in permission_list_item.attrib
assert resp.pyquery('#select_deselect_all')
resp.form['name'] = 'test editing a group'
resp = resp.form.submit()

Expand Down Expand Up @@ -1028,12 +1137,66 @@ def test_add(self):
bundle = ents.Bundle.get_by(name='test adding a bundle')
assert bundle.permissions == [perm_approve]

def test_add_select_all_permissions(self):
for i in range(3):
ents.Permission.testing_create()

resp = self.client.get('/bundles/add')

resp.form['name'] = 'test adding a bundle with select all permissions'
resp.form['select_deselect_all'] = ['select_all']

resp = resp.form.submit()
assert resp.status_code == 302
assert resp.location.endswith('/bundles')
assert resp.flashes == [('success', 'Successfully created Bundle')]

bundle = ents.Bundle.get_by(name='test adding a bundle with select all permissions')
assert bundle.permissions == ents.Permission.query.all()

def test_add_deselect_all_permissions(self):
p1 = ents.Permission.testing_create()
p2 = ents.Permission.testing_create()

resp = self.client.get('/bundles/add')

resp.form['name'] = 'test adding a bundle with deselect all permissions'
resp.form['select_deselect_all'] = ['deselect_all']
resp.form['permission_ids'] = [p1.id, p2.id]

resp = resp.form.submit()
assert resp.status_code == 302
assert resp.location.endswith('/bundles')
assert resp.flashes == [('success', 'Successfully created Bundle')]

bundle = ents.Bundle.get_by(name='test adding a bundle with deselect all permissions')
assert bundle.permissions == []

def test_select_and_deselect_throws_error(self):
resp = self.client.get('/bundles/add')

resp.form['name'] = 'test adding a bundle with select and deselect all permissions'
resp.form['select_deselect_all'] = ['select_all', 'deselect_all']

resp = resp.form.submit()
assert resp.flashes == [('error', 'Form errors detected. Please see below for details.')]
assert resp.pyquery('#select_deselect_all').siblings('.help-block').text() ==\
'You may not select all and deselect all. Please choose one.'

def test_edit(self):
bundle_edit = ents.Bundle.testing_create(permissions=[ents.Permission.testing_create()])

resp = self.client.get('/bundles/{}/edit'.format(bundle_edit.id))
assert resp.form['name'].value == bundle_edit.name
assert resp.form['permission_ids'].value == [str(obj.id) for obj in bundle_edit.permissions]
all_permissions = [p.description or p.token for p in ents.Permission.query.all()]
user_permissions = [p.description or p.token for p in bundle_edit.permissions]
listed_permissions = resp.pyquery('#permission_ids')('option')
assert len(listed_permissions) == len(all_permissions)
for permission_list_item in listed_permissions:
assert permission_list_item.text in all_permissions
if permission_list_item.text in user_permissions:
assert 'selected' in permission_list_item.attrib
assert resp.pyquery('#select_deselect_all')
resp.form['name'] = 'test editing a bundle'
resp = resp.form.submit()

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

'Flask-Login==0.4.1',
'Keg>=0.8.1',
'KegElements',
'KegElements>=0.5.21',
'inflect',
'passlib',
'shortuuid',
Expand Down

0 comments on commit 479e985

Please sign in to comment.