Merge pull request #4861 from FinnStutzenstein/userBulkViews

Bulk views for users: state, password and delete
This commit is contained in:
Finn Stutzenstein 2019-07-23 10:25:15 +02:00 committed by GitHub
commit 20dc306106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 428 additions and 161 deletions

View File

@ -118,6 +118,21 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
return this.translate.instant(plural ? 'Participants' : 'Participant'); return this.translate.instant(plural ? 'Participants' : 'Participant');
}; };
/**
* Generates a random password
*
* @param length The length of the password to generate
* @returns a random password
*/
public getRandomPassword(length: number = 10): string {
let pw = '';
const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
for (let i = 0; i < length; i++) {
pw += characters.charAt(Math.floor(Math.random() * characters.length));
}
return pw;
}
public createViewModel(user: User): ViewUser { public createViewModel(user: User): ViewUser {
const groups = this.viewModelStoreService.getMany(ViewGroup, user.groups_id); const groups = this.viewModelStoreService.getMany(ViewGroup, user.groups_id);
const viewUser = new ViewUser(user, groups); const viewUser = new ViewUser(user, groups);
@ -154,37 +169,6 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
return await this.dataSend.updateModel(updateUser); return await this.dataSend.updateModel(updateUser);
} }
/**
* Creates and saves a list of users in a bulk operation.
*
* @param newEntries
*/
public async bulkCreate(newEntries: NewEntry<ViewUser>[]): Promise<number[]> {
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. * Updates the password and sets the password without checking for the old one.
* Also resets the 'default password' to the newly created one. * Also resets the 'default password' to the newly created one.
@ -193,13 +177,9 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* @param password The password to set * @param password The password to set
* @param updateDefaultPassword Control, if the default password should be updated. * @param updateDefaultPassword Control, if the default password should be updated.
*/ */
public async resetPassword( public async resetPassword(user: ViewUser, password: string): Promise<void> {
user: ViewUser,
password: string,
updateDefaultPassword: boolean = false
): Promise<void> {
const path = `/rest/users/user/${user.id}/reset_password/`; 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<ViewUser, User, UserTi
}); });
} }
/**
* Resets the passwords of all given users to their default ones. The operator will
* not be changed (if provided in `users`).
*
* @param users The users to reset the passwords from
*/
public async bulkResetPasswordsToDefault(users: ViewUser[]): Promise<void> {
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<void> {
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<ViewUser>[]): Promise<number[]> {
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<void> {
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<void> {
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. * 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
@ -222,7 +270,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* *
* @param users All affected users * @param users All affected users
*/ */
public async sendInvitationEmail(users: ViewUser[]): Promise<string> { public async bulkSendInvitationEmail(users: ViewUser[]): Promise<string> {
const user_ids = users.map(user => user.id); const user_ids = users.map(user => user.id);
const subject = this.translate.instant(this.configService.instant('users_email_subject')); const subject = this.translate.instant(this.configService.instant('users_email_subject'));
const message = this.translate.instant(this.configService.instant('users_email_body')); const message = this.translate.instant(this.configService.instant('users_email_body'));

View File

@ -441,7 +441,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
const title = this.translate.instant('Sending an invitation email'); 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?'); 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)) { 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);
} }
} }

View File

@ -181,17 +181,17 @@
<div *osPerms="'users.can_manage'"> <div *osPerms="'users.can_manage'">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="setActiveSelected()"> <button mat-menu-item [disabled]="!selectedRows.length" (click)="setStateSelected('is_active')">
<mat-icon>block</mat-icon> <mat-icon>block</mat-icon>
<span translate>Enable/disable account ...</span> <span translate>Enable/disable account ...</span>
</button> </button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="setPresentSelected()"> <button mat-menu-item [disabled]="!selectedRows.length" (click)="setStateSelected('is_present')">
<mat-icon>check_box</mat-icon> <mat-icon>check_box</mat-icon>
<span translate>Set presence ...</span> <span translate>Set presence ...</span>
</button> </button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="setCommitteeSelected()"> <button mat-menu-item [disabled]="!selectedRows.length" (click)="setStateSelected('is_committee')">
<mat-icon>account_balance</mat-icon> <mat-icon>account_balance</mat-icon>
<span translate>Set committee ...</span> <span translate>Set committee ...</span>
</button> </button>
@ -203,7 +203,11 @@
<span translate>Send invitation email</span> <span translate>Send invitation email</span>
</button> </button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="resetPasswordsSelected()"> <button mat-menu-item [disabled]="!selectedRows.length" (click)="resetPasswordsToDefaultSelected()">
<mat-icon>vpn_key</mat-icon>
<span translate>Reset passwords to the default ones</span>
</button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="generateNewPasswordsPasswordsSelected()">
<mat-icon>vpn_key</mat-icon> <mat-icon>vpn_key</mat-icon>
<span translate>Generate new passwords</span> <span translate>Generate new passwords</span>
</button> </button>

View File

@ -287,9 +287,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
public async deleteSelected(): Promise<void> { public async deleteSelected(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete all selected participants?'); const title = this.translate.instant('Are you sure you want to delete all selected participants?');
if (await this.promptService.open(title)) { if (await this.promptService.open(title)) {
for (const user of this.selectedRows) { await this.repo.bulkDelete(this.selectedRows);
await this.repo.delete(user);
}
} }
} }
@ -323,47 +321,29 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
* Handler for bulk setting/unsetting the 'active' attribute. * Handler for bulk setting/unsetting the 'active' attribute.
* Uses selectedRows defined via multiSelect mode. * Uses selectedRows defined via multiSelect mode.
*/ */
public async setActiveSelected(): Promise<void> { public async setStateSelected(field: 'is_active' | 'is_present' | 'is_committee'): Promise<void> {
const content = this.translate.instant('Set active status for selected participants:'); let options: [string, string];
const options = [_('active'), _('inactive')]; let verboseStateName: string;
const selectedChoice = await this.choiceService.open(content, null, false, options); switch (field) {
if (selectedChoice) { case 'is_active':
const active = selectedChoice.action === options[0]; options = [_('active'), _('inactive')];
for (const user of this.selectedRows) { verboseStateName = 'active';
await this.repo.update({ is_active: active }, user); 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<void> {
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); const selectedChoice = await this.choiceService.open(content, null, false, options);
if (selectedChoice) { if (selectedChoice) {
const present = selectedChoice.action === options[0]; const value = selectedChoice.action === options[0];
for (const user of this.selectedRows) { await this.repo.bulkSetState(this.selectedRows, field, value);
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<void> {
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);
}
} }
} }
@ -375,7 +355,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
const title = this.translate.instant('Are you sure you want to send emails to all selected participants?'); 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'); const content = this.selectedRows.length + ' ' + this.translate.instant('emails');
if (await this.promptService.open(title, content)) { 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<ViewUser> 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<void> { public async resetPasswordsToDefaultSelected(): Promise<void> {
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)) { if (this.selectedRows.find(row => row.user.id === this.operator.user.id)) {
this.raiseError( this.raiseError(
this.translate.instant( this.translate.instant(
@ -403,10 +388,31 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
) )
); );
} }
for (const user of this.selectedRows.filter(u => u.user.id !== this.operator.user.id)) { this.repo.bulkResetPasswordsToDefault(this.selectedRows).then(null, this.raiseError);
const password = this.repo.getRandomPassword(); }
this.repo.resetPassword(user, password, true);
/**
* Handler for bulk generating new passwords. Needs multiSelect mode.
*/
public async generateNewPasswordsPasswordsSelected(): Promise<void> {
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);
} }
/** /**

View File

@ -1,5 +1,4 @@
import smtplib import smtplib
from random import choice
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
@ -108,14 +107,6 @@ class UserManager(BaseUserManager):
return generated_username 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): class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
""" """

View File

@ -97,7 +97,7 @@ class UserFullSerializer(ModelSerializer):
""" """
# Prepare setup password. # Prepare setup password.
if not validated_data.get("default_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"]) validated_data["password"] = make_password(validated_data["default_password"])
return validated_data return validated_data

View File

@ -79,6 +79,10 @@ class UserViewSet(ModelViewSet):
"create", "create",
"destroy", "destroy",
"reset_password", "reset_password",
"bulk_generate_passwords",
"bulk_reset_passwords_to_default",
"bulk_set_state",
"bulk_delete",
"mass_import", "mass_import",
"mass_invite_email", "mass_invite_email",
): ):
@ -144,31 +148,120 @@ class UserViewSet(ModelViewSet):
@detail_route(methods=["post"]) @detail_route(methods=["post"])
def reset_password(self, request, pk=None): def reset_password(self, request, pk=None):
""" """
View to reset the password using the requested password. View to reset the password of the given user (by url) using a provided password.
If update_defualt_password=True is given, the new password will also be set Expected data: { pasword: <the new password> }
as the default_password.
""" """
user = self.get_object() user = self.get_object()
password = request.data.get("password") password = request.data.get("password")
if not isinstance(password, str): if not isinstance(password, str):
raise ValidationError({"detail": "Password has to be a string."}) 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: try:
validate_password(password, user=request.user) validate_password(password, user=request.user)
except DjangoValidationError as errors: except DjangoValidationError as errors:
raise ValidationError({"detail": " ".join(errors)}) raise ValidationError({"detail": " ".join(errors)})
user.set_password(password) user.set_password(password)
if update_default_password:
user.default_password = password
user.save() user.save()
return Response({"detail": "Password successfully reset."}) 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: <list of 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: <list of 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: <list of 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: <list of 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"]) @list_route(methods=["post"])
@transaction.atomic @transaction.atomic
def mass_import(self, request): def mass_import(self, request):
@ -219,11 +312,7 @@ class UserViewSet(ModelViewSet):
number of emails send. number of emails send.
""" """
user_ids = request.data.get("user_ids") user_ids = request.data.get("user_ids")
if not isinstance(user_ids, list): self.assert_list_of_ints(user_ids)
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."})
# Get subject and body from the response. Do not use the config values # Get subject and body from the response. Do not use the config values
# because they might not be translated. # because they might not be translated.
subject = request.data.get("subject") subject = request.data.get("subject")
@ -268,6 +357,14 @@ 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):
""" 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): class GroupViewSetMetadata(SimpleMetadata):
""" """

View File

@ -6,7 +6,6 @@ from rest_framework.test import APIClient
from openslides.core.config import config from openslides.core.config import config
from openslides.users.models import Group, PersonalNote, User 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.autoupdate import inform_changed_data
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -218,12 +217,14 @@ class UserDelete(TestCase):
Tests delete of users via REST API. 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): def test_delete(self):
admin_client = APIClient()
admin_client.login(username="admin", password="admin")
User.objects.create(username="Test name bo3zieT3iefahng0ahqu") 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.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse( self.assertFalse(
@ -231,28 +232,50 @@ class UserDelete(TestCase):
) )
def test_delete_yourself(self): 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. # This is the builtin user 'Administrator'. The pk is valid.
admin_user_pk = 1 admin_user_pk = 1
response = self.admin_client.delete(
response = admin_client.delete(reverse("user-detail", args=[admin_user_pk])) reverse("user-detail", args=[admin_user_pk])
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 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. 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): def test_reset(self):
admin_client = APIClient()
admin_client.login(username="admin", password="admin")
user = User.objects.create(username="Test name ooMoa4ou4mohn2eo1ree") user = User.objects.create(username="Test name ooMoa4ou4mohn2eo1ree")
user.default_password = "new_password_Yuuh8OoQueePahngohy3" user.default_password = "new_password_Yuuh8OoQueePahngohy3"
user.save() user.save()
response = admin_client.post( response = self.admin_client.post(
reverse("user-reset-password", args=[user.pk]), reverse("user-reset-password", args=[user.pk]),
{"password": "new_password_Yuuh8OoQueePahngohy3_new"}, {"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): 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 = User.objects.get(username="Test name 9gt043qwvnj2d0cr")
user = serializer.create({"username": "Test name 9gt043qwvnj2d0cr"}) self.assertTrue(isinstance(user.default_password, str))
user.save() 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 def test_bulk_generate_new_passwords(self):
self.assertIsNotNone(default_password) default_password1 = "Default password e3fj3oh39hwwcbjb2qqy"
self.assertEqual(len(default_password), 8) default_password2 = "Default password 32pifjmaewrelkqwelng"
self.assertTrue(User.objects.get(pk=user.pk).check_password(default_password)) 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): class UserMassImport(TestCase):

View File

@ -1,5 +1,5 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, call, patch from unittest.mock import MagicMock, patch
from django.core.exceptions import ObjectDoesNotExist 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.Permission")
@patch("openslides.users.models.Group") @patch("openslides.users.models.Group")
class UserManagerCreateOrResetAdminUser(TestCase): class UserManagerCreateOrResetAdminUser(TestCase):