Merge pull request #4861 from FinnStutzenstein/userBulkViews
Bulk views for users: state, password and delete
This commit is contained in:
commit
20dc306106
@ -118,6 +118,21 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
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 {
|
||||
const groups = this.viewModelStoreService.getMany(ViewGroup, user.groups_id);
|
||||
const viewUser = new ViewUser(user, groups);
|
||||
@ -154,37 +169,6 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
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.
|
||||
* 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 updateDefaultPassword Control, if the default password should be updated.
|
||||
*/
|
||||
public async resetPassword(
|
||||
user: ViewUser,
|
||||
password: string,
|
||||
updateDefaultPassword: boolean = false
|
||||
): Promise<void> {
|
||||
public async resetPassword(user: ViewUser, password: string): Promise<void> {
|
||||
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.
|
||||
* 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
|
||||
*/
|
||||
public async sendInvitationEmail(users: ViewUser[]): Promise<string> {
|
||||
public async bulkSendInvitationEmail(users: ViewUser[]): Promise<string> {
|
||||
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'));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,17 +181,17 @@
|
||||
<div *osPerms="'users.can_manage'">
|
||||
<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>
|
||||
<span translate>Enable/disable account ...</span>
|
||||
</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>
|
||||
<span translate>Set presence ...</span>
|
||||
</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>
|
||||
<span translate>Set committee ...</span>
|
||||
</button>
|
||||
@ -203,7 +203,11 @@
|
||||
<span translate>Send invitation email</span>
|
||||
</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>
|
||||
<span translate>Generate new passwords</span>
|
||||
</button>
|
||||
|
@ -287,9 +287,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
||||
public async deleteSelected(): Promise<void> {
|
||||
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<ViewUser> implement
|
||||
* Handler for bulk setting/unsetting the 'active' attribute.
|
||||
* Uses selectedRows defined via multiSelect mode.
|
||||
*/
|
||||
public async setActiveSelected(): Promise<void> {
|
||||
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<void> {
|
||||
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<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);
|
||||
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<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);
|
||||
}
|
||||
const value = selectedChoice.action === options[0];
|
||||
await this.repo.bulkSetState(this.selectedRows, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 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<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)) {
|
||||
this.raiseError(
|
||||
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)) {
|
||||
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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: <the new password> }
|
||||
"""
|
||||
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: <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"])
|
||||
@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):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user