Merge pull request #5305 from tsiegleauq/weight-votes
Implement vote weight in client
This commit is contained in:
commit
0aef3f79ce
@ -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