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 {
|
||||
// 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) {
|
||||
this.sliceBanner();
|
||||
return;
|
||||
|
@ -9,8 +9,7 @@ export enum VotingError {
|
||||
POLL_WRONG_TYPE,
|
||||
USER_HAS_NO_PERMISSION,
|
||||
USER_IS_ANONYMOUS,
|
||||
USER_NOT_PRESENT,
|
||||
USER_HAS_VOTED_VALID
|
||||
USER_NOT_PRESENT
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,9 +59,6 @@ export class VotingService {
|
||||
if (!user.is_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 {
|
||||
|
@ -211,9 +211,11 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
yAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
drawOnChartArea: false
|
||||
drawBorder: false,
|
||||
drawOnChartArea: false,
|
||||
drawTicks: false
|
||||
},
|
||||
ticks: { beginAtZero: true, mirror: true, labelOffset: -20 },
|
||||
ticks: { mirror: true, labelOffset: -20 },
|
||||
stacked: true
|
||||
}
|
||||
]
|
||||
|
@ -88,12 +88,14 @@ export class CheckInputComponent extends BaseViewComponent implements OnInit, Co
|
||||
* @param obj the value from the parent form. Type "any" is required by the interface
|
||||
*/
|
||||
public writeValue(obj: string | number): void {
|
||||
if (obj && obj === this.checkboxValue) {
|
||||
if (obj || typeof obj === 'number') {
|
||||
if (obj === this.checkboxValue) {
|
||||
this.checkboxStateChanged(true);
|
||||
} else {
|
||||
this.contentForm.patchValue(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hands changes back to the parent form
|
||||
|
@ -29,6 +29,13 @@ export class AssignmentPoll extends BasePoll<
|
||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||
public static defaultGroupsConfig = 'assignment_poll_default_groups';
|
||||
public static defaultPollMethodConfig = 'assignment_poll_method';
|
||||
public static DECIMAL_FIELDS = [
|
||||
'votesvalid',
|
||||
'votesinvalid',
|
||||
'votescast',
|
||||
'amount_global_abstain',
|
||||
'amount_global_no'
|
||||
];
|
||||
|
||||
public id: number;
|
||||
public assignment_id: number;
|
||||
@ -36,6 +43,8 @@ export class AssignmentPoll extends BasePoll<
|
||||
public allow_multiple_votes_per_candidate: boolean;
|
||||
public global_no: boolean;
|
||||
public global_abstain: boolean;
|
||||
public amount_global_no: number;
|
||||
public amount_global_abstain: number;
|
||||
public description: string;
|
||||
|
||||
public get isMethodY(): boolean {
|
||||
@ -63,4 +72,8 @@ export class AssignmentPoll extends BasePoll<
|
||||
public constructor(input?: any) {
|
||||
super(AssignmentPoll.COLLECTIONSTRING, input);
|
||||
}
|
||||
|
||||
protected getDecimalFields(): string[] {
|
||||
return AssignmentPoll.DECIMAL_FIELDS;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { BaseModel } from './base-model';
|
||||
|
||||
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 {
|
||||
if (input && typeof input === 'object') {
|
||||
|
@ -6,10 +6,9 @@ export abstract class BaseOption<T> extends BaseDecimalModel<T> {
|
||||
public no: number;
|
||||
public abstain: number;
|
||||
public poll_id: number;
|
||||
public user_has_voted: boolean;
|
||||
public voted_id: number[];
|
||||
|
||||
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
||||
protected getDecimalFields(): string[] {
|
||||
return ['yes', 'no', 'abstain'];
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ export abstract class BasePoll<
|
||||
public votescast: number;
|
||||
public groups_id: number[];
|
||||
public majority_method: MajorityMethod;
|
||||
public user_has_voted: boolean;
|
||||
|
||||
public pollmethod: PM;
|
||||
public onehundred_percent_base: PB;
|
||||
@ -91,7 +92,7 @@ export abstract class BasePoll<
|
||||
return this.state + 1;
|
||||
}
|
||||
|
||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
||||
protected getDecimalFields(): string[] {
|
||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
|
||||
return VoteValueVerbose[this.value];
|
||||
}
|
||||
|
||||
protected getDecimalFields(): (keyof BaseVote<T>)[] {
|
||||
protected getDecimalFields(): string[] {
|
||||
return ['weight'];
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ const PollValues = {
|
||||
votesabstain: 'Votes abstain',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
abstain: 'Abstain'
|
||||
abstain: 'Abstain',
|
||||
amount_global_abstain: 'Global abstain',
|
||||
amount_global_no: 'Global no'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -57,21 +57,31 @@
|
||||
</mat-menu>
|
||||
</os-head-bar>
|
||||
|
||||
<div class="content-container">
|
||||
<div>
|
||||
<div *ngIf="editAssignment">
|
||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||
</div>
|
||||
<div *ngIf="!editAssignment">
|
||||
<!-- assignment meta infos-->
|
||||
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
||||
<!-- candidates list -->
|
||||
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
||||
<!-- closed polls -->
|
||||
|
||||
<!-- polls -->
|
||||
<ng-container *ngIf="assignment && assignment.polls.length">
|
||||
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
||||
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||
</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>
|
||||
|
||||
@ -196,12 +206,6 @@
|
||||
</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>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
|
@ -26,6 +26,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.new-ballot-button {
|
||||
display: flex;
|
||||
> * {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.election-document-list mat-list-item {
|
||||
height: 20px;
|
||||
}
|
||||
@ -56,10 +64,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ballot-button {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.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 -->
|
||||
<ng-container *ngIf="isAnalogPoll">
|
||||
<ng-container *ngIf="isAnalogPoll && dialogVoteForm">
|
||||
<form [formGroup]="dialogVoteForm">
|
||||
<!-- Candidates -->
|
||||
<!-- Candidates Values -->
|
||||
<div formGroupName="options">
|
||||
<div *ngFor="let option of options" class="votes-grid">
|
||||
<div>
|
||||
@ -24,6 +29,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sum Values -->
|
||||
<div *ngFor="let value of sumValues" class="votes-grid">
|
||||
<div></div>
|
||||
<os-check-input
|
||||
@ -34,6 +41,27 @@
|
||||
[formControlName]="value"
|
||||
></os-check-input>
|
||||
</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>
|
||||
|
||||
<!-- Publish Check -->
|
||||
@ -53,7 +81,8 @@
|
||||
<button
|
||||
mat-button
|
||||
(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>
|
||||
</button>
|
||||
|
@ -5,6 +5,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
@ -59,6 +60,9 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
||||
|
||||
public options: OptionsObject;
|
||||
|
||||
public globalNoEnabled: boolean;
|
||||
public globalAbstainEnabled: boolean;
|
||||
|
||||
public get isAnalogPoll(): boolean {
|
||||
return (
|
||||
this.pollForm &&
|
||||
@ -104,7 +108,7 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
||||
}
|
||||
|
||||
this.subscriptions.push(
|
||||
this.pollForm.contentForm.get('pollmethod').valueChanges.subscribe(() => {
|
||||
this.pollForm.contentForm.valueChanges.pipe(debounceTime(150), distinctUntilChanged()).subscribe(() => {
|
||||
this.createDialog();
|
||||
})
|
||||
);
|
||||
@ -112,6 +116,8 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
||||
|
||||
private setAnalogPollValues(): void {
|
||||
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'];
|
||||
if (pollmethod !== AssignmentPollMethod.Votes) {
|
||||
analogPollValues.push('N');
|
||||
@ -127,7 +133,9 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
||||
options: {},
|
||||
votesvalid: data.votesvalid,
|
||||
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) {
|
||||
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
|
||||
...this.sumValues.mapToObject(sumValue => ({
|
||||
[sumValue]: ['', [Validators.min(-2)]]
|
||||
|
@ -54,7 +54,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
if (this.poll && this.poll.user_has_not_voted) {
|
||||
if (this.poll && !this.poll.user_has_voted) {
|
||||
this.alreadyVoted = false;
|
||||
this.defineVoteOptions();
|
||||
} else {
|
||||
|
@ -39,6 +39,19 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
||||
public readonly tableChartData: Map<string, BehaviorSubject<ChartData>> = new Map();
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
protected getDecimalFields(): string[] {
|
||||
return AssignmentPoll.DECIMAL_FIELDS;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ViewAssignmentPoll extends AssignmentPoll {
|
||||
|
@ -57,12 +57,12 @@ export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotio
|
||||
*/
|
||||
private createDialog(): void {
|
||||
this.dialogVoteForm = this.fb.group({
|
||||
Y: [0, [Validators.min(-2)]],
|
||||
N: [0, [Validators.min(-2)]],
|
||||
A: [0, [Validators.min(-2)]],
|
||||
votesvalid: [0, [Validators.min(-2)]],
|
||||
votesinvalid: [0, [Validators.min(-2)]],
|
||||
votescast: [0, [Validators.min(-2)]]
|
||||
Y: ['', [Validators.min(-2)]],
|
||||
N: ['', [Validators.min(-2)]],
|
||||
A: ['', [Validators.min(-2)]],
|
||||
votesvalid: ['', [Validators.min(-2)]],
|
||||
votesinvalid: ['', [Validators.min(-2)]],
|
||||
votescast: ['', [Validators.min(-2)]]
|
||||
});
|
||||
|
||||
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">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
@ -16,3 +16,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</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 {
|
||||
background-color: $votes-yes-color;
|
||||
color: $vote-active-color;
|
||||
|
@ -22,7 +22,15 @@ export interface PollTableData {
|
||||
}
|
||||
|
||||
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;
|
||||
icon?: string;
|
||||
hide?: boolean;
|
||||
@ -176,18 +184,6 @@ export abstract class ViewBasePoll<
|
||||
|
||||
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 getContentObject(): BaseViewModel;
|
||||
|
@ -31,6 +31,28 @@ class Migration(migrations.Migration):
|
||||
name="global_no",
|
||||
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(
|
||||
model_name="assignmentpoll",
|
||||
name="groups",
|
||||
@ -76,6 +98,11 @@ class Migration(migrations.Migration):
|
||||
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(
|
||||
model_name="assignmentvote",
|
||||
name="user",
|
||||
@ -129,15 +156,6 @@ class Migration(migrations.Migration):
|
||||
name="number_poll_candidates",
|
||||
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(
|
||||
model_name="assignment",
|
||||
name="poll_description_default",
|
||||
|
@ -254,14 +254,14 @@ class AssignmentOptionManager(BaseManager):
|
||||
|
||||
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.
|
||||
"""
|
||||
return (
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("user", "poll")
|
||||
.prefetch_related("voted", "votes")
|
||||
.prefetch_related("votes")
|
||||
)
|
||||
|
||||
|
||||
@ -277,9 +277,6 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
||||
user = models.ForeignKey(
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
@ -301,7 +298,7 @@ class AssignmentPollManager(BaseManager):
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("assignment")
|
||||
.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)
|
||||
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)
|
||||
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)])
|
||||
""" For "votes" mode: The amount of votes a voter can give. """
|
||||
@ -358,26 +371,49 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
|
||||
@property
|
||||
def amount_global_no(self):
|
||||
if self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES or not self.global_no:
|
||||
def get_amount_global_abstain(self):
|
||||
if not self.global_abstain:
|
||||
return None
|
||||
no_sum = Decimal(0)
|
||||
for option in self.options.all():
|
||||
no_sum += option.no
|
||||
return no_sum
|
||||
|
||||
@property
|
||||
def amount_global_abstain(self):
|
||||
if (
|
||||
self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES
|
||||
or not self.global_abstain
|
||||
elif (
|
||||
self.type == self.TYPE_ANALOG
|
||||
or self.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||
):
|
||||
return self.db_amount_global_abstain
|
||||
else:
|
||||
return None
|
||||
abstain_sum = Decimal(0)
|
||||
for option in self.options.all():
|
||||
abstain_sum += option.abstain
|
||||
return abstain_sum
|
||||
|
||||
def set_amount_global_abstain(self, value):
|
||||
if (
|
||||
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):
|
||||
related_users = AssignmentRelatedUser.objects.filter(
|
||||
@ -404,3 +440,8 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
pass
|
||||
if not skip_autoupdate:
|
||||
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.db import transaction
|
||||
from django.db.models import F
|
||||
|
||||
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
|
||||
from openslides.utils.auth import has_perm
|
||||
@ -283,6 +284,10 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
)
|
||||
|
||||
super().perform_create(serializer)
|
||||
poll = AssignmentPoll.objects.get(pk=serializer.data["id"])
|
||||
poll.db_amount_global_abstain = Decimal(0)
|
||||
poll.db_amount_global_no = Decimal(0)
|
||||
poll.save()
|
||||
|
||||
def handle_analog_vote(self, data, poll, user):
|
||||
for field in ["votesvalid", "votesinvalid", "votescast"]:
|
||||
@ -291,9 +296,13 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
global_no_enabled = (
|
||||
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 = (
|
||||
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_data = data.get("options")
|
||||
@ -323,21 +332,8 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
)
|
||||
vote_obj.weight = vote["A"]
|
||||
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()
|
||||
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
@ -347,7 +343,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
{
|
||||
"options": {<option_id>: {"Y": <amount>, ["N": <amount>], ["A": <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
|
||||
required fields per pollmethod:
|
||||
@ -363,13 +359,11 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
- amounts must be integer numbers >= 0.
|
||||
- ids should be integers of valid option ids for this poll
|
||||
- amounts must be 0 or 1, if poll.allow_multiple_votes_per_candidate is False
|
||||
- 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:
|
||||
{<option_id>: 'Y' | 'N' [|'A']}
|
||||
- 'A' is only allowed in YNA pollmethod
|
||||
|
||||
Votes for all options have to be given
|
||||
"""
|
||||
if poll.type == AssignmentPoll.TYPE_ANALOG:
|
||||
if not isinstance(data, dict):
|
||||
@ -403,10 +397,14 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
poll.global_abstain
|
||||
and poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES
|
||||
)
|
||||
if ("global_no" in data and global_no_enabled) or (
|
||||
"global_abstain" in data and global_abstain_enabled
|
||||
):
|
||||
data["votescast"] = self.parse_vote_value(data, "votescast")
|
||||
if "amount_global_abstain" in data and global_abstain_enabled:
|
||||
data["amount_global_abstain"] = self.parse_vote_value(
|
||||
data, "amount_global_abstain"
|
||||
)
|
||||
if "amount_global_no" in data and global_no_enabled:
|
||||
data["amount_global_no"] = self.parse_vote_value(
|
||||
data, "amount_global_no"
|
||||
)
|
||||
|
||||
else:
|
||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||
@ -415,6 +413,10 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
for option_id, amount in data.items():
|
||||
if not is_int(option_id):
|
||||
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):
|
||||
raise ValidationError(
|
||||
{"detail": "Each amounts must be int"}
|
||||
@ -456,6 +458,10 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
for option_id, value in data.items():
|
||||
if not is_int(option_id):
|
||||
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 (
|
||||
poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA
|
||||
and value not in ("Y", "N", "A",)
|
||||
@ -471,33 +477,6 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
|
||||
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):
|
||||
"""
|
||||
Helper function for handle_(named|pseudoanonymous)_vote
|
||||
@ -508,9 +487,6 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
for option_id, amount in data.items():
|
||||
# Add user to the option's voted array
|
||||
option = options.get(pk=option_id)
|
||||
option.voted.add(user)
|
||||
inform_changed_data(option)
|
||||
|
||||
# skip creating votes with empty weights
|
||||
if amount == 0:
|
||||
continue
|
||||
@ -519,16 +495,15 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
)
|
||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||
else: # global_no or global_abstain
|
||||
option = options.order_by(
|
||||
"pk"
|
||||
).first() # order by is important to always get
|
||||
# the correct "first" option
|
||||
option.voted.add(user)
|
||||
inform_changed_data(option)
|
||||
vote = AssignmentVote.objects.create(
|
||||
option=option, user=user, weight=Decimal(poll.votes_amount), value=data,
|
||||
)
|
||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||
if data == "A":
|
||||
poll.amount_global_abstain = F("db_amount_global_abstain") + 1
|
||||
elif data == "N":
|
||||
poll.amount_global_no = F("db_amount_global_no") + 1
|
||||
else:
|
||||
raise RuntimeError("This should not happen")
|
||||
poll.save()
|
||||
|
||||
poll.voted.add(user)
|
||||
|
||||
def create_votes_type_named_pseudoanonymous(
|
||||
self, data, poll, check_user, vote_user
|
||||
@ -537,51 +512,37 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
options = poll.get_options()
|
||||
for option_id, result in data.items():
|
||||
option = options.get(pk=option_id)
|
||||
option.voted.add(check_user)
|
||||
inform_changed_data(option)
|
||||
vote = AssignmentVote.objects.create(
|
||||
option=option, user=vote_user, value=result
|
||||
)
|
||||
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):
|
||||
if user in poll.voted.all():
|
||||
raise ValidationError({"detail": "You have already voted"})
|
||||
|
||||
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)
|
||||
elif poll.pollmethod in (
|
||||
AssignmentPoll.POLLMETHOD_YN,
|
||||
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)
|
||||
|
||||
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||
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():
|
||||
if user in poll.voted.all():
|
||||
raise ValidationError({"detail": "You have already voted"})
|
||||
|
||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||
self.create_votes_type_votes(data, poll, user)
|
||||
|
||||
elif poll.pollmethod in (
|
||||
AssignmentPoll.POLLMETHOD_YN,
|
||||
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)
|
||||
|
||||
def convert_option_data(self, poll, data):
|
||||
|
@ -57,6 +57,11 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="motionpoll",
|
||||
name="voted",
|
||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="motionvote",
|
||||
name="user",
|
||||
@ -107,15 +112,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
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(
|
||||
model_name="motionvote",
|
||||
name="option",
|
||||
|
@ -889,14 +889,14 @@ class MotionOptionManager(BaseManager):
|
||||
|
||||
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.
|
||||
"""
|
||||
return (
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("poll")
|
||||
.prefetch_related("voted", "votes")
|
||||
.prefetch_related("votes")
|
||||
)
|
||||
|
||||
|
||||
@ -909,9 +909,6 @@ class MotionOption(RESTModelMixin, BaseOption):
|
||||
poll = models.ForeignKey(
|
||||
"MotionPoll", related_name="options", on_delete=CASCADE_AND_AUTOUPDATE
|
||||
)
|
||||
voted = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL, blank=True, related_name="motionoption_voted"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
@ -931,7 +928,7 @@ class MotionPollManager(BaseManager):
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.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")
|
||||
|
||||
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")
|
||||
|
||||
def handle_named_vote(self, data, poll, user):
|
||||
option = poll.options.get()
|
||||
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):
|
||||
option = poll.options.get()
|
||||
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.weight = Decimal("1")
|
||||
vote.save(no_delete_on_restriction=True)
|
||||
inform_changed_data(option)
|
||||
|
||||
option.voted.add(user)
|
||||
option.save()
|
||||
poll.voted.add(user)
|
||||
poll.save()
|
||||
|
||||
|
||||
class MotionOptionViewSet(BaseOptionViewSet):
|
||||
|
@ -6,40 +6,6 @@ from ..utils.access_permissions import BaseAccessPermissions
|
||||
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):
|
||||
manage_permission = "" # set by subclass
|
||||
|
||||
@ -71,10 +37,6 @@ class BaseOptionAccessPermissions(BaseAccessPermissions):
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> 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):
|
||||
data = full_data
|
||||
else:
|
||||
@ -87,6 +49,45 @@ class BaseOptionAccessPermissions(BaseAccessPermissions):
|
||||
del option["yes"]
|
||||
del option["no"]
|
||||
del option["abstain"]
|
||||
del option["voted_id"]
|
||||
data.append(option)
|
||||
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 typing import Iterable, Optional, Set, Tuple, Type
|
||||
from typing import Iterable, Optional, Tuple, Type
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator
|
||||
@ -35,8 +35,7 @@ class BaseVote(models.Model):
|
||||
|
||||
class BaseOption(models.Model):
|
||||
"""
|
||||
All subclasses must have poll attribute with the related name "options". Also
|
||||
they must have a "voted" relation to users.
|
||||
All subclasses must have poll attribute with the related name "options"
|
||||
"""
|
||||
|
||||
vote_class: Optional[Type["BaseVote"]] = None
|
||||
@ -86,8 +85,6 @@ class BaseOption(models.Model):
|
||||
vote.save()
|
||||
|
||||
def reset(self):
|
||||
self.voted.clear()
|
||||
|
||||
# Delete votes
|
||||
votes = self.get_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)
|
||||
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
||||
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
|
||||
|
||||
db_votesvalid = models.DecimalField(
|
||||
null=True,
|
||||
@ -186,7 +184,7 @@ class BasePoll(models.Model):
|
||||
if self.type == self.TYPE_ANALOG:
|
||||
return self.db_votesvalid
|
||||
else:
|
||||
return Decimal(self.amount_valid_votes())
|
||||
return Decimal(self.amount_users_voted())
|
||||
|
||||
def set_votesvalid(self, value):
|
||||
if self.type != self.TYPE_ANALOG:
|
||||
@ -199,7 +197,7 @@ class BasePoll(models.Model):
|
||||
if self.type == self.TYPE_ANALOG:
|
||||
return self.db_votesinvalid
|
||||
else:
|
||||
return Decimal(self.amount_invalid_votes())
|
||||
return Decimal(0)
|
||||
|
||||
def set_votesinvalid(self, value):
|
||||
if self.type != self.TYPE_ANALOG:
|
||||
@ -212,7 +210,7 @@ class BasePoll(models.Model):
|
||||
if self.type == self.TYPE_ANALOG:
|
||||
return self.db_votescast
|
||||
else:
|
||||
return Decimal(self.amount_voted_users())
|
||||
return Decimal(self.amount_users_voted())
|
||||
|
||||
def set_votescast(self, value):
|
||||
if self.type != self.TYPE_ANALOG:
|
||||
@ -221,32 +219,8 @@ class BasePoll(models.Model):
|
||||
|
||||
votescast = property(get_votescast, set_votescast)
|
||||
|
||||
def get_user_ids_with_valid_votes(self):
|
||||
if self.get_options().count():
|
||||
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 amount_users_voted(self):
|
||||
return len(self.voted.all())
|
||||
|
||||
def create_options(self):
|
||||
""" Should be called after creation of this model. """
|
||||
@ -284,6 +258,8 @@ class BasePoll(models.Model):
|
||||
for option in self.get_options():
|
||||
option.reset()
|
||||
|
||||
self.voted.clear()
|
||||
|
||||
# Reset state
|
||||
self.state = BasePoll.STATE_CREATED
|
||||
if self.type == self.TYPE_ANALOG:
|
||||
|
@ -22,7 +22,7 @@ class BaseVoteSerializer(ModelSerializer):
|
||||
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):
|
||||
@ -31,7 +31,6 @@ class BaseOptionSerializer(ModelSerializer):
|
||||
abstain = DecimalField(
|
||||
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||
)
|
||||
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||
|
||||
pollstate = SerializerMethodField()
|
||||
|
||||
@ -51,6 +50,7 @@ BASE_POLL_FIELDS = (
|
||||
"id",
|
||||
"onehundred_percent_base",
|
||||
"majority_method",
|
||||
"voted",
|
||||
)
|
||||
|
||||
|
||||
@ -60,6 +60,7 @@ class BasePollSerializer(ModelSerializer):
|
||||
many=True, required=False, queryset=get_group_model().objects.all()
|
||||
)
|
||||
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||
|
||||
votesvalid = DecimalField(
|
||||
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
||||
|
@ -175,12 +175,6 @@ class BasePollViewSet(ModelViewSet):
|
||||
@detail_route(methods=["POST"])
|
||||
def reset(self, request, pk):
|
||||
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()
|
||||
return Response()
|
||||
|
||||
|
@ -53,12 +53,11 @@ def test_assignment_option_db_queries():
|
||||
"""
|
||||
Tests that only the following db queries are done:
|
||||
* 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,
|
||||
= 3 queries
|
||||
= 2 queries
|
||||
"""
|
||||
create_assignment_polls()
|
||||
assert count_queries(AssignmentOption.get_elements)() == 3
|
||||
assert count_queries(AssignmentOption.get_elements)() == 2
|
||||
|
||||
|
||||
def create_assignment_polls():
|
||||
@ -93,13 +92,13 @@ def create_assignment_polls():
|
||||
username=f"test_username_{i}{j}",
|
||||
password="test_password_kbzj5L8ZtVxBllZzoW6D",
|
||||
)
|
||||
poll.voted.add(user)
|
||||
for option in poll.options.all():
|
||||
weight = random.randint(0, 10)
|
||||
if weight > 0:
|
||||
AssignmentVote.objects.create(
|
||||
user=user, option=option, value="Y", weight=Decimal(weight)
|
||||
)
|
||||
option.voted.add(user)
|
||||
|
||||
|
||||
class CreateAssignmentPoll(TestCase):
|
||||
@ -110,7 +109,7 @@ class CreateAssignmentPoll(TestCase):
|
||||
self.assignment.add_candidate(self.admin)
|
||||
|
||||
def test_simple(self):
|
||||
with self.assertNumQueries(50):
|
||||
with self.assertNumQueries(40):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-list"),
|
||||
{
|
||||
@ -1008,10 +1007,10 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
{"1": "N"},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||
vote = AssignmentVote.objects.get()
|
||||
self.assertEqual(vote.value, "N")
|
||||
self.assertEqual(vote.value, "Y")
|
||||
|
||||
def test_too_many_options(self):
|
||||
self.start_poll()
|
||||
@ -1197,14 +1196,14 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
{"1": 0, "2": 1},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
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.yes, Decimal("1"))
|
||||
self.assertEqual(option1.no, 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.abstain, Decimal("0"))
|
||||
|
||||
@ -1219,9 +1218,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
poll = AssignmentPoll.objects.get()
|
||||
option = poll.options.get(pk=1)
|
||||
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(poll.amount_global_no, Decimal("2"))
|
||||
self.assertEqual(poll.amount_global_no, Decimal("1"))
|
||||
self.assertEqual(poll.amount_global_abstain, Decimal("0"))
|
||||
|
||||
def test_global_no_forbidden(self):
|
||||
@ -1247,9 +1246,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
option = poll.options.get(pk=1)
|
||||
self.assertEqual(option.yes, 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_abstain, Decimal("2"))
|
||||
self.assertEqual(poll.amount_global_abstain, Decimal("1"))
|
||||
|
||||
def test_global_abstain_forbidden(self):
|
||||
self.poll.global_abstain = False
|
||||
@ -1302,17 +1301,6 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
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):
|
||||
self.setup_for_multiple_votes()
|
||||
self.start_poll()
|
||||
@ -1370,7 +1358,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
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())
|
||||
|
||||
def test_wrong_data_format(self):
|
||||
@ -1557,9 +1545,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(
|
||||
response, status.HTTP_200_OK
|
||||
) # new "feature" because of partial requests: empty requests work!
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_wrong_data_format(self):
|
||||
@ -1718,17 +1704,6 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
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):
|
||||
self.setup_for_multiple_votes()
|
||||
self.start_poll()
|
||||
@ -1786,7 +1761,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
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())
|
||||
|
||||
def test_wrong_data_format(self):
|
||||
@ -1909,6 +1884,8 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-option:1": {
|
||||
"abstain": "1.000000",
|
||||
@ -1919,8 +1896,6 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
"id": 1,
|
||||
@ -1971,6 +1946,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"votes_amount": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
)
|
||||
|
||||
@ -1984,7 +1960,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
vote.value = "A"
|
||||
vote.weight = Decimal("1")
|
||||
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.save(skip_autoupdate=True)
|
||||
response = self.client.post(
|
||||
@ -2026,6 +2002,8 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
||||
@ -2044,8 +2022,6 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -2083,6 +2059,8 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"title": self.poll.title,
|
||||
"description": self.description,
|
||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
||||
"votes_amount": 1,
|
||||
@ -2099,8 +2077,6 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
"id": 1,
|
||||
@ -2137,6 +2113,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"votes_amount": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
)
|
||||
|
||||
@ -2149,7 +2126,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
vote.value = "A"
|
||||
vote.weight = Decimal("1")
|
||||
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.save(skip_autoupdate=True)
|
||||
response = self.client.post(
|
||||
@ -2191,6 +2168,8 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
||||
@ -2209,8 +2188,6 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"yes": "0.000000",
|
||||
"user_id": 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:
|
||||
* 1 request to get the options,
|
||||
* 1 request to get all votes for all options,
|
||||
* 1 request to get all users that voted on the options
|
||||
= 5 queries
|
||||
= 2 queries
|
||||
"""
|
||||
create_motion_polls()
|
||||
assert count_queries(MotionOption.get_elements)() == 3
|
||||
assert count_queries(MotionOption.get_elements)() == 2
|
||||
|
||||
|
||||
def create_motion_polls():
|
||||
@ -83,7 +82,7 @@ def create_motion_polls():
|
||||
value=("Y" if k == 0 else "N"),
|
||||
weight=Decimal(1),
|
||||
)
|
||||
option.voted.add(user)
|
||||
poll.voted.add(user)
|
||||
|
||||
|
||||
class CreateMotionPoll(TestCase):
|
||||
@ -166,6 +165,8 @@ class CreateMotionPoll(TestCase):
|
||||
"votescast": "0.000000",
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"voted_id": [],
|
||||
"user_has_voted": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(autoupdate[1], [])
|
||||
@ -910,6 +911,8 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"votescast": "1.000000",
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
"pollstate": 2,
|
||||
@ -926,8 +929,6 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"poll_id": 1,
|
||||
"pollstate": 2,
|
||||
"yes": "0.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -948,7 +949,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
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], [])
|
||||
|
||||
@ -969,6 +970,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
@ -977,8 +979,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"id": 1,
|
||||
"poll_id": 1,
|
||||
"pollstate": 2,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
}, # noqa black and flake are no friends :(
|
||||
)
|
||||
|
||||
# Other users should not get a vote autoupdate
|
||||
@ -1049,6 +1050,8 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"votescast": "1.000000",
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
"pollstate": 2,
|
||||
@ -1065,8 +1068,6 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"poll_id": 1,
|
||||
"pollstate": 2,
|
||||
"yes": "0.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -1090,6 +1091,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"options_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.votescast, Decimal("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()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
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()
|
||||
self.assertEqual(vote.user, None)
|
||||
|
||||
@ -1311,6 +1313,8 @@ class PublishMotionPoll(TestCase):
|
||||
"votescast": "0.000000",
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [],
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
"pollstate": 4,
|
||||
@ -1327,8 +1331,6 @@ class PublishMotionPoll(TestCase):
|
||||
"poll_id": 1,
|
||||
"pollstate": 4,
|
||||
"yes": "0.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -1362,12 +1364,12 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
self.vote1 = MotionVote.objects.create(
|
||||
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.vote2 = MotionVote.objects.create(
|
||||
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):
|
||||
response = self.client.post(
|
||||
@ -1376,7 +1378,7 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.get_votes().count(), 2)
|
||||
self.assertEqual(poll.amount_valid_votes(), 2)
|
||||
self.assertEqual(poll.amount_users_voted(), 2)
|
||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
@ -1384,8 +1386,8 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
self.assertEqual(option.yes, Decimal("1"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
self.assertTrue(self.user1 in option.voted.all())
|
||||
self.assertTrue(self.user2 in option.voted.all())
|
||||
self.assertTrue(self.user1 in poll.voted.all())
|
||||
self.assertTrue(self.user2 in poll.voted.all())
|
||||
for vote in poll.get_votes().all():
|
||||
self.assertTrue(vote.user is None)
|
||||
|
||||
@ -1432,19 +1434,19 @@ class ResetMotionPoll(TestCase):
|
||||
self.vote1 = MotionVote.objects.create(
|
||||
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.vote2 = MotionVote.objects.create(
|
||||
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):
|
||||
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
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.votesinvalid, None)
|
||||
self.assertEqual(poll.votescast, None)
|
||||
@ -1463,12 +1465,3 @@ class ResetMotionPoll(TestCase):
|
||||
for user in (self.admin, self.user1, self.user2):
|
||||
self.assertDeletedAutoupdate(self.vote1, 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