From 8f88047090fbfa1acda3599e98043fe7ad8e80dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 14 Sep 2015 15:47:50 +0200 Subject: [PATCH] admin: turn permissions panel into a matrix of roles/accesses (#8239) This also adds "backoffice access" to this panel, so it's no longer required to go into individual roles to set that one. --- tests/test_admin_pages.py | 51 +++++++++++++++++++++++++++++++++ wcs/admin/settings.py | 57 ++++++++++++++++++++++++++++--------- wcs/qommon/form.py | 10 ++++++- wcs/qommon/static/css/dc2/admin.css | 19 +++++++++++++ 4 files changed, 123 insertions(+), 14 deletions(-) diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py index 5644e37..d46a656 100644 --- a/tests/test_admin_pages.py +++ b/tests/test_admin_pages.py @@ -2285,3 +2285,54 @@ def test_data_sources_delete(): assert resp.location == 'http://example.net/backoffice/settings/data-sources/' resp = resp.follow() assert NamedDataSource.count() == 0 + +def test_settings_permissions(): + create_superuser() + role1 = create_role() + role1.name = 'foobar1' + role1.store() + role2 = Role(name='foobar2') + role2.store() + role3 = Role(name='foobar3') + role3.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/settings/admin-permissions') + # assert all first checkboxes are checked + assert resp.forms[0]['permissions$c-0-0'].checked + assert resp.forms[0]['permissions$c-1-0'].checked + assert resp.forms[0]['permissions$c-2-0'].checked + + role2.allows_backoffice_access = False + role2.store() + resp = app.get('/backoffice/settings/admin-permissions') + assert resp.forms[0]['permissions$c-0-0'].checked + assert not resp.forms[0]['permissions$c-1-0'].checked + assert resp.forms[0]['permissions$c-2-0'].checked + + resp.forms[0]['permissions$c-0-0'].checked = False + resp.forms[0]['permissions$c-1-0'].checked = True + resp = resp.forms[0].submit() + assert Role.get(role1.id).allows_backoffice_access is False + assert Role.get(role2.id).allows_backoffice_access is True + + # give some roles access to the forms workshop (2nd checkbox) and to the + # workflows workshop (3rd) + resp = app.get('/backoffice/settings/admin-permissions') + resp.forms[0]['permissions$c-1-1'].checked = True + resp.forms[0]['permissions$c-2-1'].checked = True + resp.forms[0]['permissions$c-2-2'].checked = True + resp = resp.forms[0].submit() + pub.reload_cfg() + assert set(pub.cfg['admin-permissions']['forms']) == set([role2.id, role3.id]) + assert set(pub.cfg['admin-permissions']['workflows']) == set([role3.id]) + + # remove accesses + resp = app.get('/backoffice/settings/admin-permissions') + resp.forms[0]['permissions$c-1-1'].checked = False + resp.forms[0]['permissions$c-2-1'].checked = False + resp.forms[0]['permissions$c-2-2'].checked = False + resp = resp.forms[0].submit() + pub.reload_cfg() + assert pub.cfg['admin-permissions']['forms'] == [] + assert pub.cfg['admin-permissions']['workflows'] == [] diff --git a/wcs/admin/settings.py b/wcs/admin/settings.py index 0c3a79c..44a3336 100644 --- a/wcs/admin/settings.py +++ b/wcs/admin/settings.py @@ -16,6 +16,7 @@ import copy import cStringIO +import hashlib import mimetypes import random import os @@ -503,28 +504,36 @@ class SettingsDirectory(QommonSettingsDirectory): r += htmltext('') return r.getvalue() - def add_roles_widget(self, form, permissions_cfg, key, label): - roles = list(Role.select()) - form.add(WidgetList, key, title=label, element_type=SingleSelectWidget, - value=permissions_cfg.get(key, None), - add_element_label=_('Add Role'), - element_kwargs = { - 'render_br': False, - 'options': [(None, str('---'))] + [(x.id, x.name) for x in roles]}) - def admin_permissions(self): permissions_cfg = get_cfg('admin-permissions', {}) form = Form(enctype='multipart/form-data') - keys = [] + permissions = [_('Backoffice')] + + permission_keys = [] for k, v in get_publisher().get_admin_root().menu_items: if not k.endswith(str('/')): continue k = k.strip(str('/')) if not k: continue - self.add_roles_widget(form, permissions_cfg, k, _(v)) - keys.append(k) + permissions.append(_(v)) + permission_keys.append(k) + + rows = [] + value = [] + roles = list(Role.select(order_by='name')) + for role in roles: + rows.append(role.name) + value.append([role.allows_backoffice_access]) + for k in permission_keys: + authorised_roles = [str(x) for x in permissions_cfg.get(k) or []] + value[-1].append(bool(str(role.id) in authorised_roles)) + colrows_hash = hashlib.md5('%r-%r' % (rows, permissions)).hexdigest() + + form.add_hidden('hash', colrows_hash) + form.add(CheckboxesTableWidget, 'permissions', rows=rows, columns=permissions) + form.get_widget('permissions').set_value(value) form.add_submit('submit', _('Submit')) form.add_submit('cancel', _('Cancel')) @@ -532,15 +541,37 @@ class SettingsDirectory(QommonSettingsDirectory): if form.get_widget('cancel').parse(): return redirect('.') + if form.get_widget('hash').parse() != colrows_hash: + # The columns and rows are made of indices; permissions could be + # wrongly assigned if there were some changes to the columns and + # rows between the form being displayed and submitted. + form.get_widget('permissions').set_error( + _('Changes were made to roles or permissions while the table was displayed.')) + if not form.is_submitted() or form.has_errors(): get_response().breadcrumb.append(('admin-permissions', _('Admin Permissions'))) html_top('settings', title = _('Admin Permissions')) r = TemplateIO(html=True) + r += htmltext('
') r += htmltext('

%s

') % _('Admin Permissions') r += form.render() + r += htmltext('
') return r.getvalue() else: - cfg_submit(form, 'admin-permissions', keys) + value = form.get_widget('permissions').parse() + permissions = {} + for key in permission_keys: + permissions[key] = [] + for i, role in enumerate(roles): + permission_row = value[i] + if role.allows_backoffice_access != permission_row[0]: + role.allows_backoffice_access = permission_row[0] + role.store() + for j, key in enumerate(permission_keys): + if permission_row[j+1]: + permissions[key].append(role.id) + get_publisher().cfg['admin-permissions'] = permissions + get_publisher().write_cfg() return redirect('.') def themes(self): diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index b629f80..41db06c 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -1266,7 +1266,7 @@ class TableWidget(CompositeWidget): r = TemplateIO(html=True) r += htmltext('') for column in self.columns: - r += htmltext('') % column + r += htmltext('') % column r += htmltext('') for i, row in enumerate(self.rows): r += htmltext('') % row @@ -1328,6 +1328,14 @@ class SingleSelectTableWidget(TableWidget): return self.add(SingleSelectWidget, 'c-%s-%s' % (i, j), **widget_kwargs) +class CheckboxesTableWidget(TableWidget): + def add_widget(self, kwargs, i, j): + widget_kwargs = {'options': kwargs.get('options')} + if kwargs.has_key('readonly') and kwargs.get('readonly'): + widget_kwargs['readonly'] = 'readonly' + return self.add(CheckboxWidget, 'c-%s-%s' % (i, j), **widget_kwargs) + + class SingleSelectHintWidget(SingleSelectWidget): def separate_hint(self): diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index 59774df..d4119d3 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -1049,6 +1049,25 @@ div.WidgetDict div.content div.content input { width: calc(100% - 1em); } +div.admin-permissions thead th { + transform: rotate(-45deg); + transform-origin: 10% 0; +} + +div.admin-permissions thead th span { + width: 3em; + display: inline-block; + white-space: nowrap; +} + +div.admin-permissions tbody th { + text-align: left; + padding-right: 1ex; +} + +div.admin-permissions tbody tr:nth-child(even) { + background: #eee; +} @media print { div#sidebar { -- 2.5.1
%s%s
%s