- {{ vote.user.getShortName() }}
+
+
+
+ {{ vote.user.getShortName() }}
-
-
-
- {{ vote.user.getLevelAndNumber() }}
+
+
+
+ {{ vote.user.getLevelAndNumber() }}
+
+
+
+
+ {{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
+
+
+
+
+
+ ({{ 'represented by' | translate }}
+ {{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
+
+
+
-
-
-
- {{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
-
-
-
-
-
- ({{ 'represented by' | translate }}
- {{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
-
+
+ {{ "Anonymous" | translate }}
+ {{ "Deleted user" | translate }}
-
-
{{ 'Anonymous' | translate }}
+
+
+ {{ voteOptionStyle[vote.value].icon }}
+
+
{{ vote.valueVerbose | translate }}
+
+
-
-
- {{ voteOptionStyle[vote.value].icon }}
-
-
{{ vote.valueVerbose | translate }}
-
-
-
- {{ 'The individual votes were anonymized.' | translate }}
-
-
+
+
+
+
+
diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts
index 14d9dde30..2c2393722 100644
--- a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts
+++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.ts
@@ -10,6 +10,7 @@ import { OperatorService, Permission } from 'app/core/core-services/operator.ser
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 { 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 { ViewMotion } from 'app/site/motions/models/view-motion';
@@ -27,7 +28,7 @@ import { BasePollDetailComponentDirective } from 'app/site/polls/components/base
})
export class MotionPollDetailComponent extends BasePollDetailComponentDirective {
public motion: ViewMotion;
- public columnDefinition: PblColumnDefinition[] = [
+ public columnDefinitionSingleVotesTable: PblColumnDefinition[] = [
{
prop: 'user',
width: 'auto',
@@ -40,7 +41,7 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
}
];
- public filterProps = ['user.getFullName', 'valueVerbose'];
+ public filterPropsSingleVotesTable = ['user.getFullName', 'valueVerbose'];
public isVoteWeightActive: boolean;
@@ -62,7 +63,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
configService: ConfigService,
protected operator: OperatorService,
private router: Router,
- protected cd: ChangeDetectorRef
+ protected cd: ChangeDetectorRef,
+ protected userRepo: UserRepositoryService
) {
super(
title,
@@ -76,7 +78,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
pollService,
votesRepo,
operator,
- cd
+ cd,
+ userRepo
);
configService
.get('users_activate_vote_weight')
diff --git a/client/src/app/site/motions/services/motion-poll.service.ts b/client/src/app/site/motions/services/motion-poll.service.ts
index 276f7ef5f..ecaa027c7 100644
--- a/client/src/app/site/motions/services/motion-poll.service.ts
+++ b/client/src/app/site/motions/services/motion-poll.service.ts
@@ -137,6 +137,9 @@ export class MotionPollService extends PollService {
case PercentBase.Cast:
totalByBase = poll.votescast;
break;
+ case PercentBase.Entitled:
+ totalByBase = poll.entitled_users_at_stop.length;
+ break;
case PercentBase.Disabled:
break;
default:
diff --git a/client/src/app/site/polls/components/base-poll-detail.component.ts b/client/src/app/site/polls/components/base-poll-detail.component.ts
index 5393b4e87..61843a659 100644
--- a/client/src/app/site/polls/components/base-poll-detail.component.ts
+++ b/client/src/app/site/polls/components/base-poll-detail.component.ts
@@ -1,25 +1,29 @@
-import { ChangeDetectorRef, Directive, OnInit } from '@angular/core';
+import { ChangeDetectorRef, Directive, OnDestroy, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
+import { PblColumnDefinition } from '@pebula/ngrid';
import { Label } from 'ng2-charts';
-import { BehaviorSubject, from, Observable } from 'rxjs';
+import { BehaviorSubject, from, Observable, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { OperatorService } from 'app/core/core-services/operator.service';
import { Deferred } from 'app/core/promises/deferred';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
+import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartData } from 'app/shared/components/charts/charts.component';
+import { EntitledUsersEntry } from 'app/shared/models/poll/base-poll';
import { BaseVote } from 'app/shared/models/poll/base-vote';
import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { ViewGroup } from 'app/site/users/models/view-group';
import { ViewUser } from 'app/site/users/models/view-user';
import { BasePollRepositoryService } from '../services/base-poll-repository.service';
+import { EntitledUsersTableEntry } from './entitled-users-table/entitled-users-table.component';
import { PollService } from '../services/poll.service';
import { ViewBasePoll } from '../models/view-base-poll';
import { ViewBaseVote } from '../models/view-base-vote';
@@ -31,7 +35,7 @@ export interface BaseVoteData {
@Directive()
export abstract class BasePollDetailComponentDirective
extends BaseViewComponentDirective
- implements OnInit {
+ implements OnInit, OnDestroy {
/**
* All the groups of users.
*/
@@ -73,8 +77,13 @@ export abstract class BasePollDetailComponentDirective;
+ // The observable for the entitled-users-table
+ public entitledUsersObservable: Observable;
+
protected optionsLoaded = new Deferred();
+ private entitledUsersSubscription: Subscription;
+
/**
* Constructor
*
@@ -102,7 +111,8 @@ export abstract class BasePollDetailComponentDirective,
protected operator: OperatorService,
- protected cd: ChangeDetectorRef
+ protected cd: ChangeDetectorRef,
+ protected userRepo: UserRepositoryService
) {
super(title, translate, matSnackbar);
this.setup();
@@ -168,14 +178,10 @@ export abstract class BasePollDetailComponentDirective !voteDate.user)) {
- this.votesDataObservable = null;
- } else {
- this.votesDataObservable = from([data]);
- }
+ this.votesDataObservable = from([data]);
}
/**
@@ -196,12 +202,46 @@ export abstract class BasePollDetailComponentDirective();
+ for (const entry of this.poll.entitled_users_at_stop) {
+ userIds.add(entry.user_id);
+ if (entry.vote_delegated_to_id) {
+ userIds.add(entry.vote_delegated_to_id);
+ }
+ }
+ this.entitledUsersSubscription = this.userRepo
+ .getViewModelListObservable()
+ .pipe(
+ filter(users => !!users.length),
+ map(users => users.filter(user => userIds.has(user.id)))
+ )
+ .subscribe(users => {
+ const entries = [];
+ for (const entry of this.poll.entitled_users_at_stop) {
+ entries.push({
+ ...entry,
+ user: users.find(user => user.id === entry.user_id),
+ voted_verbose: `voted:${entry.voted}`,
+ vote_delegated_to: entry.vote_delegated_to_id
+ ? users.find(user => user.id === entry.vote_delegated_to_id)
+ : null
+ });
+ }
+ this.entitledUsersObservable = from([entries]);
+ });
+ }
+
protected userHasVoteDelegation(user: ViewUser): boolean {
/**
* This will be false if the operator does not have "can_see_extra_data"
@@ -227,4 +267,10 @@ export abstract class BasePollDetailComponentDirective
+
+
+
+ {{ col.label | translate }}
+
+
+
+
+ {{ entry.user.getFullName() }}
+ {{ 'Deleted user' | translate }}
+
+
+ check_box
+
+
+
+ represented by
+ {{ entry.vote_delegated_to.getFullName() }}
+ {{ 'Deleted user' | translate }}
+
+
+
+
+
+ {{ 'You are not allowed to see all entitled users.' | translate }}
+
diff --git a/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.scss b/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.scss
new file mode 100644
index 000000000..1bebd2055
--- /dev/null
+++ b/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.scss
@@ -0,0 +1,9 @@
+.repr-prefix {
+ color: #888;
+ font-size: smaller;
+}
+
+.no-can-see-users {
+ margin: 1em;
+ text-align: center;
+}
diff --git a/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.spec.ts b/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.spec.ts
new file mode 100644
index 000000000..b773eb164
--- /dev/null
+++ b/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.spec.ts
@@ -0,0 +1,26 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { EntitledUsersTableComponent } from './entitled-users-table.component';
+
+describe('EntitledUsersTableComponent', () => {
+ let component: EntitledUsersTableComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [E2EImportsModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(EntitledUsersTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.ts b/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.ts
new file mode 100644
index 000000000..e6f4c3f34
--- /dev/null
+++ b/client/src/app/site/polls/components/entitled-users-table/entitled-users-table.component.ts
@@ -0,0 +1,85 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Directive,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewEncapsulation
+} from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { ActivatedRoute } from '@angular/router';
+
+import { TranslateService } from '@ngx-translate/core';
+import { PblColumnDefinition } from '@pebula/ngrid';
+import { Label } from 'ng2-charts';
+import { BehaviorSubject, from, Observable, Subscription } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
+
+import { BaseComponent } from 'app/base.component';
+import { OperatorService } from 'app/core/core-services/operator.service';
+import { Deferred } from 'app/core/promises/deferred';
+import { BaseRepository } from 'app/core/repositories/base-repository';
+import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
+import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
+import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
+import { PromptService } from 'app/core/ui-services/prompt.service';
+import { ChartData } from 'app/shared/components/charts/charts.component';
+import { EntitledUsersEntry } from 'app/shared/models/poll/base-poll';
+import { BaseVote } from 'app/shared/models/poll/base-vote';
+import { BaseViewComponentDirective } from 'app/site/base/base-view';
+import { ViewGroup } from 'app/site/users/models/view-group';
+import { ViewUser } from 'app/site/users/models/view-user';
+
+export interface EntitledUsersTableEntry extends EntitledUsersEntry {
+ user_id: number;
+ user?: ViewUser;
+ voted: boolean;
+ voted_verbose: string;
+ vote_delegated_to_id?: number;
+ vote_delegated_to?: ViewUser;
+}
+
+@Component({
+ selector: 'os-entitled-users-table',
+ templateUrl: './entitled-users-table.component.html',
+ styleUrls: ['./entitled-users-table.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None
+})
+export class EntitledUsersTableComponent extends BaseComponent {
+ @Input()
+ public entitledUsersObservable: Observable;
+
+ @Input()
+ public listStorageKey: string;
+
+ public columnDefinitionEntitledUsersTable: PblColumnDefinition[] = [
+ {
+ prop: 'user_id',
+ width: 'auto',
+ label: 'Participant'
+ },
+ {
+ prop: 'voted',
+ width: 'auto',
+ label: 'Voted'
+ },
+ {
+ prop: 'delegation',
+ width: 'auto',
+ label: 'Delegated to'
+ }
+ ];
+
+ public filterPropsEntitledUsersTable = ['user.getFullName', 'vote_delegated_to.getFullName', 'voted_verbose'];
+
+ public get canSeeUsers(): boolean {
+ return this.operator.hasPerms(this.permission.usersCanSeeName);
+ }
+
+ public constructor(title: Title, translate: TranslateService, private operator: OperatorService) {
+ super(title, translate);
+ }
+}
diff --git a/client/src/app/site/polls/models/view-base-poll.ts b/client/src/app/site/polls/models/view-base-poll.ts
index 3741fb785..e588f341c 100644
--- a/client/src/app/site/polls/models/view-base-poll.ts
+++ b/client/src/app/site/polls/models/view-base-poll.ts
@@ -65,6 +65,7 @@ export const PercentBaseVerbose = {
YNA: 'Yes/No/Abstain',
valid: 'Valid votes',
cast: 'Total votes cast',
+ entitled: 'All entitled users',
disabled: 'Disabled'
};
diff --git a/client/src/app/site/polls/services/poll.service.ts b/client/src/app/site/polls/services/poll.service.ts
index a9051198e..3619b1022 100644
--- a/client/src/app/site/polls/services/poll.service.ts
+++ b/client/src/app/site/polls/services/poll.service.ts
@@ -7,6 +7,7 @@ import { ChartData, ChartDate } from 'app/shared/components/charts/charts.compon
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import {
BasePoll,
+ EntitledUsersEntry,
MajorityMethod,
PercentBase,
PollColor,
@@ -111,6 +112,7 @@ export interface PollData {
votesvalid: number;
votesinvalid: number;
votescast: number;
+ entitled_users_at_stop: EntitledUsersEntry[];
amount_global_yes?: number;
amount_global_no?: number;
amount_global_abstain?: number;
@@ -286,7 +288,11 @@ export abstract class PollService {
}
public showPercentOfValidOrCast(poll: PollData | ViewBasePoll): boolean {
- return poll.onehundred_percent_base === PercentBase.Valid || poll.onehundred_percent_base === PercentBase.Cast;
+ return (
+ poll.onehundred_percent_base === PercentBase.Valid ||
+ poll.onehundred_percent_base === PercentBase.Cast ||
+ poll.onehundred_percent_base === PercentBase.Entitled
+ );
}
public getSumTableKeys(poll: PollData | ViewBasePoll): VotingResult[] {
diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.component.ts b/client/src/app/slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.component.ts
index 56128519c..4dda68d8f 100644
--- a/client/src/app/slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.component.ts
+++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.component.ts
@@ -3,6 +3,7 @@ import { Component, Input } from '@angular/core';
import { BaseSlideComponentDirective } from 'app/slides/base-slide-component';
import { CommonListOfSpeakersSlideData, SlideSpeaker } from '../common/common-list-of-speakers-slide-data';
+// prettier-ignore
@Component({
selector: 'os-current-list-of-speakers-overlay-slide',
templateUrl: './current-list-of-speakers-overlay-slide.component.html',
diff --git a/client/src/app/slides/assignments/assignment-poll/assignment-poll-slide-data.ts b/client/src/app/slides/assignments/assignment-poll/assignment-poll-slide-data.ts
index 7b7c87a5b..326991400 100644
--- a/client/src/app/slides/assignments/assignment-poll/assignment-poll-slide-data.ts
+++ b/client/src/app/slides/assignments/assignment-poll/assignment-poll-slide-data.ts
@@ -1,5 +1,5 @@
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
-import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
+import { EntitledUsersEntry, MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
import { AssignmentTitleInformation } from 'app/site/assignments/models/view-assignment';
import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
@@ -25,6 +25,8 @@ export interface AssignmentPollSlideData extends BasePollSlideData {
abstain?: number;
}[];
+ entitled_users_at_stop: EntitledUsersEntry[];
+
// optional for published polls:
amount_global_yes?: number;
amount_global_no?: number;
diff --git a/client/src/app/slides/motions/motion-poll/motion-poll-slide-data.ts b/client/src/app/slides/motions/motion-poll/motion-poll-slide-data.ts
index 1ca3e479f..85e882ab9 100644
--- a/client/src/app/slides/motions/motion-poll/motion-poll-slide-data.ts
+++ b/client/src/app/slides/motions/motion-poll/motion-poll-slide-data.ts
@@ -1,5 +1,5 @@
import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
-import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
+import { EntitledUsersEntry, MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
import { MotionTitleInformation } from 'app/site/motions/models/view-motion';
import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
@@ -19,6 +19,8 @@ export interface MotionPollSlideData extends BasePollSlideData {
abstain?: number;
}[];
+ entitled_users_at_stop: EntitledUsersEntry[];
+
// optional for published polls:
votesvalid: number;
votesinvalid: number;
diff --git a/client/src/app/slides/polls/base-poll-slide-data.ts b/client/src/app/slides/polls/base-poll-slide-data.ts
index 1790d84bd..7c80cae70 100644
--- a/client/src/app/slides/polls/base-poll-slide-data.ts
+++ b/client/src/app/slides/polls/base-poll-slide-data.ts
@@ -1,4 +1,4 @@
-import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
+import { EntitledUsersEntry, MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
export interface BasePollSlideData {
poll: {
@@ -15,6 +15,8 @@ export interface BasePollSlideData {
abstain?: number;
}[];
+ entitled_users_at_stop: EntitledUsersEntry[];
+
votesvalid: number;
votesinvalid: number;
votescast: number;
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 8aea37414..4bfddc97b 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -10,7 +10,7 @@ services:
server:
image: os3-server-dev
- user: $UID:$GID
+ user: $USER_ID:$GROUP_ID
build:
context: ../server
dockerfile: docker/Dockerfile.dev
diff --git a/server/docker/Dockerfile.dev b/server/docker/Dockerfile.dev
index d58202842..9e19b4458 100644
--- a/server/docker/Dockerfile.dev
+++ b/server/docker/Dockerfile.dev
@@ -19,8 +19,9 @@ RUN rm -rf /var/lib/apt/lists/*
COPY requirements /app/requirements
COPY requirements.txt /app/requirements.txt
+COPY make/requirements.txt /app/requirements/make_requirements.txt
-RUN pip install -r requirements.txt -r requirements/saml.txt && \
+RUN pip install -r requirements.txt -r requirements/saml.txt -r requirements/make_requirements.txt && \
rm -rf /root/.cache/pip
EXPOSE 8000
diff --git a/server/openslides/assignments/migrations/0019_assignmentvote_user_token_1.py b/server/openslides/assignments/migrations/0019_assignmentvote_user_token_1.py
new file mode 100644
index 000000000..7308f8f9c
--- /dev/null
+++ b/server/openslides/assignments/migrations/0019_assignmentvote_user_token_1.py
@@ -0,0 +1,18 @@
+# Generated by jsangmeister on 2021-03-18 16:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("assignments", "0018_votes_amount"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="assignmentvote",
+ name="user_token",
+ field=models.CharField(null=True, max_length=16),
+ ),
+ ]
diff --git a/server/openslides/assignments/migrations/0020_assignmentvote_user_token_2.py b/server/openslides/assignments/migrations/0020_assignmentvote_user_token_2.py
new file mode 100644
index 000000000..74b38876e
--- /dev/null
+++ b/server/openslides/assignments/migrations/0020_assignmentvote_user_token_2.py
@@ -0,0 +1,16 @@
+# Generated by jsangmeister on 2021-03-25 10:41
+
+from django.db import migrations
+
+from ...poll.migrations.vote_migration_helper import set_user_tokens
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("assignments", "0019_assignmentvote_user_token_1"),
+ ]
+
+ operations = [
+ migrations.RunPython(set_user_tokens("assignments", "AssignmentVote")),
+ ]
diff --git a/server/openslides/assignments/migrations/0021_assignmentvote_user_token_3.py b/server/openslides/assignments/migrations/0021_assignmentvote_user_token_3.py
new file mode 100644
index 000000000..d531f925f
--- /dev/null
+++ b/server/openslides/assignments/migrations/0021_assignmentvote_user_token_3.py
@@ -0,0 +1,24 @@
+# Generated by jsangmeister on 2021-03-25 10:41
+
+from django.db import migrations, models
+
+import openslides.poll.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("assignments", "0020_assignmentvote_user_token_2"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="assignmentvote",
+ name="user_token",
+ field=models.CharField(
+ null=False,
+ default=openslides.poll.models.generate_user_token,
+ max_length=16,
+ ),
+ ),
+ ]
diff --git a/server/openslides/assignments/migrations/0022_assignmentpoll_change_fields_1.py b/server/openslides/assignments/migrations/0022_assignmentpoll_change_fields_1.py
new file mode 100644
index 000000000..c775e21bb
--- /dev/null
+++ b/server/openslides/assignments/migrations/0022_assignmentpoll_change_fields_1.py
@@ -0,0 +1,63 @@
+# Generated by jsangmeister on 2021-03-22 12:44
+
+import jsonfield.encoder
+import jsonfield.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("assignments", "0021_assignmentvote_user_token_3"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="assignmentpoll",
+ name="is_pseudoanonymized",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="assignmentpoll",
+ name="entitled_users_at_stop",
+ field=jsonfield.fields.JSONField(
+ dump_kwargs={
+ "cls": jsonfield.encoder.JSONEncoder,
+ "separators": (",", ":"),
+ },
+ load_kwargs={},
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="assignmentpoll",
+ name="onehundred_percent_base",
+ field=models.CharField(
+ choices=[
+ ("YN", "Yes/No per candidate"),
+ ("YNA", "Yes/No/Abstain per candidate"),
+ ("Y", "Sum of votes including general No/Abstain"),
+ ("valid", "All valid ballots"),
+ ("cast", "All casted ballots"),
+ ("entitled", "All entitled users"),
+ ("disabled", "Disabled (no percents)"),
+ ],
+ max_length=8,
+ ),
+ ),
+ migrations.RenameField(
+ model_name="assignmentpoll",
+ old_name="db_votescast",
+ new_name="votescast",
+ ),
+ migrations.RenameField(
+ model_name="assignmentpoll",
+ old_name="db_votesinvalid",
+ new_name="votesinvalid",
+ ),
+ migrations.RenameField(
+ model_name="assignmentpoll",
+ old_name="db_votesvalid",
+ new_name="votesvalid",
+ ),
+ ]
diff --git a/server/openslides/assignments/migrations/0023_assignmentpoll_change_fields_2.py b/server/openslides/assignments/migrations/0023_assignmentpoll_change_fields_2.py
new file mode 100644
index 000000000..492ac18ce
--- /dev/null
+++ b/server/openslides/assignments/migrations/0023_assignmentpoll_change_fields_2.py
@@ -0,0 +1,20 @@
+# Generated by jsangmeister on 2021-03-22 12:44
+
+from django.db import migrations
+
+from ...poll.migrations.poll_migration_helper import (
+ calculate_vote_fields,
+ set_is_pseudoanonymized,
+)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("assignments", "0022_assignmentpoll_change_fields_1"),
+ ]
+
+ operations = [
+ migrations.RunPython(set_is_pseudoanonymized("assignments", "AssignmentPoll")),
+ migrations.RunPython(calculate_vote_fields("assignments", "AssignmentPoll")),
+ ]
diff --git a/server/openslides/assignments/models.py b/server/openslides/assignments/models.py
index d57458f2f..b71633923 100644
--- a/server/openslides/assignments/models.py
+++ b/server/openslides/assignments/models.py
@@ -9,7 +9,7 @@ from openslides.agenda.models import Speaker
from openslides.core.config import config
from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile
-from openslides.poll.models import BaseOption, BasePoll, BaseVote
+from openslides.poll.models import BaseOption, BasePoll, BaseVote, BaseVoteManager
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.manager import BaseManager
@@ -212,7 +212,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
return {"title": self.title}
-class AssignmentVoteManager(BaseManager):
+class AssignmentVoteManager(BaseVoteManager):
"""
Customized model manager to support our get_prefetched_queryset method.
"""
@@ -325,6 +325,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
PERCENT_BASE_YNA = "YNA"
PERCENT_BASE_VALID = "valid"
PERCENT_BASE_CAST = "cast"
+ PERCENT_BASE_ENTITLED = "entitled"
PERCENT_BASE_DISABLED = "disabled"
PERCENT_BASES = (
(PERCENT_BASE_YN, "Yes/No per candidate"),
@@ -332,6 +333,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
(PERCENT_BASE_Y, "Sum of votes including general No/Abstain"),
(PERCENT_BASE_VALID, "All valid ballots"),
(PERCENT_BASE_CAST, "All casted ballots"),
+ (PERCENT_BASE_ENTITLED, "All entitled users"),
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
)
onehundred_percent_base = models.CharField(
diff --git a/server/openslides/assignments/views.py b/server/openslides/assignments/views.py
index be2164722..e4fb120ff 100644
--- a/server/openslides/assignments/views.py
+++ b/server/openslides/assignments/views.py
@@ -522,6 +522,7 @@ class AssignmentPollViewSet(BasePollViewSet):
"""
options = poll.get_options()
if isinstance(data, dict):
+ user_token = AssignmentVote.objects.generate_user_token()
for option_id, amount in data.items():
# Add user to the option's voted array
option = options.get(pk=option_id)
@@ -540,6 +541,7 @@ class AssignmentPollViewSet(BasePollViewSet):
delegated_user=request_user,
weight=weight,
value=value,
+ user_token=user_token,
)
inform_changed_data(vote)
else: # global_no or global_abstain
@@ -566,6 +568,7 @@ class AssignmentPollViewSet(BasePollViewSet):
request_user is the user who gives the vote, may be a delegate
"""
options = poll.get_options()
+ user_token = AssignmentVote.objects.generate_user_token()
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
for option_id, result in data.items():
option = options.get(pk=option_id)
@@ -575,6 +578,7 @@ class AssignmentPollViewSet(BasePollViewSet):
delegated_user=request_user,
value=result,
weight=weight,
+ user_token=user_token,
)
inform_changed_data(vote)
inform_changed_data(option)
diff --git a/server/openslides/motions/migrations/0038_motionvote_user_token_1.py b/server/openslides/motions/migrations/0038_motionvote_user_token_1.py
new file mode 100644
index 000000000..42cc50b2f
--- /dev/null
+++ b/server/openslides/motions/migrations/0038_motionvote_user_token_1.py
@@ -0,0 +1,18 @@
+# Generated by jsangmeister on 2021-03-18 16:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("motions", "0037_motionvote_delegated_user"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="motionvote",
+ name="user_token",
+ field=models.CharField(null=True, max_length=16),
+ ),
+ ]
diff --git a/server/openslides/motions/migrations/0039_motionvote_user_token_2.py b/server/openslides/motions/migrations/0039_motionvote_user_token_2.py
new file mode 100644
index 000000000..f525f3571
--- /dev/null
+++ b/server/openslides/motions/migrations/0039_motionvote_user_token_2.py
@@ -0,0 +1,16 @@
+# Generated by jsangmeister on 2021-03-18 16:27
+
+from django.db import migrations
+
+from ...poll.migrations.vote_migration_helper import set_user_tokens
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("motions", "0038_motionvote_user_token_1"),
+ ]
+
+ operations = [
+ migrations.RunPython(set_user_tokens("motions", "MotionVote")),
+ ]
diff --git a/server/openslides/motions/migrations/0040_motionvote_user_token_3.py b/server/openslides/motions/migrations/0040_motionvote_user_token_3.py
new file mode 100644
index 000000000..b70cc5878
--- /dev/null
+++ b/server/openslides/motions/migrations/0040_motionvote_user_token_3.py
@@ -0,0 +1,24 @@
+# Generated by jsangmeister on 2021-03-18 16:27
+
+from django.db import migrations, models
+
+import openslides.poll.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("motions", "0039_motionvote_user_token_2"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="motionvote",
+ name="user_token",
+ field=models.CharField(
+ null=False,
+ default=openslides.poll.models.generate_user_token,
+ max_length=16,
+ ),
+ ),
+ ]
diff --git a/server/openslides/motions/migrations/0041_motionpoll_change_fields_1.py b/server/openslides/motions/migrations/0041_motionpoll_change_fields_1.py
new file mode 100644
index 000000000..4158080f4
--- /dev/null
+++ b/server/openslides/motions/migrations/0041_motionpoll_change_fields_1.py
@@ -0,0 +1,62 @@
+# Generated by jsangmeister on 2021-03-22 12:44
+
+import jsonfield.encoder
+import jsonfield.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("motions", "0040_motionvote_user_token_3"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="motionpoll",
+ name="is_pseudoanonymized",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="motionpoll",
+ name="entitled_users_at_stop",
+ field=jsonfield.fields.JSONField(
+ dump_kwargs={
+ "cls": jsonfield.encoder.JSONEncoder,
+ "separators": (",", ":"),
+ },
+ load_kwargs={},
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="motionpoll",
+ name="onehundred_percent_base",
+ field=models.CharField(
+ choices=[
+ ("YN", "Yes/No"),
+ ("YNA", "Yes/No/Abstain"),
+ ("valid", "All valid ballots"),
+ ("cast", "All casted ballots"),
+ ("entitled", "All entitled users"),
+ ("disabled", "Disabled (no percents)"),
+ ],
+ max_length=8,
+ ),
+ ),
+ migrations.RenameField(
+ model_name="motionpoll",
+ old_name="db_votescast",
+ new_name="votescast",
+ ),
+ migrations.RenameField(
+ model_name="motionpoll",
+ old_name="db_votesinvalid",
+ new_name="votesinvalid",
+ ),
+ migrations.RenameField(
+ model_name="motionpoll",
+ old_name="db_votesvalid",
+ new_name="votesvalid",
+ ),
+ ]
diff --git a/server/openslides/motions/migrations/0042_motionpoll_change_fields_2.py b/server/openslides/motions/migrations/0042_motionpoll_change_fields_2.py
new file mode 100644
index 000000000..4a0237c9a
--- /dev/null
+++ b/server/openslides/motions/migrations/0042_motionpoll_change_fields_2.py
@@ -0,0 +1,20 @@
+# Generated by jsangmeister on 2021-03-22 12:44
+
+from django.db import migrations
+
+from ...poll.migrations.poll_migration_helper import (
+ calculate_vote_fields,
+ set_is_pseudoanonymized,
+)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("motions", "0041_motionpoll_change_fields_1"),
+ ]
+
+ operations = [
+ migrations.RunPython(set_is_pseudoanonymized("motions", "MotionPoll")),
+ migrations.RunPython(calculate_vote_fields("motions", "MotionPoll")),
+ ]
diff --git a/server/openslides/motions/models.py b/server/openslides/motions/models.py
index d95ad23f3..fdbb4be66 100644
--- a/server/openslides/motions/models.py
+++ b/server/openslides/motions/models.py
@@ -8,7 +8,7 @@ from openslides.agenda.mixins import AgendaItemWithListOfSpeakersMixin
from openslides.core.config import config
from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile
-from openslides.poll.models import BaseOption, BasePoll, BaseVote
+from openslides.poll.models import BaseOption, BasePoll, BaseVote, BaseVoteManager
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.manager import BaseManager
@@ -828,7 +828,7 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode
return {"title": self.title}
-class MotionVoteManager(BaseManager):
+class MotionVoteManager(BaseVoteManager):
"""
Customized model manager to support our get_prefetched_queryset method.
"""
diff --git a/server/openslides/poll/migrations/__init__.py b/server/openslides/poll/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/openslides/poll/migrations/poll_migration_helper.py b/server/openslides/poll/migrations/poll_migration_helper.py
new file mode 100644
index 000000000..3e6478ad3
--- /dev/null
+++ b/server/openslides/poll/migrations/poll_migration_helper.py
@@ -0,0 +1,36 @@
+from ..models import BasePoll
+
+
+def set_is_pseudoanonymized(poll_model_collection, poll_model_name):
+ """
+ Takes all polls of the given model and updates is_pseudoanonymized, if necessary.
+ """
+
+ def _set_is_pseudoanonymized(apps, schema_editor):
+ PollModel = apps.get_model(poll_model_collection, poll_model_name)
+ for poll in PollModel.objects.all():
+ if poll.type == BasePoll.TYPE_PSEUDOANONYMOUS or all(
+ not vote.user_id
+ for option in poll.options.all()
+ for vote in option.votes.all()
+ ):
+ poll.is_pseudoanonymized = True
+ poll.save(skip_autoupdate=True)
+
+ return _set_is_pseudoanonymized
+
+
+def calculate_vote_fields(poll_model_collection, poll_model_name):
+ """
+ Takes all polls of the given model and updates votes*, if necessary.
+ """
+
+ def _calculate_vote_fields(apps, schema_editor):
+ PollModel = apps.get_model(poll_model_collection, poll_model_name)
+ for poll in PollModel.objects.all():
+ if poll.state in (BasePoll.STATE_FINISHED, BasePoll.STATE_PUBLISHED):
+ BasePoll.calculate_votes(poll)
+ BasePoll.calculate_entitled_users(poll)
+ poll.save(skip_autoupdate=True)
+
+ return _calculate_vote_fields
diff --git a/server/openslides/poll/migrations/vote_migration_helper.py b/server/openslides/poll/migrations/vote_migration_helper.py
new file mode 100644
index 000000000..b6e09c175
--- /dev/null
+++ b/server/openslides/poll/migrations/vote_migration_helper.py
@@ -0,0 +1,24 @@
+from ..models import generate_user_token
+
+
+def set_user_tokens(vote_model_collection, vote_model_name):
+ """
+ Takes all votes of the given model and checks their tokens. For named polls,
+ multiple votes with the same user_id will get the same token.
+ """
+
+ def _set_user_token(apps, schema_editor):
+ user_token_map = {}
+ VoteModel = apps.get_model(vote_model_collection, vote_model_name)
+ for vote in VoteModel.objects.all():
+ if vote.user is not None:
+ key = (vote.user_id, vote.option.poll_id)
+ if key not in user_token_map:
+ user_token_map[key] = generate_user_token()
+ token = user_token_map[key]
+ else:
+ token = generate_user_token()
+ vote.user_token = token
+ vote.save(skip_autoupdate=True)
+
+ return _set_user_token
diff --git a/server/openslides/poll/models.py b/server/openslides/poll/models.py
index e362efbe4..d18ff0e12 100644
--- a/server/openslides/poll/models.py
+++ b/server/openslides/poll/models.py
@@ -4,12 +4,21 @@ from typing import Iterable, Optional, Tuple, Type
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models
+from django.utils.crypto import get_random_string
+from jsonfield import JSONField
+
+from openslides.utils.manager import BaseManager
from ..core.config import config
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
from ..utils.models import SET_NULL_AND_AUTOUPDATE
+def generate_user_token():
+ """ Generates a 16 character alphanumeric token. """
+ return get_random_string(16)
+
+
class BaseVote(models.Model):
"""
All subclasses must have option attribute with the related name "votes"
@@ -37,11 +46,21 @@ class BaseVote(models.Model):
on_delete=SET_NULL_AND_AUTOUPDATE,
related_name="%(class)s_delegated_votes",
)
+ user_token = models.CharField(default=generate_user_token, max_length=16)
class Meta:
abstract = True
+class BaseVoteManager(BaseManager):
+ """
+ Base vote manager that supplies the generate_user_token method.
+ """
+
+ def generate_user_token(self):
+ return generate_user_token()
+
+
class BaseOption(models.Model):
"""
All subclasses must have poll attribute with the related name "options"
@@ -134,21 +153,21 @@ class BasePoll(models.Model):
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
- db_votesvalid = models.DecimalField(
+ votesvalid = models.DecimalField(
null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
- db_votesinvalid = models.DecimalField(
+ votesinvalid = models.DecimalField(
null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
- db_votescast = models.DecimalField(
+ votescast = models.DecimalField(
null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
@@ -160,12 +179,14 @@ class BasePoll(models.Model):
PERCENT_BASE_YNA = "YNA"
PERCENT_BASE_VALID = "valid"
PERCENT_BASE_CAST = "cast"
+ PERCENT_BASE_ENTITLED = "entitled"
PERCENT_BASE_DISABLED = "disabled"
PERCENT_BASES: Iterable[Tuple[str, str]] = (
(PERCENT_BASE_YN, "Yes/No"),
(PERCENT_BASE_YNA, "Yes/No/Abstain"),
(PERCENT_BASE_VALID, "All valid ballots"),
(PERCENT_BASE_CAST, "All casted ballots"),
+ (PERCENT_BASE_ENTITLED, "All entitled users"),
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
) # type: ignore
onehundred_percent_base = models.CharField(
@@ -186,57 +207,13 @@ class BasePoll(models.Model):
max_length=14, blank=False, null=False, choices=MAJORITY_METHODS
)
+ is_pseudoanonymized = models.BooleanField(default=False)
+
+ entitled_users_at_stop = JSONField(null=True)
+
class Meta:
abstract = True
- def get_votesvalid(self):
- if self.type == self.TYPE_ANALOG:
- return self.db_votesvalid
- else:
- return Decimal(self.amount_users_voted_with_individual_weight())
-
- def set_votesvalid(self, value):
- if self.type != self.TYPE_ANALOG:
- raise ValueError("Do not set votesvalid for non analog polls")
- self.db_votesvalid = value
-
- votesvalid = property(get_votesvalid, set_votesvalid)
-
- def get_votesinvalid(self):
- if self.type == self.TYPE_ANALOG:
- return self.db_votesinvalid
- else:
- return Decimal(0)
-
- def set_votesinvalid(self, value):
- if self.type != self.TYPE_ANALOG:
- raise ValueError("Do not set votesinvalid for non analog polls")
- self.db_votesinvalid = value
-
- votesinvalid = property(get_votesinvalid, set_votesinvalid)
-
- def get_votescast(self):
- if self.type == self.TYPE_ANALOG:
- return self.db_votescast
- else:
- return Decimal(self.amount_users_voted())
-
- def set_votescast(self, value):
- if self.type != self.TYPE_ANALOG:
- raise ValueError("Do not set votescast for non analog polls")
- self.db_votescast = value
-
- 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 self.amount_users_voted()
-
def create_options(self):
""" Should be called after creation of this model. """
raise NotImplementedError()
@@ -268,6 +245,8 @@ class BasePoll(models.Model):
def pseudoanonymize(self):
for option in self.get_options():
option.pseudoanonymize()
+ self.is_pseudoanonymized = True
+ self.save()
def reset(self):
for option in self.get_options():
@@ -281,4 +260,38 @@ class BasePoll(models.Model):
self.votesvalid = None
self.votesinvalid = None
self.votescast = None
+ if self.type != self.TYPE_PSEUDOANONYMOUS:
+ self.is_pseudoanonymized = False
+ self.save()
+
+ def calculate_votes(self):
+ if self.type != BasePoll.TYPE_ANALOG:
+ self.votescast = self.voted.count()
+ if config["users_activate_vote_weight"]:
+ self.votesvalid = sum(self.voted.values_list("vote_weight", flat=True))
+ else:
+ self.votesvalid = self.votescast
+ self.votesinvalid = Decimal(0)
+
+ def calculate_entitled_users(self):
+ entitled_users = []
+ for group in self.groups.all():
+ for user in group.user_set.all():
+ if user.is_present:
+ entitled_users.append(
+ {
+ "user_id": user.id,
+ "voted": user in self.voted.all(),
+ "vote_delegated_to_id": user.vote_delegated_to_id,
+ }
+ )
+ self.entitled_users_at_stop = entitled_users
+
+ def stop(self):
+ """
+ Saves a snapshot of the current voted users into the relevant fields and stops the poll.
+ """
+ self.calculate_votes()
+ self.calculate_entitled_users()
+ self.state = self.STATE_FINISHED
self.save()
diff --git a/server/openslides/poll/serializers.py b/server/openslides/poll/serializers.py
index b18be672d..6fc9a1be6 100644
--- a/server/openslides/poll/serializers.py
+++ b/server/openslides/poll/serializers.py
@@ -5,6 +5,7 @@ from ..utils.rest_api import (
CharField,
DecimalField,
IdPrimaryKeyRelatedField,
+ JSONField,
ModelSerializer,
SerializerMethodField,
ValidationError,
@@ -18,6 +19,7 @@ BASE_VOTE_FIELDS = (
"value",
"user",
"delegated_user",
+ "user_token",
"option",
"pollstate",
)
@@ -58,7 +60,9 @@ BASE_POLL_FIELDS = (
"id",
"onehundred_percent_base",
"majority_method",
+ "is_pseudoanonymized",
"voted",
+ "entitled_users_at_stop",
)
@@ -69,27 +73,21 @@ class BasePollSerializer(ModelSerializer):
)
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
-
- votesvalid = DecimalField(
- max_digits=15, decimal_places=6, min_value=-2, read_only=True
- )
- votesinvalid = DecimalField(
- max_digits=15, decimal_places=6, min_value=-2, read_only=True
- )
- votescast = DecimalField(
- max_digits=15, decimal_places=6, min_value=-2, read_only=True
- )
+ entitled_users_at_stop = JSONField(required=False)
def create(self, validated_data):
"""
Match the 100 percent base to the pollmethod. Change the base, if it does not
- fit to the pollmethod
+ fit to the pollmethod.
+ Set is_pseudoanonymized if type is pseudoanonymous.
"""
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
validated_data["onehundred_percent_base"], validated_data["pollmethod"]
)
if new_100_percent_base is not None:
validated_data["onehundred_percent_base"] = new_100_percent_base
+ if validated_data["type"] == BasePoll.TYPE_PSEUDOANONYMOUS:
+ validated_data["is_pseudoanonymized"] = True
return super().create(validated_data)
def update(self, instance, validated_data):
@@ -100,8 +98,15 @@ class BasePollSerializer(ModelSerializer):
E.g. the pollmethod is YN, but the 100%-base is YNA, this might not be
possible (see implementing serializers to see forbidden combinations)
+
+ Also updates is_pseudoanonymized, if needed.
"""
old_100_percent_base = instance.onehundred_percent_base
+ if "type" in validated_data:
+ if validated_data["type"] == BasePoll.TYPE_PSEUDOANONYMOUS:
+ validated_data["is_pseudoanonymized"] = True
+ else:
+ validated_data["is_pseudoanonymized"] = False
instance = super().update(instance, validated_data)
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
diff --git a/server/openslides/poll/views.py b/server/openslides/poll/views.py
index 0a8c18795..e7bb24496 100644
--- a/server/openslides/poll/views.py
+++ b/server/openslides/poll/views.py
@@ -146,8 +146,7 @@ class BasePollViewSet(ModelViewSet):
if poll.state != BasePoll.STATE_STARTED:
raise ValidationError({"detail": "Wrong poll state"})
- poll.state = BasePoll.STATE_FINISHED
- poll.save()
+ poll.stop()
inform_changed_data(poll.get_votes())
inform_changed_data(poll.get_options())
self.extend_history_information(["Voting stopped"])
diff --git a/server/tests/integration/assignments/test_polls.py b/server/tests/integration/assignments/test_polls.py
index d7ad729a2..624013b91 100644
--- a/server/tests/integration/assignments/test_polls.py
+++ b/server/tests/integration/assignments/test_polls.py
@@ -6,6 +6,7 @@ from django.conf import settings
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
+from rest_framework.test import APIClient
from openslides.assignments.models import (
Assignment,
@@ -108,7 +109,7 @@ class CreateAssignmentPoll(TestCase):
self.assignment.add_candidate(self.admin)
def test_simple(self):
- with self.assertNumQueries(40):
+ with self.assertNumQueries(38):
response = self.client.post(
reverse("assignmentpoll-list"),
{
@@ -886,6 +887,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 6)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("4.64"))
self.assertEqual(poll.votesinvalid, Decimal("-2"))
self.assertEqual(poll.votescast, Decimal("-2"))
@@ -1056,6 +1058,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("4.64"))
self.assertEqual(poll.votesinvalid, Decimal("-2"))
self.assertEqual(poll.votescast, Decimal("3"))
@@ -1081,6 +1084,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -1099,11 +1103,11 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 3)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
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"))
self.assertTrue(self.admin in poll.voted.all())
option1 = poll.options.get(pk=1)
option2 = poll.options.get(pk=2)
@@ -1132,11 +1136,11 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 3)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, weight)
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(), weight)
option1 = poll.options.get(pk=1)
option2 = poll.options.get(pk=2)
option3 = poll.options.get(pk=3)
@@ -1321,6 +1325,51 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
+ def test_same_user_token(self):
+ self.add_candidate()
+ self.add_candidate()
+ self.start_poll()
+ response = self.client.post(
+ reverse("assignmentpoll-vote", args=[self.poll.pk]),
+ {"data": {"1": "Y", "2": "N", "3": "A"}},
+ format="json",
+ )
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ self.assertEqual(AssignmentVote.objects.count(), 3)
+ votes = AssignmentVote.objects.all()
+ user_token = votes[0].user_token
+ for vote in votes[1:2]:
+ assert vote.user_token == user_token
+
+ def test_valid_votes_count_with_deleted_user(self):
+ self.add_candidate()
+ self.start_poll()
+ user, user_password = self.create_user()
+ user.groups.add(GROUP_ADMIN_PK)
+ user.is_present = True
+ user.save()
+ user_client = APIClient()
+ user_client.login(username=user.username, password=user_password)
+ response = self.client.post(
+ reverse("assignmentpoll-vote", args=[self.poll.pk]),
+ {"data": {"1": "Y", "2": "N"}},
+ format="json",
+ )
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ response = user_client.post(
+ reverse("assignmentpoll-vote", args=[self.poll.pk]),
+ {"data": {"1": "N", "2": "Y"}},
+ format="json",
+ )
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ self.poll.stop()
+ response = self.client.delete(reverse("user-detail", args=[user.pk]))
+ self.assertHttpStatusVerbose(response, status.HTTP_204_NO_CONTENT)
+ poll = AssignmentPoll.objects.get()
+ self.assertEqual(poll.votesvalid, Decimal("2"))
+ self.assertEqual(poll.votesinvalid, Decimal("0"))
+ self.assertEqual(poll.votescast, Decimal("2"))
+
class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
def create_poll(self):
@@ -1343,6 +1392,7 @@ class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -1360,6 +1410,7 @@ class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 1)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -1671,6 +1722,7 @@ class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -1688,6 +1740,7 @@ class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 1)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -1986,6 +2039,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -2004,6 +2058,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 3)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -2163,6 +2218,22 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
+ def test_same_user_token(self):
+ self.add_candidate()
+ self.add_candidate()
+ self.start_poll()
+ response = self.client.post(
+ reverse("assignmentpoll-vote", args=[self.poll.pk]),
+ {"data": {"1": "Y", "2": "N", "3": "A"}},
+ format="json",
+ )
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ self.assertEqual(AssignmentVote.objects.count(), 3)
+ votes = AssignmentVote.objects.all()
+ user_token = votes[0].user_token
+ for vote in votes[1:2]:
+ assert vote.user_token == user_token
+
class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
def create_poll(self):
@@ -2185,6 +2256,7 @@ class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -2202,6 +2274,7 @@ class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 1)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -2433,6 +2506,7 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -2450,6 +2524,7 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 1)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -2681,8 +2756,9 @@ class PseudoanonymizeAssignmentPoll(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
+ poll.calculate_votes()
+ self.assertEqual(poll.is_pseudoanonymized, True)
self.assertEqual(poll.get_votes().count(), 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"))
diff --git a/server/tests/integration/motions/test_polls.py b/server/tests/integration/motions/test_polls.py
index fa7186e37..4743c72dd 100644
--- a/server/tests/integration/motions/test_polls.py
+++ b/server/tests/integration/motions/test_polls.py
@@ -112,6 +112,7 @@ class CreateMotionPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
self.assertTrue(MotionPoll.objects.exists())
poll = MotionPoll.objects.get()
+ self.assertEqual(poll.is_pseudoanonymized, False)
self.assertEqual(poll.title, "test_title_ailai4toogh3eefaa2Vo")
self.assertEqual(poll.pollmethod, "YNA")
self.assertEqual(poll.type, "named")
@@ -394,6 +395,27 @@ class UpdateMotionPoll(TestCase):
poll = MotionPoll.objects.get()
self.assertEqual(poll.type, "analog")
+ def test_patch_type_to_pseudoanonymous(self):
+ response = self.client.patch(
+ reverse("motionpoll-detail", args=[self.poll.pk]),
+ {"type": BasePoll.TYPE_PSEUDOANONYMOUS},
+ )
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ poll = MotionPoll.objects.get()
+ self.assertEqual(poll.type, BasePoll.TYPE_PSEUDOANONYMOUS)
+ self.assertTrue(poll.is_pseudoanonymized)
+
+ def test_patch_type_to_named(self):
+ self.test_patch_type_to_pseudoanonymous()
+ response = self.client.patch(
+ reverse("motionpoll-detail", args=[self.poll.pk]),
+ {"type": BasePoll.TYPE_NAMED},
+ )
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ poll = MotionPoll.objects.get()
+ self.assertEqual(poll.type, BasePoll.TYPE_NAMED)
+ self.assertFalse(poll.is_pseudoanonymized)
+
def test_patch_invalid_type(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]), {"type": "invalid"}
@@ -585,6 +607,7 @@ class VoteMotionPollAnalog(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("4.64"))
self.assertEqual(poll.votesinvalid, Decimal("-2"))
self.assertEqual(poll.votescast, Decimal("-2"))
@@ -668,6 +691,7 @@ class VoteMotionPollAnalog(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("4.64"))
self.assertEqual(poll.votesinvalid, Decimal("-2"))
self.assertEqual(poll.votescast, Decimal("3"))
@@ -715,6 +739,7 @@ class VoteMotionPollNamed(TestCase):
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -730,6 +755,7 @@ class VoteMotionPollNamed(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -754,11 +780,11 @@ class VoteMotionPollNamed(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, weight)
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_with_individual_weight(), weight)
option = poll.options.get()
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("0"))
@@ -784,6 +810,7 @@ class VoteMotionPollNamed(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -874,6 +901,7 @@ class VoteMotionPollNamed(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
@@ -893,6 +921,7 @@ class VoteMotionPollNamed(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("2"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("2"))
@@ -1007,6 +1036,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
self.assertEqual(poll.votesvalid, Decimal("0"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@@ -1022,11 +1052,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
self.assertEqual(poll.votesvalid, Decimal("1"))
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_with_individual_weight(), 1)
option = poll.options.get()
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("1"))
@@ -1142,6 +1172,42 @@ class StopMotionPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_CREATED)
+ def setup_entitled_users(self):
+ self.poll.state = MotionPoll.STATE_STARTED
+ self.poll.save()
+ self.admin = get_user_model().objects.get(username="admin")
+ self.admin.is_present = True
+ self.admin.save()
+ self.group = get_group_model().objects.get(pk=GROUP_ADMIN_PK)
+ self.poll.groups.add(self.group)
+
+ def test_stop_poll_with_entitled_users(self):
+ self.setup_entitled_users()
+ response = self.client.post(reverse("motionpoll-stop", args=[self.poll.pk]))
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ self.assertEqual(
+ MotionPoll.objects.get().entitled_users_at_stop,
+ [{"user_id": self.admin.id, "voted": False, "vote_delegated_to_id": None}],
+ )
+
+ def test_stop_poll_with_entitled_users_and_vote_delegation(self):
+ self.setup_entitled_users()
+ user, _ = self.create_user()
+ self.admin.vote_delegated_to = user
+ self.admin.save()
+ response = self.client.post(reverse("motionpoll-stop", args=[self.poll.pk]))
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ self.assertEqual(
+ MotionPoll.objects.get().entitled_users_at_stop,
+ [
+ {
+ "user_id": self.admin.id,
+ "voted": False,
+ "vote_delegated_to_id": user.id,
+ }
+ ],
+ )
+
class PublishMotionPoll(TestCase):
def advancedSetUp(self):
@@ -1213,8 +1279,9 @@ class PseudoanonymizeMotionPoll(TestCase):
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
+ poll.calculate_votes()
+ self.assertEqual(poll.is_pseudoanonymized, True)
self.assertEqual(poll.get_votes().count(), 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"))
@@ -1282,7 +1349,6 @@ 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_with_individual_weight(), 0)
self.assertEqual(poll.votesvalid, None)
self.assertEqual(poll.votesinvalid, None)
self.assertEqual(poll.votescast, None)
@@ -1292,6 +1358,24 @@ class ResetMotionPoll(TestCase):
self.assertEqual(option.abstain, Decimal("0"))
self.assertFalse(option.votes.exists())
+ def test_reset_pseudoanonymized(self):
+ self.poll.type = BasePoll.TYPE_NAMED
+ self.poll.is_pseudoanonymized = True
+ self.poll.save()
+ response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ poll = MotionPoll.objects.get()
+ self.assertFalse(poll.is_pseudoanonymized)
+
+ def test_reset_pseudoanonymous(self):
+ self.poll.type = BasePoll.TYPE_PSEUDOANONYMOUS
+ self.poll.is_pseudoanonymized = True
+ self.poll.save()
+ response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
+ self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
+ poll = MotionPoll.objects.get()
+ self.assertTrue(poll.is_pseudoanonymized)
+
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
def advancedSetUp(self):
diff --git a/server/tests/unit/motions/test_models.py b/server/tests/unit/motions/test_models.py
index ede566292..dc3af8163 100644
--- a/server/tests/unit/motions/test_models.py
+++ b/server/tests/unit/motions/test_models.py
@@ -1,7 +1,6 @@
-from decimal import Decimal
from unittest import TestCase
-from openslides.motions.models import Motion, MotionChangeRecommendation, MotionPoll
+from openslides.motions.models import Motion, MotionChangeRecommendation
# TODO: test for MotionPoll.set_options()
@@ -51,25 +50,3 @@ class MotionChangeRecommendationTest(TestCase):
other_recommendations
)
self.assertFalse(collides)
-
-
-class MotionPollAnalogFieldsTest(TestCase):
- def setUp(self):
- self.motion = Motion(
- title="test_title_OoK9IeChe2Jeib9Deeji",
- text="test_text_eichui1oobiSeit9aifo",
- )
- self.poll = MotionPoll(
- motion=self.motion,
- title="test_title_tho8PhiePh8upaex6phi",
- pollmethod="YNA",
- type=MotionPoll.TYPE_NAMED,
- )
-
- def test_not_set_vote_values(self):
- with self.assertRaises(ValueError):
- self.poll.votesvalid = Decimal("1")
- with self.assertRaises(ValueError):
- self.poll.votesinvalid = Decimal("1")
- with self.assertRaises(ValueError):
- self.poll.votescast = Decimal("1")