Implement vote weight in client

Implements vote weight in client
The user detail page has a new property
change deserialize to parse floats
change "yes"-voting to send "Y" and "0" instead of "1" and "0"
add vote weight to user list, filter, sort
add vote weight to single voting result
votesvalid and votescast respect the individual vote weight
fix parse-poll pipe and null in pdf
This commit is contained in:
Sean 2020-04-22 16:54:50 +02:00
parent 0f3d07f151
commit 97c2299aec
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)