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') {
this.getDecimalFields().forEach(field => {
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 vote_weight: number;
public get isVoteWeightOne(): boolean {
return this.vote_weight === 1;
}
public constructor(input?: Partial<User>) {
super(User.COLLECTIONSTRING, input);
}

View File

@ -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;
}
}
}

View File

@ -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">

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 { 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 {

View File

@ -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">

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 { 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 {

View File

@ -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('');
}

View File

@ -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>

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 { 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],

View File

@ -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>

View File

@ -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`);
}
/**

View File

@ -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>

View File

@ -29,5 +29,6 @@
}
.icon-group {
margin-left: 1em;
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 { 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));
}

View File

@ -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);

View File

@ -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;

View File

@ -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?
];

View File

@ -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)

View File

@ -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)

View File

@ -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. """

View File

@ -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(

View File

@ -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"))

View File

@ -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)