Merge pull request #5718 from tsiegleauq/negative-voting

Allow negative voting
This commit is contained in:
Sean 2020-11-25 10:01:12 +01:00 committed by GitHub
commit 1eee3bc56d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1253 additions and 250 deletions

View File

@ -56,6 +56,7 @@ export interface AssignmentAnalogVoteData {
votesvalid?: number; votesvalid?: number;
votesinvalid?: number; votesinvalid?: number;
votescast?: number; votescast?: number;
global_yes?: number;
global_no?: number; global_no?: number;
global_abstain?: number; global_abstain?: number;
} }

View File

@ -3,7 +3,7 @@
<tbody> <tbody>
<tr> <tr>
<th class="voting-option">{{ 'Candidates' | translate }}</th> <th class="voting-option">{{ 'Candidates' | translate }}</th>
<th class="result yes"> <th class="result yes" *ngIf="showYHeader">
<span *ngIf="!isMethodY"> <span *ngIf="!isMethodY">
{{ 'Yes' | translate }} {{ 'Yes' | translate }}
</span> </span>
@ -11,7 +11,14 @@
{{ 'Votes' | translate }} {{ 'Votes' | translate }}
</span> </span>
</th> </th>
<th class="result no" *ngIf="!isMethodY">{{ 'No' | translate }}</th> <th class="result" *ngIf="showNHeader">
<span class="no" *ngIf="!isMethodN">
{{ 'No' | translate }}
</span>
<span class="yes" *ngIf="isMethodN">
{{ 'Votes' | translate }}
</span>
</th>
<th class="result abstain" *ngIf="isMethodYNA">{{ 'Abstain' | translate }}</th> <th class="result abstain" *ngIf="isMethodYNA">{{ 'Abstain' | translate }}</th>
</tr> </tr>
<tr *ngFor="let row of tableData" [class]="row.class"> <tr *ngFor="let row of tableData" [class]="row.class">
@ -26,14 +33,19 @@
</span> </span>
</div> </div>
</td> </td>
<td class="result" *ngFor="let vote of row.value"> <td class="result" *ngFor="let vote of filterRelevantResults(row.value)">
<div class="single-result" [ngClass]="getVoteClass(vote)" *ngIf="vote && voteFitsMethod(vote)"> <div class="single-result" [ngClass]="getVoteClass(vote)">
<span> <span>
<span *ngIf="vote.showPercent"> <span *ngIf="vote.showPercent">
{{ vote.amount | pollPercentBase: poll:'assignment' }} {{ getVoteAmount(vote, row) | pollPercentBase: poll:'assignment' }}
</span> </span>
<span *ngIf="row.class === 'user'">
{{ getVoteAmount(vote, row) | parsePollNumber }}
</span>
<span *ngIf="row.class !== 'user'">
{{ vote.amount | parsePollNumber }} {{ vote.amount | parsePollNumber }}
</span> </span>
</span>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -20,8 +20,20 @@ export class AssignmentPollDetailContentComponent {
return this.poll.pollmethod; 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 { 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 { public get isMethodYN(): boolean {
@ -37,19 +49,44 @@ export class AssignmentPollDetailContentComponent {
} }
public getVoteClass(votingResult: VotingResult): string { 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 { public voteFitsMethod(result: VotingResult): boolean {
if (this.isMethodY) { if (!result.vote) {
if (result.vote === 'abstain' || result.vote === 'no') {
return false;
}
} else if (this.isMethodYN) {
if (result.vote === 'abstain') {
return false;
}
}
return true; 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;
}
}
} }

View File

@ -3,15 +3,16 @@ import { AssignmentOption } from './assignment-option';
import { BasePoll } from '../poll/base-poll'; import { BasePoll } from '../poll/base-poll';
export enum AssignmentPollMethod { export enum AssignmentPollMethod {
Y = 'Y',
YN = 'YN', YN = 'YN',
YNA = 'YNA', YNA = 'YNA',
Votes = 'votes' N = 'N'
} }
export enum AssignmentPollPercentBase { export enum AssignmentPollPercentBase {
Y = 'Y',
YN = 'YN', YN = 'YN',
YNA = 'YNA', YNA = 'YNA',
Votes = 'votes',
Valid = 'valid', Valid = 'valid',
Cast = 'cast', Cast = 'cast',
Disabled = 'disabled' Disabled = 'disabled'
@ -33,22 +34,29 @@ export class AssignmentPoll extends BasePoll<
'votesvalid', 'votesvalid',
'votesinvalid', 'votesinvalid',
'votescast', 'votescast',
'amount_global_abstain', 'amount_global_yes',
'amount_global_no' 'amount_global_no',
'amount_global_abstain'
]; ];
public id: number; public id: number;
public assignment_id: number; public assignment_id: number;
public votes_amount: number; public votes_amount: number;
public allow_multiple_votes_per_candidate: boolean; public allow_multiple_votes_per_candidate: boolean;
public global_yes: boolean;
public global_no: boolean; public global_no: boolean;
public global_abstain: boolean; public global_abstain: boolean;
public amount_global_yes: number;
public amount_global_no: number; public amount_global_no: number;
public amount_global_abstain: number; public amount_global_abstain: number;
public description: string; public description: string;
public get isMethodY(): boolean { 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 { public get isMethodYN(): boolean {
@ -64,7 +72,7 @@ export class AssignmentPoll extends BasePoll<
return ['yes', 'no']; return ['yes', 'no'];
} else if (this.pollmethod === AssignmentPollMethod.YNA) { } else if (this.pollmethod === AssignmentPollMethod.YNA) {
return ['yes', 'no', 'abstain']; return ['yes', 'no', 'abstain'];
} else if (this.pollmethod === AssignmentPollMethod.Votes) { } else if (this.pollmethod === AssignmentPollMethod.Y) {
return ['yes']; return ['yes'];
} }
} }

View File

@ -9,8 +9,9 @@ const PollValues = {
yes: 'Yes', yes: 'Yes',
no: 'No', no: 'No',
abstain: 'Abstain', abstain: 'Abstain',
amount_global_abstain: 'General Abstain', amount_global_yes: 'General Yes',
amount_global_no: 'General No' amount_global_no: 'General No',
amount_global_abstain: 'General Abstain'
}; };
/** /**

View File

@ -18,15 +18,16 @@ export interface AssignmentPollTitleInformation {
} }
export const AssignmentPollMethodVerbose = { export const AssignmentPollMethodVerbose = {
votes: _('Yes per candidate'), Y: _('Yes per candidate'),
N: _('No per candidate'),
YN: _('Yes/No per candidate'), YN: _('Yes/No per candidate'),
YNA: _('Yes/No/Abstain per candidate') YNA: _('Yes/No/Abstain per candidate')
}; };
export const AssignmentPollPercentBaseVerbose = { export const AssignmentPollPercentBaseVerbose = {
Y: _('Sum of votes including general No/Abstain'),
YN: _('Yes/No per candidate'), YN: _('Yes/No per candidate'),
YNA: _('Yes/No/Abstain per candidate'), YNA: _('Yes/No/Abstain per candidate'),
votes: _('Sum of votes including general No/Abstain'),
valid: _('All valid ballots'), valid: _('All valid ballots'),
cast: _('All casted ballots'), cast: _('All casted ballots'),
disabled: _('Disabled (no percents)') disabled: _('Disabled (no percents)')

View File

@ -45,9 +45,18 @@
<!-- Global Values --> <!-- Global Values -->
<div> <div>
<os-check-input
*ngIf="globalYesEnabled"
placeholder="{{ PollPropertyVerbose.global_yes | translate }}"
[checkboxValue]="-1"
inputType="number"
[checkboxLabel]="'majority' | translate"
formControlName="amount_global_yes"
></os-check-input>
<os-check-input <os-check-input
*ngIf="globalNoEnabled" *ngIf="globalNoEnabled"
placeholder="{{ 'General No' | translate }}" placeholder="{{ PollPropertyVerbose.global_no | translate }}"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'majority' | translate" [checkboxLabel]="'majority' | translate"
@ -56,7 +65,7 @@
<os-check-input <os-check-input
*ngIf="globalAbstainEnabled" *ngIf="globalAbstainEnabled"
placeholder="{{ 'General Abstain' | translate }}" placeholder="{{ PollPropertyVerbose.global_abstain | translate }}"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'majority' | translate" [checkboxLabel]="'majority' | translate"

View File

@ -17,6 +17,7 @@ import {
} from 'app/site/assignments/models/view-assignment-poll'; } from 'app/site/assignments/models/view-assignment-poll';
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component'; import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component'; import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
import { PollPropertyVerbose } from 'app/site/polls/models/view-base-poll';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { AssignmentPollService } from '../../services/assignment-poll.service'; import { AssignmentPollService } from '../../services/assignment-poll.service';
@ -57,12 +58,14 @@ export class AssignmentPollDialogComponent
public voteValueVerbose = VoteValueVerbose; public voteValueVerbose = VoteValueVerbose;
public generalValueVerbose = GeneralValueVerbose; public generalValueVerbose = GeneralValueVerbose;
public PollPropertyVerbose = PollPropertyVerbose;
public AssignmentPollMethodVerbose = AssignmentPollMethodVerbose; public AssignmentPollMethodVerbose = AssignmentPollMethodVerbose;
public AssignmentPollPercentBaseVerbose = AssignmentPollPercentBaseVerbose; public AssignmentPollPercentBaseVerbose = AssignmentPollPercentBaseVerbose;
public options: OptionsObject; public options: OptionsObject;
public globalYesEnabled: boolean;
public globalNoEnabled: boolean; public globalNoEnabled: boolean;
public globalAbstainEnabled: boolean; public globalAbstainEnabled: boolean;
@ -121,15 +124,25 @@ export class AssignmentPollDialogComponent
private setAnalogPollValues(): void { private setAnalogPollValues(): void {
const pollmethod = this.pollForm.contentForm.get('pollmethod').value; const pollmethod = this.pollForm.contentForm.get('pollmethod').value;
this.globalYesEnabled = this.pollForm.contentForm.get('global_yes').value;
this.globalNoEnabled = this.pollForm.contentForm.get('global_no').value; this.globalNoEnabled = this.pollForm.contentForm.get('global_no').value;
this.globalAbstainEnabled = this.pollForm.contentForm.get('global_abstain').value; this.globalAbstainEnabled = this.pollForm.contentForm.get('global_abstain').value;
const analogPollValues: VoteValue[] = ['Y'];
if (pollmethod !== AssignmentPollMethod.Votes) { const analogPollValues: VoteValue[] = [];
if (pollmethod === AssignmentPollMethod.N) {
analogPollValues.push('N');
} else {
analogPollValues.push('Y');
if (pollmethod !== AssignmentPollMethod.Y) {
analogPollValues.push('N'); analogPollValues.push('N');
} }
if (pollmethod === AssignmentPollMethod.YNA) { if (pollmethod === AssignmentPollMethod.YNA) {
analogPollValues.push('A'); analogPollValues.push('A');
} }
}
this.analogPollValues = analogPollValues; this.analogPollValues = analogPollValues;
} }
@ -139,13 +152,14 @@ export class AssignmentPollDialogComponent
votesvalid: data.votesvalid, votesvalid: data.votesvalid,
votesinvalid: data.votesinvalid, votesinvalid: data.votesinvalid,
votescast: data.votescast, votescast: data.votescast,
amount_global_yes: data.amount_global_yes,
amount_global_no: data.amount_global_no, amount_global_no: data.amount_global_no,
amount_global_abstain: data.amount_global_abstain amount_global_abstain: data.amount_global_abstain
}; };
for (const option of data.options) { for (const option of data.options) {
const votes: any = {}; const votes: any = {};
votes.Y = option.yes; votes.Y = option.yes;
if (data.pollmethod !== AssignmentPollMethod.Votes) { if (data.pollmethod !== AssignmentPollMethod.Y) {
votes.N = option.no; votes.N = option.no;
} }
if (data.pollmethod === AssignmentPollMethod.YNA) { if (data.pollmethod === AssignmentPollMethod.YNA) {
@ -178,6 +192,7 @@ export class AssignmentPollDialogComponent
) )
})) }))
), ),
amount_global_yes: ['', [Validators.min(LOWEST_VOTE_VALUE)]],
amount_global_no: ['', [Validators.min(LOWEST_VOTE_VALUE)]], amount_global_no: ['', [Validators.min(LOWEST_VOTE_VALUE)]],
amount_global_abstain: ['', [Validators.min(LOWEST_VOTE_VALUE)]], amount_global_abstain: ['', [Validators.min(LOWEST_VOTE_VALUE)]],
// insert all used global fields // insert all used global fields

View File

@ -28,7 +28,7 @@
</p> </p>
<!-- Leftover votes --> <!-- Leftover votes -->
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1"> <h4 *ngIf="(poll.isMethodY || poll.isMethodN) && poll.votes_amount > 1">
{{ 'Available votes' | translate }}: {{ 'Available votes' | translate }}:
<b> {{ getVotesAvailable(delegation) }}/{{ poll.votes_amount }} </b> <b> {{ getVotesAvailable(delegation) }}/{{ poll.votes_amount }} </b>
@ -39,9 +39,9 @@
<div *ngIf="poll.type !== PollType.Pseudoanonymous || !option.user_has_voted"> <div *ngIf="poll.type !== PollType.Pseudoanonymous || !option.user_has_voted">
<div <div
[ngClass]="{ [ngClass]="{
'yna-grid': poll.pollmethod === AssignmentPollMethod.YNA, 'yna-grid': poll.isMethodYNA,
'yn-grid': poll.pollmethod === AssignmentPollMethod.YN, 'yn-grid': poll.isMethodYN,
'single-vote-grid': poll.pollmethod === AssignmentPollMethod.Votes 'single-vote-grid': poll.isMethodY || poll.isMethodN
}" }"
> >
<div class="vote-candidate-name"> <div class="vote-candidate-name">
@ -64,7 +64,7 @@
> >
<mat-icon> {{ action.icon }}</mat-icon> <mat-icon> {{ action.icon }}</mat-icon>
</button> </button>
<span *ngIf="poll.pollmethod !== AssignmentPollMethod.Votes" class="vote-label"> <span *ngIf="poll.isMethodYN || poll.isMethodYNA" class="vote-label">
{{ action.label | translate }} {{ action.label | translate }}
</span> </span>
</div> </div>
@ -74,9 +74,26 @@
</div> </div>
<!-- global no/abstain --> <!-- global no/abstain -->
<ng-container *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && (poll.global_no || poll.global_abstain)"> <ng-container
*ngIf="(poll.isMethodY || poll.isMethodN) && (poll.global_yes || poll.global_no || poll.global_abstain)"
>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="global-option-grid"> <div class="global-option-grid">
<div *ngIf="poll.global_yes">
<button
class="vote-button"
mat-raised-button
(click)="saveGlobalVote('Y', delegation)"
[ngClass]="getGlobalYesClass(delegation)"
[disabled]="isDeliveringVote(delegation)"
>
<mat-icon>thumb_up</mat-icon>
</button>
<span class="vote-label">
{{ PollPropertyVerbose.global_yes | translate }}
</span>
</div>
<div *ngIf="poll.global_no"> <div *ngIf="poll.global_no">
<button <button
class="vote-button" class="vote-button"
@ -85,10 +102,10 @@
[ngClass]="getGlobalNoClass(delegation)" [ngClass]="getGlobalNoClass(delegation)"
[disabled]="isDeliveringVote(delegation)" [disabled]="isDeliveringVote(delegation)"
> >
<mat-icon> thumb_down </mat-icon> <mat-icon>thumb_down</mat-icon>
</button> </button>
<span class="vote-label"> <span class="vote-label">
{{ 'General No' | translate }} {{ PollPropertyVerbose.global_no | translate }}
</span> </span>
</div> </div>
@ -100,10 +117,10 @@
[ngClass]="getGlobalAbstainClass(delegation)" [ngClass]="getGlobalAbstainClass(delegation)"
[disabled]="isDeliveringVote(delegation)" [disabled]="isDeliveringVote(delegation)"
> >
<mat-icon> trip_origin</mat-icon> <mat-icon>trip_origin</mat-icon>
</button> </button>
<span class="vote-label"> <span class="vote-label">
{{ 'General Abstain' | translate }} {{ PollPropertyVerbose.global_abstain | translate }}
</span> </span>
</div> </div>
</div> </div>
@ -149,7 +166,12 @@
<ng-template #sendNow let-delegation="delegation"> <ng-template #sendNow let-delegation="delegation">
<div class="centered-button-wrapper"> <div class="centered-button-wrapper">
<button mat-flat-button color="accent" (click)="submitVote(delegation)" [disabled]="getVotesCount(delegation) == 0"> <button
mat-flat-button
color="accent"
(click)="submitVote(delegation)"
[disabled]="getVotesCount(delegation) == 0"
>
<mat-icon> how_to_vote </mat-icon> <mat-icon> how_to_vote </mat-icon>
<span> <span>
{{ 'Submit vote now' | translate }} {{ 'Submit vote now' | translate }}

View File

@ -19,6 +19,27 @@ import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-
import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component'; import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
const voteOptions = {
Yes: {
vote: 'Y',
css: 'voted-yes',
icon: 'thumb_up',
label: 'Yes'
} as VoteOption,
No: {
vote: 'N',
css: 'voted-no',
icon: 'thumb_down',
label: 'No'
} as VoteOption,
Abstain: {
vote: 'A',
css: 'voted-abstain',
icon: 'trip_origin',
label: 'Abstain'
} as VoteOption
};
@Component({ @Component({
selector: 'os-assignment-poll-vote', selector: 'os-assignment-poll-vote',
templateUrl: './assignment-poll-vote.component.html', templateUrl: './assignment-poll-vote.component.html',
@ -70,6 +91,13 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
return ''; return '';
} }
public getGlobalYesClass(user: ViewUser = this.user): string {
if (this.voteRequestData[user.id]?.global === 'Y') {
return 'voted-yes';
}
return '';
}
public getGlobalAbstainClass(user: ViewUser = this.user): string { public getGlobalAbstainClass(user: ViewUser = this.user): string {
if (this.voteRequestData[user.id]?.global === 'A') { if (this.voteRequestData[user.id]?.global === 'A') {
return 'voted-abstain'; return 'voted-abstain';
@ -85,29 +113,20 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
} }
private defineVoteOptions(): void { private defineVoteOptions(): void {
this.voteActions.push({ if (this.poll) {
vote: 'Y', if (this.poll.isMethodN) {
css: 'voted-yes', this.voteActions.push(voteOptions.No);
icon: 'thumb_up', } else {
label: 'Yes' this.voteActions.push(voteOptions.Yes);
});
if (this.poll?.pollmethod !== AssignmentPollMethod.Votes) { if (!this.poll.isMethodY) {
this.voteActions.push({ this.voteActions.push(voteOptions.No);
vote: 'N',
css: 'voted-no',
icon: 'thumb_down',
label: 'No'
});
} }
if (this.poll?.pollmethod === AssignmentPollMethod.YNA) { if (this.poll.isMethodYNA) {
this.voteActions.push({ this.voteActions.push(voteOptions.Abstain);
vote: 'A', }
css: 'voted-abstain', }
icon: 'trip_origin',
label: 'Abstain'
});
} }
} }
@ -155,7 +174,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
delete this.voteRequestData[user.id].global; delete this.voteRequestData[user.id].global;
} }
if (this.poll.pollmethod === AssignmentPollMethod.Votes) { if (this.poll.isMethodY || this.poll.isMethodN) {
const votesAmount = this.poll.votes_amount; const votesAmount = this.poll.votes_amount;
const tmpVoteRequest = this.poll.options const tmpVoteRequest = this.poll.options
.map(option => option.id) .map(option => option.id)

View File

@ -76,7 +76,7 @@ export class AssignmentPollPdfService extends PollPdfService {
subtitle = subtitle.substring(0, 90) + '...'; subtitle = subtitle.substring(0, 90) + '...';
} }
let rowsPerPage = 1; let rowsPerPage = 1;
if (poll.pollmethod === 'votes') { if (poll.pollmethod === AssignmentPollMethod.Y) {
if (poll.options.length <= 2) { if (poll.options.length <= 2) {
rowsPerPage = 4; rowsPerPage = 4;
} else if (poll.options.length <= 5) { } else if (poll.options.length <= 5) {
@ -141,12 +141,18 @@ export class AssignmentPollPdfService extends PollPdfService {
return a.weight - b.weight; return a.weight - b.weight;
}); });
const resultObject = candidates.map(cand => { const resultObject = candidates.map(cand => {
return poll.pollmethod === 'votes' return poll.pollmethod === AssignmentPollMethod.Y
? this.createBallotOption(cand.user.full_name) ? this.createBallotOption(cand.user.full_name)
: this.createYNBallotEntry(cand.user.full_name, poll.pollmethod); : this.createYNBallotEntry(cand.user.full_name, poll.pollmethod);
}); });
if (poll.pollmethod === 'votes') { if (poll.pollmethod === AssignmentPollMethod.Y) {
if (poll.global_yes) {
const yesEntry = this.createBallotOption(this.translate.instant('Yes'));
yesEntry.margin[1] = 25;
resultObject.push(yesEntry);
}
if (poll.global_no) { if (poll.global_no) {
const noEntry = this.createBallotOption(this.translate.instant('No')); const noEntry = this.createBallotOption(this.translate.instant('No'));
noEntry.margin[1] = 25; noEntry.margin[1] = 25;

View File

@ -92,6 +92,14 @@ export class AssignmentPollService extends PollService {
private getGlobalVoteKeys(poll: ViewAssignmentPoll | PollData): VotingResult[] { private getGlobalVoteKeys(poll: ViewAssignmentPoll | PollData): VotingResult[] {
return [ return [
{
vote: 'amount_global_yes',
showPercent: this.showPercentOfValidOrCast(poll),
hide:
poll.amount_global_yes === VOTE_UNDOCUMENTED ||
!poll.amount_global_yes ||
poll.pollmethod === AssignmentPollMethod.N
},
{ {
vote: 'amount_global_no', vote: 'amount_global_no',
showPercent: this.showPercentOfValidOrCast(poll), showPercent: this.showPercentOfValidOrCast(poll),
@ -109,7 +117,14 @@ export class AssignmentPollService extends PollService {
const tableData: PollTableData[] = poll.options const tableData: PollTableData[] = poll.options
.sort((a, b) => { .sort((a, b) => {
if (this.sortByVote) { if (this.sortByVote) {
if (poll.pollmethod === AssignmentPollMethod.N) {
// most no on top:
// return b.no - a.no;
// least no on top:
return a.no - b.no;
} else {
return b.yes - a.yes; return b.yes - a.yes;
}
} else { } else {
// PollData does not have weight, we need to rely on the order of things. // PollData does not have weight, we need to rely on the order of things.
if (a.weight && b.weight) { if (a.weight && b.weight) {
@ -144,6 +159,7 @@ export class AssignmentPollService extends PollService {
}); });
tableData.push(...this.formatVotingResultToTableData(this.getGlobalVoteKeys(poll), poll)); tableData.push(...this.formatVotingResultToTableData(this.getGlobalVoteKeys(poll), poll));
tableData.push(...this.formatVotingResultToTableData(super.getSumTableKeys(poll), poll)); tableData.push(...this.formatVotingResultToTableData(super.getSumTableKeys(poll), poll));
return tableData; return tableData;
} }
@ -190,7 +206,7 @@ export class AssignmentPollService extends PollService {
case AssignmentPollPercentBase.YNA: case AssignmentPollPercentBase.YNA:
totalByBase = this.sumOptionsYNA(poll); totalByBase = this.sumOptionsYNA(poll);
break; break;
case AssignmentPollPercentBase.Votes: case AssignmentPollPercentBase.Y:
totalByBase = this.sumOptionsYNA(poll); totalByBase = this.sumOptionsYNA(poll);
break; break;
case AssignmentPollPercentBase.Valid: case AssignmentPollPercentBase.Valid:

View File

@ -221,7 +221,7 @@ export class AssignmentPdfService {
private getPollResult(votingResult: PollTableData, poll: ViewAssignmentPoll): string { private getPollResult(votingResult: PollTableData, poll: ViewAssignmentPoll): string {
const resultList = votingResult.value const resultList = votingResult.value
.filter((singleResult: VotingResult) => { .filter((singleResult: VotingResult) => {
if (poll.pollmethod === AssignmentPollMethod.Votes) { if (poll.pollmethod === AssignmentPollMethod.Y) {
return singleResult.vote !== 'no' && singleResult.vote !== 'abstain'; return singleResult.vote !== 'no' && singleResult.vote !== 'abstain';
} else if (poll.pollmethod === AssignmentPollMethod.YN) { } else if (poll.pollmethod === AssignmentPollMethod.YN) {
return singleResult.vote !== 'abstain'; return singleResult.vote !== 'abstain';

View File

@ -10,7 +10,7 @@ import { VotingError, VotingService } from 'app/core/ui-services/voting.service'
import { VoteValue } from 'app/shared/models/poll/base-vote'; import { VoteValue } from 'app/shared/models/poll/base-vote';
import { BaseViewComponentDirective } from 'app/site/base/base-view'; import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewBasePoll } from '../models/view-base-poll'; import { PollPropertyVerbose, ViewBasePoll } from '../models/view-base-poll';
export interface VoteOption { export interface VoteOption {
vote?: VoteValue; vote?: VoteValue;
@ -36,6 +36,8 @@ export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> ext
protected delegations: ViewUser[]; protected delegations: ViewUser[];
public PollPropertyVerbose = PollPropertyVerbose;
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -90,7 +92,7 @@ export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> ext
} }
public getVotingError(user: ViewUser = this.user): string | void { public getVotingError(user: ViewUser = this.user): string | void {
console.log('error ', this.votingService.getVotePermissionErrorVerbose(this.poll, user)); console.log('Cannot vote because:', this.votingService.getVotePermissionErrorVerbose(this.poll, user));
return this.votingService.getVotePermissionErrorVerbose(this.poll, user); return this.votingService.getVotePermissionErrorVerbose(this.poll, user);
} }
} }

View File

@ -24,6 +24,7 @@
</mat-form-field> </mat-form-field>
<!-- Groups entitled to Vote --> <!-- Groups entitled to Vote -->
<div class="suboption">
<mat-form-field *ngIf="contentForm.get('type').value && contentForm.get('type').value !== 'analog'"> <mat-form-field *ngIf="contentForm.get('type').value && contentForm.get('type').value !== 'analog'">
<os-search-value-selector <os-search-value-selector
formControlName="groups_id" formControlName="groups_id"
@ -34,6 +35,7 @@
[inputListValues]="groupObservable" [inputListValues]="groupObservable"
></os-search-value-selector> ></os-search-value-selector>
</mat-form-field> </mat-form-field>
</div>
<!-- Poll Methods --> <!-- Poll Methods -->
<mat-form-field *ngIf="pollMethods"> <mat-form-field *ngIf="pollMethods">
@ -50,6 +52,29 @@
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
<!-- Amount of Votes and global options -->
<div class="suboption" *ngIf="showAmountAndGlobal(data)">
<mat-form-field>
<input
type="number"
matInput
placeholder="{{ PollPropertyVerbose.votes_amount | translate }}"
formControlName="votes_amount"
min="1"
required
/>
</mat-form-field>
<div class="global-options">
<mat-checkbox formControlName="global_yes">
{{ PollPropertyVerbose.global_yes | translate }}
</mat-checkbox>
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
<mat-checkbox formControlName="global_abstain">
{{ PollPropertyVerbose.global_abstain | translate }}
</mat-checkbox>
</div>
</div>
<!-- 100 Percent Base --> <!-- 100 Percent Base -->
<mat-form-field> <mat-form-field>
<mat-select <mat-select
@ -62,25 +87,5 @@
</ng-container> </ng-container>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<!-- Amount of Votes -->
<ng-container
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isCreated)"
>
<mat-form-field>
<input
type="number"
matInput
placeholder="{{ PollPropertyVerbose.votes_amount | translate }}"
formControlName="votes_amount"
min="1"
required
/>
</mat-form-field>
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
<mat-checkbox formControlName="global_abstain">{{
PollPropertyVerbose.global_abstain | translate
}}</mat-checkbox>
</ng-container>
</form> </form>
</div> </div>

View File

@ -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 { .poll-preview-meta-info-form {
display: flex; .suboption {
align-items: center; margin-left: 1.5em;
flex-wrap: wrap; }
& > * { .mat-checkbox {
flex: 1; margin-right: 2em;
margin: 0 4px; }
.global-options {
margin-bottom: 1em;
} }
} }

View File

@ -174,6 +174,11 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
this.contentForm.get('type').disable(); 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 * updates the available percent bases according to the pollmethod
* @param method the currently chosen pollmethod * @param method the currently chosen pollmethod
@ -182,10 +187,10 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
if (method) { if (method) {
let forbiddenBases = []; let forbiddenBases = [];
if (method === AssignmentPollMethod.YN) { if (method === AssignmentPollMethod.YN) {
forbiddenBases = [PercentBase.YNA, AssignmentPollPercentBase.Votes]; forbiddenBases = [PercentBase.YNA, AssignmentPollPercentBase.Y];
} else if (method === AssignmentPollMethod.YNA) { } else if (method === AssignmentPollMethod.YNA) {
forbiddenBases = [AssignmentPollPercentBase.Votes]; forbiddenBases = [AssignmentPollPercentBase.Y];
} else if (method === AssignmentPollMethod.Votes) { } else if (method === AssignmentPollMethod.Y || AssignmentPollMethod.N) {
forbiddenBases = [PercentBase.YN, PercentBase.YNA]; forbiddenBases = [PercentBase.YN, PercentBase.YNA];
} }
@ -209,16 +214,16 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
): AssignmentPollPercentBase { ): AssignmentPollPercentBase {
if ( if (
method === AssignmentPollMethod.YN && method === AssignmentPollMethod.YN &&
(base === AssignmentPollPercentBase.YNA || base === AssignmentPollPercentBase.Votes) (base === AssignmentPollPercentBase.YNA || base === AssignmentPollPercentBase.Y)
) { ) {
return AssignmentPollPercentBase.YN; return AssignmentPollPercentBase.YN;
} else if (method === AssignmentPollMethod.YNA && base === AssignmentPollPercentBase.Votes) { } else if (method === AssignmentPollMethod.YNA && base === AssignmentPollPercentBase.Y) {
return AssignmentPollPercentBase.YNA; return AssignmentPollPercentBase.YNA;
} else if ( } else if (
method === AssignmentPollMethod.Votes && method === AssignmentPollMethod.Y &&
(base === AssignmentPollPercentBase.YN || base === AssignmentPollPercentBase.YNA) (base === AssignmentPollPercentBase.YN || base === AssignmentPollPercentBase.YNA)
) { ) {
return AssignmentPollPercentBase.Votes; return AssignmentPollPercentBase.Y;
} }
return base; return base;
} }
@ -267,8 +272,10 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
: '---' : '---'
]); ]);
} }
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('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_no'), data.global_no]);
this.pollValues.push([this.pollService.getVerboseNameForKey('global_abstain'), data.global_abstain]); this.pollValues.push([this.pollService.getVerboseNameForKey('global_abstain'), data.global_abstain]);
} }
@ -284,6 +291,7 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
majority_method: ['', Validators.required], majority_method: ['', Validators.required],
votes_amount: [1, [Validators.required, Validators.min(1)]], votes_amount: [1, [Validators.required, Validators.min(1)]],
groups_id: [], groups_id: [],
global_yes: [false],
global_no: [false], global_no: [false],
global_abstain: [false] global_abstain: [false]
}); });

View File

@ -44,6 +44,7 @@ export const PollPropertyVerbose = {
state: 'State', state: 'State',
groups: 'Entitled to vote', groups: 'Entitled to vote',
votes_amount: 'Amount of votes', votes_amount: 'Amount of votes',
global_yes: 'General Yes',
global_no: 'General No', global_no: 'General No',
global_abstain: 'General Abstain' global_abstain: 'General Abstain'
}; };

View File

@ -109,6 +109,7 @@ export interface PollData {
votesvalid: number; votesvalid: number;
votesinvalid: number; votesinvalid: number;
votescast: number; votescast: number;
amount_global_yes?: number;
amount_global_no?: number; amount_global_no?: number;
amount_global_abstain?: number; amount_global_abstain?: number;
} }
@ -145,6 +146,7 @@ export interface VotingResult {
| 'votesvalid' | 'votesvalid'
| 'votesinvalid' | 'votesinvalid'
| 'votescast' | 'votescast'
| 'amount_global_yes'
| 'amount_global_no' | 'amount_global_no'
| 'amount_global_abstain'; | 'amount_global_abstain';
amount?: number; amount?: number;
@ -340,6 +342,9 @@ export abstract class PollService {
case AssignmentPollMethod.YN: { case AssignmentPollMethod.YN: {
return ['yes', 'no']; return ['yes', 'no'];
} }
case AssignmentPollMethod.N: {
return ['no'];
}
default: { default: {
return ['yes']; return ['yes'];
} }

View File

@ -25,6 +25,7 @@ export interface AssignmentPollSlideData extends BasePollSlideData {
}[]; }[];
// optional for published polls: // optional for published polls:
amount_global_yes?: number;
amount_global_no?: number; amount_global_no?: number;
amount_global_abstain?: number; amount_global_abstain?: number;
votesvalid: number; votesvalid: number;

View File

@ -17,7 +17,11 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
class AssignmentPollAccessPermissions(BasePollAccessPermissions): class AssignmentPollAccessPermissions(BasePollAccessPermissions):
base_permission = "assignments.can_see" base_permission = "assignments.can_see"
manage_permission = "assignments.can_manage" 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): class AssignmentOptionAccessPermissions(BaseOptionAccessPermissions):

View File

@ -14,7 +14,7 @@ def get_config_variables():
# Voting # Voting
yield ConfigVariable( yield ConfigVariable(
name="assignment_poll_method", name="assignment_poll_method",
default_value=AssignmentPoll.POLLMETHOD_VOTES, default_value=AssignmentPoll.POLLMETHOD_Y,
input_type="choice", input_type="choice",
label="Default election method", label="Default election method",
choices=tuple( choices=tuple(

View File

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

View File

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

View File

@ -319,24 +319,26 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
POLLMETHOD_YN = "YN" POLLMETHOD_YN = "YN"
POLLMETHOD_YNA = "YNA" POLLMETHOD_YNA = "YNA"
POLLMETHOD_VOTES = "votes" POLLMETHOD_Y = "Y"
POLLMETHOD_N = "N"
POLLMETHODS = ( POLLMETHODS = (
(POLLMETHOD_VOTES, "Yes per candidate"), (POLLMETHOD_Y, "Yes per candidate"),
(POLLMETHOD_N, "No per candidate"),
(POLLMETHOD_YN, "Yes/No per candidate"), (POLLMETHOD_YN, "Yes/No per candidate"),
(POLLMETHOD_YNA, "Yes/No/Abstain per candidate"), (POLLMETHOD_YNA, "Yes/No/Abstain per candidate"),
) )
pollmethod = models.CharField(max_length=5, choices=POLLMETHODS) pollmethod = models.CharField(max_length=5, choices=POLLMETHODS)
PERCENT_BASE_Y = "Y"
PERCENT_BASE_YN = "YN" PERCENT_BASE_YN = "YN"
PERCENT_BASE_YNA = "YNA" PERCENT_BASE_YNA = "YNA"
PERCENT_BASE_VOTES = "votes"
PERCENT_BASE_VALID = "valid" PERCENT_BASE_VALID = "valid"
PERCENT_BASE_CAST = "cast" PERCENT_BASE_CAST = "cast"
PERCENT_BASE_DISABLED = "disabled" PERCENT_BASE_DISABLED = "disabled"
PERCENT_BASES = ( PERCENT_BASES = (
(PERCENT_BASE_YN, "Yes/No per candidate"), (PERCENT_BASE_YN, "Yes/No per candidate"),
(PERCENT_BASE_YNA, "Yes/No/Abstain 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_VALID, "All valid ballots"),
(PERCENT_BASE_CAST, "All casted ballots"), (PERCENT_BASE_CAST, "All casted ballots"),
(PERCENT_BASE_DISABLED, "Disabled (no percents)"), (PERCENT_BASE_DISABLED, "Disabled (no percents)"),
@ -345,8 +347,8 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
max_length=8, blank=False, null=False, choices=PERCENT_BASES max_length=8, blank=False, null=False, choices=PERCENT_BASES
) )
global_abstain = models.BooleanField(default=True) global_yes = models.BooleanField(default=True)
db_amount_global_abstain = models.DecimalField( db_amount_global_yes = models.DecimalField(
null=True, null=True,
blank=True, blank=True,
default=Decimal("0"), default=Decimal("0"),
@ -354,6 +356,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
max_digits=15, max_digits=15,
decimal_places=6, decimal_places=6,
) )
global_no = models.BooleanField(default=True) global_no = models.BooleanField(default=True)
db_amount_global_no = models.DecimalField( db_amount_global_no = models.DecimalField(
null=True, null=True,
@ -364,6 +367,16 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
decimal_places=6, 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)]) votes_amount = models.IntegerField(default=1, validators=[MinValueValidator(1)])
""" For "votes" mode: The amount of votes a voter can give. """ """ For "votes" mode: The amount of votes a voter can give. """
@ -372,12 +385,55 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
class Meta: class Meta:
default_permissions = () 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): def get_amount_global_abstain(self):
if not self.global_abstain: if not self.global_abstain:
return None return None
elif self.type == self.TYPE_ANALOG: elif self.type == self.TYPE_ANALOG:
return self.db_amount_global_abstain 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()) return sum(option.abstain for option in self.options.all())
else: else:
return None return None
@ -391,23 +447,6 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
get_amount_global_abstain, set_amount_global_abstain 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): def create_options(self, skip_autoupdate=False):
related_users = AssignmentRelatedUser.objects.filter( related_users = AssignmentRelatedUser.objects.filter(
assignment__id=self.assignment.id assignment__id=self.assignment.id
@ -435,6 +474,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
inform_changed_data(self.assignment.list_of_speakers) inform_changed_data(self.assignment.list_of_speakers)
def reset(self): 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_no = Decimal(0)
self.db_amount_global_abstain = Decimal(0)
super().reset() super().reset()

View File

@ -86,6 +86,9 @@ async def assignment_poll_slide(
poll_data["options"].append(option_data) poll_data["options"].append(option_data)
if poll["state"] == AssignmentPoll.STATE_PUBLISHED: 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"] = ( poll_data["amount_global_no"] = (
float(poll["amount_global_no"]) if poll["amount_global_no"] else None float(poll["amount_global_no"]) if poll["amount_global_no"] else None
) )

View File

@ -77,6 +77,9 @@ class AssignmentPollSerializer(BasePollSerializer):
Serializes all polls. Serializes all polls.
""" """
amount_global_yes = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
amount_global_no = DecimalField( amount_global_no = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True max_digits=15, decimal_places=6, min_value=-2, read_only=True
) )
@ -92,6 +95,8 @@ class AssignmentPollSerializer(BasePollSerializer):
"pollmethod", "pollmethod",
"votes_amount", "votes_amount",
"allow_multiple_votes_per_candidate", "allow_multiple_votes_per_candidate",
"global_yes",
"amount_global_yes",
"global_no", "global_no",
"amount_global_no", "amount_global_no",
"global_abstain", "global_abstain",
@ -111,13 +116,13 @@ class AssignmentPollSerializer(BasePollSerializer):
Returns None, if the 100-%-base must not be changed, otherwise the correct 100-%-base. 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 ( if pollmethod == AssignmentPoll.POLLMETHOD_YN and onehundred_percent_base in (
AssignmentPoll.PERCENT_BASE_VOTES, AssignmentPoll.PERCENT_BASE_Y,
AssignmentPoll.PERCENT_BASE_YNA, AssignmentPoll.PERCENT_BASE_YNA,
): ):
return AssignmentPoll.PERCENT_BASE_YN return AssignmentPoll.PERCENT_BASE_YN
if ( if (
pollmethod == AssignmentPoll.POLLMETHOD_YNA 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: if old_100_percent_base is None:
return AssignmentPoll.PERCENT_BASE_YNA return AssignmentPoll.PERCENT_BASE_YNA
@ -129,12 +134,11 @@ class AssignmentPollSerializer(BasePollSerializer):
return old_100_percent_base return old_100_percent_base
else: else:
return pollmethod return pollmethod
if ( if pollmethod == AssignmentPoll.POLLMETHOD_Y and onehundred_percent_base in (
pollmethod == AssignmentPoll.POLLMETHOD_VOTES AssignmentPoll.PERCENT_BASE_YN,
and onehundred_percent_base AssignmentPoll.PERCENT_BASE_YNA,
in (AssignmentPoll.PERCENT_BASE_YN, AssignmentPoll.PERCENT_BASE_YNA)
): ):
return AssignmentPoll.PERCENT_BASE_VOTES return AssignmentPoll.PERCENT_BASE_Y
return None return None

View File

@ -268,21 +268,32 @@ class AssignmentPollViewSet(BasePollViewSet):
super().perform_create(serializer) super().perform_create(serializer)
poll = AssignmentPoll.objects.get(pk=serializer.data["id"]) 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_no = Decimal(0)
poll.db_amount_global_abstain = Decimal(0)
poll.save() poll.save()
def handle_analog_vote(self, data, poll): def handle_analog_vote(self, data, poll):
for field in ["votesvalid", "votesinvalid", "votescast"]: for field in ["votesvalid", "votesinvalid", "votescast"]:
setattr(poll, field, data[field]) setattr(poll, field, data[field])
global_no_enabled = ( global_yes_enabled = poll.global_yes and poll.pollmethod in (
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES 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: if global_no_enabled:
poll.amount_global_no = data.get("amount_global_no", Decimal(0)) 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: if global_abstain_enabled:
poll.amount_global_abstain = data.get("amount_global_abstain", Decimal(0)) poll.amount_global_abstain = data.get("amount_global_abstain", Decimal(0))
@ -293,6 +304,20 @@ class AssignmentPollViewSet(BasePollViewSet):
with transaction.atomic(): with transaction.atomic():
for option_id, vote in options_data.items(): for option_id, vote in options_data.items():
option = options.get(pk=int(option_id)) option = options.get(pk=int(option_id))
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()
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( vote_obj, _ = AssignmentVote.objects.get_or_create(
option=option, value="Y" option=option, value="Y"
) )
@ -315,6 +340,11 @@ class AssignmentPollViewSet(BasePollViewSet):
) )
vote_obj.weight = vote["A"] vote_obj.weight = vote["A"]
vote_obj.save() vote_obj.save()
else:
raise NotImplementedError(
f"handle_analog_vote not implemented for {poll.pollmethod}"
)
inform_changed_data(option) inform_changed_data(option)
poll.save() poll.save()
@ -326,22 +356,27 @@ class AssignmentPollViewSet(BasePollViewSet):
{ {
"options": {<option_id>: {"Y": <amount>, ["N": <amount>], ["A": <amount>] }}, "options": {<option_id>: {"Y": <amount>, ["N": <amount>], ["A": <amount>] }},
["votesvalid": <amount>], ["votesinvalid": <amount>], ["votescast": <amount>], ["votesvalid": <amount>], ["votesinvalid": <amount>], ["votescast": <amount>],
["amount_global_no": <amount>], ["amount_global_abstain": <amount>] ["amount_global_yes": <amount>],
["amount_global_no": <amount>],
["amount_global_abstain": <amount>]
} }
All amounts are decimals as strings All amounts are decimals as strings
required fields per pollmethod: required fields per pollmethod:
- votes: Y - votes: Y
- YN: YN - YN: YN
- YNA: YNA - YNA: YNA
- N: N
named|pseudoanonymous: named|pseudoanonymous:
votes: votes:
{<option_id>: <amount>} | 'N' | 'A' {<option_id>: <amount>} | 'Y' | 'N' | 'A'
- Exactly one of the three options must be given - 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 - 'N' is only valid if poll.global_no==True
- 'A' is only valid if poll.global_abstain==True - 'A' is only valid if poll.global_abstain==True
- amounts must be integer numbers >= 0. - amounts must be integer numbers >= 0.
- ids should be integers of valid option ids for this poll - 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 - 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 - The sum of all amounts must be grater than 0 and <= poll.votes_amount
YN/YNA: YN/YNA:
@ -361,6 +396,9 @@ class AssignmentPollViewSet(BasePollViewSet):
raise ValidationError({"detail": "Keys must be int"}) raise ValidationError({"detail": "Keys must be int"})
if not isinstance(value, dict): if not isinstance(value, dict):
raise ValidationError({"detail": "A dict per option is required"}) raise ValidationError({"detail": "A dict per option is required"})
if poll.pollmethod == AssignmentPoll.POLLMETHOD_N:
value["N"] = self.parse_vote_value(value, "N")
else:
value["Y"] = self.parse_vote_value(value, "Y") value["Y"] = self.parse_vote_value(value, "Y")
if poll.pollmethod in ( if poll.pollmethod in (
AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YN,
@ -373,27 +411,41 @@ class AssignmentPollViewSet(BasePollViewSet):
for field in ["votesvalid", "votesinvalid", "votescast"]: for field in ["votesvalid", "votesinvalid", "votescast"]:
data[field] = self.parse_vote_value(data, field) data[field] = self.parse_vote_value(data, field)
global_no_enabled = ( global_yes_enabled = poll.global_yes and poll.pollmethod in (
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES AssignmentPoll.POLLMETHOD_Y,
AssignmentPoll.POLLMETHOD_N,
) )
global_abstain_enabled = ( if "amount_global_yes" in data and global_yes_enabled:
poll.global_abstain data["amount_global_yes"] = self.parse_vote_value(
and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES data, "amount_global_yes"
) )
if "amount_global_abstain" in data and global_abstain_enabled:
data["amount_global_abstain"] = self.parse_vote_value( global_no_enabled = poll.global_no and poll.pollmethod in (
data, "amount_global_abstain" AssignmentPoll.POLLMETHOD_Y,
AssignmentPoll.POLLMETHOD_N,
) )
if "amount_global_no" in data and global_no_enabled: if "amount_global_no" in data and global_no_enabled:
data["amount_global_no"] = self.parse_vote_value( data["amount_global_no"] = self.parse_vote_value(
data, "amount_global_no" 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: if isinstance(data, dict) and len(data) == 0:
raise ValidationError({"details": "Empty ballots are not allowed"}) raise ValidationError({"details": "Empty ballots are not allowed"})
available_options = poll.get_options() 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): if isinstance(data, dict):
amount_sum = 0 amount_sum = 0
for option_id, amount in data.items(): for option_id, amount in data.items():
@ -426,10 +478,13 @@ class AssignmentPollViewSet(BasePollViewSet):
"args": [poll.votes_amount], "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: 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: elif data == "A" and poll.global_abstain:
return # return because we dont have to check option presence return
else: else:
raise ValidationError({"detail": "invalid data."}) raise ValidationError({"detail": "invalid data."})
@ -479,12 +534,15 @@ class AssignmentPollViewSet(BasePollViewSet):
weight = Decimal(amount) weight = Decimal(amount)
if config["users_activate_vote_weight"]: if config["users_activate_vote_weight"]:
weight *= vote_weight weight *= vote_weight
value = "Y" # POLLMETHOD_Y
if poll.pollmethod == AssignmentPoll.POLLMETHOD_N:
value = "N"
vote = AssignmentVote.objects.create( vote = AssignmentVote.objects.create(
option=option, option=option,
user=vote_user, user=vote_user,
delegated_user=request_user, delegated_user=request_user,
weight=weight, weight=weight,
value="Y", value=value,
) )
inform_changed_data(vote, no_delete_on_restriction=True) inform_changed_data(vote, no_delete_on_restriction=True)
else: # global_no or global_abstain else: # global_no or global_abstain
@ -529,7 +587,10 @@ class AssignmentPollViewSet(BasePollViewSet):
VotedModel.objects.create(assignmentpoll=poll, user=user) VotedModel.objects.create(assignmentpoll=poll, user=user)
def handle_named_vote(self, data, poll, vote_user, request_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( self.create_votes_type_votes(
data, poll, vote_user.vote_weight, vote_user, request_user data, poll, vote_user.vote_weight, vote_user, request_user
) )
@ -540,16 +601,22 @@ class AssignmentPollViewSet(BasePollViewSet):
self.create_votes_types_yn_yna( self.create_votes_types_yn_yna(
data, poll, vote_user.vote_weight, vote_user, request_user 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): 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) self.create_votes_type_votes(data, poll, user.vote_weight, None, None)
elif poll.pollmethod in ( elif poll.pollmethod in (
AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YN,
AssignmentPoll.POLLMETHOD_YNA, AssignmentPoll.POLLMETHOD_YNA,
): ):
self.create_votes_types_yn_yna(data, poll, user.vote_weight, None, None) 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): def convert_option_data(self, poll, data):
poll_options = poll.get_options() poll_options = poll.get_options()

View File

@ -129,8 +129,10 @@ class CreateAssignmentPoll(TestCase):
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YNA) self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YNA)
self.assertEqual(poll.type, "named") self.assertEqual(poll.type, "named")
# Check defaults # Check defaults
self.assertTrue(poll.global_yes)
self.assertTrue(poll.global_no) self.assertTrue(poll.global_no)
self.assertTrue(poll.global_abstain) self.assertTrue(poll.global_abstain)
self.assertEqual(poll.amount_global_yes, None)
self.assertEqual(poll.amount_global_no, None) self.assertEqual(poll.amount_global_no, None)
self.assertEqual(poll.amount_global_abstain, None) self.assertEqual(poll.amount_global_abstain, None)
self.assertFalse(poll.allow_multiple_votes_per_candidate) self.assertFalse(poll.allow_multiple_votes_per_candidate)
@ -151,6 +153,7 @@ class CreateAssignmentPoll(TestCase):
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
"majority_method": AssignmentPoll.MAJORITY_THREE_QUARTERS, "majority_method": AssignmentPoll.MAJORITY_THREE_QUARTERS,
"global_yes": False,
"global_no": False, "global_no": False,
"global_abstain": False, "global_abstain": False,
"allow_multiple_votes_per_candidate": True, "allow_multiple_votes_per_candidate": True,
@ -164,6 +167,7 @@ class CreateAssignmentPoll(TestCase):
self.assertEqual(poll.title, "test_title_ahThai4pae1pi4xoogoo") self.assertEqual(poll.title, "test_title_ahThai4pae1pi4xoogoo")
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YN) self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YN)
self.assertEqual(poll.type, "pseudoanonymous") self.assertEqual(poll.type, "pseudoanonymous")
self.assertFalse(poll.global_yes)
self.assertFalse(poll.global_no) self.assertFalse(poll.global_no)
self.assertFalse(poll.global_abstain) self.assertFalse(poll.global_abstain)
self.assertTrue(poll.allow_multiple_votes_per_candidate) self.assertTrue(poll.allow_multiple_votes_per_candidate)
@ -327,7 +331,7 @@ class CreateAssignmentPoll(TestCase):
"pollmethod": AssignmentPoll.POLLMETHOD_YNA, "pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named", "type": "named",
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_VOTES, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_Y,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE, "majority_method": AssignmentPoll.MAJORITY_SIMPLE,
}, },
) )
@ -343,7 +347,7 @@ class CreateAssignmentPoll(TestCase):
"pollmethod": AssignmentPoll.POLLMETHOD_YN, "pollmethod": AssignmentPoll.POLLMETHOD_YN,
"type": "named", "type": "named",
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_VOTES, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_Y,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE, "majority_method": AssignmentPoll.MAJORITY_SIMPLE,
}, },
) )
@ -356,7 +360,7 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"), reverse("assignmentpoll-list"),
{ {
"title": "test_title_Thoo2eiphohhi1eeXoow", "title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"type": "named", "type": "named",
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
@ -365,16 +369,14 @@ class CreateAssignmentPoll(TestCase):
) )
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED) self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
poll = AssignmentPoll.objects.get() poll = AssignmentPoll.objects.get()
self.assertEqual( self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_Y)
poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_VOTES
)
def test_create_with_votes(self): def test_create_with_votes(self):
response = self.client.post( response = self.client.post(
reverse("assignmentpoll-list"), reverse("assignmentpoll-list"),
{ {
"title": "test_title_dKbv5tV47IzY1oGHXdSz", "title": "test_title_dKbv5tV47IzY1oGHXdSz",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"type": AssignmentPoll.TYPE_ANALOG, "type": AssignmentPoll.TYPE_ANALOG,
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
@ -400,7 +402,7 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"), reverse("assignmentpoll-list"),
{ {
"title": "test_title_dKbv5tV47IzY1oGHXdSz", "title": "test_title_dKbv5tV47IzY1oGHXdSz",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"type": AssignmentPoll.TYPE_ANALOG, "type": AssignmentPoll.TYPE_ANALOG,
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
@ -408,7 +410,7 @@ class CreateAssignmentPoll(TestCase):
"votes": { "votes": {
"options": {"2": {"Y": 1}}, "options": {"2": {"Y": 1}},
"votesvalid": "-2", "votesvalid": "-2",
"votesinvalid": "-2", "votesinvalid": "11",
"votescast": "-2", "votescast": "-2",
}, },
}, },
@ -418,12 +420,12 @@ class CreateAssignmentPoll(TestCase):
self.assertEqual(poll.state, AssignmentPoll.STATE_FINISHED) self.assertEqual(poll.state, AssignmentPoll.STATE_FINISHED)
self.assertTrue(AssignmentVote.objects.exists()) 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( response = self.client.post(
reverse("assignmentpoll-list"), reverse("assignmentpoll-list"),
{ {
"title": "test_title_dKbv5tV47IzY1oGHXdSz", "title": "test_title_dKbv5tV47IzY1oGHXdSz",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"type": AssignmentPoll.TYPE_ANALOG, "type": AssignmentPoll.TYPE_ANALOG,
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
@ -442,12 +444,46 @@ class CreateAssignmentPoll(TestCase):
self.assertEqual(poll.state, AssignmentPoll.STATE_PUBLISHED) self.assertEqual(poll.state, AssignmentPoll.STATE_PUBLISHED)
self.assertTrue(AssignmentVote.objects.exists()) 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): def test_create_with_invalid_votes(self):
response = self.client.post( response = self.client.post(
reverse("assignmentpoll-list"), reverse("assignmentpoll-list"),
{ {
"title": "test_title_dKbv5tV47IzY1oGHXdSz", "title": "test_title_dKbv5tV47IzY1oGHXdSz",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"type": AssignmentPoll.TYPE_ANALOG, "type": AssignmentPoll.TYPE_ANALOG,
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
@ -468,7 +504,7 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"), reverse("assignmentpoll-list"),
{ {
"title": "test_title_dKbv5tV47IzY1oGHXdSz", "title": "test_title_dKbv5tV47IzY1oGHXdSz",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"type": AssignmentPoll.TYPE_NAMED, "type": AssignmentPoll.TYPE_NAMED,
"assignment_id": self.assignment.id, "assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA, "onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
@ -500,9 +536,9 @@ class UpdateAssignmentPoll(TestCase):
self.poll = AssignmentPoll.objects.create( self.poll = AssignmentPoll.objects.create(
assignment=self.assignment, assignment=self.assignment,
title="test_title_beeFaihuNae1vej2ai8m", title="test_title_beeFaihuNae1vej2ai8m",
pollmethod=AssignmentPoll.POLLMETHOD_VOTES, pollmethod=AssignmentPoll.POLLMETHOD_Y,
type=BasePoll.TYPE_NAMED, type=BasePoll.TYPE_NAMED,
onehundred_percent_base=AssignmentPoll.PERCENT_BASE_VOTES, onehundred_percent_base=AssignmentPoll.PERCENT_BASE_Y,
majority_method=AssignmentPoll.MAJORITY_SIMPLE, majority_method=AssignmentPoll.MAJORITY_SIMPLE,
) )
self.poll.create_options() self.poll.create_options()
@ -545,7 +581,7 @@ class UpdateAssignmentPoll(TestCase):
) )
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
poll = AssignmentPoll.objects.get() poll = AssignmentPoll.objects.get()
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_VOTES) self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_Y)
def test_patch_type(self): def test_patch_type(self):
response = self.client.patch( response = self.client.patch(
@ -631,9 +667,7 @@ class UpdateAssignmentPoll(TestCase):
) )
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
poll = AssignmentPoll.objects.get() poll = AssignmentPoll.objects.get()
self.assertEqual( self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_Y)
poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_VOTES
)
def test_patch_majority_method(self): def test_patch_majority_method(self):
response = self.client.patch( response = self.client.patch(
@ -658,7 +692,8 @@ class UpdateAssignmentPoll(TestCase):
reverse("assignmentpoll-detail", args=[self.poll.pk]), reverse("assignmentpoll-detail", args=[self.poll.pk]),
{ {
"title": "test_title_ees6Tho8ahheen4cieja", "title": "test_title_ees6Tho8ahheen4cieja",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES, "pollmethod": AssignmentPoll.POLLMETHOD_Y,
"global_yes": True,
"global_no": True, "global_no": True,
"global_abstain": False, "global_abstain": False,
"allow_multiple_votes_per_candidate": True, "allow_multiple_votes_per_candidate": True,
@ -668,9 +703,11 @@ class UpdateAssignmentPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get() poll = AssignmentPoll.objects.get()
self.assertEqual(poll.title, "test_title_ees6Tho8ahheen4cieja") 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.assertTrue(poll.global_no)
self.assertFalse(poll.global_abstain) 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_no, Decimal("0"))
self.assertEqual(poll.amount_global_abstain, None) self.assertEqual(poll.amount_global_abstain, None)
self.assertTrue(poll.allow_multiple_votes_per_candidate) self.assertTrue(poll.allow_multiple_votes_per_candidate)
@ -1220,12 +1257,12 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.assertFalse(AssignmentVote.objects.exists()) self.assertFalse(AssignmentVote.objects.exists())
class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass): class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
def create_poll(self): def create_poll(self):
return AssignmentPoll.objects.create( return AssignmentPoll.objects.create(
assignment=self.assignment, assignment=self.assignment,
title="test_title_Zrvh146QAdq7t6iSDwZk", title="test_title_Zrvh146QAdq7t6iSDwZk",
pollmethod=AssignmentPoll.POLLMETHOD_VOTES, pollmethod=AssignmentPoll.POLLMETHOD_Y,
type=BasePoll.TYPE_NAMED, type=BasePoll.TYPE_NAMED,
) )
@ -1296,6 +1333,34 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.assertEqual(option2.no, Decimal("0")) self.assertEqual(option2.no, Decimal("0"))
self.assertEqual(option2.abstain, 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): def test_global_no(self):
self.poll.votes_amount = 2 self.poll.votes_amount = 2
self.poll.save() self.poll.save()
@ -1309,6 +1374,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.assertEqual(option.yes, Decimal("0")) self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("1")) self.assertEqual(option.no, Decimal("1"))
self.assertEqual(option.abstain, Decimal("0")) 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_no, Decimal("1"))
self.assertEqual(poll.amount_global_abstain, Decimal("0")) self.assertEqual(poll.amount_global_abstain, Decimal("0"))
@ -1336,6 +1402,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.assertEqual(option.yes, Decimal("0")) self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("0")) self.assertEqual(option.no, Decimal("0"))
self.assertEqual(option.abstain, Decimal("1")) 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_no, Decimal("0"))
self.assertEqual(poll.amount_global_abstain, Decimal("1")) self.assertEqual(poll.amount_global_abstain, Decimal("1"))
@ -1505,6 +1572,321 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.assertFalse(AssignmentVote.objects.exists()) 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): class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
def create_poll(self): def create_poll(self):
return AssignmentPoll.objects.create( return AssignmentPoll.objects.create(
@ -1698,12 +2080,12 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.assertFalse(AssignmentVote.objects.exists()) self.assertFalse(AssignmentVote.objects.exists())
class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass): class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
def create_poll(self): def create_poll(self):
return AssignmentPoll.objects.create( return AssignmentPoll.objects.create(
assignment=self.assignment, assignment=self.assignment,
title="test_title_Zrvh146QAdq7t6iSDwZk", title="test_title_Zrvh146QAdq7t6iSDwZk",
pollmethod=AssignmentPoll.POLLMETHOD_VOTES, pollmethod=AssignmentPoll.POLLMETHOD_Y,
type=BasePoll.TYPE_PSEUDOANONYMOUS, type=BasePoll.TYPE_PSEUDOANONYMOUS,
) )
@ -1933,6 +2315,241 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.assertFalse(AssignmentVote.objects.exists()) 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 # test autoupdates
class VoteAssignmentPollAutoupdatesBaseClass(TestCase): class VoteAssignmentPollAutoupdatesBaseClass(TestCase):
poll_type = "" # set by subclass, defines which poll type we use poll_type = "" # set by subclass, defines which poll type we use
@ -1994,10 +2611,12 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"assignments/assignment-poll:1": { "assignments/assignment-poll:1": {
"allow_multiple_votes_per_candidate": False, "allow_multiple_votes_per_candidate": False,
"assignment_id": 1, "assignment_id": 1,
"global_abstain": True, "global_yes": True,
"global_no": True, "global_no": True,
"amount_global_abstain": None, "global_abstain": True,
"amount_global_yes": None,
"amount_global_no": None, "amount_global_no": None,
"amount_global_abstain": None,
"groups_id": [GROUP_DELEGATE_PK], "groups_id": [GROUP_DELEGATE_PK],
"id": 1, "id": 1,
"options_id": [1], "options_id": [1],
@ -2064,8 +2683,9 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
{ {
"allow_multiple_votes_per_candidate": False, "allow_multiple_votes_per_candidate": False,
"assignment_id": 1, "assignment_id": 1,
"global_abstain": True, "global_yes": True,
"global_no": True, "global_no": True,
"global_abstain": True,
"pollmethod": AssignmentPoll.POLLMETHOD_YNA, "pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"state": AssignmentPoll.STATE_STARTED, "state": AssignmentPoll.STATE_STARTED,
"type": AssignmentPoll.TYPE_NAMED, "type": AssignmentPoll.TYPE_NAMED,
@ -2114,12 +2734,14 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
autoupdate[0]["assignments/assignment-poll:1"], autoupdate[0]["assignments/assignment-poll:1"],
{ {
"allow_multiple_votes_per_candidate": False, "allow_multiple_votes_per_candidate": False,
"amount_global_abstain": None, "amount_global_yes": None,
"amount_global_no": None, "amount_global_no": None,
"amount_global_abstain": None,
"assignment_id": 1, "assignment_id": 1,
"description": "test_description_paiquei5ahpie1wu8ohW", "description": "test_description_paiquei5ahpie1wu8ohW",
"global_abstain": True, "global_yes": True,
"global_no": True, "global_no": True,
"global_abstain": True,
"groups_id": [GROUP_DELEGATE_PK], "groups_id": [GROUP_DELEGATE_PK],
"id": 1, "id": 1,
"majority_method": "two_thirds", "majority_method": "two_thirds",
@ -2186,10 +2808,12 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"assignments/assignment-poll:1": { "assignments/assignment-poll:1": {
"allow_multiple_votes_per_candidate": False, "allow_multiple_votes_per_candidate": False,
"assignment_id": 1, "assignment_id": 1,
"global_abstain": True, "global_yes": True,
"global_no": True, "global_no": True,
"amount_global_abstain": None, "global_abstain": True,
"amount_global_yes": None,
"amount_global_no": None, "amount_global_no": None,
"amount_global_abstain": None,
"groups_id": [GROUP_DELEGATE_PK], "groups_id": [GROUP_DELEGATE_PK],
"id": 1, "id": 1,
"options_id": [1], "options_id": [1],
@ -2241,8 +2865,9 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
{ {
"allow_multiple_votes_per_candidate": False, "allow_multiple_votes_per_candidate": False,
"assignment_id": 1, "assignment_id": 1,
"global_abstain": True, "global_yes": True,
"global_no": True, "global_no": True,
"global_abstain": True,
"pollmethod": AssignmentPoll.POLLMETHOD_YNA, "pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"state": AssignmentPoll.STATE_STARTED, "state": AssignmentPoll.STATE_STARTED,
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS, "type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
@ -2291,12 +2916,14 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
{ {
"assignments/assignment-poll:1": { "assignments/assignment-poll:1": {
"allow_multiple_votes_per_candidate": False, "allow_multiple_votes_per_candidate": False,
"amount_global_abstain": None, "amount_global_yes": None,
"amount_global_no": None, "amount_global_no": None,
"amount_global_abstain": None,
"assignment_id": 1, "assignment_id": 1,
"description": "test_description_paiquei5ahpie1wu8ohW", "description": "test_description_paiquei5ahpie1wu8ohW",
"global_abstain": True, "global_yes": True,
"global_no": True, "global_no": True,
"global_abstain": True,
"groups_id": [GROUP_DELEGATE_PK], "groups_id": [GROUP_DELEGATE_PK],
"id": 1, "id": 1,
"majority_method": "two_thirds", "majority_method": "two_thirds",