From d9c02c944f8b554849d25eba7bd8ffca0fed961a Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 21 Dec 2021 11:47:52 +0100 Subject: [PATCH 2/2] manager: add child roles in role members view (#59664) --- src/authentic2/manager/forms.py | 58 ++++++ src/authentic2/manager/role_views.py | 187 ++++++++++++++---- src/authentic2/manager/tables.py | 2 +- .../authentic2/manager/role_members.html | 2 +- .../manager/role_members_table.html | 2 +- src/authentic2/manager/urls.py | 5 + tests/test_a2_rbac.py | 6 +- tests/test_manager.py | 34 +++- tests/test_role_manager.py | 128 +++++++++++- tests/utils.py | 22 ++- 10 files changed, 382 insertions(+), 64 deletions(-) diff --git a/src/authentic2/manager/forms.py b/src/authentic2/manager/forms.py index 654ac95f..dd671037 100644 --- a/src/authentic2/manager/forms.py +++ b/src/authentic2/manager/forms.py @@ -26,9 +26,11 @@ from django import forms from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError +from django.urls import reverse from django.utils.text import slugify from django.utils.translation import pgettext, ugettext from django.utils.translation import ugettext_lazy as _ +from django_select2.forms import HeavySelect2Widget from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role from authentic2.a2_rbac.utils import generate_slug, get_default_ou @@ -855,3 +857,59 @@ class RolesCsvImportForm(LimitQuerysetFormMixin, forms.Form): def add_line_error(self, error, line): error = _('%(error)s (line %(number)d)') % {'error': error, 'number': line + 1} self.add_error('import_file', error) + + +class HeavySelect2WidgetNoCache(HeavySelect2Widget): + def set_to_cache(self): + pass + + +class ChooseUserOrRoleForm(FormWithRequest, forms.Form): + user_or_role = forms.CharField(label=_('Add to role')) + action = forms.CharField(initial='add', widget=forms.HiddenInput) + + def __init__(self, *args, **kwargs): + self.role = kwargs.pop('role', None) + super().__init__(*args, **kwargs) + self.fields['user_or_role'].widget = HeavySelect2WidgetNoCache( + data_url=reverse('user-or-role-select2-json', kwargs={'pk': self.role.pk}) + ) + + def clean(self): + super().clean() + try: + object_type, pk = self.cleaned_data.get('user_or_role', '').split('-') + except (ValueError, TypeError): + return + + try: + pk = int(pk) + except ValueError: + return + + if object_type == 'user': + try: + self.cleaned_data['user'] = self.get_user_queryset(self.request.user, self.role).get(pk=pk) + except User.DoesNotExist: + return + elif object_type == 'role': + try: + self.cleaned_data['role'] = self.get_role_queryset(self.request.user, self.role).get(pk=pk) + except Role.DoesNotExist: + return + + @staticmethod + def get_role_queryset(user, role): + qs = Role.objects.exclude(pk=role.pk) + + perm = '%s.search_%s' % (Role._meta.app_label, Role._meta.model_name) + return user.filter_by_perm(perm, qs) + + @staticmethod + def get_user_queryset(user, role): + qs = User.objects.all() + if app_settings.ROLE_MEMBERS_FROM_OU and role.ou: + qs = qs.filter(ou=role.ou) + + perm = '%s.search_%s' % (User._meta.app_label, User._meta.model_name) + return user.filter_by_perm(perm, qs) diff --git a/src/authentic2/manager/role_views.py b/src/authentic2/manager/role_views.py index ebefb9bd..cfac61f8 100644 --- a/src/authentic2/manager/role_views.py +++ b/src/authentic2/manager/role_views.py @@ -15,19 +15,23 @@ # along with this program. If not, see . import json +from functools import reduce from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType +from django.core import signing from django.core.exceptions import PermissionDenied, ValidationError +from django.core.paginator import EmptyPage, Paginator from django.db import transaction -from django.db.models import BooleanField, Count, ExpressionWrapper, F, Prefetch, Q +from django.db.models import BooleanField, Count, ExpressionWrapper, F, Prefetch, Q, Value from django.db.models.functions import Cast +from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView, TemplateView +from django.views.generic import DetailView, FormView, TemplateView from django.views.generic.detail import SingleObjectMixin from authentic2 import data_transfer, hooks @@ -37,9 +41,11 @@ from authentic2.apps.journal.views import JournalViewWithContext from authentic2.forms.profile import modelform_factory from authentic2.utils.misc import redirect -from . import app_settings, forms, resources, tables, views +from . import forms, resources, tables, views from .journal_views import BaseJournalView -from .utils import has_show_username +from .utils import get_ou_count, has_show_username, label_from_user + +User = get_user_model() class RolesMixin: @@ -168,7 +174,7 @@ edit = RoleEditView.as_view() class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTableView): template_name = 'authentic2/manager/role_members.html' - form_class = forms.ChooseUserForm + form_class = forms.ChooseUserOrRoleForm success_url = '.' search_form_class = forms.RoleMembersSearchForm permissions = ['a2_rbac.view_role'] @@ -192,13 +198,16 @@ class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTable self._can_manage_members = value def dispatch(self, request, *args, **kwargs): - self.children = views.filter_view(self.request, self.get_object().children(include_self=False)) + self.children = views.filter_view( + self.request, self.get_object().children(include_self=False, annotate=True) + ) return super().dispatch(request, *args, **kwargs) def get_table_data(self): if self.view_all_members: return super().get_table_data() members = views.filter_view(self.request, self.object.members.all()) + members = members.annotate(direct=Value(True, output_field=BooleanField())) members = self.filter_by_search(members) return list(self.children) + list(members) @@ -211,48 +220,62 @@ class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTable return self.search_form.is_valid() and self.search_form.cleaned_data.get('all_members') def form_valid(self, form): - user = form.cleaned_data['user'] action = form.cleaned_data['action'] - if self.can_manage_members: + if not self.can_manage_members: + messages.warning(self.request, _('You are not authorized')) + elif 'user' in form.cleaned_data: if action == 'add': - if self.object.members.filter(pk=user.pk).exists(): - messages.warning(self.request, _('User already in this role.')) - else: - self.object.members.add(user) - hooks.call_hooks( - 'event', - name='manager-add-role-member', - user=self.request.user, - role=self.object, - member=user, - ) - self.request.journal.record( - 'manager.role.membership.grant', role=self.object, member=user - ) + self.add_user(form.cleaned_data['user']) elif action == 'remove': - if not self.object.members.filter(pk=user.pk).exists(): - messages.warning(self.request, _('User was not in this role.')) - else: - self.object.members.remove(user) - hooks.call_hooks( - 'event', - name='manager-remove-role-member', - user=self.request.user, - role=self.object, - member=user, - ) - self.request.journal.record( - 'manager.role.membership.removal', role=self.object, member=user - ) - else: - messages.warning(self.request, _('You are not authorized')) + self.remove_user(form.cleaned_data['user']) + elif 'role' in form.cleaned_data: + if action == 'add': + self.add_role(form.cleaned_data['role']) + elif action == 'remove': + self.remove_role(form.cleaned_data['role']) return super().form_valid(form) + def add_user(self, user): + if self.object.members.filter(pk=user.pk).exists(): + messages.warning(self.request, _('User already in this role.')) + else: + self.object.members.add(user) + hooks.call_hooks( + 'event', name='manager-add-role-member', user=self.request.user, role=self.object, member=user + ) + self.request.journal.record('manager.role.membership.grant', role=self.object, member=user) + + def remove_user(self, user): + if not self.object.members.filter(pk=user.pk).exists(): + messages.warning(self.request, _('User was not in this role.')) + else: + self.object.members.remove(user) + hooks.call_hooks( + 'event', + name='manager-remove-role-member', + user=self.request.user, + role=self.object, + member=user, + ) + self.request.journal.record('manager.role.membership.removal', role=self.object, member=user) + + def add_role(self, role): + self.object.add_child(role) + hooks.call_hooks( + 'event', name='manager-add-child-role', user=self.request.user, parent=self.object, child=role + ) + self.request.journal.record('manager.role.inheritance.addition', parent=self.object, child=role) + + def remove_role(self, role): + self.object.remove_child(role) + hooks.call_hooks( + 'event', name='manager-remove-child-role', user=self.request.user, parent=self.object, child=role + ) + self.request.journal.record('manager.role.inheritance.removal', parent=self.object, child=role) + def get_form_kwargs(self): kwargs = super().get_form_kwargs() - # if role's members can only be from the same OU we filter user based on the role's OU - if app_settings.ROLE_MEMBERS_FROM_OU: - kwargs['ou'] = self.object.ou + kwargs['role'] = self.object return kwargs def get_search_form_kwargs(self): @@ -783,3 +806,85 @@ class RolesJournal(views.SearchOUMixin, views.PermissionMixin, JournalViewWithCo roles_journal = RolesJournal.as_view() + + +class UserOrRoleSelect2View(DetailView): + form_class = forms.ChooseUserOrRoleForm + model = Role + + def get(self, request, *args, **kwargs): + if not self.request.user.is_authenticated or not hasattr(self.request.user, 'filter_by_perm'): + raise Http404('Invalid user') + + role = self.get_object() + + field_id = self.kwargs.get('field_id', self.request.GET.get('field_id', None)) + try: + signing.loads(field_id) + except (signing.SignatureExpired, signing.BadSignature): + raise Http404('Invalid or expired signature.') + + search_term = request.GET.get('term', '') + try: + page_number = int(request.GET.get('page', 1)) + except ValueError: + page_number = 1 + + role_qs = self.form_class.get_role_queryset(self.request.user, role) + children = role.children(annotate=True) + children = children.annotate(is_direct=Cast('direct', output_field=BooleanField())) + role_qs = role_qs.exclude(pk__in=children.filter(is_direct=True)) + role_qs = self.filter_queryset(role_qs, search_term, ['name', 'service__name', 'ou__name']) + + role_paginator = Paginator(role_qs, 10) + try: + role_page = role_paginator.page(page_number) + except EmptyPage: + role_page = [] + else: + has_next = role_page.has_next() + + user_page = [] + if len(role_page) < 10: + user_qs = self.form_class.get_user_queryset(self.request.user, role) + user_qs = user_qs.exclude(roles=role) + user_qs = self.filter_queryset( + user_qs, search_term, ['username', 'first_name', 'last_name', 'email'] + ) + + page_number = page_number - role_paginator.num_pages + 1 + user_paginator = Paginator(user_qs, 10) + try: + user_page = user_paginator.page(page_number) + except EmptyPage: + has_next = False + else: + has_next = user_page.has_next() + + return JsonResponse( + { + "results": [self.get_choice(obj) for obj in list(role_page) + list(user_page)], + "more": has_next, + } + ) + + @staticmethod + def filter_queryset(qs, search_term, search_fields): + lookups = Q() + for term in [term for term in search_term.split() if not term == '']: + lookups &= reduce(Q.__or__, (Q(**{'%s__icontains' % field: term}) for field in search_fields)) + return qs.filter(lookups) + + def get_choice(self, obj): + if isinstance(obj, Role): + text = str(obj) + if obj.ou and get_ou_count() > 1: + text = f'{obj.ou} - {obj}' + key = 'role-%s' + elif isinstance(obj, User): + text = label_from_user(obj) + key = 'user-%s' + return {'id': key % obj.pk, 'text': text} + + +user_or_role_select2 = UserOrRoleSelect2View.as_view() diff --git a/src/authentic2/manager/tables.py b/src/authentic2/manager/tables.py index 1e35ff52..0e919623 100644 --- a/src/authentic2/manager/tables.py +++ b/src/authentic2/manager/tables.py @@ -93,7 +93,7 @@ class RoleMembersTable(UserTable): ) class Meta(UserTable.Meta): - pass + row_attrs = {"data-pk": lambda record: 'user-%s' % record.pk} class UserOrRoleColumn(UserLinkColumn): diff --git a/src/authentic2/manager/templates/authentic2/manager/role_members.html b/src/authentic2/manager/templates/authentic2/manager/role_members.html index a554153c..aa949ae7 100644 --- a/src/authentic2/manager/templates/authentic2/manager/role_members.html +++ b/src/authentic2/manager/templates/authentic2/manager/role_members.html @@ -70,7 +70,7 @@ {% endif %} {% if view.can_manage_members %} -
+ {% csrf_token %} {{ form }} diff --git a/src/authentic2/manager/templates/authentic2/manager/role_members_table.html b/src/authentic2/manager/templates/authentic2/manager/role_members_table.html index b3bb20cd..060975b6 100644 --- a/src/authentic2/manager/templates/authentic2/manager/role_members_table.html +++ b/src/authentic2/manager/templates/authentic2/manager/role_members_table.html @@ -6,5 +6,5 @@ {% endblock %} {% block table.tbody.last.column %} - {% if table.context.view.can_manage_members and row.record.direct %}{% endif %} + {% if table.context.view.can_manage_members and row.record.direct %}{% endif %} {% endblock %} diff --git a/src/authentic2/manager/urls.py b/src/authentic2/manager/urls.py index 7147a9ee..b8b2146b 100644 --- a/src/authentic2/manager/urls.py +++ b/src/authentic2/manager/urls.py @@ -157,6 +157,11 @@ urlpatterns = required( url(r'^roles/(?P\d+)/edit/$', role_views.edit, name='a2-manager-role-edit'), url(r'^roles/(?P\d+)/permissions/$', role_views.permissions, name='a2-manager-role-permissions'), url(r'^roles/(?P\d+)/journal/$', role_views.journal, name='a2-manager-role-journal'), + url( + r'^roles/(?P\d+)/user-or-role-select2.json$', + role_views.user_or_role_select2, + name='user-or-role-select2-json', + ), # Authentic2 organizational units url(r'^organizational-units/$', ou_views.listing, name='a2-manager-ous'), url(r'^organizational-units/add/$', ou_views.add, name='a2-manager-ou-add'), diff --git a/tests/test_a2_rbac.py b/tests/test_a2_rbac.py index fab63dfd..04e2fe6f 100644 --- a/tests/test_a2_rbac.py +++ b/tests/test_a2_rbac.py @@ -402,7 +402,8 @@ def test_admin_role_user_view(db, settings, app, admin, simple_user, ou1, user_o response = response.click('role_ou1') select2_json = request_select2(app, response) assert select2_json['more'] is False - assert {result['id'] for result in select2_json['results']} == {simple_user.id, user_ou1.id, admin.id} + user_ids = {int(x['id'].split('-')[1]) for x in select2_json['results'] if x['id'].startswith('user')} + assert user_ids == {simple_user.id, user_ou1.id, admin.id} # with A2_RBAC_ROLE_ADMIN_RESTRICT_TO_OU_USERS after a reload of the admin # page, we should only see user from the same OU as the role @@ -412,7 +413,8 @@ def test_admin_role_user_view(db, settings, app, admin, simple_user, ou1, user_o response = response.click('role_ou1') select2_json = request_select2(app, response) assert select2_json['more'] is False - assert {result['id'] for result in select2_json['results']} == {user_ou1.id} + user_ids = {int(x['id'].split('-')[1]) for x in select2_json['results'] if x['id'].startswith('user')} + assert user_ids == {user_ou1.id} def test_no_managed_ct(transactional_db, settings): diff --git a/tests/test_manager.py b/tests/test_manager.py index c241af61..85068603 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -207,16 +207,15 @@ def test_role_members_from_ou(app, superuser, simple_user, settings): url = reverse('a2-manager-role-members', kwargs={'pk': r.pk}) response = login(app, superuser, url) - assert not response.context['form'].fields['user'].queryset.query.where - select2_json = request_select2(app, response) - assert len(select2_json['results']) == 2 + select2_json = request_select2(app, response, fetch_all=True) + assert len([x for x in select2_json['results'] if x['id'].startswith('user')]) == 2 settings.A2_MANAGER_ROLE_MEMBERS_FROM_OU = True response = app.get(url) - assert response.context['form'].fields['user'].queryset.query.where - select2_json = request_select2(app, response) - assert len(select2_json['results']) == 1 - assert select2_json['results'][0]['id'] == simple_user.pk + select2_json = request_select2(app, response, fetch_all=True) + user_choices = [x for x in select2_json['results'] if x['id'].startswith('user')] + assert len(user_choices) == 1 + assert user_choices[0]['id'] == 'user-%s' % simple_user.pk def test_manager_create_user(superuser_or_admin, app, settings): @@ -937,8 +936,8 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role): # user can add members response = app.get('/manage/roles/%s/' % simple_role.pk) - form = response.forms['add-user'] - form['user'].force_value(admin.pk) + form = response.forms['add-member'] + form['user_or_role'].force_value('user-%s' % admin.pk) response = form.submit().follow() assert simple_role in admin.roles.all() @@ -946,7 +945,7 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role): q = response.pyquery.remove_namespaces() assert q('table tbody tr td .icon-remove-sign') token = str(response.context['csrf_token']) - params = {'action': 'remove', 'user': admin.pk, 'csrfmiddlewaretoken': token} + params = {'action': 'remove', 'user_or_role': 'user-%s' % admin.pk, 'csrfmiddlewaretoken': token} app.post('/manage/roles/%s/' % simple_role.pk, params=params) assert simple_role not in admin.roles.all() @@ -979,6 +978,21 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role): response = app.post('/manage/roles/%s/parents/' % role.pk, params=params) assert simple_role not in role.parents() + # user can add role as a member through role members form + response = app.get('/manage/roles/%s/' % simple_role.pk) + form = response.forms['add-member'] + form['user_or_role'].force_value('role-%s' % role.pk) + response = form.submit().follow() + assert role in simple_role.children() + + # user can delete role members + q = response.pyquery.remove_namespaces() + assert q('table tbody tr td .icon-remove-sign') + token = str(response.context['csrf_token']) + params = {'action': 'remove', 'user_or_role': 'role-%s' % role.pk, 'csrfmiddlewaretoken': token} + app.post('/manage/roles/%s/' % simple_role.pk, params=params) + assert role not in simple_role.children() + # try to add arbitrary role admin_role = Role.objects.get(slug='_a2-manager') response = app.get('/manage/roles/%s/parents/' % role.pk) diff --git a/tests/test_role_manager.py b/tests/test_role_manager.py index 365887a6..b9734baa 100644 --- a/tests/test_role_manager.py +++ b/tests/test_role_manager.py @@ -24,7 +24,7 @@ from authentic2.a2_rbac.models import OrganizationalUnit, Role from authentic2.a2_rbac.utils import get_default_ou from authentic2.custom_user.models import User -from .utils import login, text_content +from .utils import login, request_select2, text_content def test_manager_role_export(app, admin, ou1, role_ou1, ou2, role_ou2): @@ -475,3 +475,129 @@ def test_role_members_user_role_mixed_table(app, superuser, settings, simple_rol resp = resp.click('Add a role as a member') assert 'Role a' in resp.text + + # add child role to child + grandchild = Role.objects.create(name='grandchild') + role.add_child(grandchild) + + resp = app.get(url) + rows = [text_content(el) for el in resp.pyquery('tr td.name')] + assert rows == ['Members of role Role a', 'Members of role grandchild', 'Jôhn Dôe'] + + # remove icon is not shown for indirect child + assert len(resp.pyquery('tr td a.js-remove-object')) == 2 + + +def test_role_members_user_role_mixed_field_choices( + app, superuser, settings, simple_role, simple_user, role_ou1 +): + url = reverse('a2-manager-role-members', kwargs={'pk': simple_role.pk}) + resp = login(app, superuser, url) + + select2_json = request_select2(app, resp) + assert len(select2_json['results']) == 10 + assert select2_json['more'] is True + + select2_json = request_select2(app, resp, fetch_all=True) + assert len(select2_json['results']) == 17 + choices = [x['text'] for x in select2_json['results']] + assert choices == [ + 'Default organizational unit - Managers of role "simple role"', + 'Default organizational unit - Roles - Default organizational unit', + 'Default organizational unit - Services - Default organizational unit', + 'Default organizational unit - Users - Default organizational unit', + 'OU1 - role_ou1', + 'OU1 - Roles - OU1', + 'OU1 - Services - OU1', + 'OU1 - Users - OU1', + 'Manager', + 'Manager of organizational units', + 'Manager of roles', + 'Manager of services', + 'Manager of users', + 'Managers of "Default organizational unit"', + 'Managers of "OU1"', + 'Jôhn Dôe - user@example.net - user', + 'super user - superuser@example.net - superuser', + ] + + select2_json = request_select2(app, resp, term='user') + choices = [x['text'] for x in select2_json['results']] + assert choices == [ + 'Default organizational unit - Users - Default organizational unit', + 'OU1 - Users - OU1', + 'Manager of users', + 'Jôhn Dôe - user@example.net - user', + 'super user - superuser@example.net - superuser', + ] + assert select2_json['more'] is False + + select2_json = request_select2(app, resp, term='Manager') + assert len(select2_json['results']) == 8 + select2_json = request_select2(app, resp, term='Manager of') + assert len(select2_json['results']) == 7 + select2_json = request_select2(app, resp, term='Manager of serv') + assert len(select2_json['results']) == 1 + + for i in range(25): + Role.objects.create(name=f'test_role_{i}') + select2_json = request_select2(app, resp, term='test_role_', fetch_all=True) + assert len(select2_json['results']) == 25 + + for i in range(25): + User.objects.create(username=f'test_user_{i}') + select2_json = request_select2(app, resp, term='test_user_', fetch_all=True) + assert len(select2_json['results']) == 25 + + +def test_role_members_user_role_add_remove(app, superuser, settings, simple_role, simple_user, role_ou1): + url = reverse('a2-manager-role-members', kwargs={'pk': simple_role.pk}) + resp = login(app, superuser, url) + + select2_json = request_select2(app, resp, term='Jôhn') + assert len(select2_json['results']) == 1 + form = resp.forms['add-member'] + form['user_or_role'].force_value(select2_json['results'][0]['id']) + resp = form.submit().follow() + assert 'Jôhn Dôe' in resp.text + + select2_json = request_select2(app, resp, term='Jôhn') + assert len(select2_json['results']) == 0 + + data_pks = [row.attrib['data-pk'] for row in resp.pyquery('table tbody tr')] + assert data_pks == ['user-%s' % simple_user.pk] + data_pk_args = [row.attrib['data-pk-arg'] for row in resp.pyquery('table tbody tr td a.js-remove-object')] + assert data_pk_args == ['user_or_role'] + + select2_json = request_select2(app, resp, term='role_ou1') + assert len(select2_json['results']) == 1 + form = resp.forms['add-member'] + form['user_or_role'].force_value(select2_json['results'][0]['id']) + resp = form.submit().follow() + assert 'role_ou1' in resp.text + + select2_json = request_select2(app, resp, term='role_ou1') + assert len(select2_json['results']) == 0 + + data_pks = [row.attrib['data-pk'] for row in resp.pyquery('table tbody tr')] + assert data_pks == ['role-%s' % role_ou1.pk, 'user-%s' % simple_user.pk] + data_pk_args = [row.attrib['data-pk-arg'] for row in resp.pyquery('table tbody tr td a.js-remove-object')] + assert data_pk_args == ['user_or_role', 'user_or_role'] + + # simulate click on Jôhn Dôe delete icon + token = str(resp.context['csrf_token']) + params = {'action': 'remove', 'user_or_role': 'user-%s' % simple_user.pk, 'csrfmiddlewaretoken': token} + resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow() + assert 'Jôhn Dôe' not in resp.text + + # simulate click on role_ou1 delete icon + token = str(resp.context['csrf_token']) + params = {'action': 'remove', 'user_or_role': 'role-%s' % role_ou1.pk, 'csrfmiddlewaretoken': token} + resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow() + assert 'role_ou1' not in resp.text + + # invalid choices are ignored + for invalid_choice in ('', 'wrong-wrong', 'user-', 'user-xxx', 'role', 'user-99999'): + form = resp.forms['add-member'] + form['user_or_role'].force_value(invalid_choice) + resp = form.submit().maybe_follow() diff --git a/tests/utils.py b/tests/utils.py index 7d03c348..4ce94753 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -228,17 +228,25 @@ def find_free_tcp_port(): return s.getsockname()[1] -def request_select2(app, response, term='', get_kwargs=None): +def request_select2(app, response, term='', fetch_all=False, page=1, get_kwargs=None): select2_url = response.pyquery('select')[0].attrib['data-ajax--url'] 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}, **(get_kwargs or {}) - ) - if select2_response['content-type'] == 'application/json': - return select2_response.json - else: + + params = {'field_id': select2_field_id, 'term': term} + if page: + params['page'] = page + + select2_response = app.get(select2_url, params=params, **(get_kwargs or {})) + if select2_response['content-type'] != 'application/json': return select2_response + select2_json = select2_response.json + results = select2_json['results'] + if fetch_all and select2_json['more']: + results.extend(request_select2(app, response, term, fetch_all, page + 1, get_kwargs)['results']) + + return select2_json + @contextmanager def run_on_commit_hooks(): -- 2.30.2