Add global no and abstain to form
Minur UI changes Minor Chart enhancements Server Changes
This commit is contained in:
parent
8fe5a0c9f4
commit
3c36441967
@ -34,7 +34,7 @@ export class VotingBannerService {
|
|||||||
*/
|
*/
|
||||||
private checkForVotablePolls(polls: ViewBasePoll[]): void {
|
private checkForVotablePolls(polls: ViewBasePoll[]): void {
|
||||||
// display no banner if in history mode or there are no polls to vote
|
// display no banner if in history mode or there are no polls to vote
|
||||||
const pollsToVote = polls.filter(poll => this.votingService.canVote(poll) && !poll.user_has_voted_valid);
|
const pollsToVote = polls.filter(poll => this.votingService.canVote(poll) && !poll.user_has_voted);
|
||||||
if ((this.OSStatus.isInHistoryMode && this.currentBanner) || !pollsToVote.length) {
|
if ((this.OSStatus.isInHistoryMode && this.currentBanner) || !pollsToVote.length) {
|
||||||
this.sliceBanner();
|
this.sliceBanner();
|
||||||
return;
|
return;
|
||||||
|
@ -9,8 +9,7 @@ export enum VotingError {
|
|||||||
POLL_WRONG_TYPE,
|
POLL_WRONG_TYPE,
|
||||||
USER_HAS_NO_PERMISSION,
|
USER_HAS_NO_PERMISSION,
|
||||||
USER_IS_ANONYMOUS,
|
USER_IS_ANONYMOUS,
|
||||||
USER_NOT_PRESENT,
|
USER_NOT_PRESENT
|
||||||
USER_HAS_VOTED_VALID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,9 +59,6 @@ export class VotingService {
|
|||||||
if (!user.is_present) {
|
if (!user.is_present) {
|
||||||
return VotingError.USER_NOT_PRESENT;
|
return VotingError.USER_NOT_PRESENT;
|
||||||
}
|
}
|
||||||
if (poll.type === PollType.Pseudoanonymous && poll.user_has_voted_valid) {
|
|
||||||
return VotingError.USER_HAS_VOTED_VALID;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVotePermissionErrorVerbose(poll: ViewBasePoll): string | void {
|
public getVotePermissionErrorVerbose(poll: ViewBasePoll): string | void {
|
||||||
|
@ -211,9 +211,11 @@ export class ChartsComponent extends BaseViewComponent {
|
|||||||
yAxes: [
|
yAxes: [
|
||||||
{
|
{
|
||||||
gridLines: {
|
gridLines: {
|
||||||
drawOnChartArea: false
|
drawBorder: false,
|
||||||
|
drawOnChartArea: false,
|
||||||
|
drawTicks: false
|
||||||
},
|
},
|
||||||
ticks: { beginAtZero: true, mirror: true, labelOffset: -20 },
|
ticks: { mirror: true, labelOffset: -20 },
|
||||||
stacked: true
|
stacked: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -88,10 +88,12 @@ export class CheckInputComponent extends BaseViewComponent implements OnInit, Co
|
|||||||
* @param obj the value from the parent form. Type "any" is required by the interface
|
* @param obj the value from the parent form. Type "any" is required by the interface
|
||||||
*/
|
*/
|
||||||
public writeValue(obj: string | number): void {
|
public writeValue(obj: string | number): void {
|
||||||
if (obj && obj === this.checkboxValue) {
|
if (obj || typeof obj === 'number') {
|
||||||
this.checkboxStateChanged(true);
|
if (obj === this.checkboxValue) {
|
||||||
} else {
|
this.checkboxStateChanged(true);
|
||||||
this.contentForm.patchValue(obj);
|
} else {
|
||||||
|
this.contentForm.patchValue(obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,13 @@ export class AssignmentPoll extends BasePoll<
|
|||||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||||
public static defaultGroupsConfig = 'assignment_poll_default_groups';
|
public static defaultGroupsConfig = 'assignment_poll_default_groups';
|
||||||
public static defaultPollMethodConfig = 'assignment_poll_method';
|
public static defaultPollMethodConfig = 'assignment_poll_method';
|
||||||
|
public static DECIMAL_FIELDS = [
|
||||||
|
'votesvalid',
|
||||||
|
'votesinvalid',
|
||||||
|
'votescast',
|
||||||
|
'amount_global_abstain',
|
||||||
|
'amount_global_no'
|
||||||
|
];
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
public assignment_id: number;
|
public assignment_id: number;
|
||||||
@ -36,6 +43,8 @@ export class AssignmentPoll extends BasePoll<
|
|||||||
public allow_multiple_votes_per_candidate: boolean;
|
public allow_multiple_votes_per_candidate: boolean;
|
||||||
public global_no: boolean;
|
public global_no: boolean;
|
||||||
public global_abstain: boolean;
|
public global_abstain: boolean;
|
||||||
|
public amount_global_no: number;
|
||||||
|
public amount_global_abstain: number;
|
||||||
public description: string;
|
public description: string;
|
||||||
|
|
||||||
public get isMethodY(): boolean {
|
public get isMethodY(): boolean {
|
||||||
@ -63,4 +72,8 @@ export class AssignmentPoll extends BasePoll<
|
|||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(AssignmentPoll.COLLECTIONSTRING, input);
|
super(AssignmentPoll.COLLECTIONSTRING, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getDecimalFields(): string[] {
|
||||||
|
return AssignmentPoll.DECIMAL_FIELDS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { BaseModel } from './base-model';
|
import { BaseModel } from './base-model';
|
||||||
|
|
||||||
export abstract class BaseDecimalModel<T = any> extends BaseModel<T> {
|
export abstract class BaseDecimalModel<T = any> extends BaseModel<T> {
|
||||||
protected abstract getDecimalFields(): (keyof this)[];
|
// TODO: no more elegant solution available in current Typescript
|
||||||
|
protected abstract getDecimalFields(): string[];
|
||||||
|
|
||||||
public deserialize(input: any): void {
|
public deserialize(input: any): void {
|
||||||
if (input && typeof input === 'object') {
|
if (input && typeof input === 'object') {
|
||||||
|
@ -6,10 +6,9 @@ export abstract class BaseOption<T> extends BaseDecimalModel<T> {
|
|||||||
public no: number;
|
public no: number;
|
||||||
public abstain: number;
|
public abstain: number;
|
||||||
public poll_id: number;
|
public poll_id: number;
|
||||||
public user_has_voted: boolean;
|
|
||||||
public voted_id: number[];
|
public voted_id: number[];
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
protected getDecimalFields(): string[] {
|
||||||
return ['yes', 'no', 'abstain'];
|
return ['yes', 'no', 'abstain'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ export abstract class BasePoll<
|
|||||||
public votescast: number;
|
public votescast: number;
|
||||||
public groups_id: number[];
|
public groups_id: number[];
|
||||||
public majority_method: MajorityMethod;
|
public majority_method: MajorityMethod;
|
||||||
|
public user_has_voted: boolean;
|
||||||
|
|
||||||
public pollmethod: PM;
|
public pollmethod: PM;
|
||||||
public onehundred_percent_base: PB;
|
public onehundred_percent_base: PB;
|
||||||
@ -91,7 +92,7 @@ export abstract class BasePoll<
|
|||||||
return this.state + 1;
|
return this.state + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
protected getDecimalFields(): string[] {
|
||||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
|
|||||||
return VoteValueVerbose[this.value];
|
return VoteValueVerbose[this.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BaseVote<T>)[] {
|
protected getDecimalFields(): string[] {
|
||||||
return ['weight'];
|
return ['weight'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,9 @@ const PollValues = {
|
|||||||
votesabstain: 'Votes abstain',
|
votesabstain: 'Votes abstain',
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No',
|
no: 'No',
|
||||||
abstain: 'Abstain'
|
abstain: 'Abstain',
|
||||||
|
amount_global_abstain: 'Global abstain',
|
||||||
|
amount_global_no: 'Global no'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,21 +57,31 @@
|
|||||||
</mat-menu>
|
</mat-menu>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<div class="content-container">
|
<div>
|
||||||
<div *ngIf="editAssignment">
|
<div *ngIf="editAssignment">
|
||||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!editAssignment">
|
<div *ngIf="!editAssignment">
|
||||||
<!-- assignment meta infos-->
|
<!-- assignment meta infos-->
|
||||||
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
||||||
<!-- candidates list -->
|
|
||||||
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
<!-- polls -->
|
||||||
<!-- closed polls -->
|
|
||||||
<ng-container *ngIf="assignment && assignment.polls.length">
|
<ng-container *ngIf="assignment && assignment.polls.length">
|
||||||
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
||||||
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- New Ballot button -->
|
||||||
|
<div class="new-ballot-button" *ngIf="assignment && hasPerms('createPoll')">
|
||||||
|
<button mat-stroked-button (click)="openDialog()">
|
||||||
|
<mat-icon color="primary">poll</mat-icon>
|
||||||
|
<span translate>New ballot</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- candidates list -->
|
||||||
|
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -196,12 +206,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ballot-button" *ngIf="assignment && hasPerms('createPoll')">
|
|
||||||
<button mat-button (click)="openDialog()">
|
|
||||||
<mat-icon color="primary">poll</mat-icon>
|
|
||||||
<span translate>New ballot</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -26,6 +26,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-ballot-button {
|
||||||
|
display: flex;
|
||||||
|
> * {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.election-document-list mat-list-item {
|
.election-document-list mat-list-item {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
@ -56,10 +64,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-button {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.candidate-list-separator {
|
.candidate-list-separator {
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
<os-poll-form [data]="pollData" [pollMethods]="AssignmentPollMethodVerbose" [percentBases]="AssignmentPollPercentBaseVerbose" #pollForm></os-poll-form>
|
<os-poll-form
|
||||||
|
[data]="pollData"
|
||||||
|
[pollMethods]="AssignmentPollMethodVerbose"
|
||||||
|
[percentBases]="AssignmentPollPercentBaseVerbose"
|
||||||
|
#pollForm
|
||||||
|
></os-poll-form>
|
||||||
|
|
||||||
<!-- Analog voting -->
|
<!-- Analog voting -->
|
||||||
<ng-container *ngIf="isAnalogPoll">
|
<ng-container *ngIf="isAnalogPoll && dialogVoteForm">
|
||||||
<form [formGroup]="dialogVoteForm">
|
<form [formGroup]="dialogVoteForm">
|
||||||
<!-- Candidates -->
|
<!-- Candidates Values -->
|
||||||
<div formGroupName="options">
|
<div formGroupName="options">
|
||||||
<div *ngFor="let option of options" class="votes-grid">
|
<div *ngFor="let option of options" class="votes-grid">
|
||||||
<div>
|
<div>
|
||||||
@ -24,6 +29,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sum Values -->
|
||||||
<div *ngFor="let value of sumValues" class="votes-grid">
|
<div *ngFor="let value of sumValues" class="votes-grid">
|
||||||
<div></div>
|
<div></div>
|
||||||
<os-check-input
|
<os-check-input
|
||||||
@ -34,6 +41,27 @@
|
|||||||
[formControlName]="value"
|
[formControlName]="value"
|
||||||
></os-check-input>
|
></os-check-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Global Values -->
|
||||||
|
<div>
|
||||||
|
<os-check-input
|
||||||
|
*ngIf="globalNoEnabled"
|
||||||
|
placeholder="{{ 'Global No' | translate }}"
|
||||||
|
[checkboxValue]="-1"
|
||||||
|
inputType="number"
|
||||||
|
[checkboxLabel]="'majority' | translate"
|
||||||
|
formControlName="amount_global_no"
|
||||||
|
></os-check-input>
|
||||||
|
|
||||||
|
<os-check-input
|
||||||
|
*ngIf="globalAbstainEnabled"
|
||||||
|
placeholder="{{ 'Global Abstain' | translate }}"
|
||||||
|
[checkboxValue]="-1"
|
||||||
|
inputType="number"
|
||||||
|
[checkboxLabel]="'majority' | translate"
|
||||||
|
formControlName="amount_global_abstain"
|
||||||
|
></os-check-input>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Publish Check -->
|
<!-- Publish Check -->
|
||||||
@ -53,7 +81,8 @@
|
|||||||
<button
|
<button
|
||||||
mat-button
|
mat-button
|
||||||
(click)="submitPoll()"
|
(click)="submitPoll()"
|
||||||
[disabled]="!pollForm.contentForm || pollForm.contentForm.invalid || dialogVoteForm.invalid"
|
*ngIf="pollForm && dialogVoteForm && pollForm.contentForm"
|
||||||
|
[disabled]="pollForm.contentForm.invalid || dialogVoteForm.invalid"
|
||||||
>
|
>
|
||||||
<span translate>Save</span>
|
<span translate>Save</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -5,6 +5,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
|
||||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||||
@ -59,6 +60,9 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
|||||||
|
|
||||||
public options: OptionsObject;
|
public options: OptionsObject;
|
||||||
|
|
||||||
|
public globalNoEnabled: boolean;
|
||||||
|
public globalAbstainEnabled: boolean;
|
||||||
|
|
||||||
public get isAnalogPoll(): boolean {
|
public get isAnalogPoll(): boolean {
|
||||||
return (
|
return (
|
||||||
this.pollForm &&
|
this.pollForm &&
|
||||||
@ -104,7 +108,7 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.pollForm.contentForm.get('pollmethod').valueChanges.subscribe(() => {
|
this.pollForm.contentForm.valueChanges.pipe(debounceTime(150), distinctUntilChanged()).subscribe(() => {
|
||||||
this.createDialog();
|
this.createDialog();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -112,6 +116,8 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
|||||||
|
|
||||||
private setAnalogPollValues(): void {
|
private setAnalogPollValues(): void {
|
||||||
const pollmethod = this.pollForm.contentForm.get('pollmethod').value;
|
const pollmethod = this.pollForm.contentForm.get('pollmethod').value;
|
||||||
|
this.globalNoEnabled = this.pollForm.contentForm.get('global_no').value;
|
||||||
|
this.globalAbstainEnabled = this.pollForm.contentForm.get('global_abstain').value;
|
||||||
const analogPollValues: VoteValue[] = ['Y'];
|
const analogPollValues: VoteValue[] = ['Y'];
|
||||||
if (pollmethod !== AssignmentPollMethod.Votes) {
|
if (pollmethod !== AssignmentPollMethod.Votes) {
|
||||||
analogPollValues.push('N');
|
analogPollValues.push('N');
|
||||||
@ -127,7 +133,9 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
|||||||
options: {},
|
options: {},
|
||||||
votesvalid: data.votesvalid,
|
votesvalid: data.votesvalid,
|
||||||
votesinvalid: data.votesinvalid,
|
votesinvalid: data.votesinvalid,
|
||||||
votescast: data.votescast
|
votescast: data.votescast,
|
||||||
|
amount_global_no: data.amount_global_no,
|
||||||
|
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 = {};
|
||||||
@ -165,6 +173,8 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
|||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
|
amount_global_no: ['', [Validators.min(-2)]],
|
||||||
|
amount_global_abstain: ['', [Validators.min(-2)]],
|
||||||
// insert all used global fields
|
// insert all used global fields
|
||||||
...this.sumValues.mapToObject(sumValue => ({
|
...this.sumValues.mapToObject(sumValue => ({
|
||||||
[sumValue]: ['', [Validators.min(-2)]]
|
[sumValue]: ['', [Validators.min(-2)]]
|
||||||
|
@ -54,7 +54,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
if (this.poll && this.poll.user_has_not_voted) {
|
if (this.poll && !this.poll.user_has_voted) {
|
||||||
this.alreadyVoted = false;
|
this.alreadyVoted = false;
|
||||||
this.defineVoteOptions();
|
this.defineVoteOptions();
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,6 +39,19 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
|||||||
public readonly tableChartData: Map<string, BehaviorSubject<ChartData>> = new Map();
|
public readonly tableChartData: Map<string, BehaviorSubject<ChartData>> = new Map();
|
||||||
public readonly pollClassType = PollClassType.Assignment;
|
public readonly pollClassType = PollClassType.Assignment;
|
||||||
|
|
||||||
|
protected globalVoteKeys: VotingResult[] = [
|
||||||
|
{
|
||||||
|
vote: 'amount_global_no',
|
||||||
|
showPercent: false,
|
||||||
|
hide: this.poll.amount_global_no === -2 || this.poll.amount_global_no === 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'amount_global_abstain',
|
||||||
|
showPercent: false,
|
||||||
|
hide: this.poll.amount_global_abstain === -2 || this.poll.amount_global_abstain === 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
public get pollmethodVerbose(): string {
|
public get pollmethodVerbose(): string {
|
||||||
return AssignmentPollMethodVerbose[this.pollmethod];
|
return AssignmentPollMethodVerbose[this.pollmethod];
|
||||||
}
|
}
|
||||||
@ -98,8 +111,31 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
|||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tableData.push(
|
||||||
|
...this.globalVoteKeys
|
||||||
|
.filter(key => {
|
||||||
|
return !key.hide;
|
||||||
|
})
|
||||||
|
.map(key => ({
|
||||||
|
votingOption: key.vote,
|
||||||
|
class: 'sums',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
amount: this[key.vote],
|
||||||
|
hide: key.hide,
|
||||||
|
showPercent: key.showPercent
|
||||||
|
} as VotingResult
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
return tableData;
|
return tableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getDecimalFields(): string[] {
|
||||||
|
return AssignmentPoll.DECIMAL_FIELDS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewAssignmentPoll extends AssignmentPoll {
|
export interface ViewAssignmentPoll extends AssignmentPoll {
|
||||||
|
@ -57,12 +57,12 @@ export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotio
|
|||||||
*/
|
*/
|
||||||
private createDialog(): void {
|
private createDialog(): void {
|
||||||
this.dialogVoteForm = this.fb.group({
|
this.dialogVoteForm = this.fb.group({
|
||||||
Y: [0, [Validators.min(-2)]],
|
Y: ['', [Validators.min(-2)]],
|
||||||
N: [0, [Validators.min(-2)]],
|
N: ['', [Validators.min(-2)]],
|
||||||
A: [0, [Validators.min(-2)]],
|
A: ['', [Validators.min(-2)]],
|
||||||
votesvalid: [0, [Validators.min(-2)]],
|
votesvalid: ['', [Validators.min(-2)]],
|
||||||
votesinvalid: [0, [Validators.min(-2)]],
|
votesinvalid: ['', [Validators.min(-2)]],
|
||||||
votescast: [0, [Validators.min(-2)]]
|
votescast: ['', [Validators.min(-2)]]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.pollData.poll) {
|
if (this.pollData.poll) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
|
||||||
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||||
</div>
|
</div>
|
||||||
@ -16,3 +16,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #userHasVotes>
|
||||||
|
<div class="user-has-voted">
|
||||||
|
<os-icon-container icon="check">
|
||||||
|
{{ 'You already voted on this poll' | translate }}
|
||||||
|
</os-icon-container>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
@ -17,6 +17,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-has-voted {
|
||||||
|
display: flex;
|
||||||
|
> * {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.voted-yes {
|
.voted-yes {
|
||||||
background-color: $votes-yes-color;
|
background-color: $votes-yes-color;
|
||||||
color: $vote-active-color;
|
color: $vote-active-color;
|
||||||
|
@ -22,7 +22,15 @@ export interface PollTableData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VotingResult {
|
export interface VotingResult {
|
||||||
vote?: 'yes' | 'no' | 'abstain' | 'votesvalid' | 'votesinvalid' | 'votescast';
|
vote?:
|
||||||
|
| 'yes'
|
||||||
|
| 'no'
|
||||||
|
| 'abstain'
|
||||||
|
| 'votesvalid'
|
||||||
|
| 'votesinvalid'
|
||||||
|
| 'votescast'
|
||||||
|
| 'amount_global_no'
|
||||||
|
| 'amount_global_abstain';
|
||||||
amount?: number;
|
amount?: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
@ -176,18 +184,6 @@ export abstract class ViewBasePoll<
|
|||||||
|
|
||||||
public canBeVotedFor: () => boolean;
|
public canBeVotedFor: () => boolean;
|
||||||
|
|
||||||
public get user_has_voted_invalid(): boolean {
|
|
||||||
return this.options.some(option => option.user_has_voted) && !this.user_has_voted_valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get user_has_voted_valid(): boolean {
|
|
||||||
return this.options.every(option => option.user_has_voted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get user_has_not_voted(): boolean {
|
|
||||||
return this.options.every(option => !option.user_has_voted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract getSlide(): ProjectorElementBuildDeskriptor;
|
public abstract getSlide(): ProjectorElementBuildDeskriptor;
|
||||||
|
|
||||||
public abstract getContentObject(): BaseViewModel;
|
public abstract getContentObject(): BaseViewModel;
|
||||||
|
@ -31,6 +31,28 @@ class Migration(migrations.Migration):
|
|||||||
name="global_no",
|
name="global_no",
|
||||||
field=models.BooleanField(default=True),
|
field=models.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="db_amount_global_abstain",
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
decimal_places=6,
|
||||||
|
max_digits=15,
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="db_amount_global_no",
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
decimal_places=6,
|
||||||
|
max_digits=15,
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="assignmentpoll",
|
model_name="assignmentpoll",
|
||||||
name="groups",
|
name="groups",
|
||||||
@ -76,6 +98,11 @@ class Migration(migrations.Migration):
|
|||||||
default=1, validators=[django.core.validators.MinValueValidator(1)]
|
default=1, validators=[django.core.validators.MinValueValidator(1)]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="voted",
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="assignmentvote",
|
model_name="assignmentvote",
|
||||||
name="user",
|
name="user",
|
||||||
@ -129,15 +156,6 @@ class Migration(migrations.Migration):
|
|||||||
name="number_poll_candidates",
|
name="number_poll_candidates",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
|
||||||
model_name="assignmentoption",
|
|
||||||
name="voted",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
related_name="assignmentoption_voted",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="assignment",
|
model_name="assignment",
|
||||||
name="poll_description_default",
|
name="poll_description_default",
|
||||||
|
@ -254,14 +254,14 @@ class AssignmentOptionManager(BaseManager):
|
|||||||
|
|
||||||
def get_prefetched_queryset(self, *args, **kwargs):
|
def get_prefetched_queryset(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all voted users. In the background we
|
Returns the normal queryset. In the background we
|
||||||
join and prefetch all related models.
|
join and prefetch all related models.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_prefetched_queryset(*args, **kwargs)
|
.get_prefetched_queryset(*args, **kwargs)
|
||||||
.select_related("user", "poll")
|
.select_related("user", "poll")
|
||||||
.prefetch_related("voted", "votes")
|
.prefetch_related("votes")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -277,9 +277,6 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
|||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
)
|
)
|
||||||
voted = models.ManyToManyField(
|
|
||||||
settings.AUTH_USER_MODEL, blank=True, related_name="assignmentoption_voted"
|
|
||||||
)
|
|
||||||
weight = models.IntegerField(default=0)
|
weight = models.IntegerField(default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -301,7 +298,7 @@ class AssignmentPollManager(BaseManager):
|
|||||||
.get_prefetched_queryset(*args, **kwargs)
|
.get_prefetched_queryset(*args, **kwargs)
|
||||||
.select_related("assignment")
|
.select_related("assignment")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
"options", "options__user", "options__votes", "options__voted", "groups"
|
"options", "options__user", "options__votes", "voted", "groups"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -348,7 +345,23 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
)
|
)
|
||||||
|
|
||||||
global_abstain = models.BooleanField(default=True)
|
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,
|
||||||
|
)
|
||||||
global_no = models.BooleanField(default=True)
|
global_no = models.BooleanField(default=True)
|
||||||
|
db_amount_global_no = 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. """
|
||||||
@ -358,26 +371,49 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
@property
|
def get_amount_global_abstain(self):
|
||||||
def amount_global_no(self):
|
if not self.global_abstain:
|
||||||
if self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES or not self.global_no:
|
|
||||||
return None
|
return None
|
||||||
no_sum = Decimal(0)
|
elif (
|
||||||
for option in self.options.all():
|
self.type == self.TYPE_ANALOG
|
||||||
no_sum += option.no
|
or self.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
return no_sum
|
|
||||||
|
|
||||||
@property
|
|
||||||
def amount_global_abstain(self):
|
|
||||||
if (
|
|
||||||
self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES
|
|
||||||
or not self.global_abstain
|
|
||||||
):
|
):
|
||||||
|
return self.db_amount_global_abstain
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
abstain_sum = Decimal(0)
|
|
||||||
for option in self.options.all():
|
def set_amount_global_abstain(self, value):
|
||||||
abstain_sum += option.abstain
|
if (
|
||||||
return abstain_sum
|
self.type != self.TYPE_ANALOG
|
||||||
|
and self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES
|
||||||
|
):
|
||||||
|
raise ValueError("Do not set amount_global_abstain YN/YNA polls")
|
||||||
|
self.db_amount_global_abstain = value
|
||||||
|
|
||||||
|
amount_global_abstain = property(
|
||||||
|
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
|
||||||
|
or self.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
|
):
|
||||||
|
return self.db_amount_global_no
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_amount_global_no(self, value):
|
||||||
|
if (
|
||||||
|
self.type != self.TYPE_ANALOG
|
||||||
|
and self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES
|
||||||
|
):
|
||||||
|
raise ValueError("Do not set amount_global_no YN/YNA 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(
|
||||||
@ -404,3 +440,8 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
pass
|
pass
|
||||||
if not skip_autoupdate:
|
if not skip_autoupdate:
|
||||||
inform_changed_data(self.assignment.list_of_speakers)
|
inform_changed_data(self.assignment.list_of_speakers)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.db_amount_global_abstain = Decimal(0)
|
||||||
|
self.db_amount_global_no = Decimal(0)
|
||||||
|
super().reset()
|
||||||
|
@ -2,6 +2,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import F
|
||||||
|
|
||||||
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
|
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
|
||||||
from openslides.utils.auth import has_perm
|
from openslides.utils.auth import has_perm
|
||||||
@ -283,6 +284,10 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
super().perform_create(serializer)
|
super().perform_create(serializer)
|
||||||
|
poll = AssignmentPoll.objects.get(pk=serializer.data["id"])
|
||||||
|
poll.db_amount_global_abstain = Decimal(0)
|
||||||
|
poll.db_amount_global_no = Decimal(0)
|
||||||
|
poll.save()
|
||||||
|
|
||||||
def handle_analog_vote(self, data, poll, user):
|
def handle_analog_vote(self, data, poll, user):
|
||||||
for field in ["votesvalid", "votesinvalid", "votescast"]:
|
for field in ["votesvalid", "votesinvalid", "votescast"]:
|
||||||
@ -291,9 +296,13 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
global_no_enabled = (
|
global_no_enabled = (
|
||||||
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
poll.global_no and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
)
|
)
|
||||||
|
if global_no_enabled:
|
||||||
|
poll.amount_global_no = data.get("amount_global_no", Decimal(0))
|
||||||
global_abstain_enabled = (
|
global_abstain_enabled = (
|
||||||
poll.global_abstain and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
poll.global_abstain and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
)
|
)
|
||||||
|
if global_abstain_enabled:
|
||||||
|
poll.amount_global_abstain = data.get("amount_global_abstain", Decimal(0))
|
||||||
|
|
||||||
options = poll.get_options()
|
options = poll.get_options()
|
||||||
options_data = data.get("options")
|
options_data = data.get("options")
|
||||||
@ -323,21 +332,8 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
)
|
)
|
||||||
vote_obj.weight = vote["A"]
|
vote_obj.weight = vote["A"]
|
||||||
vote_obj.save()
|
vote_obj.save()
|
||||||
|
inform_changed_data(option)
|
||||||
|
|
||||||
# Create votes for global no and global abstain
|
|
||||||
first_option = options.first()
|
|
||||||
if "global_no" in data and global_no_enabled:
|
|
||||||
vote_obj, _ = AssignmentVote.objects.get_or_create(
|
|
||||||
option=first_option, value="N"
|
|
||||||
)
|
|
||||||
vote_obj.weight = data["votescast"]
|
|
||||||
vote_obj.save()
|
|
||||||
if "global_abstain" in data and global_abstain_enabled:
|
|
||||||
vote_obj, _ = AssignmentVote.objects.get_or_create(
|
|
||||||
option=first_option, value="A"
|
|
||||||
)
|
|
||||||
vote_obj.weight = data["votescast"]
|
|
||||||
vote_obj.save()
|
|
||||||
poll.save()
|
poll.save()
|
||||||
|
|
||||||
def validate_vote_data(self, data, poll, user):
|
def validate_vote_data(self, data, poll, user):
|
||||||
@ -347,7 +343,7 @@ 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>],
|
||||||
["global_no": <amount>], ["global_abstain": <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:
|
||||||
@ -363,13 +359,11 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
- 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
|
||||||
- The sum of all amounts must be grater then 0 and <= poll.votes_amount
|
- The sum of all amounts must be grater than 0 and <= poll.votes_amount
|
||||||
|
|
||||||
YN/YNA:
|
YN/YNA:
|
||||||
{<option_id>: 'Y' | 'N' [|'A']}
|
{<option_id>: 'Y' | 'N' [|'A']}
|
||||||
- 'A' is only allowed in YNA pollmethod
|
- 'A' is only allowed in YNA pollmethod
|
||||||
|
|
||||||
Votes for all options have to be given
|
|
||||||
"""
|
"""
|
||||||
if poll.type == AssignmentPoll.TYPE_ANALOG:
|
if poll.type == AssignmentPoll.TYPE_ANALOG:
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@ -403,10 +397,14 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
poll.global_abstain
|
poll.global_abstain
|
||||||
and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||||
)
|
)
|
||||||
if ("global_no" in data and global_no_enabled) or (
|
if "amount_global_abstain" in data and global_abstain_enabled:
|
||||||
"global_abstain" in data and global_abstain_enabled
|
data["amount_global_abstain"] = self.parse_vote_value(
|
||||||
):
|
data, "amount_global_abstain"
|
||||||
data["votescast"] = self.parse_vote_value(data, "votescast")
|
)
|
||||||
|
if "amount_global_no" in data and global_no_enabled:
|
||||||
|
data["amount_global_no"] = self.parse_vote_value(
|
||||||
|
data, "amount_global_no"
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||||
@ -415,6 +413,10 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
for option_id, amount in data.items():
|
for option_id, amount in data.items():
|
||||||
if not is_int(option_id):
|
if not is_int(option_id):
|
||||||
raise ValidationError({"detail": "Each id must be an int."})
|
raise ValidationError({"detail": "Each id must be an int."})
|
||||||
|
if not AssignmentOption.objects.filter(id=option_id).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": f"Option {option_id} does not exist."}
|
||||||
|
)
|
||||||
if not is_int(amount):
|
if not is_int(amount):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"detail": "Each amounts must be int"}
|
{"detail": "Each amounts must be int"}
|
||||||
@ -456,6 +458,10 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
for option_id, value in data.items():
|
for option_id, value in data.items():
|
||||||
if not is_int(option_id):
|
if not is_int(option_id):
|
||||||
raise ValidationError({"detail": "Keys must be int"})
|
raise ValidationError({"detail": "Keys must be int"})
|
||||||
|
if not AssignmentOption.objects.filter(id=option_id).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": f"Option {option_id} does not exist."}
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA
|
poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA
|
||||||
and value not in ("Y", "N", "A",)
|
and value not in ("Y", "N", "A",)
|
||||||
@ -471,33 +477,6 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
|
|
||||||
options_data = data
|
options_data = data
|
||||||
|
|
||||||
db_option_ids = set(option.id for option in poll.get_options())
|
|
||||||
data_option_ids = set(int(option_id) for option_id in options_data.keys())
|
|
||||||
|
|
||||||
# Just for named/pseudoanonymous with YN/YNA skip the all-options-given check
|
|
||||||
if poll.type not in (
|
|
||||||
AssignmentPoll.TYPE_NAMED,
|
|
||||||
AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
|
||||||
) or poll.pollmethod not in (
|
|
||||||
AssignmentPoll.POLLMETHOD_YN,
|
|
||||||
AssignmentPoll.POLLMETHOD_YNA,
|
|
||||||
):
|
|
||||||
# Check if all options were given
|
|
||||||
if data_option_ids != db_option_ids:
|
|
||||||
raise ValidationError(
|
|
||||||
{"error": "You have to provide values for all options"}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not data_option_ids.issubset(db_option_ids):
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"error": "You gave the following invalid option ids: "
|
|
||||||
+ ", ".join(
|
|
||||||
str(id) for id in data_option_ids.difference(db_option_ids)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_votes_type_votes(self, data, poll, user):
|
def create_votes_type_votes(self, data, poll, user):
|
||||||
"""
|
"""
|
||||||
Helper function for handle_(named|pseudoanonymous)_vote
|
Helper function for handle_(named|pseudoanonymous)_vote
|
||||||
@ -508,9 +487,6 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
for option_id, amount in data.items():
|
for option_id, amount in data.items():
|
||||||
# Add user to the option's voted array
|
# Add user to the option's voted array
|
||||||
option = options.get(pk=option_id)
|
option = options.get(pk=option_id)
|
||||||
option.voted.add(user)
|
|
||||||
inform_changed_data(option)
|
|
||||||
|
|
||||||
# skip creating votes with empty weights
|
# skip creating votes with empty weights
|
||||||
if amount == 0:
|
if amount == 0:
|
||||||
continue
|
continue
|
||||||
@ -519,16 +495,15 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
)
|
)
|
||||||
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
|
||||||
option = options.order_by(
|
if data == "A":
|
||||||
"pk"
|
poll.amount_global_abstain = F("db_amount_global_abstain") + 1
|
||||||
).first() # order by is important to always get
|
elif data == "N":
|
||||||
# the correct "first" option
|
poll.amount_global_no = F("db_amount_global_no") + 1
|
||||||
option.voted.add(user)
|
else:
|
||||||
inform_changed_data(option)
|
raise RuntimeError("This should not happen")
|
||||||
vote = AssignmentVote.objects.create(
|
poll.save()
|
||||||
option=option, user=user, weight=Decimal(poll.votes_amount), value=data,
|
|
||||||
)
|
poll.voted.add(user)
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
|
||||||
|
|
||||||
def create_votes_type_named_pseudoanonymous(
|
def create_votes_type_named_pseudoanonymous(
|
||||||
self, data, poll, check_user, vote_user
|
self, data, poll, check_user, vote_user
|
||||||
@ -537,51 +512,37 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
options = poll.get_options()
|
options = poll.get_options()
|
||||||
for option_id, result in data.items():
|
for option_id, result in data.items():
|
||||||
option = options.get(pk=option_id)
|
option = options.get(pk=option_id)
|
||||||
option.voted.add(check_user)
|
|
||||||
inform_changed_data(option)
|
|
||||||
vote = AssignmentVote.objects.create(
|
vote = AssignmentVote.objects.create(
|
||||||
option=option, user=vote_user, value=result
|
option=option, user=vote_user, value=result
|
||||||
)
|
)
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||||
|
inform_changed_data(option, no_delete_on_restriction=True)
|
||||||
|
|
||||||
|
poll.voted.add(check_user)
|
||||||
|
|
||||||
def handle_named_vote(self, data, poll, user):
|
def handle_named_vote(self, data, poll, user):
|
||||||
|
if user in poll.voted.all():
|
||||||
|
raise ValidationError({"detail": "You have already voted"})
|
||||||
|
|
||||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||||
# Instead of reusing all existing votes for the user, delete all previous votes
|
|
||||||
for vote in poll.get_votes().filter(user=user):
|
|
||||||
vote.delete()
|
|
||||||
self.create_votes_type_votes(data, poll, user)
|
self.create_votes_type_votes(data, poll, user)
|
||||||
elif poll.pollmethod in (
|
elif poll.pollmethod in (
|
||||||
AssignmentPoll.POLLMETHOD_YN,
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
AssignmentPoll.POLLMETHOD_YNA,
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
):
|
):
|
||||||
# Delete all votes for the given options
|
|
||||||
option_ids = list(data.keys())
|
|
||||||
for vote in AssignmentVote.objects.filter(
|
|
||||||
user=user, option_id__in=option_ids
|
|
||||||
):
|
|
||||||
vote.delete()
|
|
||||||
self.create_votes_type_named_pseudoanonymous(data, poll, user, user)
|
self.create_votes_type_named_pseudoanonymous(data, poll, user, user)
|
||||||
|
|
||||||
def handle_pseudoanonymous_vote(self, data, poll, user):
|
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||||
|
if user in poll.voted.all():
|
||||||
|
raise ValidationError({"detail": "You have already voted"})
|
||||||
|
|
||||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||||
# check if the user has already voted
|
|
||||||
for option in poll.get_options():
|
|
||||||
if user in option.voted.all():
|
|
||||||
raise ValidationError({"detail": "You have already voted"})
|
|
||||||
self.create_votes_type_votes(data, poll, user)
|
self.create_votes_type_votes(data, poll, user)
|
||||||
|
|
||||||
elif poll.pollmethod in (
|
elif poll.pollmethod in (
|
||||||
AssignmentPoll.POLLMETHOD_YN,
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
AssignmentPoll.POLLMETHOD_YNA,
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
):
|
):
|
||||||
# Ensure, that the user has not voted any of the given options yet.
|
|
||||||
options = poll.get_options()
|
|
||||||
for option_id in data.keys():
|
|
||||||
option = options.get(pk=option_id)
|
|
||||||
if user in option.voted.all():
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": f"You have already voted for option {option.pk}"}
|
|
||||||
)
|
|
||||||
self.create_votes_type_named_pseudoanonymous(data, poll, user, None)
|
self.create_votes_type_named_pseudoanonymous(data, poll, user, None)
|
||||||
|
|
||||||
def convert_option_data(self, poll, data):
|
def convert_option_data(self, poll, data):
|
||||||
|
@ -57,6 +57,11 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="voted",
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="motionvote",
|
model_name="motionvote",
|
||||||
name="user",
|
name="user",
|
||||||
@ -107,15 +112,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
|
||||||
model_name="motionoption",
|
|
||||||
name="voted",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
related_name="motionoption_voted",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="motionvote",
|
model_name="motionvote",
|
||||||
name="option",
|
name="option",
|
||||||
|
@ -889,14 +889,14 @@ class MotionOptionManager(BaseManager):
|
|||||||
|
|
||||||
def get_prefetched_queryset(self, *args, **kwargs):
|
def get_prefetched_queryset(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all voted users. In the background we
|
Returns the normal queryset. In the background we
|
||||||
join and prefetch all related models.
|
join and prefetch all related models.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_prefetched_queryset(*args, **kwargs)
|
.get_prefetched_queryset(*args, **kwargs)
|
||||||
.select_related("poll")
|
.select_related("poll")
|
||||||
.prefetch_related("voted", "votes")
|
.prefetch_related("votes")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -909,9 +909,6 @@ class MotionOption(RESTModelMixin, BaseOption):
|
|||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey(
|
||||||
"MotionPoll", related_name="options", on_delete=CASCADE_AND_AUTOUPDATE
|
"MotionPoll", related_name="options", on_delete=CASCADE_AND_AUTOUPDATE
|
||||||
)
|
)
|
||||||
voted = models.ManyToManyField(
|
|
||||||
settings.AUTH_USER_MODEL, blank=True, related_name="motionoption_voted"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
@ -931,7 +928,7 @@ class MotionPollManager(BaseManager):
|
|||||||
super()
|
super()
|
||||||
.get_prefetched_queryset(*args, **kwargs)
|
.get_prefetched_queryset(*args, **kwargs)
|
||||||
.select_related("motion")
|
.select_related("motion")
|
||||||
.prefetch_related("options", "options__votes", "options__voted", "groups")
|
.prefetch_related("options", "options__votes", "voted", "groups")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1224,26 +1224,27 @@ class MotionPollViewSet(BasePollViewSet):
|
|||||||
raise ValidationError("Data must be Y or N")
|
raise ValidationError("Data must be Y or N")
|
||||||
|
|
||||||
if poll.type == MotionPoll.TYPE_PSEUDOANONYMOUS:
|
if poll.type == MotionPoll.TYPE_PSEUDOANONYMOUS:
|
||||||
if user in poll.options.get().voted.all():
|
if user in poll.voted.all():
|
||||||
raise ValidationError("You already voted on this poll")
|
raise ValidationError("You already voted on this poll")
|
||||||
|
|
||||||
def handle_named_vote(self, data, poll, user):
|
def handle_named_vote(self, data, poll, user):
|
||||||
option = poll.options.get()
|
option = poll.options.get()
|
||||||
vote, _ = MotionVote.objects.get_or_create(user=user, option=option)
|
vote, _ = MotionVote.objects.get_or_create(user=user, option=option)
|
||||||
self.handle_named_and_pseudoanonymous_vote(vote, data, user, option)
|
self.handle_named_and_pseudoanonymous_vote(data, user, poll, option, vote)
|
||||||
|
|
||||||
def handle_pseudoanonymous_vote(self, data, poll, user):
|
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||||
option = poll.options.get()
|
option = poll.options.get()
|
||||||
vote = MotionVote.objects.create(user=None, option=option)
|
vote = MotionVote.objects.create(user=None, option=option)
|
||||||
self.handle_named_and_pseudoanonymous_vote(vote, data, user, option)
|
self.handle_named_and_pseudoanonymous_vote(data, user, poll, option, vote)
|
||||||
|
|
||||||
def handle_named_and_pseudoanonymous_vote(self, vote, data, user, option):
|
def handle_named_and_pseudoanonymous_vote(self, data, user, poll, option, vote):
|
||||||
vote.value = data
|
vote.value = data
|
||||||
vote.weight = Decimal("1")
|
vote.weight = Decimal("1")
|
||||||
vote.save(no_delete_on_restriction=True)
|
vote.save(no_delete_on_restriction=True)
|
||||||
|
inform_changed_data(option)
|
||||||
|
|
||||||
option.voted.add(user)
|
poll.voted.add(user)
|
||||||
option.save()
|
poll.save()
|
||||||
|
|
||||||
|
|
||||||
class MotionOptionViewSet(BaseOptionViewSet):
|
class MotionOptionViewSet(BaseOptionViewSet):
|
||||||
|
@ -6,40 +6,6 @@ from ..utils.access_permissions import BaseAccessPermissions
|
|||||||
from ..utils.auth import async_has_perm
|
from ..utils.auth import async_has_perm
|
||||||
|
|
||||||
|
|
||||||
class BasePollAccessPermissions(BaseAccessPermissions):
|
|
||||||
manage_permission = "" # set by subclass
|
|
||||||
|
|
||||||
additional_fields: List[str] = []
|
|
||||||
""" Add fields to be removed from each unpublished poll """
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Poll-managers have full access, even during an active poll.
|
|
||||||
Non-published polls will be restricted:
|
|
||||||
- Remove votes* values from the poll
|
|
||||||
- Remove yes/no/abstain fields from options
|
|
||||||
- Remove fields given in self.assitional_fields from the poll
|
|
||||||
"""
|
|
||||||
if await async_has_perm(user_id, self.manage_permission):
|
|
||||||
data = full_data
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
for poll in full_data:
|
|
||||||
if poll["state"] != BasePoll.STATE_PUBLISHED:
|
|
||||||
poll = json.loads(
|
|
||||||
json.dumps(poll)
|
|
||||||
) # copy, so we can remove some fields.
|
|
||||||
del poll["votesvalid"]
|
|
||||||
del poll["votesinvalid"]
|
|
||||||
del poll["votescast"]
|
|
||||||
for field in self.additional_fields:
|
|
||||||
del poll[field]
|
|
||||||
data.append(poll)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class BaseVoteAccessPermissions(BaseAccessPermissions):
|
class BaseVoteAccessPermissions(BaseAccessPermissions):
|
||||||
manage_permission = "" # set by subclass
|
manage_permission = "" # set by subclass
|
||||||
|
|
||||||
@ -71,10 +37,6 @@ class BaseOptionAccessPermissions(BaseAccessPermissions):
|
|||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
# add has_voted for all users to check whether op has voted
|
|
||||||
for option in full_data:
|
|
||||||
option["user_has_voted"] = user_id in option["voted_id"]
|
|
||||||
|
|
||||||
if await async_has_perm(user_id, self.manage_permission):
|
if await async_has_perm(user_id, self.manage_permission):
|
||||||
data = full_data
|
data = full_data
|
||||||
else:
|
else:
|
||||||
@ -87,6 +49,45 @@ class BaseOptionAccessPermissions(BaseAccessPermissions):
|
|||||||
del option["yes"]
|
del option["yes"]
|
||||||
del option["no"]
|
del option["no"]
|
||||||
del option["abstain"]
|
del option["abstain"]
|
||||||
del option["voted_id"]
|
|
||||||
data.append(option)
|
data.append(option)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class BasePollAccessPermissions(BaseAccessPermissions):
|
||||||
|
manage_permission = "" # set by subclass
|
||||||
|
|
||||||
|
additional_fields: List[str] = []
|
||||||
|
""" Add fields to be removed from each unpublished poll """
|
||||||
|
|
||||||
|
async def get_restricted_data(
|
||||||
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Poll-managers have full access, even during an active poll.
|
||||||
|
Non-published polls will be restricted:
|
||||||
|
- Remove votes* values from the poll
|
||||||
|
- Remove yes/no/abstain fields from options
|
||||||
|
- Remove fields given in self.assitional_fields from the poll
|
||||||
|
"""
|
||||||
|
|
||||||
|
# add has_voted for all users to check whether op has voted
|
||||||
|
for poll in full_data:
|
||||||
|
poll["user_has_voted"] = user_id in poll["voted_id"]
|
||||||
|
|
||||||
|
if await async_has_perm(user_id, self.manage_permission):
|
||||||
|
data = full_data
|
||||||
|
else:
|
||||||
|
data = []
|
||||||
|
for poll in full_data:
|
||||||
|
if poll["state"] != BasePoll.STATE_PUBLISHED:
|
||||||
|
poll = json.loads(
|
||||||
|
json.dumps(poll)
|
||||||
|
) # copy, so we can remove some fields.
|
||||||
|
del poll["votesvalid"]
|
||||||
|
del poll["votesinvalid"]
|
||||||
|
del poll["votescast"]
|
||||||
|
del poll["voted_id"]
|
||||||
|
for field in self.additional_fields:
|
||||||
|
del poll[field]
|
||||||
|
data.append(poll)
|
||||||
|
return data
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Iterable, Optional, Set, Tuple, Type
|
from typing import Iterable, Optional, Tuple, Type
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -35,8 +35,7 @@ class BaseVote(models.Model):
|
|||||||
|
|
||||||
class BaseOption(models.Model):
|
class BaseOption(models.Model):
|
||||||
"""
|
"""
|
||||||
All subclasses must have poll attribute with the related name "options". Also
|
All subclasses must have poll attribute with the related name "options"
|
||||||
they must have a "voted" relation to users.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vote_class: Optional[Type["BaseVote"]] = None
|
vote_class: Optional[Type["BaseVote"]] = None
|
||||||
@ -86,8 +85,6 @@ class BaseOption(models.Model):
|
|||||||
vote.save()
|
vote.save()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.voted.clear()
|
|
||||||
|
|
||||||
# Delete votes
|
# Delete votes
|
||||||
votes = self.get_votes()
|
votes = self.get_votes()
|
||||||
votes_id = [vote.id for vote in votes]
|
votes_id = [vote.id for vote in votes]
|
||||||
@ -126,6 +123,7 @@ class BasePoll(models.Model):
|
|||||||
|
|
||||||
title = models.CharField(max_length=255, blank=True, null=False)
|
title = models.CharField(max_length=255, blank=True, null=False)
|
||||||
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
||||||
|
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
|
||||||
|
|
||||||
db_votesvalid = models.DecimalField(
|
db_votesvalid = models.DecimalField(
|
||||||
null=True,
|
null=True,
|
||||||
@ -186,7 +184,7 @@ class BasePoll(models.Model):
|
|||||||
if self.type == self.TYPE_ANALOG:
|
if self.type == self.TYPE_ANALOG:
|
||||||
return self.db_votesvalid
|
return self.db_votesvalid
|
||||||
else:
|
else:
|
||||||
return Decimal(self.amount_valid_votes())
|
return Decimal(self.amount_users_voted())
|
||||||
|
|
||||||
def set_votesvalid(self, value):
|
def set_votesvalid(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -199,7 +197,7 @@ class BasePoll(models.Model):
|
|||||||
if self.type == self.TYPE_ANALOG:
|
if self.type == self.TYPE_ANALOG:
|
||||||
return self.db_votesinvalid
|
return self.db_votesinvalid
|
||||||
else:
|
else:
|
||||||
return Decimal(self.amount_invalid_votes())
|
return Decimal(0)
|
||||||
|
|
||||||
def set_votesinvalid(self, value):
|
def set_votesinvalid(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -212,7 +210,7 @@ class BasePoll(models.Model):
|
|||||||
if self.type == self.TYPE_ANALOG:
|
if self.type == self.TYPE_ANALOG:
|
||||||
return self.db_votescast
|
return self.db_votescast
|
||||||
else:
|
else:
|
||||||
return Decimal(self.amount_voted_users())
|
return Decimal(self.amount_users_voted())
|
||||||
|
|
||||||
def set_votescast(self, value):
|
def set_votescast(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -221,32 +219,8 @@ class BasePoll(models.Model):
|
|||||||
|
|
||||||
votescast = property(get_votescast, set_votescast)
|
votescast = property(get_votescast, set_votescast)
|
||||||
|
|
||||||
def get_user_ids_with_valid_votes(self):
|
def amount_users_voted(self):
|
||||||
if self.get_options().count():
|
return len(self.voted.all())
|
||||||
initial_option = self.get_options()[0]
|
|
||||||
user_ids = set(map(lambda u: u.id, initial_option.voted.all()))
|
|
||||||
for option in self.get_options():
|
|
||||||
user_ids = user_ids.intersection(
|
|
||||||
set(map(lambda u: u.id, option.voted.all()))
|
|
||||||
)
|
|
||||||
return list(user_ids)
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_all_voted_user_ids(self):
|
|
||||||
user_ids: Set[int] = set()
|
|
||||||
for option in self.get_options():
|
|
||||||
user_ids.update(map(lambda u: u.id, option.voted.all()))
|
|
||||||
return list(user_ids)
|
|
||||||
|
|
||||||
def amount_valid_votes(self):
|
|
||||||
return len(self.get_user_ids_with_valid_votes())
|
|
||||||
|
|
||||||
def amount_invalid_votes(self):
|
|
||||||
return self.amount_voted_users() - self.amount_valid_votes()
|
|
||||||
|
|
||||||
def amount_voted_users(self):
|
|
||||||
return len(self.get_all_voted_user_ids())
|
|
||||||
|
|
||||||
def create_options(self):
|
def create_options(self):
|
||||||
""" Should be called after creation of this model. """
|
""" Should be called after creation of this model. """
|
||||||
@ -284,6 +258,8 @@ class BasePoll(models.Model):
|
|||||||
for option in self.get_options():
|
for option in self.get_options():
|
||||||
option.reset()
|
option.reset()
|
||||||
|
|
||||||
|
self.voted.clear()
|
||||||
|
|
||||||
# Reset state
|
# Reset state
|
||||||
self.state = BasePoll.STATE_CREATED
|
self.state = BasePoll.STATE_CREATED
|
||||||
if self.type == self.TYPE_ANALOG:
|
if self.type == self.TYPE_ANALOG:
|
||||||
|
@ -22,7 +22,7 @@ class BaseVoteSerializer(ModelSerializer):
|
|||||||
return vote.option.poll.state
|
return vote.option.poll.state
|
||||||
|
|
||||||
|
|
||||||
BASE_OPTION_FIELDS = ("id", "yes", "no", "abstain", "poll_id", "pollstate", "voted")
|
BASE_OPTION_FIELDS = ("id", "yes", "no", "abstain", "poll_id", "pollstate")
|
||||||
|
|
||||||
|
|
||||||
class BaseOptionSerializer(ModelSerializer):
|
class BaseOptionSerializer(ModelSerializer):
|
||||||
@ -31,7 +31,6 @@ class BaseOptionSerializer(ModelSerializer):
|
|||||||
abstain = DecimalField(
|
abstain = 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
|
||||||
)
|
)
|
||||||
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
|
||||||
|
|
||||||
pollstate = SerializerMethodField()
|
pollstate = SerializerMethodField()
|
||||||
|
|
||||||
@ -51,6 +50,7 @@ BASE_POLL_FIELDS = (
|
|||||||
"id",
|
"id",
|
||||||
"onehundred_percent_base",
|
"onehundred_percent_base",
|
||||||
"majority_method",
|
"majority_method",
|
||||||
|
"voted",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -60,6 +60,7 @@ class BasePollSerializer(ModelSerializer):
|
|||||||
many=True, required=False, queryset=get_group_model().objects.all()
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
)
|
)
|
||||||
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
votesvalid = DecimalField(
|
votesvalid = 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
|
||||||
|
@ -175,12 +175,6 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def reset(self, request, pk):
|
def reset(self, request, pk):
|
||||||
poll = self.get_object()
|
poll = self.get_object()
|
||||||
|
|
||||||
if poll.state not in (BasePoll.STATE_FINISHED, BasePoll.STATE_PUBLISHED):
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "You can only reset this poll after it is finished"}
|
|
||||||
)
|
|
||||||
|
|
||||||
poll.reset()
|
poll.reset()
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
@ -53,12 +53,11 @@ def test_assignment_option_db_queries():
|
|||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 1 request to get the options,
|
* 1 request to get the options,
|
||||||
* 1 request to get all users that voted on the options,
|
|
||||||
* 1 request to get all votes for all options,
|
* 1 request to get all votes for all options,
|
||||||
= 3 queries
|
= 2 queries
|
||||||
"""
|
"""
|
||||||
create_assignment_polls()
|
create_assignment_polls()
|
||||||
assert count_queries(AssignmentOption.get_elements)() == 3
|
assert count_queries(AssignmentOption.get_elements)() == 2
|
||||||
|
|
||||||
|
|
||||||
def create_assignment_polls():
|
def create_assignment_polls():
|
||||||
@ -93,13 +92,13 @@ def create_assignment_polls():
|
|||||||
username=f"test_username_{i}{j}",
|
username=f"test_username_{i}{j}",
|
||||||
password="test_password_kbzj5L8ZtVxBllZzoW6D",
|
password="test_password_kbzj5L8ZtVxBllZzoW6D",
|
||||||
)
|
)
|
||||||
|
poll.voted.add(user)
|
||||||
for option in poll.options.all():
|
for option in poll.options.all():
|
||||||
weight = random.randint(0, 10)
|
weight = random.randint(0, 10)
|
||||||
if weight > 0:
|
if weight > 0:
|
||||||
AssignmentVote.objects.create(
|
AssignmentVote.objects.create(
|
||||||
user=user, option=option, value="Y", weight=Decimal(weight)
|
user=user, option=option, value="Y", weight=Decimal(weight)
|
||||||
)
|
)
|
||||||
option.voted.add(user)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateAssignmentPoll(TestCase):
|
class CreateAssignmentPoll(TestCase):
|
||||||
@ -110,7 +109,7 @@ class CreateAssignmentPoll(TestCase):
|
|||||||
self.assignment.add_candidate(self.admin)
|
self.assignment.add_candidate(self.admin)
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
with self.assertNumQueries(50):
|
with self.assertNumQueries(40):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("assignmentpoll-list"),
|
reverse("assignmentpoll-list"),
|
||||||
{
|
{
|
||||||
@ -1008,10 +1007,10 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
{"1": "N"},
|
{"1": "N"},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||||
vote = AssignmentVote.objects.get()
|
vote = AssignmentVote.objects.get()
|
||||||
self.assertEqual(vote.value, "N")
|
self.assertEqual(vote.value, "Y")
|
||||||
|
|
||||||
def test_too_many_options(self):
|
def test_too_many_options(self):
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
@ -1197,14 +1196,14 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
{"1": 0, "2": 1},
|
{"1": 0, "2": 1},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
option1 = poll.options.get(pk=1)
|
option1 = poll.options.get(pk=1)
|
||||||
option2 = poll.options.get(pk=2)
|
option2 = poll.options.get(pk=2)
|
||||||
self.assertEqual(option1.yes, Decimal("0"))
|
self.assertEqual(option1.yes, Decimal("1"))
|
||||||
self.assertEqual(option1.no, Decimal("0"))
|
self.assertEqual(option1.no, Decimal("0"))
|
||||||
self.assertEqual(option1.abstain, Decimal("0"))
|
self.assertEqual(option1.abstain, Decimal("0"))
|
||||||
self.assertEqual(option2.yes, Decimal("1"))
|
self.assertEqual(option2.yes, Decimal("0"))
|
||||||
self.assertEqual(option2.no, Decimal("0"))
|
self.assertEqual(option2.no, Decimal("0"))
|
||||||
self.assertEqual(option2.abstain, Decimal("0"))
|
self.assertEqual(option2.abstain, Decimal("0"))
|
||||||
|
|
||||||
@ -1219,9 +1218,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
option = poll.options.get(pk=1)
|
option = poll.options.get(pk=1)
|
||||||
self.assertEqual(option.yes, Decimal("0"))
|
self.assertEqual(option.yes, Decimal("0"))
|
||||||
self.assertEqual(option.no, Decimal("2"))
|
self.assertEqual(option.no, Decimal("0"))
|
||||||
self.assertEqual(option.abstain, Decimal("0"))
|
self.assertEqual(option.abstain, Decimal("0"))
|
||||||
self.assertEqual(poll.amount_global_no, Decimal("2"))
|
self.assertEqual(poll.amount_global_no, Decimal("1"))
|
||||||
self.assertEqual(poll.amount_global_abstain, Decimal("0"))
|
self.assertEqual(poll.amount_global_abstain, Decimal("0"))
|
||||||
|
|
||||||
def test_global_no_forbidden(self):
|
def test_global_no_forbidden(self):
|
||||||
@ -1247,9 +1246,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
option = poll.options.get(pk=1)
|
option = poll.options.get(pk=1)
|
||||||
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("2"))
|
self.assertEqual(option.abstain, 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("2"))
|
self.assertEqual(poll.amount_global_abstain, Decimal("1"))
|
||||||
|
|
||||||
def test_global_abstain_forbidden(self):
|
def test_global_abstain_forbidden(self):
|
||||||
self.poll.global_abstain = False
|
self.poll.global_abstain = False
|
||||||
@ -1302,17 +1301,6 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
def test_missing_option(self):
|
|
||||||
self.add_candidate()
|
|
||||||
self.start_poll()
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
|
||||||
{"1": 1},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
|
||||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
|
||||||
|
|
||||||
def test_too_many_options(self):
|
def test_too_many_options(self):
|
||||||
self.setup_for_multiple_votes()
|
self.setup_for_multiple_votes()
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
@ -1370,7 +1358,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
def test_missing_data(self):
|
def test_missing_data(self):
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertFalse(AssignmentVote.objects.exists())
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
def test_wrong_data_format(self):
|
def test_wrong_data_format(self):
|
||||||
@ -1557,9 +1545,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
def test_missing_data(self):
|
def test_missing_data(self):
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
response, status.HTTP_200_OK
|
|
||||||
) # new "feature" because of partial requests: empty requests work!
|
|
||||||
self.assertFalse(AssignmentVote.objects.exists())
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
def test_wrong_data_format(self):
|
def test_wrong_data_format(self):
|
||||||
@ -1718,17 +1704,6 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
def test_missing_option(self):
|
|
||||||
self.add_candidate()
|
|
||||||
self.start_poll()
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
|
||||||
{"1": 1},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
|
||||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
|
||||||
|
|
||||||
def test_too_many_options(self):
|
def test_too_many_options(self):
|
||||||
self.setup_for_multiple_votes()
|
self.setup_for_multiple_votes()
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
@ -1786,7 +1761,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
|||||||
def test_missing_data(self):
|
def test_missing_data(self):
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertFalse(AssignmentVote.objects.exists())
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
def test_wrong_data_format(self):
|
def test_wrong_data_format(self):
|
||||||
@ -1909,6 +1884,8 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
|||||||
"votescast": "1.000000",
|
"votescast": "1.000000",
|
||||||
"votesinvalid": "0.000000",
|
"votesinvalid": "0.000000",
|
||||||
"votesvalid": "1.000000",
|
"votesvalid": "1.000000",
|
||||||
|
"user_has_voted": False,
|
||||||
|
"voted_id": [self.user.id],
|
||||||
},
|
},
|
||||||
"assignments/assignment-option:1": {
|
"assignments/assignment-option:1": {
|
||||||
"abstain": "1.000000",
|
"abstain": "1.000000",
|
||||||
@ -1919,8 +1896,6 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
|||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"user_has_voted": False,
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
},
|
||||||
"assignments/assignment-vote:1": {
|
"assignments/assignment-vote:1": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@ -1971,6 +1946,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
|||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"votes_amount": 1,
|
"votes_amount": 1,
|
||||||
|
"user_has_voted": user == self.user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1984,7 +1960,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
|||||||
vote.value = "A"
|
vote.value = "A"
|
||||||
vote.weight = Decimal("1")
|
vote.weight = Decimal("1")
|
||||||
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
||||||
option.voted.add(self.user.id)
|
self.poll.voted.add(self.user.id)
|
||||||
self.poll.state = AssignmentPoll.STATE_FINISHED
|
self.poll.state = AssignmentPoll.STATE_FINISHED
|
||||||
self.poll.save(skip_autoupdate=True)
|
self.poll.save(skip_autoupdate=True)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -2026,6 +2002,8 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
|||||||
"votescast": "1.000000",
|
"votescast": "1.000000",
|
||||||
"votesinvalid": "0.000000",
|
"votesinvalid": "0.000000",
|
||||||
"votesvalid": "1.000000",
|
"votesvalid": "1.000000",
|
||||||
|
"user_has_voted": user == self.user,
|
||||||
|
"voted_id": [self.user.id],
|
||||||
},
|
},
|
||||||
"assignments/assignment-vote:1": {
|
"assignments/assignment-vote:1": {
|
||||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
||||||
@ -2044,8 +2022,6 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
|||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -2083,6 +2059,8 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|||||||
"title": self.poll.title,
|
"title": self.poll.title,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
||||||
|
"user_has_voted": False,
|
||||||
|
"voted_id": [self.user.id],
|
||||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
||||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
||||||
"votes_amount": 1,
|
"votes_amount": 1,
|
||||||
@ -2099,8 +2077,6 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"user_has_voted": False,
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
},
|
||||||
"assignments/assignment-vote:1": {
|
"assignments/assignment-vote:1": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@ -2137,6 +2113,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"votes_amount": 1,
|
"votes_amount": 1,
|
||||||
|
"user_has_voted": user == self.user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2149,7 +2126,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|||||||
vote.value = "A"
|
vote.value = "A"
|
||||||
vote.weight = Decimal("1")
|
vote.weight = Decimal("1")
|
||||||
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
||||||
option.voted.add(self.user.id)
|
self.poll.voted.add(self.user.id)
|
||||||
self.poll.state = AssignmentPoll.STATE_FINISHED
|
self.poll.state = AssignmentPoll.STATE_FINISHED
|
||||||
self.poll.save(skip_autoupdate=True)
|
self.poll.save(skip_autoupdate=True)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -2191,6 +2168,8 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|||||||
"votescast": "1.000000",
|
"votescast": "1.000000",
|
||||||
"votesinvalid": "0.000000",
|
"votesinvalid": "0.000000",
|
||||||
"votesvalid": "1.000000",
|
"votesvalid": "1.000000",
|
||||||
|
"user_has_voted": user == self.user,
|
||||||
|
"voted_id": [self.user.id],
|
||||||
},
|
},
|
||||||
"assignments/assignment-vote:1": {
|
"assignments/assignment-vote:1": {
|
||||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
||||||
@ -2209,8 +2188,6 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -47,11 +47,10 @@ def test_motion_option_db_queries():
|
|||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 1 request to get the options,
|
* 1 request to get the options,
|
||||||
* 1 request to get all votes for all options,
|
* 1 request to get all votes for all options,
|
||||||
* 1 request to get all users that voted on the options
|
= 2 queries
|
||||||
= 5 queries
|
|
||||||
"""
|
"""
|
||||||
create_motion_polls()
|
create_motion_polls()
|
||||||
assert count_queries(MotionOption.get_elements)() == 3
|
assert count_queries(MotionOption.get_elements)() == 2
|
||||||
|
|
||||||
|
|
||||||
def create_motion_polls():
|
def create_motion_polls():
|
||||||
@ -83,7 +82,7 @@ def create_motion_polls():
|
|||||||
value=("Y" if k == 0 else "N"),
|
value=("Y" if k == 0 else "N"),
|
||||||
weight=Decimal(1),
|
weight=Decimal(1),
|
||||||
)
|
)
|
||||||
option.voted.add(user)
|
poll.voted.add(user)
|
||||||
|
|
||||||
|
|
||||||
class CreateMotionPoll(TestCase):
|
class CreateMotionPoll(TestCase):
|
||||||
@ -166,6 +165,8 @@ class CreateMotionPoll(TestCase):
|
|||||||
"votescast": "0.000000",
|
"votescast": "0.000000",
|
||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"voted_id": [],
|
||||||
|
"user_has_voted": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(autoupdate[1], [])
|
self.assertEqual(autoupdate[1], [])
|
||||||
@ -910,6 +911,8 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
|||||||
"votescast": "1.000000",
|
"votescast": "1.000000",
|
||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"user_has_voted": False,
|
||||||
|
"voted_id": [self.user.id],
|
||||||
},
|
},
|
||||||
"motions/motion-vote:1": {
|
"motions/motion-vote:1": {
|
||||||
"pollstate": 2,
|
"pollstate": 2,
|
||||||
@ -926,8 +929,6 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
|||||||
"poll_id": 1,
|
"poll_id": 1,
|
||||||
"pollstate": 2,
|
"pollstate": 2,
|
||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_has_voted": False,
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -948,7 +949,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
autoupdate[0]["motions/motion-option:1"],
|
autoupdate[0]["motions/motion-option:1"],
|
||||||
{"id": 1, "poll_id": 1, "pollstate": 2, "user_has_voted": True},
|
{"id": 1, "poll_id": 1, "pollstate": 2},
|
||||||
)
|
)
|
||||||
self.assertEqual(autoupdate[1], [])
|
self.assertEqual(autoupdate[1], [])
|
||||||
|
|
||||||
@ -969,6 +970,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
|||||||
"groups_id": [GROUP_DELEGATE_PK],
|
"groups_id": [GROUP_DELEGATE_PK],
|
||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"user_has_voted": user == self.user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -977,8 +979,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"poll_id": 1,
|
"poll_id": 1,
|
||||||
"pollstate": 2,
|
"pollstate": 2,
|
||||||
"user_has_voted": user == self.user,
|
}, # noqa black and flake are no friends :(
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Other users should not get a vote autoupdate
|
# Other users should not get a vote autoupdate
|
||||||
@ -1049,6 +1050,8 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
|||||||
"votescast": "1.000000",
|
"votescast": "1.000000",
|
||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"user_has_voted": False,
|
||||||
|
"voted_id": [self.user.id],
|
||||||
},
|
},
|
||||||
"motions/motion-vote:1": {
|
"motions/motion-vote:1": {
|
||||||
"pollstate": 2,
|
"pollstate": 2,
|
||||||
@ -1065,8 +1068,6 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
|||||||
"poll_id": 1,
|
"poll_id": 1,
|
||||||
"pollstate": 2,
|
"pollstate": 2,
|
||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_has_voted": False,
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1090,6 +1091,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
|||||||
"groups_id": [GROUP_DELEGATE_PK],
|
"groups_id": [GROUP_DELEGATE_PK],
|
||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"user_has_voted": user == self.user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1153,12 +1155,12 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
|||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.get_votes().count(), 1)
|
self.assertEqual(poll.get_votes().count(), 1)
|
||||||
self.assertEqual(poll.amount_valid_votes(), 1)
|
self.assertEqual(poll.amount_users_voted(), 1)
|
||||||
option = poll.options.get()
|
option = poll.options.get()
|
||||||
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.assertTrue(self.admin in option.voted.all())
|
self.assertTrue(self.admin in poll.voted.all())
|
||||||
vote = option.votes.get()
|
vote = option.votes.get()
|
||||||
self.assertEqual(vote.user, None)
|
self.assertEqual(vote.user, None)
|
||||||
|
|
||||||
@ -1311,6 +1313,8 @@ class PublishMotionPoll(TestCase):
|
|||||||
"votescast": "0.000000",
|
"votescast": "0.000000",
|
||||||
"options_id": [1],
|
"options_id": [1],
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"user_has_voted": False,
|
||||||
|
"voted_id": [],
|
||||||
},
|
},
|
||||||
"motions/motion-vote:1": {
|
"motions/motion-vote:1": {
|
||||||
"pollstate": 4,
|
"pollstate": 4,
|
||||||
@ -1327,8 +1331,6 @@ class PublishMotionPoll(TestCase):
|
|||||||
"poll_id": 1,
|
"poll_id": 1,
|
||||||
"pollstate": 4,
|
"pollstate": 4,
|
||||||
"yes": "0.000000",
|
"yes": "0.000000",
|
||||||
"user_has_voted": False,
|
|
||||||
"voted_id": [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1362,12 +1364,12 @@ class PseudoanonymizeMotionPoll(TestCase):
|
|||||||
self.vote1 = MotionVote.objects.create(
|
self.vote1 = MotionVote.objects.create(
|
||||||
user=self.user1, option=self.option, value="Y", weight=Decimal(1)
|
user=self.user1, option=self.option, value="Y", weight=Decimal(1)
|
||||||
)
|
)
|
||||||
self.option.voted.add(self.user1)
|
self.poll.voted.add(self.user1)
|
||||||
self.user2, _ = self.create_user()
|
self.user2, _ = self.create_user()
|
||||||
self.vote2 = MotionVote.objects.create(
|
self.vote2 = MotionVote.objects.create(
|
||||||
user=self.user2, option=self.option, value="N", weight=Decimal(1)
|
user=self.user2, option=self.option, value="N", weight=Decimal(1)
|
||||||
)
|
)
|
||||||
self.option.voted.add(self.user2)
|
self.poll.voted.add(self.user2)
|
||||||
|
|
||||||
def test_pseudoanonymize_poll(self):
|
def test_pseudoanonymize_poll(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -1376,7 +1378,7 @@ class PseudoanonymizeMotionPoll(TestCase):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
self.assertEqual(poll.get_votes().count(), 2)
|
self.assertEqual(poll.get_votes().count(), 2)
|
||||||
self.assertEqual(poll.amount_valid_votes(), 2)
|
self.assertEqual(poll.amount_users_voted(), 2)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("2"))
|
self.assertEqual(poll.votescast, Decimal("2"))
|
||||||
@ -1384,8 +1386,8 @@ class PseudoanonymizeMotionPoll(TestCase):
|
|||||||
self.assertEqual(option.yes, Decimal("1"))
|
self.assertEqual(option.yes, Decimal("1"))
|
||||||
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.assertTrue(self.user1 in option.voted.all())
|
self.assertTrue(self.user1 in poll.voted.all())
|
||||||
self.assertTrue(self.user2 in option.voted.all())
|
self.assertTrue(self.user2 in poll.voted.all())
|
||||||
for vote in poll.get_votes().all():
|
for vote in poll.get_votes().all():
|
||||||
self.assertTrue(vote.user is None)
|
self.assertTrue(vote.user is None)
|
||||||
|
|
||||||
@ -1432,19 +1434,19 @@ class ResetMotionPoll(TestCase):
|
|||||||
self.vote1 = MotionVote.objects.create(
|
self.vote1 = MotionVote.objects.create(
|
||||||
user=self.user1, option=self.option, value="Y", weight=Decimal(1)
|
user=self.user1, option=self.option, value="Y", weight=Decimal(1)
|
||||||
)
|
)
|
||||||
self.option.voted.add(self.user1)
|
self.poll.voted.add(self.user1)
|
||||||
self.user2, _ = self.create_user()
|
self.user2, _ = self.create_user()
|
||||||
self.vote2 = MotionVote.objects.create(
|
self.vote2 = MotionVote.objects.create(
|
||||||
user=self.user2, option=self.option, value="N", weight=Decimal(1)
|
user=self.user2, option=self.option, value="N", weight=Decimal(1)
|
||||||
)
|
)
|
||||||
self.option.voted.add(self.user2)
|
self.poll.voted.add(self.user2)
|
||||||
|
|
||||||
def test_reset_poll(self):
|
def test_reset_poll(self):
|
||||||
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
self.assertEqual(poll.get_votes().count(), 0)
|
self.assertEqual(poll.get_votes().count(), 0)
|
||||||
self.assertEqual(poll.amount_valid_votes(), 0)
|
self.assertEqual(poll.amount_users_voted(), 0)
|
||||||
self.assertEqual(poll.votesvalid, None)
|
self.assertEqual(poll.votesvalid, None)
|
||||||
self.assertEqual(poll.votesinvalid, None)
|
self.assertEqual(poll.votesinvalid, None)
|
||||||
self.assertEqual(poll.votescast, None)
|
self.assertEqual(poll.votescast, None)
|
||||||
@ -1463,12 +1465,3 @@ class ResetMotionPoll(TestCase):
|
|||||||
for user in (self.admin, self.user1, self.user2):
|
for user in (self.admin, self.user1, self.user2):
|
||||||
self.assertDeletedAutoupdate(self.vote1, user=user)
|
self.assertDeletedAutoupdate(self.vote1, user=user)
|
||||||
self.assertDeletedAutoupdate(self.vote2, user=user)
|
self.assertDeletedAutoupdate(self.vote2, user=user)
|
||||||
|
|
||||||
def test_reset_wrong_state(self):
|
|
||||||
self.poll.state = MotionPoll.STATE_STARTED
|
|
||||||
self.poll.save()
|
|
||||||
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
|
||||||
poll = MotionPoll.objects.get()
|
|
||||||
self.assertTrue(poll.get_votes().exists())
|
|
||||||
self.assertEqual(poll.amount_valid_votes(), 2)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user