From b5cb694fc738aa150990fc4ddec8c2ca7a8d98fe Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 23 Nov 2020 18:37:00 +0100 Subject: [PATCH] Allow negative voting Adds "no" as the opposite of "votes" as assignment poll method Added global_yes, enabled new voting mode `N` in the server Layout, Tables, Charts, Projector, Vote CSS, Cleanups, Percent bases, analog votes and more --- .../assignment-poll-repository.service.ts | 1 + ...ignment-poll-detail-content.component.html | 24 +- ...ssignment-poll-detail-content.component.ts | 59 +- .../models/assignments/assignment-poll.ts | 20 +- .../app/shared/pipes/poll-key-verbose.pipe.ts | 5 +- .../models/view-assignment-poll.ts | 5 +- .../assignment-poll-dialog.component.html | 13 +- .../assignment-poll-dialog.component.ts | 27 +- .../assignment-poll-vote.component.html | 44 +- .../assignment-poll-vote.component.ts | 63 +- .../services/assignment-poll-pdf.service.ts | 12 +- .../services/assignment-poll.service.ts | 20 +- .../services/assignment-pdf.service.ts | 2 +- .../components/base-poll-vote.component.ts | 6 +- .../poll-form/poll-form.component.html | 65 +- .../poll-form/poll-form.component.scss | 33 +- .../poll-form/poll-form.component.ts | 24 +- .../app/site/polls/models/view-base-poll.ts | 1 + .../app/site/polls/services/poll.service.ts | 5 + .../assignment-poll-slide-data.ts | 1 + .../assignments/access_permissions.py | 6 +- .../assignments/config_variables.py | 2 +- .../migrations/0016_negative_votes.py | 74 ++ .../assignments/migrations/0017_vote_to_y.py | 30 + server/openslides/assignments/models.py | 90 ++- server/openslides/assignments/projector.py | 3 + server/openslides/assignments/serializers.py | 18 +- server/openslides/assignments/views.py | 153 ++-- .../integration/assignments/test_polls.py | 697 +++++++++++++++++- 29 files changed, 1253 insertions(+), 250 deletions(-) create mode 100644 server/openslides/assignments/migrations/0016_negative_votes.py create mode 100644 server/openslides/assignments/migrations/0017_vote_to_y.py diff --git a/client/src/app/core/repositories/assignments/assignment-poll-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-poll-repository.service.ts index acf234cea..d570d53bb 100644 --- a/client/src/app/core/repositories/assignments/assignment-poll-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-poll-repository.service.ts @@ -56,6 +56,7 @@ export interface AssignmentAnalogVoteData { votesvalid?: number; votesinvalid?: number; votescast?: number; + global_yes?: number; global_no?: number; global_abstain?: number; } diff --git a/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.html b/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.html index 1d30173af..c8815b5c3 100644 --- a/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.html +++ b/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.html @@ -3,7 +3,7 @@ {{ 'Candidates' | translate }} - + {{ 'Yes' | translate }} @@ -11,7 +11,14 @@ {{ 'Votes' | translate }} - {{ 'No' | translate }} + + + {{ 'No' | translate }} + + + {{ 'Votes' | translate }} + + {{ 'Abstain' | translate }} @@ -26,13 +33,18 @@ - -
+ +
- {{ vote.amount | pollPercentBase: poll:'assignment' }} + {{ getVoteAmount(vote, row) | pollPercentBase: poll:'assignment' }} + + + {{ getVoteAmount(vote, row) | parsePollNumber }} + + + {{ vote.amount | parsePollNumber }} - {{ vote.amount | parsePollNumber }}
diff --git a/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.ts b/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.ts index 03fe1e3c5..d16d898ee 100644 --- a/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.ts +++ b/client/src/app/shared/components/assignment-poll-detail-content/assignment-poll-detail-content.component.ts @@ -20,8 +20,20 @@ export class AssignmentPollDetailContentComponent { return this.poll.pollmethod; } + public get showYHeader(): boolean { + return this.isMethodY || this.isMethodYN || this.isMethodYNA; + } + + public get showNHeader(): boolean { + return this.isMethodN || this.isMethodYN || this.isMethodYNA; + } + public get isMethodY(): boolean { - return this.method === AssignmentPollMethod.Votes; + return this.method === AssignmentPollMethod.Y; + } + + public get isMethodN(): boolean { + return this.method === AssignmentPollMethod.N; } public get isMethodYN(): boolean { @@ -37,19 +49,44 @@ export class AssignmentPollDetailContentComponent { } public getVoteClass(votingResult: VotingResult): string { - return votingResult.vote; + const votingClass = votingResult.vote; + if (this.isMethodN && votingClass === 'no') { + return 'yes'; + } else { + return votingClass; + } + } + + public filterRelevantResults(votingResult: VotingResult[]): VotingResult[] { + return votingResult.filter(result => { + return result && this.voteFitsMethod(result); + }); + } + + public getVoteAmount(vote: VotingResult, row: PollTableData): number { + if (this.isMethodN && row.class === 'user') { + if (vote.amount < 0) { + return vote.amount; + } else { + return this.poll.votesvalid - vote.amount; + } + } else { + return vote.amount; + } } public voteFitsMethod(result: VotingResult): boolean { - if (this.isMethodY) { - if (result.vote === 'abstain' || result.vote === 'no') { - return false; - } - } else if (this.isMethodYN) { - if (result.vote === 'abstain') { - return false; - } + if (!result.vote) { + return true; + } + if (this.isMethodY) { + return result.vote === 'yes'; + } else if (this.isMethodN) { + return result.vote === 'no'; + } else if (this.isMethodYN) { + return result.vote !== 'abstain'; + } else { + return true; } - return true; } } diff --git a/client/src/app/shared/models/assignments/assignment-poll.ts b/client/src/app/shared/models/assignments/assignment-poll.ts index 7fdda5b29..1381a9ebf 100644 --- a/client/src/app/shared/models/assignments/assignment-poll.ts +++ b/client/src/app/shared/models/assignments/assignment-poll.ts @@ -3,15 +3,16 @@ import { AssignmentOption } from './assignment-option'; import { BasePoll } from '../poll/base-poll'; export enum AssignmentPollMethod { + Y = 'Y', YN = 'YN', YNA = 'YNA', - Votes = 'votes' + N = 'N' } export enum AssignmentPollPercentBase { + Y = 'Y', YN = 'YN', YNA = 'YNA', - Votes = 'votes', Valid = 'valid', Cast = 'cast', Disabled = 'disabled' @@ -33,22 +34,29 @@ export class AssignmentPoll extends BasePoll< 'votesvalid', 'votesinvalid', 'votescast', - 'amount_global_abstain', - 'amount_global_no' + 'amount_global_yes', + 'amount_global_no', + 'amount_global_abstain' ]; public id: number; public assignment_id: number; public votes_amount: number; public allow_multiple_votes_per_candidate: boolean; + public global_yes: boolean; public global_no: boolean; public global_abstain: boolean; + public amount_global_yes: number; public amount_global_no: number; public amount_global_abstain: number; public description: string; public get isMethodY(): boolean { - return this.pollmethod === AssignmentPollMethod.Votes; + return this.pollmethod === AssignmentPollMethod.Y; + } + + public get isMethodN(): boolean { + return this.pollmethod === AssignmentPollMethod.N; } public get isMethodYN(): boolean { @@ -64,7 +72,7 @@ export class AssignmentPoll extends BasePoll< return ['yes', 'no']; } else if (this.pollmethod === AssignmentPollMethod.YNA) { return ['yes', 'no', 'abstain']; - } else if (this.pollmethod === AssignmentPollMethod.Votes) { + } else if (this.pollmethod === AssignmentPollMethod.Y) { return ['yes']; } } diff --git a/client/src/app/shared/pipes/poll-key-verbose.pipe.ts b/client/src/app/shared/pipes/poll-key-verbose.pipe.ts index 3a4c7cf48..4b276178e 100644 --- a/client/src/app/shared/pipes/poll-key-verbose.pipe.ts +++ b/client/src/app/shared/pipes/poll-key-verbose.pipe.ts @@ -9,8 +9,9 @@ const PollValues = { yes: 'Yes', no: 'No', abstain: 'Abstain', - amount_global_abstain: 'General Abstain', - amount_global_no: 'General No' + amount_global_yes: 'General Yes', + amount_global_no: 'General No', + amount_global_abstain: 'General Abstain' }; /** diff --git a/client/src/app/site/assignments/models/view-assignment-poll.ts b/client/src/app/site/assignments/models/view-assignment-poll.ts index 48ea0f612..227b7b34c 100644 --- a/client/src/app/site/assignments/models/view-assignment-poll.ts +++ b/client/src/app/site/assignments/models/view-assignment-poll.ts @@ -18,15 +18,16 @@ export interface AssignmentPollTitleInformation { } export const AssignmentPollMethodVerbose = { - votes: _('Yes per candidate'), + Y: _('Yes per candidate'), + N: _('No per candidate'), YN: _('Yes/No per candidate'), YNA: _('Yes/No/Abstain per candidate') }; export const AssignmentPollPercentBaseVerbose = { + Y: _('Sum of votes including general No/Abstain'), YN: _('Yes/No per candidate'), YNA: _('Yes/No/Abstain per candidate'), - votes: _('Sum of votes including general No/Abstain'), valid: _('All valid ballots'), cast: _('All casted ballots'), disabled: _('Disabled (no percents)') diff --git a/client/src/app/site/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html b/client/src/app/site/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html index 84de94055..b6660d18e 100644 --- a/client/src/app/site/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -45,9 +45,18 @@
+ + -

+

{{ 'Available votes' | translate }}: {{ getVotesAvailable(delegation) }}/{{ poll.votes_amount }} @@ -39,9 +39,9 @@
@@ -64,7 +64,7 @@ > {{ action.icon }} - + {{ action.label | translate }}
@@ -74,9 +74,26 @@
- +
+
+ + + {{ PollPropertyVerbose.global_yes | translate }} + +
+
- {{ 'General No' | translate }} + {{ PollPropertyVerbose.global_no | translate }}
@@ -100,10 +117,10 @@ [ngClass]="getGlobalAbstainClass(delegation)" [disabled]="isDeliveringVote(delegation)" > - trip_origin + trip_origin - {{ 'General Abstain' | translate }} + {{ PollPropertyVerbose.global_abstain | translate }}
@@ -149,7 +166,12 @@
-
diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.scss b/client/src/app/site/polls/components/poll-form/poll-form.component.scss index e219f1795..d1b0a636e 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.scss +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.scss @@ -9,31 +9,16 @@ } } -.poll-preview-meta-info { - display: flex; - justify-content: space-between; - margin: 10px 0; - - .short-description { - flex: 1; - padding: 0 5px; - display: inline-block; - span { - display: block; - } - &-label { - font-size: 75%; - } - } -} - .poll-preview-meta-info-form { - display: flex; - align-items: center; - flex-wrap: wrap; + .suboption { + margin-left: 1.5em; + } - & > * { - flex: 1; - margin: 0 4px; + .mat-checkbox { + margin-right: 2em; + } + + .global-options { + margin-bottom: 1em; } } diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.ts b/client/src/app/site/polls/components/poll-form/poll-form.component.ts index bbdcc920d..dab08dbc9 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.ts @@ -174,6 +174,11 @@ export class PollFormComponent this.contentForm.get('type').disable(); } + public showAmountAndGlobal(data: any): boolean { + const selectedPollMethod = this.contentForm.get('pollmethod').value; + return (selectedPollMethod === 'Y' || selectedPollMethod === 'N') && (!data || !data.state || data.isCreated); + } + /** * updates the available percent bases according to the pollmethod * @param method the currently chosen pollmethod @@ -182,10 +187,10 @@ export class PollFormComponent if (method) { let forbiddenBases = []; if (method === AssignmentPollMethod.YN) { - forbiddenBases = [PercentBase.YNA, AssignmentPollPercentBase.Votes]; + forbiddenBases = [PercentBase.YNA, AssignmentPollPercentBase.Y]; } else if (method === AssignmentPollMethod.YNA) { - forbiddenBases = [AssignmentPollPercentBase.Votes]; - } else if (method === AssignmentPollMethod.Votes) { + forbiddenBases = [AssignmentPollPercentBase.Y]; + } else if (method === AssignmentPollMethod.Y || AssignmentPollMethod.N) { forbiddenBases = [PercentBase.YN, PercentBase.YNA]; } @@ -209,16 +214,16 @@ export class PollFormComponent ): AssignmentPollPercentBase { if ( method === AssignmentPollMethod.YN && - (base === AssignmentPollPercentBase.YNA || base === AssignmentPollPercentBase.Votes) + (base === AssignmentPollPercentBase.YNA || base === AssignmentPollPercentBase.Y) ) { return AssignmentPollPercentBase.YN; - } else if (method === AssignmentPollMethod.YNA && base === AssignmentPollPercentBase.Votes) { + } else if (method === AssignmentPollMethod.YNA && base === AssignmentPollPercentBase.Y) { return AssignmentPollPercentBase.YNA; } else if ( - method === AssignmentPollMethod.Votes && + method === AssignmentPollMethod.Y && (base === AssignmentPollPercentBase.YN || base === AssignmentPollPercentBase.YNA) ) { - return AssignmentPollPercentBase.Votes; + return AssignmentPollPercentBase.Y; } return base; } @@ -267,8 +272,10 @@ export class PollFormComponent : '---' ]); } - if (data.pollmethod === 'votes') { + + if (data.pollmethod === 'Y' || data.pollmethod === 'N') { this.pollValues.push([this.pollService.getVerboseNameForKey('votes_amount'), data.votes_amount]); + this.pollValues.push([this.pollService.getVerboseNameForKey('global_yes'), data.global_yes]); this.pollValues.push([this.pollService.getVerboseNameForKey('global_no'), data.global_no]); this.pollValues.push([this.pollService.getVerboseNameForKey('global_abstain'), data.global_abstain]); } @@ -284,6 +291,7 @@ export class PollFormComponent majority_method: ['', Validators.required], votes_amount: [1, [Validators.required, Validators.min(1)]], groups_id: [], + global_yes: [false], global_no: [false], global_abstain: [false] }); 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 9652bc47b..20a45d2fd 100644 --- a/client/src/app/site/polls/models/view-base-poll.ts +++ b/client/src/app/site/polls/models/view-base-poll.ts @@ -44,6 +44,7 @@ export const PollPropertyVerbose = { state: 'State', groups: 'Entitled to vote', votes_amount: 'Amount of votes', + global_yes: 'General Yes', global_no: 'General No', global_abstain: 'General Abstain' }; diff --git a/client/src/app/site/polls/services/poll.service.ts b/client/src/app/site/polls/services/poll.service.ts index ea31691bd..989433b90 100644 --- a/client/src/app/site/polls/services/poll.service.ts +++ b/client/src/app/site/polls/services/poll.service.ts @@ -109,6 +109,7 @@ export interface PollData { votesvalid: number; votesinvalid: number; votescast: number; + amount_global_yes?: number; amount_global_no?: number; amount_global_abstain?: number; } @@ -145,6 +146,7 @@ export interface VotingResult { | 'votesvalid' | 'votesinvalid' | 'votescast' + | 'amount_global_yes' | 'amount_global_no' | 'amount_global_abstain'; amount?: number; @@ -340,6 +342,9 @@ export abstract class PollService { case AssignmentPollMethod.YN: { return ['yes', 'no']; } + case AssignmentPollMethod.N: { + return ['no']; + } default: { return ['yes']; } 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 4e821c495..de4512813 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 @@ -25,6 +25,7 @@ export interface AssignmentPollSlideData extends BasePollSlideData { }[]; // optional for published polls: + amount_global_yes?: number; amount_global_no?: number; amount_global_abstain?: number; votesvalid: number; diff --git a/server/openslides/assignments/access_permissions.py b/server/openslides/assignments/access_permissions.py index ee64c404c..e707f9e08 100644 --- a/server/openslides/assignments/access_permissions.py +++ b/server/openslides/assignments/access_permissions.py @@ -17,7 +17,11 @@ class AssignmentAccessPermissions(BaseAccessPermissions): class AssignmentPollAccessPermissions(BasePollAccessPermissions): base_permission = "assignments.can_see" manage_permission = "assignments.can_manage" - additional_fields = ["amount_global_no", "amount_global_abstain"] + additional_fields = [ + "amount_global_yes", + "amount_global_no", + "amount_global_abstain", + ] class AssignmentOptionAccessPermissions(BaseOptionAccessPermissions): diff --git a/server/openslides/assignments/config_variables.py b/server/openslides/assignments/config_variables.py index 87985bb7f..da083956b 100644 --- a/server/openslides/assignments/config_variables.py +++ b/server/openslides/assignments/config_variables.py @@ -13,7 +13,7 @@ def get_config_variables(): # Voting yield ConfigVariable( name="assignment_poll_method", - default_value=AssignmentPoll.POLLMETHOD_VOTES, + default_value=AssignmentPoll.POLLMETHOD_Y, input_type="choice", label="Default election method", choices=tuple( diff --git a/server/openslides/assignments/migrations/0016_negative_votes.py b/server/openslides/assignments/migrations/0016_negative_votes.py new file mode 100644 index 000000000..7406b7760 --- /dev/null +++ b/server/openslides/assignments/migrations/0016_negative_votes.py @@ -0,0 +1,74 @@ +# Generated by Django 2.2.15 on 2020-11-24 06:44 + +from decimal import Decimal + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("assignments", "0015_assignmentvote_delegated_user"), + ] + + operations = [ + migrations.AddField( + model_name="assignmentpoll", + name="db_amount_global_yes", + field=models.DecimalField( + blank=True, + decimal_places=6, + default=Decimal("0"), + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal("-2"))], + ), + ), + migrations.AddField( + model_name="assignmentpoll", + name="global_yes", + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name="assignmentpoll", + name="pollmethod", + field=models.CharField( + choices=[ + ("votes", "Yes per candidate"), + ("N", "No per candidate"), + ("YN", "Yes/No per candidate"), + ("YNA", "Yes/No/Abstain per candidate"), + ], + max_length=5, + ), + ), + 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"), + ("disabled", "Disabled (no percents)"), + ], + max_length=8, + ), + ), + migrations.AlterField( + model_name="assignmentpoll", + name="pollmethod", + field=models.CharField( + choices=[ + ("Y", "Yes per candidate"), + ("N", "No per candidate"), + ("YN", "Yes/No per candidate"), + ("YNA", "Yes/No/Abstain per candidate"), + ], + max_length=5, + ), + ), + ] diff --git a/server/openslides/assignments/migrations/0017_vote_to_y.py b/server/openslides/assignments/migrations/0017_vote_to_y.py new file mode 100644 index 000000000..e11ae0177 --- /dev/null +++ b/server/openslides/assignments/migrations/0017_vote_to_y.py @@ -0,0 +1,30 @@ +# Generated by Finn Stutzenstein on 2020-11-24 06:44 + +from django.db import migrations + + +def votes_to_y(apps, schema_editor): + AssignmentPoll = apps.get_model("assignments", "AssignmentPoll") + for poll in AssignmentPoll.objects.all(): + changed = False + if poll.pollmethod == "votes": + poll.pollmethod = "Y" + changed = True + + if poll.onehundred_percent_base == "votes": + poll.onehundred_percent_base = "Y" + changed = True + + if changed: + poll.save(skip_autoupdate=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ("assignments", "0016_negative_votes"), + ] + + operations = [ + migrations.RunPython(votes_to_y), + ] diff --git a/server/openslides/assignments/models.py b/server/openslides/assignments/models.py index 8c8202b68..b8fa9e91f 100644 --- a/server/openslides/assignments/models.py +++ b/server/openslides/assignments/models.py @@ -319,24 +319,26 @@ class AssignmentPoll(RESTModelMixin, BasePoll): POLLMETHOD_YN = "YN" POLLMETHOD_YNA = "YNA" - POLLMETHOD_VOTES = "votes" + POLLMETHOD_Y = "Y" + POLLMETHOD_N = "N" POLLMETHODS = ( - (POLLMETHOD_VOTES, "Yes per candidate"), + (POLLMETHOD_Y, "Yes per candidate"), + (POLLMETHOD_N, "No per candidate"), (POLLMETHOD_YN, "Yes/No per candidate"), (POLLMETHOD_YNA, "Yes/No/Abstain per candidate"), ) pollmethod = models.CharField(max_length=5, choices=POLLMETHODS) + PERCENT_BASE_Y = "Y" PERCENT_BASE_YN = "YN" PERCENT_BASE_YNA = "YNA" - PERCENT_BASE_VOTES = "votes" PERCENT_BASE_VALID = "valid" PERCENT_BASE_CAST = "cast" PERCENT_BASE_DISABLED = "disabled" PERCENT_BASES = ( (PERCENT_BASE_YN, "Yes/No per candidate"), (PERCENT_BASE_YNA, "Yes/No/Abstain per candidate"), - (PERCENT_BASE_VOTES, "Sum of votes including general No/Abstain"), + (PERCENT_BASE_Y, "Sum of votes including general No/Abstain"), (PERCENT_BASE_VALID, "All valid ballots"), (PERCENT_BASE_CAST, "All casted ballots"), (PERCENT_BASE_DISABLED, "Disabled (no percents)"), @@ -345,8 +347,8 @@ class AssignmentPoll(RESTModelMixin, BasePoll): max_length=8, blank=False, null=False, choices=PERCENT_BASES ) - global_abstain = models.BooleanField(default=True) - db_amount_global_abstain = models.DecimalField( + global_yes = models.BooleanField(default=True) + db_amount_global_yes = models.DecimalField( null=True, blank=True, default=Decimal("0"), @@ -354,6 +356,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll): max_digits=15, decimal_places=6, ) + global_no = models.BooleanField(default=True) db_amount_global_no = models.DecimalField( null=True, @@ -364,6 +367,16 @@ class AssignmentPoll(RESTModelMixin, BasePoll): decimal_places=6, ) + global_abstain = models.BooleanField(default=True) + db_amount_global_abstain = models.DecimalField( + null=True, + blank=True, + default=Decimal("0"), + validators=[MinValueValidator(Decimal("-2"))], + max_digits=15, + decimal_places=6, + ) + votes_amount = models.IntegerField(default=1, validators=[MinValueValidator(1)]) """ For "votes" mode: The amount of votes a voter can give. """ @@ -372,12 +385,55 @@ class AssignmentPoll(RESTModelMixin, BasePoll): class Meta: default_permissions = () + def get_amount_global_yes(self): + if not self.global_yes: + return None + elif self.type == self.TYPE_ANALOG: + return self.db_amount_global_yes + elif self.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ): + return sum(option.yes for option in self.options.all()) + else: + return None + + def set_amount_global_yes(self, value): + if self.type != self.TYPE_ANALOG: + raise ValueError("Do not set amount_global_yes for non analog polls") + self.db_amount_global_yes = value + + amount_global_yes = property(get_amount_global_yes, set_amount_global_yes) + + def get_amount_global_no(self): + if not self.global_no: + return None + elif self.type == self.TYPE_ANALOG: + return self.db_amount_global_no + elif self.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ): + return sum(option.no for option in self.options.all()) + else: + return None + + def set_amount_global_no(self, value): + if self.type != self.TYPE_ANALOG: + raise ValueError("Do not set amount_global_no for non analog polls") + self.db_amount_global_no = value + + amount_global_no = property(get_amount_global_no, set_amount_global_no) + def get_amount_global_abstain(self): if not self.global_abstain: return None elif self.type == self.TYPE_ANALOG: return self.db_amount_global_abstain - elif self.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: + elif self.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ): return sum(option.abstain for option in self.options.all()) else: return None @@ -391,23 +447,6 @@ class AssignmentPoll(RESTModelMixin, BasePoll): get_amount_global_abstain, set_amount_global_abstain ) - def get_amount_global_no(self): - if not self.global_no: - return None - elif self.type == self.TYPE_ANALOG: - return self.db_amount_global_no - elif self.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: - return sum(option.no for option in self.options.all()) - else: - return None - - def set_amount_global_no(self, value): - if self.type != self.TYPE_ANALOG: - raise ValueError("Do not set amount_global_no for non analog polls") - self.db_amount_global_no = value - - amount_global_no = property(get_amount_global_no, set_amount_global_no) - def create_options(self, skip_autoupdate=False): related_users = AssignmentRelatedUser.objects.filter( assignment__id=self.assignment.id @@ -435,6 +474,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll): inform_changed_data(self.assignment.list_of_speakers) def reset(self): - self.db_amount_global_abstain = Decimal(0) + self.db_amount_global_yes = Decimal(0) self.db_amount_global_no = Decimal(0) + self.db_amount_global_abstain = Decimal(0) super().reset() diff --git a/server/openslides/assignments/projector.py b/server/openslides/assignments/projector.py index 97c0be82a..1cd07523d 100644 --- a/server/openslides/assignments/projector.py +++ b/server/openslides/assignments/projector.py @@ -86,6 +86,9 @@ async def assignment_poll_slide( poll_data["options"].append(option_data) if poll["state"] == AssignmentPoll.STATE_PUBLISHED: + poll_data["amount_global_yes"] = ( + float(poll["amount_global_yes"]) if poll["amount_global_yes"] else None + ) poll_data["amount_global_no"] = ( float(poll["amount_global_no"]) if poll["amount_global_no"] else None ) diff --git a/server/openslides/assignments/serializers.py b/server/openslides/assignments/serializers.py index 47cae0aed..2ca605e5c 100644 --- a/server/openslides/assignments/serializers.py +++ b/server/openslides/assignments/serializers.py @@ -77,6 +77,9 @@ class AssignmentPollSerializer(BasePollSerializer): Serializes all polls. """ + amount_global_yes = DecimalField( + max_digits=15, decimal_places=6, min_value=-2, read_only=True + ) amount_global_no = DecimalField( max_digits=15, decimal_places=6, min_value=-2, read_only=True ) @@ -92,6 +95,8 @@ class AssignmentPollSerializer(BasePollSerializer): "pollmethod", "votes_amount", "allow_multiple_votes_per_candidate", + "global_yes", + "amount_global_yes", "global_no", "amount_global_no", "global_abstain", @@ -111,13 +116,13 @@ class AssignmentPollSerializer(BasePollSerializer): Returns None, if the 100-%-base must not be changed, otherwise the correct 100-%-base. """ if pollmethod == AssignmentPoll.POLLMETHOD_YN and onehundred_percent_base in ( - AssignmentPoll.PERCENT_BASE_VOTES, + AssignmentPoll.PERCENT_BASE_Y, AssignmentPoll.PERCENT_BASE_YNA, ): return AssignmentPoll.PERCENT_BASE_YN if ( pollmethod == AssignmentPoll.POLLMETHOD_YNA - and onehundred_percent_base == AssignmentPoll.PERCENT_BASE_VOTES + and onehundred_percent_base == AssignmentPoll.PERCENT_BASE_Y ): if old_100_percent_base is None: return AssignmentPoll.PERCENT_BASE_YNA @@ -129,12 +134,11 @@ class AssignmentPollSerializer(BasePollSerializer): return old_100_percent_base else: return pollmethod - if ( - pollmethod == AssignmentPoll.POLLMETHOD_VOTES - and onehundred_percent_base - in (AssignmentPoll.PERCENT_BASE_YN, AssignmentPoll.PERCENT_BASE_YNA) + if pollmethod == AssignmentPoll.POLLMETHOD_Y and onehundred_percent_base in ( + AssignmentPoll.PERCENT_BASE_YN, + AssignmentPoll.PERCENT_BASE_YNA, ): - return AssignmentPoll.PERCENT_BASE_VOTES + return AssignmentPoll.PERCENT_BASE_Y return None diff --git a/server/openslides/assignments/views.py b/server/openslides/assignments/views.py index 3ed5c6bdd..ef072754d 100644 --- a/server/openslides/assignments/views.py +++ b/server/openslides/assignments/views.py @@ -268,21 +268,32 @@ class AssignmentPollViewSet(BasePollViewSet): super().perform_create(serializer) poll = AssignmentPoll.objects.get(pk=serializer.data["id"]) - poll.db_amount_global_abstain = Decimal(0) + poll.db_amount_global_yes = Decimal(0) poll.db_amount_global_no = Decimal(0) + poll.db_amount_global_abstain = Decimal(0) poll.save() def handle_analog_vote(self, data, poll): for field in ["votesvalid", "votesinvalid", "votescast"]: setattr(poll, field, data[field]) - global_no_enabled = ( - poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES + global_yes_enabled = poll.global_yes and poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ) + if global_yes_enabled: + poll.amount_global_yes = data.get("amount_global_yes", Decimal(0)) + + global_no_enabled = poll.global_no and poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, ) if global_no_enabled: poll.amount_global_no = data.get("amount_global_no", Decimal(0)) - global_abstain_enabled = ( - poll.global_abstain and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES + + global_abstain_enabled = poll.global_abstain and poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, ) if global_abstain_enabled: poll.amount_global_abstain = data.get("amount_global_abstain", Decimal(0)) @@ -293,28 +304,47 @@ class AssignmentPollViewSet(BasePollViewSet): with transaction.atomic(): for option_id, vote in options_data.items(): option = options.get(pk=int(option_id)) - vote_obj, _ = AssignmentVote.objects.get_or_create( - option=option, value="Y" - ) - vote_obj.weight = vote["Y"] - vote_obj.save() - if poll.pollmethod in ( - AssignmentPoll.POLLMETHOD_YN, - AssignmentPoll.POLLMETHOD_YNA, - ): + if poll.pollmethod == AssignmentPoll.POLLMETHOD_N: vote_obj, _ = AssignmentVote.objects.get_or_create( option=option, value="N" ) vote_obj.weight = vote["N"] vote_obj.save() - if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA: + elif poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_YN, + AssignmentPoll.POLLMETHOD_YNA, + ): + # All three methods have a Y vote_obj, _ = AssignmentVote.objects.get_or_create( - option=option, value="A" + option=option, value="Y" ) - vote_obj.weight = vote["A"] + vote_obj.weight = vote["Y"] vote_obj.save() + + if poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_YN, + AssignmentPoll.POLLMETHOD_YNA, + ): + vote_obj, _ = AssignmentVote.objects.get_or_create( + option=option, value="N" + ) + vote_obj.weight = vote["N"] + vote_obj.save() + + if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA: + vote_obj, _ = AssignmentVote.objects.get_or_create( + option=option, value="A" + ) + vote_obj.weight = vote["A"] + vote_obj.save() + + else: + raise NotImplementedError( + f"handle_analog_vote not implemented for {poll.pollmethod}" + ) inform_changed_data(option) poll.save() @@ -326,22 +356,27 @@ class AssignmentPollViewSet(BasePollViewSet): { "options": {: {"Y": , ["N": ], ["A": ] }}, ["votesvalid": ], ["votesinvalid": ], ["votescast": ], - ["amount_global_no": ], ["amount_global_abstain": ] + ["amount_global_yes": ], + ["amount_global_no": ], + ["amount_global_abstain": ] } All amounts are decimals as strings required fields per pollmethod: - votes: Y - YN: YN - YNA: YNA + - N: N named|pseudoanonymous: votes: - {: } | 'N' | 'A' + {: } | 'Y' | 'N' | 'A' - Exactly one of the three options must be given + - 'Y' is only valid if poll.global_yes==True - 'N' is only valid if poll.global_no==True - 'A' is only valid if poll.global_abstain==True - amounts must be integer numbers >= 0. - ids should be integers of valid option ids for this poll - amounts must be 0 or 1, if poll.allow_multiple_votes_per_candidate is False + - if an option is not given, 0 is assumed - The sum of all amounts must be grater than 0 and <= poll.votes_amount YN/YNA: @@ -361,39 +396,56 @@ class AssignmentPollViewSet(BasePollViewSet): raise ValidationError({"detail": "Keys must be int"}) if not isinstance(value, dict): raise ValidationError({"detail": "A dict per option is required"}) - value["Y"] = self.parse_vote_value(value, "Y") - if poll.pollmethod in ( - AssignmentPoll.POLLMETHOD_YN, - AssignmentPoll.POLLMETHOD_YNA, - ): + if poll.pollmethod == AssignmentPoll.POLLMETHOD_N: value["N"] = self.parse_vote_value(value, "N") - if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA: - value["A"] = self.parse_vote_value(value, "A") + else: + value["Y"] = self.parse_vote_value(value, "Y") + if poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_YN, + AssignmentPoll.POLLMETHOD_YNA, + ): + value["N"] = self.parse_vote_value(value, "N") + if poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA: + value["A"] = self.parse_vote_value(value, "A") for field in ["votesvalid", "votesinvalid", "votescast"]: data[field] = self.parse_vote_value(data, field) - global_no_enabled = ( - poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES + global_yes_enabled = poll.global_yes and poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, ) - global_abstain_enabled = ( - poll.global_abstain - and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES - ) - if "amount_global_abstain" in data and global_abstain_enabled: - data["amount_global_abstain"] = self.parse_vote_value( - data, "amount_global_abstain" + if "amount_global_yes" in data and global_yes_enabled: + data["amount_global_yes"] = self.parse_vote_value( + data, "amount_global_yes" ) + + global_no_enabled = poll.global_no and poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ) if "amount_global_no" in data and global_no_enabled: data["amount_global_no"] = self.parse_vote_value( data, "amount_global_no" ) - else: + global_abstain_enabled = poll.global_abstain and poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ) + if "amount_global_abstain" in data and global_abstain_enabled: + data["amount_global_abstain"] = self.parse_vote_value( + data, "amount_global_abstain" + ) + + else: # non-analog polls if isinstance(data, dict) and len(data) == 0: raise ValidationError({"details": "Empty ballots are not allowed"}) available_options = poll.get_options() - if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: + if poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ): if isinstance(data, dict): amount_sum = 0 for option_id, amount in data.items(): @@ -426,10 +478,13 @@ class AssignmentPollViewSet(BasePollViewSet): "args": [poll.votes_amount], } ) + # return, if there is a global vote, because we dont have to check option presence + elif data == "Y" and poll.global_yes: + return elif data == "N" and poll.global_no: - return # return because we dont have to check option presence + return elif data == "A" and poll.global_abstain: - return # return because we dont have to check option presence + return else: raise ValidationError({"detail": "invalid data."}) @@ -479,12 +534,15 @@ class AssignmentPollViewSet(BasePollViewSet): weight = Decimal(amount) if config["users_activate_vote_weight"]: weight *= vote_weight + value = "Y" # POLLMETHOD_Y + if poll.pollmethod == AssignmentPoll.POLLMETHOD_N: + value = "N" vote = AssignmentVote.objects.create( option=option, user=vote_user, delegated_user=request_user, weight=weight, - value="Y", + value=value, ) inform_changed_data(vote, no_delete_on_restriction=True) else: # global_no or global_abstain @@ -529,7 +587,10 @@ class AssignmentPollViewSet(BasePollViewSet): VotedModel.objects.create(assignmentpoll=poll, user=user) def handle_named_vote(self, data, poll, vote_user, request_user): - if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: + if poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ): self.create_votes_type_votes( data, poll, vote_user.vote_weight, vote_user, request_user ) @@ -540,16 +601,22 @@ class AssignmentPollViewSet(BasePollViewSet): self.create_votes_types_yn_yna( data, poll, vote_user.vote_weight, vote_user, request_user ) + else: + raise NotImplementedError(f"The method {poll.pollmethod} is not supported!") def handle_pseudoanonymous_vote(self, data, poll, user): - if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: + if poll.pollmethod in ( + AssignmentPoll.POLLMETHOD_Y, + AssignmentPoll.POLLMETHOD_N, + ): self.create_votes_type_votes(data, poll, user.vote_weight, None, None) - elif poll.pollmethod in ( AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YNA, ): self.create_votes_types_yn_yna(data, poll, user.vote_weight, None, None) + else: + raise NotImplementedError(f"The method {poll.pollmethod} is not supported!") def convert_option_data(self, poll, data): poll_options = poll.get_options() diff --git a/server/tests/integration/assignments/test_polls.py b/server/tests/integration/assignments/test_polls.py index 14519daff..2c642f2b9 100644 --- a/server/tests/integration/assignments/test_polls.py +++ b/server/tests/integration/assignments/test_polls.py @@ -129,8 +129,10 @@ class CreateAssignmentPoll(TestCase): self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YNA) self.assertEqual(poll.type, "named") # Check defaults + self.assertTrue(poll.global_yes) self.assertTrue(poll.global_no) self.assertTrue(poll.global_abstain) + self.assertEqual(poll.amount_global_yes, None) self.assertEqual(poll.amount_global_no, None) self.assertEqual(poll.amount_global_abstain, None) self.assertFalse(poll.allow_multiple_votes_per_candidate) @@ -151,6 +153,7 @@ class CreateAssignmentPoll(TestCase): "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "majority_method": AssignmentPoll.MAJORITY_THREE_QUARTERS, + "global_yes": False, "global_no": False, "global_abstain": False, "allow_multiple_votes_per_candidate": True, @@ -164,6 +167,7 @@ class CreateAssignmentPoll(TestCase): self.assertEqual(poll.title, "test_title_ahThai4pae1pi4xoogoo") self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YN) self.assertEqual(poll.type, "pseudoanonymous") + self.assertFalse(poll.global_yes) self.assertFalse(poll.global_no) self.assertFalse(poll.global_abstain) self.assertTrue(poll.allow_multiple_votes_per_candidate) @@ -327,7 +331,7 @@ class CreateAssignmentPoll(TestCase): "pollmethod": AssignmentPoll.POLLMETHOD_YNA, "type": "named", "assignment_id": self.assignment.id, - "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_VOTES, + "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_Y, "majority_method": AssignmentPoll.MAJORITY_SIMPLE, }, ) @@ -343,7 +347,7 @@ class CreateAssignmentPoll(TestCase): "pollmethod": AssignmentPoll.POLLMETHOD_YN, "type": "named", "assignment_id": self.assignment.id, - "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_VOTES, + "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_Y, "majority_method": AssignmentPoll.MAJORITY_SIMPLE, }, ) @@ -356,7 +360,7 @@ class CreateAssignmentPoll(TestCase): reverse("assignmentpoll-list"), { "title": "test_title_Thoo2eiphohhi1eeXoow", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, "type": "named", "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, @@ -365,16 +369,14 @@ class CreateAssignmentPoll(TestCase): ) self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED) poll = AssignmentPoll.objects.get() - self.assertEqual( - poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_VOTES - ) + self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_Y) def test_create_with_votes(self): response = self.client.post( reverse("assignmentpoll-list"), { "title": "test_title_dKbv5tV47IzY1oGHXdSz", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, "type": AssignmentPoll.TYPE_ANALOG, "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, @@ -400,7 +402,7 @@ class CreateAssignmentPoll(TestCase): reverse("assignmentpoll-list"), { "title": "test_title_dKbv5tV47IzY1oGHXdSz", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, "type": AssignmentPoll.TYPE_ANALOG, "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, @@ -408,7 +410,7 @@ class CreateAssignmentPoll(TestCase): "votes": { "options": {"2": {"Y": 1}}, "votesvalid": "-2", - "votesinvalid": "-2", + "votesinvalid": "11", "votescast": "-2", }, }, @@ -418,12 +420,12 @@ class CreateAssignmentPoll(TestCase): self.assertEqual(poll.state, AssignmentPoll.STATE_FINISHED) self.assertTrue(AssignmentVote.objects.exists()) - def test_create_with_votes_publish_immediately(self): + def test_create_with_votes_publish_immediately_method_y(self): response = self.client.post( reverse("assignmentpoll-list"), { "title": "test_title_dKbv5tV47IzY1oGHXdSz", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, "type": AssignmentPoll.TYPE_ANALOG, "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, @@ -442,12 +444,46 @@ class CreateAssignmentPoll(TestCase): self.assertEqual(poll.state, AssignmentPoll.STATE_PUBLISHED) self.assertTrue(AssignmentVote.objects.exists()) + def test_create_with_votes_publish_immediately_method_n(self): + response = self.client.post( + reverse("assignmentpoll-list"), + { + "title": "test_title_greoGKPO3FeBAfwpefl3", + "pollmethod": AssignmentPoll.POLLMETHOD_N, + "type": AssignmentPoll.TYPE_ANALOG, + "assignment_id": self.assignment.id, + "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, + "majority_method": AssignmentPoll.MAJORITY_SIMPLE, + "votes": { + "options": {"1": {"N": 1}}, + "votesvalid": "-2", + "votesinvalid": "-2", + "votescast": "-2", + "amount_global_yes": 1, + "amount_global_no": 2, + "amount_global_abstain": 3, + }, + "publish_immediately": "1", + }, + ) + self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_PUBLISHED) + self.assertTrue(AssignmentVote.objects.exists()) + self.assertEquals(poll.amount_global_yes, Decimal("1")) + self.assertEquals(poll.amount_global_no, Decimal("2")) + self.assertEquals(poll.amount_global_abstain, Decimal("3")) + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("0")) + self.assertEqual(option.no, Decimal("1")) + self.assertEqual(option.abstain, Decimal("0")) + def test_create_with_invalid_votes(self): response = self.client.post( reverse("assignmentpoll-list"), { "title": "test_title_dKbv5tV47IzY1oGHXdSz", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, "type": AssignmentPoll.TYPE_ANALOG, "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, @@ -468,7 +504,7 @@ class CreateAssignmentPoll(TestCase): reverse("assignmentpoll-list"), { "title": "test_title_dKbv5tV47IzY1oGHXdSz", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, "type": AssignmentPoll.TYPE_NAMED, "assignment_id": self.assignment.id, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, @@ -500,9 +536,9 @@ class UpdateAssignmentPoll(TestCase): self.poll = AssignmentPoll.objects.create( assignment=self.assignment, title="test_title_beeFaihuNae1vej2ai8m", - pollmethod=AssignmentPoll.POLLMETHOD_VOTES, + pollmethod=AssignmentPoll.POLLMETHOD_Y, type=BasePoll.TYPE_NAMED, - onehundred_percent_base=AssignmentPoll.PERCENT_BASE_VOTES, + onehundred_percent_base=AssignmentPoll.PERCENT_BASE_Y, majority_method=AssignmentPoll.MAJORITY_SIMPLE, ) self.poll.create_options() @@ -545,7 +581,7 @@ class UpdateAssignmentPoll(TestCase): ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) poll = AssignmentPoll.objects.get() - self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_VOTES) + self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_Y) def test_patch_type(self): response = self.client.patch( @@ -631,9 +667,7 @@ class UpdateAssignmentPoll(TestCase): ) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) poll = AssignmentPoll.objects.get() - self.assertEqual( - poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_VOTES - ) + self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_Y) def test_patch_majority_method(self): response = self.client.patch( @@ -658,7 +692,8 @@ class UpdateAssignmentPoll(TestCase): reverse("assignmentpoll-detail", args=[self.poll.pk]), { "title": "test_title_ees6Tho8ahheen4cieja", - "pollmethod": AssignmentPoll.POLLMETHOD_VOTES, + "pollmethod": AssignmentPoll.POLLMETHOD_Y, + "global_yes": True, "global_no": True, "global_abstain": False, "allow_multiple_votes_per_candidate": True, @@ -668,9 +703,11 @@ class UpdateAssignmentPoll(TestCase): self.assertHttpStatusVerbose(response, status.HTTP_200_OK) poll = AssignmentPoll.objects.get() self.assertEqual(poll.title, "test_title_ees6Tho8ahheen4cieja") - self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_VOTES) + self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_Y) + self.assertTrue(poll.global_yes) self.assertTrue(poll.global_no) self.assertFalse(poll.global_abstain) + self.assertEqual(poll.amount_global_yes, Decimal("0")) self.assertEqual(poll.amount_global_no, Decimal("0")) self.assertEqual(poll.amount_global_abstain, None) self.assertTrue(poll.allow_multiple_votes_per_candidate) @@ -1220,12 +1257,12 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass): self.assertFalse(AssignmentVote.objects.exists()) -class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): +class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass): def create_poll(self): return AssignmentPoll.objects.create( assignment=self.assignment, title="test_title_Zrvh146QAdq7t6iSDwZk", - pollmethod=AssignmentPoll.POLLMETHOD_VOTES, + pollmethod=AssignmentPoll.POLLMETHOD_Y, type=BasePoll.TYPE_NAMED, ) @@ -1296,6 +1333,34 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.assertEqual(option2.no, Decimal("0")) self.assertEqual(option2.abstain, Decimal("0")) + def test_global_yes(self): + self.poll.votes_amount = 2 + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "Y"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("1")) + self.assertEqual(option.no, Decimal("0")) + self.assertEqual(option.abstain, Decimal("0")) + self.assertEqual(poll.amount_global_yes, Decimal("1")) + self.assertEqual(poll.amount_global_no, Decimal("0")) + self.assertEqual(poll.amount_global_abstain, Decimal("0")) + + def test_global_yes_forbidden(self): + self.poll.global_yes = False + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "Y"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + self.assertEqual(AssignmentPoll.objects.get().amount_global_yes, None) + def test_global_no(self): self.poll.votes_amount = 2 self.poll.save() @@ -1309,6 +1374,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.assertEqual(option.yes, Decimal("0")) self.assertEqual(option.no, Decimal("1")) self.assertEqual(option.abstain, Decimal("0")) + self.assertEqual(poll.amount_global_yes, Decimal("0")) self.assertEqual(poll.amount_global_no, Decimal("1")) self.assertEqual(poll.amount_global_abstain, Decimal("0")) @@ -1336,6 +1402,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.assertEqual(option.yes, Decimal("0")) self.assertEqual(option.no, Decimal("0")) self.assertEqual(option.abstain, Decimal("1")) + self.assertEqual(poll.amount_global_yes, Decimal("0")) self.assertEqual(poll.amount_global_no, Decimal("0")) self.assertEqual(poll.amount_global_abstain, Decimal("1")) @@ -1505,6 +1572,321 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): self.assertFalse(AssignmentVote.objects.exists()) +class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_4oi49ckKFk39SDIfj30s", + pollmethod=AssignmentPoll.POLLMETHOD_N, + type=BasePoll.TYPE_NAMED, + ) + + def setup_for_multiple_votes(self): + self.poll.allow_multiple_votes_per_candidate = True + self.poll.votes_amount = 3 + self.poll.save() + self.add_candidate() + + def test_start_poll(self): + response = self.client.post( + reverse("assignmentpoll-start", args=[self.poll.pk]) + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + self.assertEqual(poll.votesvalid, Decimal("0")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("0")) + self.assertFalse(poll.get_votes().exists()) + + def test_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1, "2": 0}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 1) + poll = AssignmentPoll.objects.get() + 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.assertTrue(self.admin in poll.voted.all()) + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("1")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_change_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1, "2": 0}}, + format="json", + ) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 0, "2": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("1")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_global_yes(self): + self.poll.votes_amount = 2 + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "Y"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("1")) + self.assertEqual(option.no, Decimal("0")) + self.assertEqual(option.abstain, Decimal("0")) + self.assertEqual(poll.amount_global_yes, Decimal("1")) + self.assertEqual(poll.amount_global_no, Decimal("0")) + self.assertEqual(poll.amount_global_abstain, Decimal("0")) + + def test_global_yes_forbidden(self): + self.poll.global_yes = False + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "Y"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + self.assertEqual(AssignmentPoll.objects.get().amount_global_yes, None) + + def test_global_no(self): + self.poll.votes_amount = 2 + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("0")) + self.assertEqual(option.no, Decimal("1")) + self.assertEqual(option.abstain, Decimal("0")) + self.assertEqual(poll.amount_global_yes, Decimal("0")) + self.assertEqual(poll.amount_global_no, Decimal("1")) + self.assertEqual(poll.amount_global_abstain, Decimal("0")) + + def test_global_no_forbidden(self): + self.poll.global_no = False + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + self.assertEqual(AssignmentPoll.objects.get().amount_global_no, None) + + def test_global_abstain(self): + self.poll.votes_amount = 2 + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option = poll.options.get(pk=1) + self.assertEqual(option.yes, Decimal("0")) + self.assertEqual(option.no, Decimal("0")) + self.assertEqual(option.abstain, Decimal("1")) + self.assertEqual(poll.amount_global_yes, Decimal("0")) + self.assertEqual(poll.amount_global_no, Decimal("0")) + self.assertEqual(poll.amount_global_abstain, Decimal("1")) + + def test_global_abstain_forbidden(self): + self.poll.global_abstain = False + self.poll.save() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"} + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + self.assertEqual(AssignmentPoll.objects.get().amount_global_abstain, None) + + def test_negative_vote(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": -1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_multiple_votes(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 2, "2": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("2")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("1")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_multiple_votes_wrong_amount(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 2, "2": 2}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_too_many_options(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1, "2": 1, "3": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_options(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"2": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_no_permissions(self): + self.start_poll() + self.make_admin_delegate() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_anonymous(self): + self.start_poll() + gclient = self.create_guest_client() + response = gclient.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_vote_not_present(self): + self.start_poll() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_state(self): + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_missing_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + poll = AssignmentPoll.objects.get() + self.assertNotIn(self.admin.id, poll.voted.all()) + + def test_wrong_data_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": [1, 2, 5]}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_option_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": "string"}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_option_id_type(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"id": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_vote_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": [None]}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): def create_poll(self): return AssignmentPoll.objects.create( @@ -1698,12 +2080,12 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass): self.assertFalse(AssignmentVote.objects.exists()) -class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): +class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass): def create_poll(self): return AssignmentPoll.objects.create( assignment=self.assignment, title="test_title_Zrvh146QAdq7t6iSDwZk", - pollmethod=AssignmentPoll.POLLMETHOD_VOTES, + pollmethod=AssignmentPoll.POLLMETHOD_Y, type=BasePoll.TYPE_PSEUDOANONYMOUS, ) @@ -1933,6 +2315,241 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): self.assertFalse(AssignmentVote.objects.exists()) +class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass): + def create_poll(self): + return AssignmentPoll.objects.create( + assignment=self.assignment, + title="test_title_wWPOVJgL9afm83eamf3e", + pollmethod=AssignmentPoll.POLLMETHOD_N, + type=BasePoll.TYPE_PSEUDOANONYMOUS, + ) + + def setup_for_multiple_votes(self): + self.poll.allow_multiple_votes_per_candidate = True + self.poll.votes_amount = 3 + self.poll.save() + self.add_candidate() + + def test_start_poll(self): + response = self.client.post( + reverse("assignmentpoll-start", args=[self.poll.pk]) + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED) + self.assertEqual(poll.votesvalid, Decimal("0")) + self.assertEqual(poll.votesinvalid, Decimal("0")) + self.assertEqual(poll.votescast, Decimal("0")) + self.assertFalse(poll.get_votes().exists()) + + def test_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1, "2": 0}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + self.assertEqual(AssignmentVote.objects.count(), 1) + poll = AssignmentPoll.objects.get() + 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.assertTrue(self.admin in poll.voted.all()) + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("1")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + for vote in poll.get_votes(): + self.assertIsNone(vote.user) + + def test_change_vote(self): + self.add_candidate() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1, "2": 0}}, + format="json", + ) + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 0, "2": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("1")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("0")) + self.assertEqual(option2.abstain, Decimal("0")) + + def test_negative_vote(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": -1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_multiple_votes(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 2, "2": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_200_OK) + poll = AssignmentPoll.objects.get() + option1 = poll.options.get(pk=1) + option2 = poll.options.get(pk=2) + self.assertEqual(option1.yes, Decimal("0")) + self.assertEqual(option1.no, Decimal("2")) + self.assertEqual(option1.abstain, Decimal("0")) + self.assertEqual(option2.yes, Decimal("0")) + self.assertEqual(option2.no, Decimal("1")) + self.assertEqual(option2.abstain, Decimal("0")) + for vote in poll.get_votes(): + self.assertIsNone(vote.user) + + def test_multiple_votes_wrong_amount(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 2, "2": 2}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_too_many_options(self): + self.setup_for_multiple_votes() + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1, "2": 1, "3": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_options(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"2": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_no_permissions(self): + self.start_poll() + self.make_admin_delegate() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_anonymous(self): + self.start_poll() + gclient = self.create_guest_client() + response = gclient.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_vote_not_present(self): + self.start_poll() + self.admin.is_present = False + self.admin.save() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_state(self): + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_missing_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}} + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + poll = AssignmentPoll.objects.get() + self.assertNotIn(self.admin.id, poll.voted.all()) + + def test_wrong_data_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"data": [1, 2, 5]}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_option_format(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": "string"}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentPoll.objects.get().get_votes().exists()) + + def test_wrong_option_id_type(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"id": 1}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + def test_wrong_vote_data(self): + self.start_poll() + response = self.client.post( + reverse("assignmentpoll-vote", args=[self.poll.pk]), + {"data": {"1": [None]}}, + format="json", + ) + self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) + self.assertFalse(AssignmentVote.objects.exists()) + + # test autoupdates class VoteAssignmentPollAutoupdatesBaseClass(TestCase): poll_type = "" # set by subclass, defines which poll type we use @@ -1994,10 +2611,12 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) "assignments/assignment-poll:1": { "allow_multiple_votes_per_candidate": False, "assignment_id": 1, - "global_abstain": True, + "global_yes": True, "global_no": True, - "amount_global_abstain": None, + "global_abstain": True, + "amount_global_yes": None, "amount_global_no": None, + "amount_global_abstain": None, "groups_id": [GROUP_DELEGATE_PK], "id": 1, "options_id": [1], @@ -2064,8 +2683,9 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) { "allow_multiple_votes_per_candidate": False, "assignment_id": 1, - "global_abstain": True, + "global_yes": True, "global_no": True, + "global_abstain": True, "pollmethod": AssignmentPoll.POLLMETHOD_YNA, "state": AssignmentPoll.STATE_STARTED, "type": AssignmentPoll.TYPE_NAMED, @@ -2114,12 +2734,14 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass) autoupdate[0]["assignments/assignment-poll:1"], { "allow_multiple_votes_per_candidate": False, - "amount_global_abstain": None, + "amount_global_yes": None, "amount_global_no": None, + "amount_global_abstain": None, "assignment_id": 1, "description": "test_description_paiquei5ahpie1wu8ohW", - "global_abstain": True, + "global_yes": True, "global_no": True, + "global_abstain": True, "groups_id": [GROUP_DELEGATE_PK], "id": 1, "majority_method": "two_thirds", @@ -2186,10 +2808,12 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( "assignments/assignment-poll:1": { "allow_multiple_votes_per_candidate": False, "assignment_id": 1, - "global_abstain": True, + "global_yes": True, "global_no": True, - "amount_global_abstain": None, + "global_abstain": True, + "amount_global_yes": None, "amount_global_no": None, + "amount_global_abstain": None, "groups_id": [GROUP_DELEGATE_PK], "id": 1, "options_id": [1], @@ -2241,8 +2865,9 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( { "allow_multiple_votes_per_candidate": False, "assignment_id": 1, - "global_abstain": True, + "global_yes": True, "global_no": True, + "global_abstain": True, "pollmethod": AssignmentPoll.POLLMETHOD_YNA, "state": AssignmentPoll.STATE_STARTED, "type": AssignmentPoll.TYPE_PSEUDOANONYMOUS, @@ -2291,12 +2916,14 @@ class VoteAssignmentPollPseudoanonymousAutoupdates( { "assignments/assignment-poll:1": { "allow_multiple_votes_per_candidate": False, - "amount_global_abstain": None, + "amount_global_yes": None, "amount_global_no": None, + "amount_global_abstain": None, "assignment_id": 1, "description": "test_description_paiquei5ahpie1wu8ohW", - "global_abstain": True, + "global_yes": True, "global_no": True, + "global_abstain": True, "groups_id": [GROUP_DELEGATE_PK], "id": 1, "majority_method": "two_thirds",