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:
parent
0f3d07f151
commit
97c2299aec
@ -7,7 +7,7 @@ export abstract class BaseDecimalModel<T = any> extends BaseModel<T> {
|
||||
if (input && typeof input === 'object') {
|
||||
this.getDecimalFields().forEach(field => {
|
||||
if (input[field] !== undefined) {
|
||||
input[field] = parseInt(input[field], 10);
|
||||
input[field] = parseFloat(input[field]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ export class User extends BaseDecimalModel<User> {
|
||||
public auth_type?: UserAuthType;
|
||||
public vote_weight: number;
|
||||
|
||||
public get isVoteWeightOne(): boolean {
|
||||
return this.vote_weight === 1;
|
||||
}
|
||||
|
||||
public constructor(input?: Partial<User>) {
|
||||
super(User.COLLECTIONSTRING, input);
|
||||
}
|
||||
|
@ -11,14 +11,13 @@ export class ParsePollNumberPipe implements PipeTransform {
|
||||
public constructor(private translate: TranslateService) {}
|
||||
|
||||
public transform(value: number): number | string {
|
||||
const input = Math.trunc(value);
|
||||
switch (input) {
|
||||
switch (value) {
|
||||
case VOTE_MAJORITY:
|
||||
return this.translate.instant('majority');
|
||||
case VOTE_UNDOCUMENTED:
|
||||
return this.translate.instant('undocumented');
|
||||
default:
|
||||
return input;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,8 +70,13 @@
|
||||
<div *pblNgridCellDef="'user'; row as vote">
|
||||
<div *ngIf="vote.user">
|
||||
{{ vote.user.getShortName() }}
|
||||
<div class="user-subtitle" *ngIf="vote.user.getLevelAndNumber()">
|
||||
{{ vote.user.getLevelAndNumber() }}
|
||||
<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">
|
||||
|
@ -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 { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-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 { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
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 isVoteWeightActive: boolean;
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
@ -41,12 +44,16 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
groupRepo: GroupRepositoryService,
|
||||
prompt: PromptService,
|
||||
pollDialog: AssignmentPollDialogService,
|
||||
configService: ConfigService,
|
||||
protected pollService: AssignmentPollService,
|
||||
votesRepo: AssignmentVoteRepositoryService,
|
||||
private operator: OperatorService,
|
||||
private router: Router
|
||||
) {
|
||||
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 {
|
||||
|
@ -45,7 +45,7 @@
|
||||
[filterProps]="filterProps"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="true"
|
||||
[vScrollFixed]="60"
|
||||
[vScrollFixed]="-1"
|
||||
listStorageKey="motion-poll-vote"
|
||||
[cssClasses]="{ 'single-votes-table': true }"
|
||||
>
|
||||
@ -56,7 +56,18 @@
|
||||
|
||||
<!-- Content -->
|
||||
<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>
|
||||
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
||||
|
@ -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 { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-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 { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
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 isVoteWeightActive: boolean;
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
@ -52,10 +55,14 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
|
||||
pollDialog: MotionPollDialogService,
|
||||
pollService: MotionPollService,
|
||||
votesRepo: MotionVoteRepositoryService,
|
||||
configService: ConfigService,
|
||||
private operator: OperatorService,
|
||||
private router: Router
|
||||
) {
|
||||
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 {
|
||||
|
@ -381,7 +381,12 @@ export class MotionPdfService {
|
||||
column1.push(`${votingOption}:`);
|
||||
if (value.showPercent) {
|
||||
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 {
|
||||
column2.push('');
|
||||
}
|
||||
|
@ -111,7 +111,13 @@
|
||||
|
||||
<div>
|
||||
<!-- Strucuture Level -->
|
||||
<mat-form-field class="form70 distance">
|
||||
<mat-form-field
|
||||
class="distance"
|
||||
[ngClass]="{
|
||||
form37: showVoteWeight,
|
||||
form70: !showVoteWeight
|
||||
}"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
@ -119,8 +125,15 @@
|
||||
formControlName="structure_level"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Participant Number -->
|
||||
<mat-form-field class="form25 force-min-with">
|
||||
<mat-form-field
|
||||
[ngClass]="{
|
||||
distance: showVoteWeight,
|
||||
form37: showVoteWeight,
|
||||
form25: !showVoteWeight
|
||||
}"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
@ -128,6 +141,17 @@
|
||||
formControlName="number"
|
||||
/>
|
||||
</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>
|
||||
@ -275,6 +299,12 @@
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
<div *ngIf="user.default_password">
|
||||
<h4>{{ 'Initial password' | translate }}</h4>
|
||||
|
@ -11,10 +11,12 @@ import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-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 { genders } from 'app/shared/models/users/user';
|
||||
import { OneOfValidator } from 'app/shared/validators/one-of-validator';
|
||||
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 { ViewGroup } from '../../models/view-group';
|
||||
import { ViewUser } from '../../models/view-user';
|
||||
@ -76,6 +78,12 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
|
||||
|
||||
private userBackends: UserBackends | null = null;
|
||||
|
||||
private isVoteWeightActive: boolean;
|
||||
|
||||
public get showVoteWeight(): boolean {
|
||||
return this.pollService.isElectronicVotingEnabled && this.isVoteWeightActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for user
|
||||
*
|
||||
@ -103,12 +111,17 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
|
||||
private promptService: PromptService,
|
||||
private pdfService: UserPdfExportService,
|
||||
private groupRepo: GroupRepositoryService,
|
||||
private constantsService: ConstantsService
|
||||
private constantsService: ConstantsService,
|
||||
private pollService: PollService,
|
||||
configService: ConfigService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
this.createForm();
|
||||
|
||||
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);
|
||||
}
|
||||
@ -157,6 +170,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
|
||||
gender: [''],
|
||||
structure_level: [''],
|
||||
number: [''],
|
||||
vote_weight: [],
|
||||
about_me: [''],
|
||||
groups_id: [''],
|
||||
is_present: [true],
|
||||
|
@ -51,20 +51,9 @@
|
||||
</span>
|
||||
<br />
|
||||
<div class="code red-warning-text">
|
||||
<span>{{ 'Title' | translate }}</span
|
||||
>, <span>{{ 'Given name' | translate }}</span
|
||||
>, <span>{{ 'Surname' | translate }}</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>
|
||||
<span *ngFor="let entry of headerRow; let last = last">
|
||||
{{ entry | translate }}<span *ngIf="!last">, </span>
|
||||
</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
@ -282,35 +271,46 @@
|
||||
<mat-checkbox disabled [checked]="entry.newEntry.is_active"> </mat-checkbox>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="is_present">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Is present' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let entry">
|
||||
<mat-checkbox disabled [checked]="entry.newEntry.is_present"> </mat-checkbox>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="is_committee">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Is committee' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let entry">
|
||||
<mat-checkbox disabled [checked]="entry.newEntry.is_committee"> </mat-checkbox>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="default_password">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Initial password' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.default_password }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="email">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Email' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.email }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Username' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.username }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="gender">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Gender' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.gender }} </mat-cell>
|
||||
</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-row [ngClass]="getStateClass(row)" *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>
|
||||
</table>
|
||||
|
@ -21,6 +21,24 @@ import { UserImportService } from '../../services/user-import.service';
|
||||
export class UserImportListComponent extends BaseImportListComponentDirective<User> {
|
||||
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
|
||||
*
|
||||
@ -47,22 +65,6 @@ export class UserImportListComponent extends BaseImportListComponentDirective<Us
|
||||
* Triggers an example csv download
|
||||
*/
|
||||
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 = [
|
||||
[
|
||||
'Dr.',
|
||||
@ -78,7 +80,8 @@ export class UserImportListComponent extends BaseImportListComponentDirective<Us
|
||||
'initialPassword',
|
||||
null,
|
||||
'mmustermann',
|
||||
'm'
|
||||
'm',
|
||||
1.0
|
||||
],
|
||||
[
|
||||
null,
|
||||
@ -94,12 +97,13 @@ export class UserImportListComponent extends BaseImportListComponentDirective<Us
|
||||
null,
|
||||
'john.doe@email.com',
|
||||
'jdoe',
|
||||
'diverse'
|
||||
'diverse',
|
||||
2.0
|
||||
],
|
||||
[null, 'Julia', 'Bloggs', 'London', null, null, null, null, null, null, null, null, 'jbloggs', 'f'],
|
||||
[null, null, 'Executive Board', null, null, null, null, null, null, 1, null, null, 'executive', null]
|
||||
[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, 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`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,18 +30,26 @@
|
||||
(dataSourceChange)="onDataSourceChange($event)"
|
||||
>
|
||||
<!-- 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>
|
||||
<div class="nameCell">
|
||||
<span>{{ name }}</span>
|
||||
</div>
|
||||
<div class="icon-group">
|
||||
<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>{{ user.short_name }}</div>
|
||||
<div class="user-subtitle" *ngIf="showVoteWeight">
|
||||
{{ 'Vote weight' | translate }}: {{ user.vote_weight }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-group">
|
||||
<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>
|
||||
|
||||
|
@ -29,5 +29,6 @@
|
||||
}
|
||||
|
||||
.icon-group {
|
||||
margin-left: 1em;
|
||||
z-index: 3;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { genders } from 'app/shared/models/users/user';
|
||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
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 { UserPdfExportService } from '../../services/user-pdf-export.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');
|
||||
}
|
||||
|
||||
private isVoteWeightActive: boolean;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
public get showVoteWeight(): boolean {
|
||||
return this.pollService.isElectronicVotingEnabled && this.isVoteWeightActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the columns to show
|
||||
*/
|
||||
@ -173,13 +180,15 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
||||
public sortService: UserSortListService,
|
||||
config: ConfigService,
|
||||
private userPdf: UserPdfExportService,
|
||||
private dialog: MatDialog
|
||||
private dialog: MatDialog,
|
||||
private pollService: PollService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, storage);
|
||||
|
||||
// enable multiSelect for this listView
|
||||
this.canMultiSelect = true;
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,14 @@ export class UserFilterListService extends BaseFilterListService<ViewUser> {
|
||||
{ condition: true, label: this.translate.instant('Got 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);
|
||||
|
@ -33,7 +33,8 @@ export class UserImportService extends BaseImportService<User> {
|
||||
'default_password',
|
||||
'email',
|
||||
'username',
|
||||
'gender'
|
||||
'gender',
|
||||
'vote_weight'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -117,6 +118,13 @@ export class UserImportService extends BaseImportService<User> {
|
||||
case 'number':
|
||||
newViewUser.number = line[idx];
|
||||
break;
|
||||
case 'vote_weight':
|
||||
if (!line[idx]) {
|
||||
newViewUser[this.expectedHeader[idx]] = 1;
|
||||
} else {
|
||||
newViewUser[this.expectedHeader[idx]] = line[idx];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
newViewUser[this.expectedHeader[idx]] = line[idx];
|
||||
break;
|
||||
|
@ -31,6 +31,7 @@ export class UserSortListService extends BaseSortListService<ViewUser> {
|
||||
{ property: 'is_committee', label: 'Is committee' },
|
||||
{ property: 'number', label: 'Participant number' },
|
||||
{ property: 'structure_level', label: 'Structure level' },
|
||||
{ property: 'vote_weight', label: 'Vote weight' },
|
||||
{ property: 'comment' }
|
||||
// TODO email send?
|
||||
];
|
||||
|
@ -3,6 +3,7 @@ from decimal import Decimal
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
|
||||
from openslides.core.config import config
|
||||
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
|
||||
from openslides.utils.auth import has_perm
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
@ -489,14 +490,20 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
# skip creating votes with empty weights
|
||||
if amount == 0:
|
||||
continue
|
||||
weight = Decimal(amount)
|
||||
if config["users_activate_vote_weight"]:
|
||||
weight *= user.vote_weight
|
||||
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)
|
||||
else: # global_no or global_abstain
|
||||
option = options[0]
|
||||
weight = (
|
||||
user.vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
||||
)
|
||||
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(option)
|
||||
@ -512,13 +519,15 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
vote_user is the one put into the vote
|
||||
"""
|
||||
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():
|
||||
option = options.get(pk=option_id)
|
||||
vote = AssignmentVote.objects.create(
|
||||
option=option,
|
||||
user=vote_user,
|
||||
value=result,
|
||||
weight=check_user.vote_weight,
|
||||
option=option, user=vote_user, value=result, weight=weight,
|
||||
)
|
||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||
inform_changed_data(option, no_delete_on_restriction=True)
|
||||
|
@ -1,3 +1,4 @@
|
||||
from decimal import Decimal
|
||||
from typing import List, Set
|
||||
|
||||
import jsonschema
|
||||
@ -1227,16 +1228,20 @@ class MotionPollViewSet(BasePollViewSet):
|
||||
VotedModel.objects.create(motionpoll=poll, user=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):
|
||||
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()
|
||||
vote = MotionVote.objects.create(user=user, option=option)
|
||||
vote = MotionVote.objects.create(user=vote_user, option=option)
|
||||
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)
|
||||
inform_changed_data(option)
|
||||
|
||||
|
@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
||||
from ..utils.models import SET_NULL_AND_AUTOUPDATE
|
||||
|
||||
@ -184,7 +185,7 @@ class BasePoll(models.Model):
|
||||
if self.type == self.TYPE_ANALOG:
|
||||
return self.db_votesvalid
|
||||
else:
|
||||
return Decimal(self.amount_users_voted())
|
||||
return Decimal(self.amount_users_voted_with_individual_weight())
|
||||
|
||||
def set_votesvalid(self, value):
|
||||
if self.type != self.TYPE_ANALOG:
|
||||
@ -210,7 +211,7 @@ class BasePoll(models.Model):
|
||||
if self.type == self.TYPE_ANALOG:
|
||||
return self.db_votescast
|
||||
else:
|
||||
return Decimal(self.amount_users_voted())
|
||||
return Decimal(self.amount_users_voted_with_individual_weight())
|
||||
|
||||
def set_votescast(self, value):
|
||||
if self.type != self.TYPE_ANALOG:
|
||||
@ -219,8 +220,11 @@ class BasePoll(models.Model):
|
||||
|
||||
votescast = property(get_votescast, set_votescast)
|
||||
|
||||
def amount_users_voted(self):
|
||||
return len(self.voted.all())
|
||||
def amount_users_voted_with_individual_weight(self):
|
||||
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):
|
||||
""" Should be called after creation of this model. """
|
||||
|
@ -44,6 +44,15 @@ def get_config_variables():
|
||||
group="Participants",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name="users_activate_vote_weight",
|
||||
default_value=False,
|
||||
input_type="boolean",
|
||||
label="Activate vote weight",
|
||||
weight=513,
|
||||
group="Participants",
|
||||
)
|
||||
|
||||
# PDF
|
||||
|
||||
yield ConfigVariable(
|
||||
|
@ -15,6 +15,7 @@ from openslides.assignments.models import (
|
||||
AssignmentPoll,
|
||||
AssignmentVote,
|
||||
)
|
||||
from openslides.core.config import config
|
||||
from openslides.poll.models import BasePoll
|
||||
from openslides.utils.auth import get_group_model
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
@ -982,6 +983,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), Decimal("1"))
|
||||
option1 = poll.options.get(pk=1)
|
||||
option2 = poll.options.get(pk=2)
|
||||
option3 = poll.options.get(pk=3)
|
||||
@ -995,6 +997,43 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertEqual(option3.no, Decimal("0"))
|
||||
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):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
@ -2233,7 +2272,7 @@ class PseudoanonymizeAssignmentPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
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.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
|
@ -763,6 +763,36 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
vote = option.votes.get()
|
||||
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):
|
||||
self.start_poll()
|
||||
@ -1155,7 +1185,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("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()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
@ -1378,7 +1408,7 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
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.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
@ -1446,7 +1476,7 @@ class ResetMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
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.votesinvalid, None)
|
||||
self.assertEqual(poll.votescast, None)
|
||||
|
Loading…
Reference in New Issue
Block a user