From 1e177917d00e499d3a81158b367bdaacd11b9e9b Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Wed, 17 Jul 2019 11:24:01 +0200 Subject: [PATCH] Bulk views for users: state, password and delete --- .../users/user-repository.service.ts | 124 +++++++---- .../user-detail/user-detail.component.ts | 2 +- .../user-list/user-list.component.html | 12 +- .../user-list/user-list.component.ts | 98 ++++----- openslides/users/models.py | 9 - openslides/users/serializers.py | 2 +- openslides/users/views.py | 129 ++++++++++-- tests/integration/users/test_viewset.py | 193 +++++++++++++++--- tests/unit/users/test_models.py | 20 +- 9 files changed, 428 insertions(+), 161 deletions(-) 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 6a6021e20..0bb86f2ad 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -118,6 +118,21 @@ export class UserRepositoryService extends BaseRepository[]): Promise { - const data = newEntries.map(entry => { - return { ...entry.newEntry.user, importTrackId: entry.importTrackId }; - }); - const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as { - detail: string; - importedTrackIds: number[]; - }; - return response.importedTrackIds; - } - - /** - * Generates a random password - * - * @param length THe length of the password to generate - * @returns a random password - */ - public getRandomPassword(length: number = 8): string { - let pw = ''; - const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; - for (let i = 0; i < length; i++) { - pw += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return pw; - } - /** * Updates the password and sets the password without checking for the old one. * Also resets the 'default password' to the newly created one. @@ -193,13 +177,9 @@ export class UserRepositoryService extends BaseRepository { + public async resetPassword(user: ViewUser, password: string): Promise { const path = `/rest/users/user/${user.id}/reset_password/`; - await this.httpService.post(path, { password: password, update_default_password: updateDefaultPassword }); + await this.httpService.post(path, { password: password }); } /** @@ -215,6 +195,74 @@ export class UserRepositoryService extends BaseRepository { + await this.httpService.post('/rest/users/user/bulk_reset_passwords_to_default/', { + user_ids: users.map(user => user.id) + }); + } + + /** + * Generates new random passwords for many users. The default password will be set to these. The + * operator will not be changed (if provided in `users`). + * + * @param users The users to generate new passwords for + */ + public async bulkGenerateNewPasswords(users: ViewUser[]): Promise { + await this.httpService.post('/rest/users/user/bulk_generate_passwords/', { + user_ids: users.map(user => user.id) + }); + } + + /** + * Creates and saves a list of users in a bulk operation. + * + * @param newEntries + */ + public async bulkCreate(newEntries: NewEntry[]): Promise { + const data = newEntries.map(entry => { + return { ...entry.newEntry.user, importTrackId: entry.importTrackId }; + }); + const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as { + detail: string; + importedTrackIds: number[]; + }; + return response.importedTrackIds; + } + + /** + * Deletes many users. The operator will not be deleted (if included in `uisers`) + * + * @param users The users to delete + */ + public async bulkDelete(users: ViewUser[]): Promise { + await this.httpService.post('/rest/users/user/bulk_delete/', { user_ids: users.map(user => user.id) }); + } + + /** + * Sets the state of many users. The "state" means any boolean attribute of a user like active or committee. + * + * @param users The users to set the state + * @param field The boolean field to set + * @param value The value to set this field to. + */ + public async bulkSetState( + users: ViewUser[], + field: 'is_active' | 'is_present' | 'is_committee', + value: boolean + ): Promise { + await this.httpService.post('/rest/users/user/bulk_set_state/', { + user_ids: users.map(user => user.id), + field: field, + value: value + }); + } + /** * 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 @@ -222,7 +270,7 @@ export class UserRepositoryService extends BaseRepository { + public async bulkSendInvitationEmail(users: ViewUser[]): Promise { const user_ids = users.map(user => user.id); const subject = this.translate.instant(this.configService.instant('users_email_subject')); const message = this.translate.instant(this.configService.instant('users_email_body')); diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.ts b/client/src/app/site/users/components/user-detail/user-detail.component.ts index e138feb0c..5e972466b 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.ts +++ b/client/src/app/site/users/components/user-detail/user-detail.component.ts @@ -441,7 +441,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit { const title = this.translate.instant('Sending an invitation email'); const content = this.translate.instant('Are you sure you want to send an invitation email to the user?'); if (await this.promptService.open(title, content)) { - this.repo.sendInvitationEmail([this.user]).then(this.raiseError, this.raiseError); + this.repo.bulkSendInvitationEmail([this.user]).then(this.raiseError, this.raiseError); } } diff --git a/client/src/app/site/users/components/user-list/user-list.component.html b/client/src/app/site/users/components/user-list/user-list.component.html index ac3d610f7..0ae2fe2bb 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.html +++ b/client/src/app/site/users/components/user-list/user-list.component.html @@ -181,17 +181,17 @@
- - - @@ -203,7 +203,11 @@ Send invitation email - + 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 4530e3de2..a8231a56b 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 @@ -287,9 +287,7 @@ export class UserListComponent extends BaseListViewComponent implement public async deleteSelected(): Promise { const title = this.translate.instant('Are you sure you want to delete all selected participants?'); if (await this.promptService.open(title)) { - for (const user of this.selectedRows) { - await this.repo.delete(user); - } + await this.repo.bulkDelete(this.selectedRows); } } @@ -323,47 +321,29 @@ export class UserListComponent extends BaseListViewComponent implement * Handler for bulk setting/unsetting the 'active' attribute. * Uses selectedRows defined via multiSelect mode. */ - public async setActiveSelected(): Promise { - const content = this.translate.instant('Set active status for selected participants:'); - const options = [_('active'), _('inactive')]; - const selectedChoice = await this.choiceService.open(content, null, false, options); - if (selectedChoice) { - const active = selectedChoice.action === options[0]; - for (const user of this.selectedRows) { - await this.repo.update({ is_active: active }, user); - } + public async setStateSelected(field: 'is_active' | 'is_present' | 'is_committee'): Promise { + let options: [string, string]; + let verboseStateName: string; + switch (field) { + case 'is_active': + options = [_('active'), _('inactive')]; + verboseStateName = 'active'; + break; + case 'is_present': + options = [_('present'), _('absent')]; + verboseStateName = 'present'; + break; + case 'is_committee': + options = [_('committee'), _('no committee')]; + verboseStateName = 'committee'; + break; } - } + const content = this.translate.instant(`Set ${verboseStateName} status for selected participants:`); - /** - * Handler for bulk setting/unsetting the 'is present' attribute. - * Uses selectedRows defined via multiSelect mode. - */ - public async setPresentSelected(): Promise { - const content = this.translate.instant('Set presence status for selected participants:'); - const options = [_('present'), _('absent')]; const selectedChoice = await this.choiceService.open(content, null, false, options); if (selectedChoice) { - const present = selectedChoice.action === options[0]; - for (const user of this.selectedRows) { - await this.repo.update({ is_present: present }, user); - } - } - } - - /** - * Handler for bulk setting/unsetting the 'is committee' attribute. - * Uses selectedRows defined via multiSelect mode. - */ - public async setCommitteeSelected(): Promise { - const content = this.translate.instant('Set committee status for selected participants:'); - const options = [_('committee'), _('no committee')]; - const selectedChoice = await this.choiceService.open(content, null, false, options); - if (selectedChoice) { - const committee = selectedChoice.action === options[0]; - for (const user of this.selectedRows) { - await this.repo.update({ is_committee: committee }, user); - } + const value = selectedChoice.action === options[0]; + await this.repo.bulkSetState(this.selectedRows, field, value); } } @@ -375,7 +355,7 @@ export class UserListComponent extends BaseListViewComponent implement const title = this.translate.instant('Are you sure you want to send emails to all selected participants?'); const content = this.selectedRows.length + ' ' + this.translate.instant('emails'); if (await this.promptService.open(title, content)) { - this.repo.sendInvitationEmail(this.selectedRows).then(this.raiseError, this.raiseError); + await this.repo.bulkSendInvitationEmail(this.selectedRows); } } @@ -393,9 +373,14 @@ export class UserListComponent extends BaseListViewComponent implement } /** - * Handler for bulk setting new passwords. Needs multiSelect mode. + * Handler for bulk resetting passwords to the default ones. Needs multiSelect mode. */ - public async resetPasswordsSelected(): Promise { + public async resetPasswordsToDefaultSelected(): Promise { + const title = this.translate.instant('Are you sure you want to reset all passwords to the default ones?'); + if (!(await this.promptService.open(title))) { + return; + } + if (this.selectedRows.find(row => row.user.id === this.operator.user.id)) { this.raiseError( this.translate.instant( @@ -403,10 +388,31 @@ export class UserListComponent extends BaseListViewComponent implement ) ); } - for (const user of this.selectedRows.filter(u => u.user.id !== this.operator.user.id)) { - const password = this.repo.getRandomPassword(); - this.repo.resetPassword(user, password, true); + this.repo.bulkResetPasswordsToDefault(this.selectedRows).then(null, this.raiseError); + } + + /** + * Handler for bulk generating new passwords. Needs multiSelect mode. + */ + public async generateNewPasswordsPasswordsSelected(): Promise { + const title = this.translate.instant( + 'Are you sure you want to generate new passwords for all selected participants?' + ); + const content = this.translate.instant( + 'Note, that the default password will be changed to the new generated one.' + ); + if (!(await this.promptService.open(title, content))) { + return; } + + if (this.selectedRows.find(row => row.user.id === this.operator.user.id)) { + this.raiseError( + this.translate.instant( + 'Note: Your own password was not changed. Please use the password change dialog instead.' + ) + ); + } + this.repo.bulkGenerateNewPasswords(this.selectedRows).then(null, this.raiseError); } /** diff --git a/openslides/users/models.py b/openslides/users/models.py index 275277304..016537962 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -1,5 +1,4 @@ import smtplib -from random import choice from django.conf import settings from django.contrib.auth.hashers import make_password @@ -108,14 +107,6 @@ class UserManager(BaseUserManager): return generated_username - def generate_password(self): - """ - Generates a random passwort. Do not use l, o, I, O, 1 or 0. - """ - chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" - size = 8 - return "".join([choice(chars) for i in range(size)]) - class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): """ diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index 81517beec..8aca8e33a 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -97,7 +97,7 @@ class UserFullSerializer(ModelSerializer): """ # Prepare setup password. if not validated_data.get("default_password"): - validated_data["default_password"] = User.objects.generate_password() + validated_data["default_password"] = User.objects.make_random_password() validated_data["password"] = make_password(validated_data["default_password"]) return validated_data diff --git a/openslides/users/views.py b/openslides/users/views.py index f2059e230..3f8caf3cc 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -79,6 +79,10 @@ class UserViewSet(ModelViewSet): "create", "destroy", "reset_password", + "bulk_generate_passwords", + "bulk_reset_passwords_to_default", + "bulk_set_state", + "bulk_delete", "mass_import", "mass_invite_email", ): @@ -144,31 +148,120 @@ class UserViewSet(ModelViewSet): @detail_route(methods=["post"]) def reset_password(self, request, pk=None): """ - View to reset the password using the requested password. - If update_defualt_password=True is given, the new password will also be set - as the default_password. + View to reset the password of the given user (by url) using a provided password. + Expected data: { pasword: } """ user = self.get_object() password = request.data.get("password") if not isinstance(password, str): raise ValidationError({"detail": "Password has to be a string."}) - update_default_password = request.data.get("update_default_password", False) - if not isinstance(update_default_password, bool): - raise ValidationError( - {"detail": "update_default_password has to be a boolean."} - ) - try: validate_password(password, user=request.user) except DjangoValidationError as errors: raise ValidationError({"detail": " ".join(errors)}) user.set_password(password) - if update_default_password: - user.default_password = password user.save() return Response({"detail": "Password successfully reset."}) + @list_route(methods=["post"]) + def bulk_generate_passwords(self, request): + """ + Generates new random passwords for many users. The request user is excluded + and the default password will be set to the new generated passwords. + Expected data: { user_ids: } + """ + ids = request.data.get("user_ids") + self.assert_list_of_ints(ids) + + # Exclude the request user + users = User.objects.exclude(pk=request.user.id).filter(pk__in=ids) + for user in users: + password = User.objects.make_random_password() + user.set_password(password) + user.default_password = password + user.save() + return Response() + + @list_route(methods=["post"]) + def bulk_reset_passwords_to_default(self, request): + """ + resets the password of all given users to their default ones. The + request user is excluded. + Expected data: { user_ids: } + """ + ids = request.data.get("user_ids") + self.assert_list_of_ints(ids) + + # Exclude the request user + users = User.objects.exclude(pk=request.user.id).filter(pk__in=ids) + # Validate all default passwords + for user in users: + try: + validate_password(user.default_password, user=user) + except DjangoValidationError as errors: + errors = " ".join(errors) + raise ValidationError( + { + "detail": f'The default password of user "{user.username}" is not valid: {errors}' + } + ) + + # Reset passwords + for user in users: + user.set_password(user.default_password) + user.save() + return Response() + + @list_route(methods=["post"]) + def bulk_set_state(self, request): + """ + Sets the "state" of may users. The "state" means boolean attributes like active + or committee of a user. If 'is_active' is choosen, the request user will be + removed from the list of user ids. Expected data: + + { + user_ids: + field: 'is_active' | 'is_present' | 'is_committee' + value: True|False + } + """ + + ids = request.data.get("user_ids") + self.assert_list_of_ints(ids) + + field = request.data.get("field") + if field not in ("is_active", "is_present", "is_committee"): + raise ValidationError({"detail": "Unsupported field"}) + value = request.data.get("value") + if not isinstance(value, bool): + raise ValidationError({"detail": "value must be true or false"}) + + users = User.objects.filter(pk__in=ids) + if field == "is_active": + users = users.exclude(pk=request.user.id) + for user in users: + setattr(user, field, value) + user.save() + + return Response() + + @list_route(methods=["post"]) + def bulk_delete(self, request): + """ + Deletes many users. The request user will be excluded. Expected data: + { user_ids: } + """ + ids = request.data.get("user_ids") + self.assert_list_of_ints(ids) + + # Exclude the request user + users = User.objects.exclude(pk=request.user.id).filter(pk__in=ids) + for user in list(users): + user.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) + @list_route(methods=["post"]) @transaction.atomic def mass_import(self, request): @@ -219,11 +312,7 @@ class UserViewSet(ModelViewSet): number of emails send. """ user_ids = request.data.get("user_ids") - if not isinstance(user_ids, list): - raise ValidationError({"detail": "User_ids has to be a list."}) - for user_id in user_ids: - if not isinstance(user_id, int): - raise ValidationError({"detail": "User_id has to be an int."}) + self.assert_list_of_ints(user_ids) # Get subject and body from the response. Do not use the config values # because they might not be translated. subject = request.data.get("subject") @@ -268,6 +357,14 @@ class UserViewSet(ModelViewSet): {"count": len(success_users), "no_email_ids": user_pks_without_email} ) + def assert_list_of_ints(self, 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"}) + for id in ids: + if not isinstance(id, 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 1d20c005c..c6465c292 100644 --- a/tests/integration/users/test_viewset.py +++ b/tests/integration/users/test_viewset.py @@ -6,7 +6,6 @@ from rest_framework.test import APIClient from openslides.core.config import config from openslides.users.models import Group, PersonalNote, User -from openslides.users.serializers import UserFullSerializer from openslides.utils.autoupdate import inform_changed_data from openslides.utils.test import TestCase @@ -218,12 +217,14 @@ class UserDelete(TestCase): Tests delete of users via REST API. """ + def setUp(self): + self.admin_client = APIClient() + self.admin_client.login(username="admin", password="admin") + def test_delete(self): - admin_client = APIClient() - admin_client.login(username="admin", password="admin") User.objects.create(username="Test name bo3zieT3iefahng0ahqu") - response = admin_client.delete(reverse("user-detail", args=["2"])) + response = self.admin_client.delete(reverse("user-detail", args=["2"])) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse( @@ -231,28 +232,50 @@ class UserDelete(TestCase): ) def test_delete_yourself(self): - admin_client = APIClient() - admin_client.login(username="admin", password="admin") # This is the builtin user 'Administrator'. The pk is valid. admin_user_pk = 1 - - response = admin_client.delete(reverse("user-detail", args=[admin_user_pk])) - + response = self.admin_client.delete( + reverse("user-detail", args=[admin_user_pk]) + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_bulk_delete(self): + # create 10 users: + ids = [] + for i in range(10): + user = User(username=f"user_{i}") + user.save() + ids.append(user.id) -class UserResetPassword(TestCase): + response = self.admin_client.post( + reverse("user-bulk-delete"), {"user_ids": ids}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(User.objects.filter(pk__in=ids).exists()) + + def test_bulk_delete_self(self): + """ The own id should be excluded, so nothing should happen. """ + response = self.admin_client.post( + reverse("user-bulk-delete"), {"user_ids": [1]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue(User.objects.filter(pk=1).exists()) + + +class UserPassword(TestCase): """ Tests resetting users password via REST API by a manager. """ + def setUp(self): + self.admin_client = APIClient() + self.admin_client.login(username="admin", password="admin") + def test_reset(self): - admin_client = APIClient() - admin_client.login(username="admin", password="admin") user = User.objects.create(username="Test name ooMoa4ou4mohn2eo1ree") user.default_password = "new_password_Yuuh8OoQueePahngohy3" user.save() - response = admin_client.post( + response = self.admin_client.post( reverse("user-reset-password", args=[user.pk]), {"password": "new_password_Yuuh8OoQueePahngohy3_new"}, ) @@ -263,23 +286,139 @@ class UserResetPassword(TestCase): ) ) - """ - Tests whether a random password is set as default and actual password - if no default password is provided. - """ - def test_set_random_initial_password(self): - admin_client = APIClient() - admin_client.login(username="admin", password="admin") + """ + Tests whether a random password is set if no default password is given. The password + must be set as the default and real password. + """ + response = self.admin_client.post( + reverse("user-list"), {"username": "Test name 9gt043qwvnj2d0cr"} + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) - serializer = UserFullSerializer() - user = serializer.create({"username": "Test name 9gt043qwvnj2d0cr"}) - user.save() + user = User.objects.get(username="Test name 9gt043qwvnj2d0cr") + self.assertTrue(isinstance(user.default_password, str)) + self.assertTrue(len(user.default_password) >= 8) + self.assertTrue(user.check_password(user.default_password)) - default_password = User.objects.get(pk=user.pk).default_password - self.assertIsNotNone(default_password) - self.assertEqual(len(default_password), 8) - self.assertTrue(User.objects.get(pk=user.pk).check_password(default_password)) + def test_bulk_generate_new_passwords(self): + default_password1 = "Default password e3fj3oh39hwwcbjb2qqy" + default_password2 = "Default password 32pifjmaewrelkqwelng" + user1 = User.objects.create( + username="Test name r9uJoqq1k0fk09i39elq", + default_password=default_password1, + ) + user2 = User.objects.create( + username="Test name poqwhfjpofmouivg73NU", + default_password=default_password2, + ) + user1.set_password(default_password1) + user2.set_password(default_password2) + self.assertTrue(user1.check_password(default_password1)) + self.assertTrue(user2.check_password(default_password2)) + + response = self.admin_client.post( + reverse("user-bulk-generate-passwords"), + {"user_ids": [user1.id, user2.id]}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + user1 = User.objects.get(username="Test name r9uJoqq1k0fk09i39elq") + user2 = User.objects.get(username="Test name poqwhfjpofmouivg73NU") + self.assertTrue(default_password1 != user1.default_password) + self.assertTrue(default_password2 != user2.default_password) + self.assertTrue(len(user1.default_password) >= 8) + self.assertTrue(len(user2.default_password) >= 8) + self.assertTrue(user1.check_password(user1.default_password)) + self.assertTrue(user2.check_password(user2.default_password)) + + def test_bulk_reset_passwords_to_default_ones(self): + default_password1 = "Default password e3fj3oh39hwwcbjb2qqy" + default_password2 = "Default password 32pifjmaewrelkqwelng" + user1 = User.objects.create( + username="Test name pefkjOf9m8efNspuhPFq", + default_password=default_password1, + ) + user2 = User.objects.create( + username="Test name qpymcmbmntiwoE97ev7C", + default_password=default_password2, + ) + user1.set_password("") + user2.set_password("") + self.assertFalse(user1.check_password(default_password1)) + self.assertFalse(user2.check_password(default_password2)) + + response = self.admin_client.post( + reverse("user-bulk-reset-passwords-to-default"), + {"user_ids": [user1.id, user2.id]}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + user1 = User.objects.get(username="Test name pefkjOf9m8efNspuhPFq") + user2 = User.objects.get(username="Test name qpymcmbmntiwoE97ev7C") + self.assertTrue(user1.check_password(default_password1)) + self.assertTrue(user2.check_password(default_password2)) + + +class UserBulkSetState(TestCase): + """ + Tests setting states of users. + """ + + def setUp(self): + self.client = APIClient() + self.client.login(username="admin", password="admin") + admin = User.objects.get() + admin.is_active = True + admin.is_present = True + admin.is_committee = True + admin.save() + + def test_set_is_present(self): + response = self.client.post( + reverse("user-bulk-set-state"), + {"user_ids": [1], "field": "is_present", "value": False}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(User.objects.get().is_active) + self.assertFalse(User.objects.get().is_present) + self.assertTrue(User.objects.get().is_committee) + + def test_invalid_field(self): + response = self.client.post( + reverse("user-bulk-set-state"), + {"user_ids": [1], "field": "invalid", "value": False}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertTrue(User.objects.get().is_active) + self.assertTrue(User.objects.get().is_present) + self.assertTrue(User.objects.get().is_committee) + + def test_invalid_value(self): + response = self.client.post( + reverse("user-bulk-set-state"), + {"user_ids": [1], "field": "is_active", "value": "invalid"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertTrue(User.objects.get().is_active) + self.assertTrue(User.objects.get().is_present) + self.assertTrue(User.objects.get().is_committee) + + def test_set_active_not_self(self): + response = self.client.post( + reverse("user-bulk-set-state"), + {"user_ids": [1], "field": "is_active", "value": False}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(User.objects.get().is_active) + self.assertTrue(User.objects.get().is_present) + self.assertTrue(User.objects.get().is_committee) class UserMassImport(TestCase): diff --git a/tests/unit/users/test_models.py b/tests/unit/users/test_models.py index 69924e49d..de07d5089 100644 --- a/tests/unit/users/test_models.py +++ b/tests/unit/users/test_models.py @@ -1,5 +1,5 @@ from unittest import TestCase -from unittest.mock import MagicMock, call, patch +from unittest.mock import MagicMock, patch from django.core.exceptions import ObjectDoesNotExist @@ -115,24 +115,6 @@ class UserManagerGenerateUsername(TestCase): ) -@patch("openslides.users.models.choice") -class UserManagerGeneratePassword(TestCase): - def test_normal(self, mock_choice): - """ - Test normal run of the method. - """ - mock_choice.side_effect = tuple("test_password") - - self.assertEqual(UserManager().generate_password(), "test_pas") - # choice has to be called 8 times - mock_choice.assert_has_calls( - [ - call("abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") - for _ in range(8) - ] - ) - - @patch("openslides.users.models.Permission") @patch("openslides.users.models.Group") class UserManagerCreateOrResetAdminUser(TestCase):