Merge pull request #5305 from tsiegleauq/weight-votes

Implement vote weight in client
This commit is contained in:
Emanuel Schütze 2020-04-22 17:28:02 +02:00 committed by GitHub
commit 0aef3f79ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 295 additions and 78 deletions

View File

@ -7,7 +7,7 @@ export abstract class BaseDecimalModel<T = any> extends BaseModel<T> {
if (input && typeof input === 'object') { if (input && typeof input === 'object') {
this.getDecimalFields().forEach(field => { this.getDecimalFields().forEach(field => {
if (input[field] !== undefined) { if (input[field] !== undefined) {
input[field] = parseInt(input[field], 10); input[field] = parseFloat(input[field]);
} }
}); });
} }

View File

@ -37,6 +37,10 @@ export class User extends BaseDecimalModel<User> {
public auth_type?: UserAuthType; public auth_type?: UserAuthType;
public vote_weight: number; public vote_weight: number;
public get isVoteWeightOne(): boolean {
return this.vote_weight === 1;
}
public constructor(input?: Partial<User>) { public constructor(input?: Partial<User>) {
super(User.COLLECTIONSTRING, input); super(User.COLLECTIONSTRING, input);
} }

View File

@ -11,14 +11,13 @@ export class ParsePollNumberPipe implements PipeTransform {
public constructor(private translate: TranslateService) {} public constructor(private translate: TranslateService) {}
public transform(value: number): number | string { public transform(value: number): number | string {
const input = Math.trunc(value); switch (value) {
switch (input) {
case VOTE_MAJORITY: case VOTE_MAJORITY:
return this.translate.instant('majority'); return this.translate.instant('majority');
case VOTE_UNDOCUMENTED: case VOTE_UNDOCUMENTED:
return this.translate.instant('undocumented'); return this.translate.instant('undocumented');
default: default:
return input; return value;
} }
} }
} }

View File

@ -70,8 +70,13 @@
<div *pblNgridCellDef="'user'; row as vote"> <div *pblNgridCellDef="'user'; row as vote">
<div *ngIf="vote.user"> <div *ngIf="vote.user">
{{ vote.user.getShortName() }} {{ vote.user.getShortName() }}
<div class="user-subtitle" *ngIf="vote.user.getLevelAndNumber()"> <div class="user-subtitle">
{{ vote.user.getLevelAndNumber() }} <div *ngIf="vote.user.getLevelAndNumber()">
{{ vote.user.getLevelAndNumber() }}
</div>
<div *ngIf="isVoteWeightActive">
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
</div>
</div> </div>
</div> </div>
<div *ngIf="!vote.user"> <div *ngIf="!vote.user">

View File

@ -10,6 +10,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service'; import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { VoteValue } from 'app/shared/models/poll/base-vote'; import { VoteValue } from 'app/shared/models/poll/base-vote';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component'; import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
@ -32,6 +33,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
public candidatesLabels: string[] = []; public candidatesLabels: string[] = [];
public isVoteWeightActive: boolean;
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -41,12 +44,16 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
groupRepo: GroupRepositoryService, groupRepo: GroupRepositoryService,
prompt: PromptService, prompt: PromptService,
pollDialog: AssignmentPollDialogService, pollDialog: AssignmentPollDialogService,
configService: ConfigService,
protected pollService: AssignmentPollService, protected pollService: AssignmentPollService,
votesRepo: AssignmentVoteRepositoryService, votesRepo: AssignmentVoteRepositoryService,
private operator: OperatorService, private operator: OperatorService,
private router: Router private router: Router
) { ) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo); super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
configService
.get<boolean>('users_activate_vote_weight')
.subscribe(active => (this.isVoteWeightActive = active));
} }
protected createVotesData(): void { protected createVotesData(): void {

View File

@ -45,7 +45,7 @@
[filterProps]="filterProps" [filterProps]="filterProps"
[allowProjector]="false" [allowProjector]="false"
[fullScreen]="true" [fullScreen]="true"
[vScrollFixed]="60" [vScrollFixed]="-1"
listStorageKey="motion-poll-vote" listStorageKey="motion-poll-vote"
[cssClasses]="{ 'single-votes-table': true }" [cssClasses]="{ 'single-votes-table': true }"
> >
@ -56,7 +56,18 @@
<!-- Content --> <!-- Content -->
<div *pblNgridCellDef="'user'; row as vote"> <div *pblNgridCellDef="'user'; row as vote">
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div> <div *ngIf="vote.user">
{{ vote.user.getShortName() }}
<div class="user-subtitle">
<div *ngIf="vote.user.getLevelAndNumber()">
{{ vote.user.getLevelAndNumber() }}
</div>
<div *ngIf="isVoteWeightActive">
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
</div>
</div>
</div>
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div> <div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
</div> </div>
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell"> <div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">

View File

@ -10,6 +10,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service'; import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
@ -41,6 +42,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
public filterProps = ['user.getFullName', 'valueVerbose']; public filterProps = ['user.getFullName', 'valueVerbose'];
public isVoteWeightActive: boolean;
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -52,10 +55,14 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
pollDialog: MotionPollDialogService, pollDialog: MotionPollDialogService,
pollService: MotionPollService, pollService: MotionPollService,
votesRepo: MotionVoteRepositoryService, votesRepo: MotionVoteRepositoryService,
configService: ConfigService,
private operator: OperatorService, private operator: OperatorService,
private router: Router private router: Router
) { ) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo); super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
configService
.get<boolean>('users_activate_vote_weight')
.subscribe(active => (this.isVoteWeightActive = active));
} }
protected createVotesData(): void { protected createVotesData(): void {

View File

@ -381,7 +381,12 @@ export class MotionPdfService {
column1.push(`${votingOption}:`); column1.push(`${votingOption}:`);
if (value.showPercent) { if (value.showPercent) {
const resultInPercent = this.motionPollService.getVoteValueInPercent(value.amount, poll); const resultInPercent = this.motionPollService.getVoteValueInPercent(value.amount, poll);
column2.push(`(${resultInPercent})`); // hard check for "null" since 0 is a valid number in this case
if (resultInPercent !== null) {
column2.push(`(${resultInPercent})`);
} else {
column2.push('');
}
} else { } else {
column2.push(''); column2.push('');
} }

View File

@ -111,7 +111,13 @@
<div> <div>
<!-- Strucuture Level --> <!-- Strucuture Level -->
<mat-form-field class="form70 distance"> <mat-form-field
class="distance"
[ngClass]="{
form37: showVoteWeight,
form70: !showVoteWeight
}"
>
<input <input
type="text" type="text"
matInput matInput
@ -119,8 +125,15 @@
formControlName="structure_level" formControlName="structure_level"
/> />
</mat-form-field> </mat-form-field>
<!-- Participant Number --> <!-- Participant Number -->
<mat-form-field class="form25 force-min-with"> <mat-form-field
[ngClass]="{
distance: showVoteWeight,
form37: showVoteWeight,
form25: !showVoteWeight
}"
>
<input <input
type="text" type="text"
matInput matInput
@ -128,6 +141,17 @@
formControlName="number" formControlName="number"
/> />
</mat-form-field> </mat-form-field>
<!-- Vote weight -->
<mat-form-field class="form16 force-min-with" *ngIf="showVoteWeight">
<!-- TODO Input type should be number with limited decimal spaces -->
<input
type="number"
matInput
placeholder="{{ 'Vote weight' | translate }}"
formControlName="vote_weight"
/>
</mat-form-field>
</div> </div>
<div> <div>
@ -275,6 +299,12 @@
</div> </div>
<div *ngIf="isAllowed('manage')"> <div *ngIf="isAllowed('manage')">
<!-- Vote weight -->
<div *ngIf="user.vote_weight && showVoteWeight">
<h4>{{ 'Vote weight' | translate }}</h4>
<span>{{ user.vote_weight }}</span>
</div>
<!-- Initial Password --> <!-- Initial Password -->
<div *ngIf="user.default_password"> <div *ngIf="user.default_password">
<h4>{{ 'Initial password' | translate }}</h4> <h4>{{ 'Initial password' | translate }}</h4>

View File

@ -11,10 +11,12 @@ import { ConstantsService } from 'app/core/core-services/constants.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { genders } from 'app/shared/models/users/user'; import { genders } from 'app/shared/models/users/user';
import { OneOfValidator } from 'app/shared/validators/one-of-validator'; import { OneOfValidator } from 'app/shared/validators/one-of-validator';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { PollService } from 'app/site/polls/services/poll.service';
import { UserPdfExportService } from '../../services/user-pdf-export.service'; import { UserPdfExportService } from '../../services/user-pdf-export.service';
import { ViewGroup } from '../../models/view-group'; import { ViewGroup } from '../../models/view-group';
import { ViewUser } from '../../models/view-user'; import { ViewUser } from '../../models/view-user';
@ -76,6 +78,12 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
private userBackends: UserBackends | null = null; private userBackends: UserBackends | null = null;
private isVoteWeightActive: boolean;
public get showVoteWeight(): boolean {
return this.pollService.isElectronicVotingEnabled && this.isVoteWeightActive;
}
/** /**
* Constructor for user * Constructor for user
* *
@ -103,12 +111,17 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
private promptService: PromptService, private promptService: PromptService,
private pdfService: UserPdfExportService, private pdfService: UserPdfExportService,
private groupRepo: GroupRepositoryService, private groupRepo: GroupRepositoryService,
private constantsService: ConstantsService private constantsService: ConstantsService,
private pollService: PollService,
configService: ConfigService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.createForm(); this.createForm();
this.constantsService.get<UserBackends>('UserBackends').subscribe(backends => (this.userBackends = backends)); this.constantsService.get<UserBackends>('UserBackends').subscribe(backends => (this.userBackends = backends));
configService
.get<boolean>('users_activate_vote_weight')
.subscribe(active => (this.isVoteWeightActive = active));
this.groupRepo.getViewModelListObservableWithoutDefaultGroup().subscribe(this.groups); this.groupRepo.getViewModelListObservableWithoutDefaultGroup().subscribe(this.groups);
} }
@ -157,6 +170,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
gender: [''], gender: [''],
structure_level: [''], structure_level: [''],
number: [''], number: [''],
vote_weight: [],
about_me: [''], about_me: [''],
groups_id: [''], groups_id: [''],
is_present: [true], is_present: [true],

View File

@ -51,20 +51,9 @@
</span> </span>
<br /> <br />
<div class="code red-warning-text"> <div class="code red-warning-text">
<span>{{ 'Title' | translate }}</span <span *ngFor="let entry of headerRow; let last = last">
>, <span>{{ 'Given name' | translate }}</span {{ entry | translate }}<span *ngIf="!last">, </span>
>, <span>{{ 'Surname' | translate }}</span </span>
>, <span>{{ 'Structure level' | translate }}</span
>, <span>{{ 'Participant number' | translate }}</span
>, <span>{{ 'Groups' | translate }}</span
>, <span>{{ 'Comment' | translate }}</span
>, <span>{{ 'Is active' | translate }}</span
>, <span>{{ 'Is present' | translate }}</span
>, <span>{{ 'Is committee' | translate }}</span
>, <span>{{ 'Initial password' | translate }}</span
>, <span>{{ 'Email' | translate }}</span
>, <span>{{ 'Username' | translate }}</span
>, <span>{{ 'Gender' | translate }}</span>
</div> </div>
<ul> <ul>
<li> <li>
@ -282,35 +271,46 @@
<mat-checkbox disabled [checked]="entry.newEntry.is_active"> </mat-checkbox> <mat-checkbox disabled [checked]="entry.newEntry.is_active"> </mat-checkbox>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="is_present"> <ng-container matColumnDef="is_present">
<mat-header-cell *matHeaderCellDef>{{ 'Is present' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Is present' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> <mat-cell *matCellDef="let entry">
<mat-checkbox disabled [checked]="entry.newEntry.is_present"> </mat-checkbox> <mat-checkbox disabled [checked]="entry.newEntry.is_present"> </mat-checkbox>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="is_committee"> <ng-container matColumnDef="is_committee">
<mat-header-cell *matHeaderCellDef>{{ 'Is committee' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Is committee' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> <mat-cell *matCellDef="let entry">
<mat-checkbox disabled [checked]="entry.newEntry.is_committee"> </mat-checkbox> <mat-checkbox disabled [checked]="entry.newEntry.is_committee"> </mat-checkbox>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="default_password"> <ng-container matColumnDef="default_password">
<mat-header-cell *matHeaderCellDef>{{ 'Initial password' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Initial password' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.default_password }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.default_password }} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="email"> <ng-container matColumnDef="email">
<mat-header-cell *matHeaderCellDef>{{ 'Email' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Email' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.email }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.email }} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="username"> <ng-container matColumnDef="username">
<mat-header-cell *matHeaderCellDef>{{ 'Username' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Username' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.username }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.username }} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="gender"> <ng-container matColumnDef="gender">
<mat-header-cell *matHeaderCellDef>{{ 'Gender' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Gender' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.gender }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.gender }} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="vote_weight">
<mat-header-cell *matHeaderCellDef>{{ 'Vote weight' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.vote_weight }} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row [ngClass]="getStateClass(row)" *matRowDef="let row; columns: getColumnDefinition()"> </mat-row> <mat-row [ngClass]="getStateClass(row)" *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>
</table> </table>

View File

@ -21,6 +21,24 @@ import { UserImportService } from '../../services/user-import.service';
export class UserImportListComponent extends BaseImportListComponentDirective<User> { export class UserImportListComponent extends BaseImportListComponentDirective<User> {
public textAreaForm: FormGroup; public textAreaForm: FormGroup;
public headerRow = [
'Title',
'Given name',
'Surname',
'Structure level',
'Participant number',
'Groups',
'Comment',
'Is active',
'Is present',
'Is a committee',
'Initial password',
'Email',
'Username',
'Gender',
'Vote weight'
];
/** /**
* Constructor for list view bases * Constructor for list view bases
* *
@ -47,22 +65,6 @@ export class UserImportListComponent extends BaseImportListComponentDirective<Us
* Triggers an example csv download * Triggers an example csv download
*/ */
public downloadCsvExample(): void { public downloadCsvExample(): void {
const headerRow = [
'Title',
'Given name',
'Surname',
'Structure level',
'Participant number',
'Groups',
'Comment',
'Is active',
'Is present',
'Is a committee',
'Initial password',
'Email',
'Username',
'Gender'
];
const rows = [ const rows = [
[ [
'Dr.', 'Dr.',
@ -78,7 +80,8 @@ export class UserImportListComponent extends BaseImportListComponentDirective<Us
'initialPassword', 'initialPassword',
null, null,
'mmustermann', 'mmustermann',
'm' 'm',
1.0
], ],
[ [
null, null,
@ -94,12 +97,13 @@ export class UserImportListComponent extends BaseImportListComponentDirective<Us
null, null,
'john.doe@email.com', 'john.doe@email.com',
'jdoe', 'jdoe',
'diverse' 'diverse',
2.0
], ],
[null, 'Julia', 'Bloggs', 'London', null, null, null, null, null, null, null, null, 'jbloggs', 'f'], [null, 'Julia', 'Bloggs', 'London', null, null, null, null, null, null, null, null, 'jbloggs', 'f', 1.5],
[null, null, 'Executive Board', null, null, null, null, null, null, 1, null, null, 'executive', null] [null, null, 'Executive Board', null, null, null, null, null, null, 1, null, null, 'executive', null, 2.5]
]; ];
this.exporter.dummyCSVExport(headerRow, rows, `${this.translate.instant('participants-example')}.csv`); this.exporter.dummyCSVExport(this.headerRow, rows, `${this.translate.instant('participants-example')}.csv`);
} }
/** /**

View File

@ -30,18 +30,26 @@
(dataSourceChange)="onDataSourceChange($event)" (dataSourceChange)="onDataSourceChange($event)"
> >
<!-- Name column --> <!-- Name column -->
<div *pblNgridCellDef="'short_name'; value as name; row as user; rowContext as rowContext" class="cell-slot fill"> <div *pblNgridCellDef="'short_name'; row as user; rowContext as rowContext" class="cell-slot fill">
<a class="detail-link" [routerLink]="user.id" *ngIf="!isMultiSelect"></a> <a class="detail-link" [routerLink]="user.id" *ngIf="!isMultiSelect"></a>
<div class="nameCell"> <div class="nameCell">
<span>{{ name }}</span> <div>
</div> <div>{{ user.short_name }}</div>
<div class="icon-group"> <div class="user-subtitle" *ngIf="showVoteWeight">
<mat-icon matTooltip="{{ 'Is committee' | translate }}" *ngIf="user.is_committee">account_balance</mat-icon> {{ 'Vote weight' | translate }}: {{ user.vote_weight }}
<mat-icon </div>
matTooltip="{{ 'Inactive' | translate }}" </div>
*ngIf="!user.is_active && this.operator.hasPerms('users.see_extra')" <div class="icon-group">
>block</mat-icon <mat-icon matTooltip="{{ 'Is committee' | translate }}" *ngIf="user.is_committee">
> account_balance
</mat-icon>
<mat-icon
matTooltip="{{ 'Inactive' | translate }}"
*ngIf="!user.is_active && this.operator.hasPerms('users.see_extra')"
>
block
</mat-icon>
</div>
</div> </div>
</div> </div>

View File

@ -29,5 +29,6 @@
} }
.icon-group { .icon-group {
margin-left: 1em;
z-index: 3; z-index: 3;
} }

View File

@ -19,6 +19,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { genders } from 'app/shared/models/users/user'; import { genders } from 'app/shared/models/users/user';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings'; import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseListViewComponent } from 'app/site/base/base-list-view'; import { BaseListViewComponent } from 'app/site/base/base-list-view';
import { PollService } from 'app/site/polls/services/poll.service';
import { UserFilterListService } from '../../services/user-filter-list.service'; import { UserFilterListService } from '../../services/user-filter-list.service';
import { UserPdfExportService } from '../../services/user-pdf-export.service'; import { UserPdfExportService } from '../../services/user-pdf-export.service';
import { UserSortListService } from '../../services/user-sort-list.service'; import { UserSortListService } from '../../services/user-sort-list.service';
@ -98,6 +99,8 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
return this._presenceViewConfigured && this.operator.hasPerms('users.can_manage'); return this._presenceViewConfigured && this.operator.hasPerms('users.can_manage');
} }
private isVoteWeightActive: boolean;
/** /**
* Helper to check for main button permissions * Helper to check for main button permissions
* *
@ -107,6 +110,10 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
return this.operator.hasPerms('users.can_manage'); return this.operator.hasPerms('users.can_manage');
} }
public get showVoteWeight(): boolean {
return this.pollService.isElectronicVotingEnabled && this.isVoteWeightActive;
}
/** /**
* Define the columns to show * Define the columns to show
*/ */
@ -173,13 +180,15 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
public sortService: UserSortListService, public sortService: UserSortListService,
config: ConfigService, config: ConfigService,
private userPdf: UserPdfExportService, private userPdf: UserPdfExportService,
private dialog: MatDialog private dialog: MatDialog,
private pollService: PollService
) { ) {
super(titleService, translate, matSnackBar, storage); super(titleService, translate, matSnackBar, storage);
// enable multiSelect for this listView // enable multiSelect for this listView
this.canMultiSelect = true; this.canMultiSelect = true;
config.get<boolean>('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state)); config.get<boolean>('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state));
config.get<boolean>('users_activate_vote_weight').subscribe(active => (this.isVoteWeightActive = active));
config.get<boolean>(this.selfPresentConfStr).subscribe(allowed => (this.allowSelfSetPresent = allowed)); config.get<boolean>(this.selfPresentConfStr).subscribe(allowed => (this.allowSelfSetPresent = allowed));
} }

View File

@ -85,6 +85,14 @@ export class UserFilterListService extends BaseFilterListService<ViewUser> {
{ condition: true, label: this.translate.instant('Got an email') }, { condition: true, label: this.translate.instant('Got an email') },
{ condition: false, label: this.translate.instant("Didn't get an email") } { condition: false, label: this.translate.instant("Didn't get an email") }
] ]
},
{
property: 'isVoteWeightOne',
label: this.translate.instant('Vote Weight'),
options: [
{ condition: false, label: this.translate.instant('Has changed vote weight') },
{ condition: true, label: this.translate.instant('Has unchanged vote weight') }
]
} }
]; ];
return staticFilterOptions.concat(this.userGroupFilterOptions); return staticFilterOptions.concat(this.userGroupFilterOptions);

View File

@ -33,7 +33,8 @@ export class UserImportService extends BaseImportService<User> {
'default_password', 'default_password',
'email', 'email',
'username', 'username',
'gender' 'gender',
'vote_weight'
]; ];
/** /**
@ -117,6 +118,13 @@ export class UserImportService extends BaseImportService<User> {
case 'number': case 'number':
newViewUser.number = line[idx]; newViewUser.number = line[idx];
break; break;
case 'vote_weight':
if (!line[idx]) {
newViewUser[this.expectedHeader[idx]] = 1;
} else {
newViewUser[this.expectedHeader[idx]] = line[idx];
}
break;
default: default:
newViewUser[this.expectedHeader[idx]] = line[idx]; newViewUser[this.expectedHeader[idx]] = line[idx];
break; break;

View File

@ -31,6 +31,7 @@ export class UserSortListService extends BaseSortListService<ViewUser> {
{ property: 'is_committee', label: 'Is committee' }, { property: 'is_committee', label: 'Is committee' },
{ property: 'number', label: 'Participant number' }, { property: 'number', label: 'Participant number' },
{ property: 'structure_level', label: 'Structure level' }, { property: 'structure_level', label: 'Structure level' },
{ property: 'vote_weight', label: 'Vote weight' },
{ property: 'comment' } { property: 'comment' }
// TODO email send? // TODO email send?
]; ];

View File

@ -3,6 +3,7 @@ from decimal import Decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
from openslides.core.config import config
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
from openslides.utils.auth import has_perm from openslides.utils.auth import has_perm
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
@ -489,14 +490,20 @@ class AssignmentPollViewSet(BasePollViewSet):
# skip creating votes with empty weights # skip creating votes with empty weights
if amount == 0: if amount == 0:
continue continue
weight = Decimal(amount)
if config["users_activate_vote_weight"]:
weight *= user.vote_weight
vote = AssignmentVote.objects.create( vote = AssignmentVote.objects.create(
option=option, user=user, weight=Decimal(amount), value="Y" option=option, user=user, weight=weight, value="Y"
) )
inform_changed_data(vote, no_delete_on_restriction=True) inform_changed_data(vote, no_delete_on_restriction=True)
else: # global_no or global_abstain else: # global_no or global_abstain
option = options[0] option = options[0]
weight = (
user.vote_weight if config["users_activate_vote_weight"] else Decimal(1)
)
vote = AssignmentVote.objects.create( vote = AssignmentVote.objects.create(
option=option, user=user, weight=Decimal(1), value=data option=option, user=user, weight=weight, value=data
) )
inform_changed_data(vote, no_delete_on_restriction=True) inform_changed_data(vote, no_delete_on_restriction=True)
inform_changed_data(option) inform_changed_data(option)
@ -512,13 +519,15 @@ class AssignmentPollViewSet(BasePollViewSet):
vote_user is the one put into the vote vote_user is the one put into the vote
""" """
options = poll.get_options() options = poll.get_options()
weight = (
check_user.vote_weight
if config["users_activate_vote_weight"]
else Decimal(1)
)
for option_id, result in data.items(): for option_id, result in data.items():
option = options.get(pk=option_id) option = options.get(pk=option_id)
vote = AssignmentVote.objects.create( vote = AssignmentVote.objects.create(
option=option, option=option, user=vote_user, value=result, weight=weight,
user=vote_user,
value=result,
weight=check_user.vote_weight,
) )
inform_changed_data(vote, no_delete_on_restriction=True) inform_changed_data(vote, no_delete_on_restriction=True)
inform_changed_data(option, no_delete_on_restriction=True) inform_changed_data(option, no_delete_on_restriction=True)

View File

@ -1,3 +1,4 @@
from decimal import Decimal
from typing import List, Set from typing import List, Set
import jsonschema import jsonschema
@ -1227,16 +1228,20 @@ class MotionPollViewSet(BasePollViewSet):
VotedModel.objects.create(motionpoll=poll, user=user) VotedModel.objects.create(motionpoll=poll, user=user)
def handle_named_vote(self, data, poll, user): def handle_named_vote(self, data, poll, user):
self.handle_named_and_pseudoanonymous_vote(data, user.vote_weight, user, poll) self.handle_named_and_pseudoanonymous_vote(data, user, user, poll)
def handle_pseudoanonymous_vote(self, data, poll, user): def handle_pseudoanonymous_vote(self, data, poll, user):
self.handle_named_and_pseudoanonymous_vote(data, user.vote_weight, None, poll) self.handle_named_and_pseudoanonymous_vote(data, user, None, poll)
def handle_named_and_pseudoanonymous_vote(self, data, weight, user, poll): def handle_named_and_pseudoanonymous_vote(self, data, weight_user, vote_user, poll):
option = poll.options.get() option = poll.options.get()
vote = MotionVote.objects.create(user=user, option=option) vote = MotionVote.objects.create(user=vote_user, option=option)
vote.value = data vote.value = data
vote.weight = weight vote.weight = (
weight_user.vote_weight
if config["users_activate_vote_weight"]
else Decimal(1)
)
vote.save(no_delete_on_restriction=True) vote.save(no_delete_on_restriction=True)
inform_changed_data(option) inform_changed_data(option)

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from ..core.config import config
from ..utils.autoupdate import inform_changed_data, inform_deleted_data from ..utils.autoupdate import inform_changed_data, inform_deleted_data
from ..utils.models import SET_NULL_AND_AUTOUPDATE from ..utils.models import SET_NULL_AND_AUTOUPDATE
@ -184,7 +185,7 @@ class BasePoll(models.Model):
if self.type == self.TYPE_ANALOG: if self.type == self.TYPE_ANALOG:
return self.db_votesvalid return self.db_votesvalid
else: else:
return Decimal(self.amount_users_voted()) return Decimal(self.amount_users_voted_with_individual_weight())
def set_votesvalid(self, value): def set_votesvalid(self, value):
if self.type != self.TYPE_ANALOG: if self.type != self.TYPE_ANALOG:
@ -210,7 +211,7 @@ class BasePoll(models.Model):
if self.type == self.TYPE_ANALOG: if self.type == self.TYPE_ANALOG:
return self.db_votescast return self.db_votescast
else: else:
return Decimal(self.amount_users_voted()) return Decimal(self.amount_users_voted_with_individual_weight())
def set_votescast(self, value): def set_votescast(self, value):
if self.type != self.TYPE_ANALOG: if self.type != self.TYPE_ANALOG:
@ -219,8 +220,11 @@ class BasePoll(models.Model):
votescast = property(get_votescast, set_votescast) votescast = property(get_votescast, set_votescast)
def amount_users_voted(self): def amount_users_voted_with_individual_weight(self):
return len(self.voted.all()) if config["users_activate_vote_weight"]:
return sum(user.vote_weight for user in self.voted.all())
else:
return len(self.voted.all())
def create_options(self): def create_options(self):
""" Should be called after creation of this model. """ """ Should be called after creation of this model. """

View File

@ -44,6 +44,15 @@ def get_config_variables():
group="Participants", group="Participants",
) )
yield ConfigVariable(
name="users_activate_vote_weight",
default_value=False,
input_type="boolean",
label="Activate vote weight",
weight=513,
group="Participants",
)
# PDF # PDF
yield ConfigVariable( yield ConfigVariable(

View File

@ -15,6 +15,7 @@ from openslides.assignments.models import (
AssignmentPoll, AssignmentPoll,
AssignmentVote, AssignmentVote,
) )
from openslides.core.config import config
from openslides.poll.models import BasePoll from openslides.poll.models import BasePoll
from openslides.utils.auth import get_group_model from openslides.utils.auth import get_group_model
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
@ -982,6 +983,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.assertEqual(poll.votesinvalid, Decimal("0")) self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1")) self.assertEqual(poll.votescast, Decimal("1"))
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.amount_users_voted_with_individual_weight(), Decimal("1"))
option1 = poll.options.get(pk=1) option1 = poll.options.get(pk=1)
option2 = poll.options.get(pk=2) option2 = poll.options.get(pk=2)
option3 = poll.options.get(pk=3) option3 = poll.options.get(pk=3)
@ -995,6 +997,43 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.assertEqual(option3.no, Decimal("0")) self.assertEqual(option3.no, Decimal("0"))
self.assertEqual(option3.abstain, Decimal("1")) self.assertEqual(option3.abstain, Decimal("1"))
def test_vote_with_voteweight(self):
config["users_activate_vote_weight"] = True
self.admin.vote_weight = weight = Decimal("4.2")
self.admin.save()
self.add_candidate()
self.add_candidate()
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "2": "N", "3": "A"},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 3)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.votesvalid, weight)
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, weight)
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
option1 = poll.options.get(pk=1)
option2 = poll.options.get(pk=2)
option3 = poll.options.get(pk=3)
self.assertEqual(option1.yes, weight)
self.assertEqual(option1.no, Decimal("0"))
self.assertEqual(option1.abstain, Decimal("0"))
self.assertEqual(option2.yes, Decimal("0"))
self.assertEqual(option2.no, weight)
self.assertEqual(option2.abstain, Decimal("0"))
self.assertEqual(option3.yes, Decimal("0"))
self.assertEqual(option3.no, Decimal("0"))
self.assertEqual(option3.abstain, weight)
def test_vote_without_voteweight(self):
self.admin.vote_weight = Decimal("4.2")
self.admin.save()
self.test_vote()
def test_change_vote(self): def test_change_vote(self):
self.start_poll() self.start_poll()
response = self.client.post( response = self.client.post(
@ -2233,7 +2272,7 @@ class PseudoanonymizeAssignmentPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get() poll = AssignmentPoll.objects.get()
self.assertEqual(poll.get_votes().count(), 2) self.assertEqual(poll.get_votes().count(), 2)
self.assertEqual(poll.amount_users_voted(), 2) self.assertEqual(poll.amount_users_voted_with_individual_weight(), 2)
self.assertEqual(poll.votesvalid, Decimal("2")) self.assertEqual(poll.votesvalid, Decimal("2"))
self.assertEqual(poll.votesinvalid, Decimal("0")) self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("2")) self.assertEqual(poll.votescast, Decimal("2"))

View File

@ -763,6 +763,36 @@ class VoteMotionPollNamed(TestCase):
self.assertEqual(option.abstain, Decimal("0")) self.assertEqual(option.abstain, Decimal("0"))
vote = option.votes.get() vote = option.votes.get()
self.assertEqual(vote.user, self.admin) self.assertEqual(vote.user, self.admin)
self.assertEqual(vote.weight, Decimal("1"))
def test_vote_with_voteweight(self):
config["users_activate_vote_weight"] = True
self.start_poll()
self.make_admin_delegate()
self.make_admin_present()
self.admin.vote_weight = weight = Decimal("3.5")
self.admin.save()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.votesvalid, weight)
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, weight)
self.assertEqual(poll.get_votes().count(), 1)
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
option = poll.options.get()
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("0"))
self.assertEqual(option.abstain, weight)
vote = option.votes.get()
self.assertEqual(vote.weight, weight)
def test_vote_without_voteweight(self):
self.admin.vote_weight = Decimal("3.5")
self.admin.save()
self.test_vote()
def test_change_vote(self): def test_change_vote(self):
self.start_poll() self.start_poll()
@ -1155,7 +1185,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.assertEqual(poll.votesinvalid, Decimal("0")) self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1")) self.assertEqual(poll.votescast, Decimal("1"))
self.assertEqual(poll.get_votes().count(), 1) self.assertEqual(poll.get_votes().count(), 1)
self.assertEqual(poll.amount_users_voted(), 1) self.assertEqual(poll.amount_users_voted_with_individual_weight(), 1)
option = poll.options.get() option = poll.options.get()
self.assertEqual(option.yes, Decimal("0")) self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("1")) self.assertEqual(option.no, Decimal("1"))
@ -1378,7 +1408,7 @@ class PseudoanonymizeMotionPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
self.assertEqual(poll.get_votes().count(), 2) self.assertEqual(poll.get_votes().count(), 2)
self.assertEqual(poll.amount_users_voted(), 2) self.assertEqual(poll.amount_users_voted_with_individual_weight(), 2)
self.assertEqual(poll.votesvalid, Decimal("2")) self.assertEqual(poll.votesvalid, Decimal("2"))
self.assertEqual(poll.votesinvalid, Decimal("0")) self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("2")) self.assertEqual(poll.votescast, Decimal("2"))
@ -1446,7 +1476,7 @@ class ResetMotionPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
self.assertEqual(poll.get_votes().count(), 0) self.assertEqual(poll.get_votes().count(), 0)
self.assertEqual(poll.amount_users_voted(), 0) self.assertEqual(poll.amount_users_voted_with_individual_weight(), 0)
self.assertEqual(poll.votesvalid, None) self.assertEqual(poll.votesvalid, None)
self.assertEqual(poll.votesinvalid, None) self.assertEqual(poll.votesinvalid, None)
self.assertEqual(poll.votescast, None) self.assertEqual(poll.votescast, None)