Merge pull request #4868 from FinnStutzenstein/userBulkViews

Group add/remove bulk view
This commit is contained in:
Sean 2019-07-25 11:27:28 +02:00 committed by GitHub
commit ec853e5aba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 18 deletions

View File

@ -263,6 +263,21 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
}); });
} }
/**
* Alters groups of all given users. Either adds or removes the given groups.
*
* @param users Affected users
* @param action add or remove the groups
* @param groupIds All group ids to add or remove
*/
public async bulkAlterGroups(users: ViewUser[], action: 'add' | 'remove', groupIds: number[]): Promise<void> {
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. * 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 * This string should always be shown, becuase even in success cases, some users may not get

View File

@ -290,8 +290,7 @@
<div *ngIf="user.groups && user.groups.length > 0"> <div *ngIf="user.groups && user.groups.length > 0">
<h4 translate>Groups</h4> <h4 translate>Groups</h4>
<span *ngFor="let group of user.groups; let last = last"> <span *ngFor="let group of user.groups; let last = last">
{{ group.getTitle() | translate }} {{ group.getTitle() | translate }}<span *ngIf="!last">,&nbsp;</span>
<span *ngIf="!last">,&nbsp;</span>
</span> </span>
</div> </div>

View File

@ -302,18 +302,8 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
const choices = [_('add group(s)'), _('remove group(s)')]; const choices = [_('add group(s)'), _('remove group(s)')];
const selectedChoice = await this.choiceService.open(content, this.groupRepo.getViewModelList(), true, choices); const selectedChoice = await this.choiceService.open(content, this.groupRepo.getViewModelList(), true, choices);
if (selectedChoice) { if (selectedChoice) {
for (const user of this.selectedRows) { const action = selectedChoice.action === choices[0] ? 'add' : 'remove';
const newGroups = [...user.groups_id]; await this.repo.bulkAlterGroups(this.selectedRows, action, selectedChoice.items as number[]);
(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);
}
} }
} }

View File

@ -82,6 +82,7 @@ class UserViewSet(ModelViewSet):
"bulk_generate_passwords", "bulk_generate_passwords",
"bulk_reset_passwords_to_default", "bulk_reset_passwords_to_default",
"bulk_set_state", "bulk_set_state",
"bulk_alter_groups",
"bulk_delete", "bulk_delete",
"mass_import", "mass_import",
"mass_invite_email", "mass_invite_email",
@ -246,6 +247,40 @@ class UserViewSet(ModelViewSet):
return Response() 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: <list of ids>,
action: "add" | "remove",
group_ids: <list of 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"]) @list_route(methods=["post"])
def bulk_delete(self, request): def bulk_delete(self, request):
""" """
@ -357,13 +392,13 @@ class UserViewSet(ModelViewSet):
{"count": len(success_users), "no_email_ids": user_pks_without_email} {"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. """ """ Asserts, that ids is a list of ints. Raises a ValidationError, if not. """
if not isinstance(ids, list): 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: for id in ids:
if not isinstance(id, int): 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): class GroupViewSetMetadata(SimpleMetadata):

View File

@ -9,7 +9,12 @@ from openslides.users.models import Group, PersonalNote, User
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.test import TestCase 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 from ..helpers import count_queries
@ -421,6 +426,80 @@ class UserBulkSetState(TestCase):
self.assertTrue(User.objects.get().is_committee) 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): class UserMassImport(TestCase):
""" """
Tests mass import of users. Tests mass import of users.