Skip to content

Commit

Permalink
Lock participant out of meeting (#3815)
Browse files Browse the repository at this point in the history
  • Loading branch information
reiterl committed Sep 26, 2024
1 parent 953f551 commit fccfc7d
Show file tree
Hide file tree
Showing 26 changed files with 335 additions and 41 deletions.
4 changes: 3 additions & 1 deletion client/src/app/domain/definitions/permission.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ Meeting specific information: Structure level, Group, Participant number, About
},
{
display_name: _(`Can see sensitive data`),
help_text: _(`Can see email, username and SSO identification of all participants.`),
help_text: _(
`Can see email, username, membership number, SSO identification and locked out state of all participants.`
),
value: Permission.userCanSeeSensitiveData
},
{
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/domain/models/meeting-users/meeting-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class MeetingUser extends BaseDecimalModel<MeetingUser> {
public readonly number!: string;
public readonly about_me!: string;
public readonly vote_weight!: number;
public readonly locked_out!: boolean;

public user_id!: Id;
public meeting_id!: Id;
Expand Down Expand Up @@ -42,6 +43,7 @@ export class MeetingUser extends BaseDecimalModel<MeetingUser> {
`number`,
`about_me`,
`vote_weight`,
`locked_out`,
`user_id`,
`meeting_id`,
`personal_note_ids`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class MeetingUserRepositoryService extends BaseMeetingRelatedRepository<V
`vote_weight`,
`comment`,
`user_id`,
`number`
`number`,
`locked_out`
]);

const detailFields: TypedFieldset<MeetingUser> = [`about_me`, `user_id`, `meeting_id`];
Expand All @@ -57,7 +58,8 @@ export class MeetingUserRepositoryService extends BaseMeetingRelatedRepository<V
vote_delegated_to_id: partialUser.vote_delegated_to_id,
vote_delegations_from_ids: partialUser.vote_delegations_from_ids,
structure_level_ids: partialUser.structure_level_ids,
group_ids: partialUser.group_ids
group_ids: partialUser.group_ids,
locked_out: partialUser.locked_out
};

if (Object.values(partialPayload).filter(val => val !== undefined).length > 1 && partialPayload.meeting_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ export type RawUser = FullNameInformation & Identifiable & Displayable & { fqid:
export type GeneralUser = ViewUser & ViewMeetingUser;

/**
* Unified type name for state fields like `is_active`, `is_physical_person` and `is_present_in_meetings`.
* Unified type name for state fields like `is_active`, `is_physical_person`, `is_present_in_meetings`
* and 'locked_out'.
*/
export type UserStateField = 'is_active' | 'is_present_in_meetings' | 'is_physical_person';
export type UserStateField = 'is_active' | 'is_present_in_meetings' | 'is_physical_person' | 'locked_out';

export interface AssignMeetingsPayload {
meeting_ids: Id[];
Expand Down Expand Up @@ -130,7 +131,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {

const participantListFields: TypedFieldset<User> = participantListFieldsMinimal
.concat(filterableListFields)
.concat([`is_present_in_meeting_ids`, `default_password`]);
.concat([`is_present_in_meeting_ids`, `default_password`, `committee_ids`, `committee_management_ids`]);

const detailFields: TypedFieldset<User> = [`default_password`, `can_change_own_password`];

Expand Down Expand Up @@ -543,7 +544,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
`comment`,
`about_me`,
`number`,
`structure_level`
`structure_level`,
`locked_out`
];
if (!create) {
fields.push(`member_number`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<os-sidenav #sideNav>
<os-sidenav #sideNav [logoLink]="showMeetingNav ? ['/'] : ['/' + meeting.id]">
<ng-template osSidenavDrawerContent>
<!-- navigation -->
<div class="main-nav">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { UserExport } from 'src/app/domain/models/users/user.export';
import { CsvColumnDefinitionProperty, CsvColumnsDefinition } from 'src/app/gateways/export/csv-export.service';
import {
CsvColumnDefinitionMap,
CsvColumnDefinitionProperty,
CsvColumnsDefinition
} from 'src/app/gateways/export/csv-export.service';
import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user';

import { MeetingCsvExportForBackendService } from '../../../../services/export/meeting-csv-export-for-backend.service';
Expand All @@ -13,6 +17,7 @@ export interface ParticipantExport extends UserExport {
comment?: string;
is_present_in_meeting_ids?: string | boolean;
group_ids?: string;
locked_out?: boolean;
}

@Injectable({
Expand Down Expand Up @@ -49,6 +54,12 @@ export class ParticipantCsvExportService {
this.csvExport.export(
participants,
participantColumns.map(key => {
if (key === `locked_out`) {
return {
label: `locked_out`,
map: user => (user.is_locked_out ? `1` : ``)
} as CsvColumnDefinitionMap<ViewUser>;
}
return {
property: key
} as CsvColumnDefinitionProperty<ViewUser>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export const participantsExportExample: any = [
default_structure_level: `Berlin`,
default_vote_weight: 1.0,
group_ids: `Delegates, Staff`,
member_number: `123`
member_number: `123`,
locked_out: false
},
{
first_name: `John`,
Expand All @@ -37,7 +38,8 @@ export const participantsExportExample: any = [
username: `jbloggs`,
gender: `female`,
default_vote_weight: 1.5,
member_number: `234`
member_number: `234`,
locked_out: true
},
{
last_name: `Executive Board`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ export class ViewGroup extends BaseHasMeetingUsersViewModel<Group> {
}

public hasPermission(perm: Permission): boolean {
return this.permissions?.some(
permission => permission === perm || permissionChildren[permission]?.includes(perm)
return (
this.isAdminGroup ||
this.permissions?.some(permission => permission === perm || permissionChildren[permission]?.includes(perm))
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[editMode]="isEditingSubject.value"
[goBack]="!isAllowed('seeOtherUsers')"
[hasMainButton]="isAllowed('changePersonal') && !!user"
[isSaveButtonEnabled]="isFormValid"
[isSaveButtonEnabled]="saveButtonEnabled"
[mainActionTooltip]="'Edit' | translate"
[nav]="false"
[saveAction]="getSaveAction()"
Expand Down Expand Up @@ -80,7 +80,7 @@ <h2>
[patchFormValueFn]="patchFormValueFn"
[shouldEnableFormControlFn]="shouldEnableFormControlFn"
[user]="user"
(changeEvent)="personalInfoFormValue = $event"
(changeEvent)="updateByValueChange($event)"
(errorEvent)="formErrors = $event"
(submitEvent)="saveUser()"
(validEvent)="isFormValid = $event"
Expand All @@ -98,6 +98,22 @@ <h2>{{ 'Meeting specific information' | translate }}</h2>
<span>{{ 'present' | translate }}</span>
</mat-checkbox>
</div>
<div [formGroup]="form">
<!-- Locked out? -->
<mat-checkbox
formControlName="locked_out"
matTooltipPosition="right"
matTooltip="{{ 'Lock out user from this meeting.' | translate }}"
>
<span>{{ 'locked out' | translate }}</span>
</mat-checkbox>
</div>
@if (isLockedOutAndCanManage) {
<p class="red-warning-text padding-left-12" translate>
It is not allowed to set the permisson UserCanManage to a locked out user. Please unset the
lockout before adding a group with UserCanManage.
</p>
}
<div>
<!-- Strucuture Level -->
<mat-form-field
Expand Down Expand Up @@ -299,10 +315,21 @@ <h4>{{ 'Comment' | translate }}</h4>
}
@if (isAllowed('seeName')) {
<div class="flex-vertical-center margin-top-12">
<span>{{ (user?.isPresentInMeeting() ? 'Is present' : 'Is not present') | translate }}</span>
<mat-icon class="margin-4">
{{ user!.isPresentInMeeting() ? 'check_box' : 'check_box_outline_blank' }}
</mat-icon>
<span>{{ (user?.isPresentInMeeting() ? 'Is present' : 'Is not present') | translate }}</span>
</div>
<div class="flex-vertical-center margin-top-12">
<!-- Locked out? -->
@if (isAllowed('seeSensitiveData')) {
<mat-icon class="margin-4" [class.red-warning-text]="user!.isLockedOutOfMeeting()">
{{ user!.isLockedOutOfMeeting() ? 'visibility_off' : 'check_box_outline_blank' }}
</mat-icon>
<span>
{{ (user?.isLockedOutOfMeeting() ? 'Is locked out' : 'Is not locked out') | translate }}
</span>
}
</div>
}
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ os-user-detail-view h2 {
:host ::ng-deep .detail-view {
@include detail-view-appearance;
}

.padding-left-12 {
padding-left: 12px;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { marker as _ } from '@colsen1991/ngx-translate-extract-marker';
Expand All @@ -7,6 +7,7 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { Id } from 'src/app/domain/definitions/key-types';
import { OML } from 'src/app/domain/definitions/organization-permission';
import { Permission } from 'src/app/domain/definitions/permission';
import { UserDetailViewComponent } from 'src/app/site/modules/user-components';
import { BaseMeetingComponent } from 'src/app/site/pages/meetings/base/base-meeting.component';
import { ViewGroup } from 'src/app/site/pages/meetings/pages/participants';
import {
Expand Down Expand Up @@ -35,6 +36,9 @@ import { ViewStructureLevel } from '../../../structure-levels/view-models';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParticipantDetailViewComponent extends BaseMeetingComponent {
@ViewChild(UserDetailViewComponent)
private userDetailView;

public participantSubscriptionConfig = getParticipantMinimalSubscriptionConfig(this.activeMeetingId);

public readonly additionalFormControls = {
Expand All @@ -46,7 +50,8 @@ export class ParticipantDetailViewComponent extends BaseMeetingComponent {
group_ids: [``],
vote_delegations_from_ids: [``],
vote_delegated_to_id: [``],
is_present: [``]
is_present: [``],
locked_out: [``]
};

public sortFn = (groupA: ViewGroup, groupB: ViewGroup): number => groupA.weight - groupB.weight;
Expand All @@ -64,6 +69,9 @@ export class ParticipantDetailViewComponent extends BaseMeetingComponent {
if (controlName === `is_present` && user) {
return user.isPresentInMeeting();
}
if (controlName === `locked_out` && user) {
return user.isLockedOutOfMeeting();
}
const value = user?.[controlName as keyof ViewUser] || null;
return typeof value === `function` ? value.bind(user)(this.activeMeetingId!) : value;
};
Expand Down Expand Up @@ -157,6 +165,22 @@ export class ParticipantDetailViewComponent extends BaseMeetingComponent {
return this._isVoteDelegationEnabled;
}

public get saveButtonEnabled(): boolean {
return this.isFormValid && !this.isLockedOutAndCanManage;
}

public get isLockedOutAndCanManage(): boolean {
const lockedOutHelper = this.personalInfoFormValue?.locked_out ?? this.user?.is_locked_out;
return lockedOutHelper && this.checkSelectedGroupsCanManage();
}

public get lockoutCheckboxDisabled(): boolean {
const other = this.user?.id !== this.operator.operatorId;
const notChanged = (this.personalInfoFormValue?.locked_out ?? null) === null;
const isLockedOut = this.user?.is_locked_out;
return notChanged && !isLockedOut && (this.checkSelectedGroupsCanManage() || !other);
}

private _userId: Id | undefined = undefined; // Not initialized
private _isVoteWeightEnabled = false;
private _isVoteDelegationEnabled = false;
Expand Down Expand Up @@ -338,6 +362,17 @@ export class ParticipantDetailViewComponent extends BaseMeetingComponent {
}
}

public updateByValueChange(event: any): void {
this.personalInfoFormValue = event;
if (this.userDetailView.personalInfoForm.get(`locked_out`).disabled !== this.lockoutCheckboxDisabled) {
if (this.lockoutCheckboxDisabled) {
this.userDetailView.personalInfoForm.get(`locked_out`).disable();
} else {
this.userDetailView.personalInfoForm.get(`locked_out`).enable();
}
}
}

private async createUser(): Promise<void> {
const partialUser = { ...this.personalInfoFormValue };

Expand Down Expand Up @@ -424,4 +459,8 @@ export class ParticipantDetailViewComponent extends BaseMeetingComponent {
private goToAllUsers(): void {
this.router.navigate([this.activeMeetingId, `participants`]);
}

private checkSelectedGroupsCanManage(): boolean {
return this.usersGroups.some(group => group.hasPermission(Permission.userCanManage));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ <h3>{{ 'Suitable accounts found' | translate }}</h3>
[patchFormValueFn]="patchFormValueFn"
[shouldEnableFormControlFn]="shouldEnableFormControlFn"
[user]="user"
(changeEvent)="personalInfoFormValue = $event"
(changeEvent)="updateByValueChange($event)"
(errorEvent)="formErrors = $event"
(validEvent)="isFormValid = $event"
>
Expand All @@ -153,7 +153,21 @@ <h2>{{ 'Meeting specific information' | translate }}</h2>
>
<span>{{ 'present' | translate }}</span>
</mat-checkbox>
<!-- Locked out? -->
<mat-checkbox
formControlName="locked_out"
matTooltipPosition="right"
matTooltip="{{ 'Lock out user from this meeting.' | translate }}"
>
<span>{{ 'locked out' | translate }}</span>
</mat-checkbox>
</div>
@if (isLockedOutAndCanManage) {
<p class="red-warning-text padding-left-12" translate>
It is not allowed to set the permisson UserCanManage to a locked out user.
Please unset the lockout before adding a group with UserCanManage.
</p>
}
<div>
<!-- Strucuture Level -->
<mat-form-field class="form100 force-min-width">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatStepper } from '@angular/material/stepper';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
import { Id } from 'src/app/domain/definitions/key-types';
import { Permission } from 'src/app/domain/definitions/permission';
import { User } from 'src/app/domain/models/users/user';
import { SearchUsersPresenterService } from 'src/app/gateways/presenter/search-users-presenter.service';
import { createEmailValidator } from 'src/app/infrastructure/utils/validators/email';
Expand Down Expand Up @@ -51,7 +52,8 @@ export class ParticipantCreateWizardComponent extends BaseMeetingComponent imple
group_ids: [``],
vote_delegations_from_ids: [``],
vote_delegated_to_id: [``],
is_present: [``]
is_present: [``],
locked_out: [``]
};

public get randomPasswordFn(): (() => string) | null {
Expand Down Expand Up @@ -319,4 +321,32 @@ export class ParticipantCreateWizardComponent extends BaseMeetingComponent imple
}
}
}

public get isLockedOutAndCanManage(): boolean {
const lockedOutHelper = this.personalInfoFormValue?.locked_out ?? this.user?.is_locked_out;
return lockedOutHelper && this.checkSelectedGroupsCanManage();
}

public get lockoutCheckboxDisabled(): boolean {
const notChanged = (this.personalInfoFormValue?.locked_out ?? null) === null;
const isLockedOut = this.user?.is_locked_out;
return notChanged && !isLockedOut && this.checkSelectedGroupsCanManage();
}

public updateByValueChange(event: any): void {
this.personalInfoFormValue = event;
if (this.detailView.personalInfoForm.get(`locked_out`).disabled !== this.lockoutCheckboxDisabled) {
if (this.lockoutCheckboxDisabled) {
this.detailView.personalInfoForm.get(`locked_out`).disable();
} else {
this.detailView.personalInfoForm.get(`locked_out`).enable();
}
}
}

private checkSelectedGroupsCanManage(): boolean {
return (this.detailView.personalInfoForm.get(`group_ids`).value ?? [])
.map((id: Id): ViewGroup => this.groupRepo.getViewModel(id))
.some(group => group.hasPermission(Permission.userCanManage));
}
}
Loading

0 comments on commit fccfc7d

Please sign in to comment.