diff --git a/client/src/app/core/repositories/users/user-repository.service.ts b/client/src/app/core/repositories/users/user-repository.service.ts index 0bb86f2ad..7d1a3cab8 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -263,6 +263,21 @@ export class UserRepositoryService extends BaseRepository { + await this.httpService.post('/rest/users/user/bulk_alter_groups/', { + user_ids: users.map(user => user.id), + action: action, + group_ids: groupIds + }); + } + /** * Sends invitation emails to all given users. Returns a prepared string to show the user. * This string should always be shown, becuase even in success cases, some users may not get diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.html b/client/src/app/site/users/components/user-detail/user-detail.component.html index 454d04f4e..083d5f368 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.html +++ b/client/src/app/site/users/components/user-detail/user-detail.component.html @@ -290,8 +290,7 @@

Groups

- {{ group.getTitle() | translate }} - + {{ group.getTitle() | translate }}
diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index a8231a56b..8d661db4a 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -302,18 +302,8 @@ export class UserListComponent extends BaseListViewComponent implement const choices = [_('add group(s)'), _('remove group(s)')]; const selectedChoice = await this.choiceService.open(content, this.groupRepo.getViewModelList(), true, choices); if (selectedChoice) { - for (const user of this.selectedRows) { - const newGroups = [...user.groups_id]; - (selectedChoice.items as number[]).forEach(newChoice => { - const idx = newGroups.indexOf(newChoice); - if (idx < 0 && selectedChoice.action === choices[0]) { - newGroups.push(newChoice); - } else if (idx >= 0 && selectedChoice.action === choices[1]) { - newGroups.splice(idx, 1); - } - }); - await this.repo.update({ groups_id: newGroups }, user); - } + const action = selectedChoice.action === choices[0] ? 'add' : 'remove'; + await this.repo.bulkAlterGroups(this.selectedRows, action, selectedChoice.items as number[]); } } diff --git a/openslides/users/views.py b/openslides/users/views.py index 3f8caf3cc..ddd2206ef 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -82,6 +82,7 @@ class UserViewSet(ModelViewSet): "bulk_generate_passwords", "bulk_reset_passwords_to_default", "bulk_set_state", + "bulk_alter_groups", "bulk_delete", "mass_import", "mass_invite_email", @@ -246,6 +247,40 @@ class UserViewSet(ModelViewSet): return Response() + @list_route(methods=["post"]) + def bulk_alter_groups(self, request): + """ + Adds or removes groups from given users. The request user is excluded. + Expected data: + { + user_ids: , + action: "add" | "remove", + group_ids: + } + """ + user_ids = request.data.get("user_ids") + self.assert_list_of_ints(user_ids) + group_ids = request.data.get("group_ids") + self.assert_list_of_ints(group_ids, ids_name="groups_id") + + action = request.data.get("action") + if action not in ("add", "remove"): + raise ValidationError({"detail": "The action must be add or remove"}) + + users = User.objects.exclude(pk=request.user.id).filter(pk__in=user_ids) + groups = list(Group.objects.filter(pk__in=group_ids)) + + for user in users: + if action == "add": + user.groups.add(*groups) + else: + user.groups.remove(*groups) + # Maybe some group assignments have changed. Better delete the restricted user cache + async_to_sync(element_cache.del_user)(user.pk) + + inform_changed_data(users) + return Response() + @list_route(methods=["post"]) def bulk_delete(self, request): """ @@ -357,13 +392,13 @@ class UserViewSet(ModelViewSet): {"count": len(success_users), "no_email_ids": user_pks_without_email} ) - def assert_list_of_ints(self, ids): + def assert_list_of_ints(self, ids, ids_name="user_ids"): """ Asserts, that ids is a list of ints. Raises a ValidationError, if not. """ if not isinstance(ids, list): - raise ValidationError({"detail": "user_ids must be a list"}) + raise ValidationError({"detail": f"{ids_name} must be a list"}) for id in ids: if not isinstance(id, int): - raise ValidationError({"detail": "every id must be a int"}) + raise ValidationError({"detail": "Every id must be a int"}) class GroupViewSetMetadata(SimpleMetadata): diff --git a/tests/integration/users/test_viewset.py b/tests/integration/users/test_viewset.py index c6465c292..ed4a2f5fc 100644 --- a/tests/integration/users/test_viewset.py +++ b/tests/integration/users/test_viewset.py @@ -9,7 +9,12 @@ from openslides.users.models import Group, PersonalNote, User from openslides.utils.autoupdate import inform_changed_data from openslides.utils.test import TestCase -from ...common_groups import GROUP_DEFAULT_PK, GROUP_DELEGATE_PK, GROUP_STAFF_PK +from ...common_groups import ( + GROUP_ADMIN_PK, + GROUP_DEFAULT_PK, + GROUP_DELEGATE_PK, + GROUP_STAFF_PK, +) from ..helpers import count_queries @@ -421,6 +426,80 @@ class UserBulkSetState(TestCase): self.assertTrue(User.objects.get().is_committee) +class UserBulkAlterGroups(TestCase): + """ + Tests altering groups of users. + """ + + def setUp(self): + self.client = APIClient() + self.client.login(username="admin", password="admin") + self.admin = User.objects.get() + self.user = User.objects.create(username="Test name apfj31fa0ovmc8cqc8e8") + + def test_add(self): + self.assertEqual(self.user.groups.count(), 0) + response = self.client.post( + reverse("user-bulk-alter-groups"), + { + "user_ids": [self.user.pk], + "action": "add", + "group_ids": [GROUP_DELEGATE_PK, GROUP_STAFF_PK], + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.user.groups.count(), 2) + self.assertTrue(self.user.groups.filter(pk=GROUP_DELEGATE_PK).exists()) + self.assertTrue(self.user.groups.filter(pk=GROUP_STAFF_PK).exists()) + + def test_remove(self): + groups = Group.objects.filter( + pk__in=[GROUP_DEFAULT_PK, GROUP_DELEGATE_PK, GROUP_STAFF_PK] + ) + self.user.groups.set(groups) + response = self.client.post( + reverse("user-bulk-alter-groups"), + { + "user_ids": [self.user.pk], + "action": "remove", + "group_ids": [GROUP_DEFAULT_PK, GROUP_STAFF_PK], + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.user.groups.count(), 1) + self.assertTrue(self.user.groups.filter(pk=GROUP_DELEGATE_PK).exists()) + + def test_no_request_user(self): + self.assertEqual(self.admin.groups.count(), 1) + self.assertEqual(self.admin.groups.get().pk, GROUP_ADMIN_PK) + response = self.client.post( + reverse("user-bulk-alter-groups"), + { + "user_ids": [self.admin.pk], + "action": "add", + "group_ids": [GROUP_DELEGATE_PK], + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.admin.groups.count(), 1) + self.assertEqual(self.admin.groups.get().pk, GROUP_ADMIN_PK) + + def test_invalid_action(self): + response = self.client.post( + reverse("user-bulk-alter-groups"), + { + "user_ids": [self.admin.pk], + "action": "invalid", + "group_ids": [GROUP_DELEGATE_PK], + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + class UserMassImport(TestCase): """ Tests mass import of users.