Merge pull request #5305 from tsiegleauq/weight-votes

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

View File

@ -7,7 +7,7 @@ export abstract class BaseDecimalModel<T = any> extends BaseModel<T> {
if (input && typeof input === 'object') {
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)