From ddb8b3e52847084b07e0be228861a83e8bc12b72 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Sun, 12 Jul 2020 09:28:12 +0200 Subject: [PATCH] manager: show indirect members of roles real roles (#44927) --- src/authentic2/manager/role_views.py | 6 ++++-- src/authentic2/manager/tables.py | 5 +++++ tests/test_role_manager.py | 29 +++++++++++++++++++++++++--- tests/utils.py | 19 ++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/authentic2/manager/role_views.py b/src/authentic2/manager/role_views.py index 98e05307..22594e1c 100644 --- a/src/authentic2/manager/role_views.py +++ b/src/authentic2/manager/role_views.py @@ -23,7 +23,7 @@ from django.views.generic import FormView, TemplateView from django.views.generic.detail import SingleObjectMixin from django.contrib import messages from django.contrib.contenttypes.models import ContentType -from django.db.models.query import Q +from django.db.models.query import Q, Prefetch from django.db.models import Count from django.contrib.auth import get_user_model @@ -158,7 +158,9 @@ class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTable self._can_manage_members = value def get_table_queryset(self): - return self.object.all_members() + children = self.object.children(include_self=False) + via_prefetch = Prefetch('roles', queryset=children, to_attr='via') + return self.object.all_members().prefetch_related(via_prefetch) def form_valid(self, form): user = form.cleaned_data['user'] diff --git a/src/authentic2/manager/tables.py b/src/authentic2/manager/tables.py index 099acf14..89d71f1a 100644 --- a/src/authentic2/manager/tables.py +++ b/src/authentic2/manager/tables.py @@ -68,6 +68,11 @@ class UserTable(tables.Table): class RoleMembersTable(UserTable): direct = tables.BooleanColumn(verbose_name=_('Direct member'), orderable=False) + via = tables.TemplateColumn( + '{% for role in record.via %}' + '{{ role }}{% if not forloop.last %}, {% endif %}' + '{% endfor %}', + verbose_name=_('Inherited from'), orderable=False) class Meta(UserTable.Meta): pass diff --git a/tests/test_role_manager.py b/tests/test_role_manager.py index c2764602..d11d6eb3 100644 --- a/tests/test_role_manager.py +++ b/tests/test_role_manager.py @@ -13,10 +13,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.encoding import force_bytes -from django.utils.encoding import force_text -from .utils import login +from django.utils.encoding import force_bytes, force_text + +from authentic2.custom_user.models import User +from authentic2.a2_rbac.models import Role + +from .utils import login, text_content def test_manager_role_export(app, admin, ou1, role_ou1, ou2, role_ou2): @@ -88,3 +91,23 @@ def test_manager_role_name_uniqueness_multiple_ou(app, admin, ou1): response.form.set('name', 'Role1') response = response.form.submit('Save') assert response.pyquery('.error').text() == 'Name already used' + + +def test_role_members_via(app, admin): + user1 = User.objects.create(username='user1') + user2 = User.objects.create(username='user2') + role1 = Role.objects.create(name='role1') + role2 = Role.objects.create(name='role2') + + role1.add_child(role2) + user1.roles.add(role1) + user2.roles.add(role2) + + response = login(app, admin, '/manage/roles/%s/' % role1.id) + rows = list(zip([text_content(el) for el in response.pyquery('tr td.username')], + [text_content(el) for el in response.pyquery('tr td.direct')], + [text_content(el) for el in response.pyquery('tr td.via')])) + assert rows == [ + ('user1', '✔', ''), + ('user2', '✘', 'role2'), + ] diff --git a/tests/utils.py b/tests/utils.py index 2c0ccc12..19e6e048 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -215,3 +215,22 @@ def request_select2(app, response, term=''): select2_field_id = response.pyquery('select')[0].attrib['data-field_id'] select2_response = app.get(select2_url, params={'field_id': select2_field_id, 'term': term}) return select2_response.json + + +def text_content(node): + '''Extract text content from node and all its children. Equivalent to + xmlNodeGetContent from libxml.''' + + if node is None: + return '' + + def helper(node): + s = [] + if node.text: + s.append(node.text) + for child in node: + s.extend(helper(child)) + if child.tail: + s.append(child.tail) + return s + return u''.join(helper(node)) -- 2.26.2