WIP: Partial requests
This commit is contained in:
parent
d4599a435b
commit
0b37c5a857
@ -22,6 +22,12 @@ const AssignmentOptionRelations: RelationDefinition[] = [
|
|||||||
ownKey: 'votes',
|
ownKey: 'votes',
|
||||||
foreignViewModel: ViewAssignmentVote
|
foreignViewModel: ViewAssignmentVote
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'voted_id',
|
||||||
|
ownKey: 'voted',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'M2O',
|
type: 'M2O',
|
||||||
ownIdKey: 'poll_id',
|
ownIdKey: 'poll_id',
|
||||||
|
@ -14,7 +14,6 @@ import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignmen
|
|||||||
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
@ -25,12 +24,6 @@ const AssignmentPollRelations: RelationDefinition[] = [
|
|||||||
ownKey: 'groups',
|
ownKey: 'groups',
|
||||||
foreignViewModel: ViewGroup
|
foreignViewModel: ViewGroup
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'M2M',
|
|
||||||
ownIdKey: 'voted_id',
|
|
||||||
ownKey: 'voted',
|
|
||||||
foreignViewModel: ViewUser
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'O2M',
|
type: 'O2M',
|
||||||
ownIdKey: 'options_id',
|
ownIdKey: 'options_id',
|
||||||
|
@ -10,6 +10,7 @@ import { MotionOption } from 'app/shared/models/motions/motion-option';
|
|||||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { BaseRepository } from '../base-repository';
|
import { BaseRepository } from '../base-repository';
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
@ -21,6 +22,12 @@ const MotionOptionRelations: RelationDefinition[] = [
|
|||||||
ownKey: 'votes',
|
ownKey: 'votes',
|
||||||
foreignViewModel: ViewMotionVote
|
foreignViewModel: ViewMotionVote
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'voted_id',
|
||||||
|
ownKey: 'voted',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'M2O',
|
type: 'M2O',
|
||||||
ownIdKey: 'poll_id',
|
ownIdKey: 'poll_id',
|
||||||
|
@ -14,7 +14,6 @@ import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
|||||||
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
@ -25,12 +24,6 @@ const MotionPollRelations: RelationDefinition[] = [
|
|||||||
ownKey: 'groups',
|
ownKey: 'groups',
|
||||||
foreignViewModel: ViewGroup
|
foreignViewModel: ViewGroup
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'M2M',
|
|
||||||
ownIdKey: 'voted_id',
|
|
||||||
ownKey: 'voted',
|
|
||||||
foreignViewModel: ViewUser
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'O2M',
|
type: 'O2M',
|
||||||
ownIdKey: 'options_id',
|
ownIdKey: 'options_id',
|
||||||
|
@ -31,7 +31,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);
|
const pollsToVote = polls.filter(poll => this.votingService.canVote(poll) && !poll.user_has_voted_valid);
|
||||||
if ((this.OSStatus.isInHistoryMode && this.currentBanner) || !pollsToVote.length) {
|
if ((this.OSStatus.isInHistoryMode && this.currentBanner) || !pollsToVote.length) {
|
||||||
this.sliceBanner();
|
this.sliceBanner();
|
||||||
return;
|
return;
|
||||||
|
@ -10,7 +10,7 @@ export enum VotingError {
|
|||||||
USER_HAS_NO_PERMISSION,
|
USER_HAS_NO_PERMISSION,
|
||||||
USER_IS_ANONYMOUS,
|
USER_IS_ANONYMOUS,
|
||||||
USER_NOT_PRESENT,
|
USER_NOT_PRESENT,
|
||||||
USER_HAS_VOTED
|
USER_HAS_VOTED_VALID
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,8 +60,8 @@ 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) {
|
if (poll.type === PollType.Pseudoanonymous && poll.user_has_voted_valid) {
|
||||||
return VotingError.USER_HAS_VOTED;
|
return VotingError.USER_HAS_VOTED_VALID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ 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[];
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
protected getDecimalFields(): (keyof BaseOption<T>)[] {
|
||||||
return ['yes', 'no', 'abstain'];
|
return ['yes', 'no', 'abstain'];
|
||||||
|
@ -46,10 +46,8 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
|||||||
public votesinvalid: number;
|
public votesinvalid: number;
|
||||||
public votescast: number;
|
public votescast: number;
|
||||||
public groups_id: number[];
|
public groups_id: number[];
|
||||||
public voted_id: number[];
|
|
||||||
public majority_method: MajorityMethod;
|
public majority_method: MajorityMethod;
|
||||||
public onehundred_percent_base: PercentBase;
|
public onehundred_percent_base: PercentBase;
|
||||||
public user_has_voted: boolean;
|
|
||||||
|
|
||||||
public get isCreated(): boolean {
|
public get isCreated(): boolean {
|
||||||
return this.state === PollState.Created;
|
return this.state === PollState.Created;
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||||
|
|
||||||
|
<!-- TODO: Someone should make this pretty -->
|
||||||
|
<span *ngIf="poll.user_has_voted_valid">Your vote is valid!</span>
|
||||||
|
<span *ngIf="poll.user_has_voted_invalid">DANGER: Your vote is invalid!</span>
|
||||||
|
<span *ngIf="poll.user_has_not_voted">You have not give any voting here!</span>
|
||||||
|
|
||||||
<!-- Leftover votes -->
|
<!-- Leftover votes -->
|
||||||
<h4 *ngIf="poll.pollmethod === pollMethods.Votes">
|
<h4 *ngIf="poll.pollmethod === pollMethods.Votes">
|
||||||
{{ 'Votes for this poll' | translate }}: {{ poll.votes_amount }}
|
{{ 'Votes for this poll' | translate }}: {{ poll.votes_amount }}
|
||||||
@ -13,6 +19,9 @@
|
|||||||
|
|
||||||
<!-- Options and Actions -->
|
<!-- Options and Actions -->
|
||||||
<div *ngFor="let option of poll.options; let i = index">
|
<div *ngFor="let option of poll.options; let i = index">
|
||||||
|
<div *ngIf="option.user_has_voted">
|
||||||
|
TODO: DO not show buttons, becuase, the user has already voted for this one
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'yna-grid': poll.pollmethod === pollMethods.YNA,
|
'yna-grid': poll.pollmethod === pollMethods.YNA,
|
||||||
|
@ -112,21 +112,28 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
}
|
}
|
||||||
|
|
||||||
public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void {
|
public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void {
|
||||||
const pollOptionIds = this.getPollOptionIds();
|
let requestData;
|
||||||
const requestMap = pollOptionIds.reduce((o, n) => {
|
if (this.poll.pollmethod === AssignmentPollMethods.Votes) {
|
||||||
if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) {
|
const pollOptionIds = this.getPollOptionIds();
|
||||||
o[n] = 1;
|
requestData = pollOptionIds.reduce((o, n) => {
|
||||||
} else {
|
if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) {
|
||||||
o[n] = 0;
|
o[n] = 1; // TODO: allow multiple votes per candidate
|
||||||
}
|
} else {
|
||||||
|
o[n] = 0;
|
||||||
return o;
|
}
|
||||||
}, {});
|
return o;
|
||||||
|
}, {});
|
||||||
this.pollRepo.vote(JSON.stringify(requestMap), this.poll.id).catch(this.raiseError);
|
} else {
|
||||||
|
// YN/YNA
|
||||||
|
requestData = {};
|
||||||
|
requestData[optionId] = vote;
|
||||||
|
}
|
||||||
|
this.pollRepo.vote(requestData, this.poll.id).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveGlobalVote(globalVote: 'N' | 'A'): void {
|
public saveGlobalVote(globalVote: 'N' | 'A'): void {
|
||||||
|
// This may be a bug in angulars HTTP client: A string is not quoted to be valid json.
|
||||||
|
// Maybe they expect a string to be alrady a jsonified object.
|
||||||
this.pollRepo.vote(`"${globalVote}"`, this.poll.id).catch(this.raiseError);
|
this.pollRepo.vote(`"${globalVote}"`, this.poll.id).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,8 @@
|
|||||||
<span>{{ poll.stateVerbose }}</span>
|
<span>{{ poll.stateVerbose }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div *pblNgridCellDef="'votability'; row as poll;" class="cell-slot fill">
|
<div *pblNgridCellDef="'votability'; row as poll;" class="cell-slot fill">
|
||||||
<mat-icon *ngIf="poll.user_has_voted" color="accent" matTooltip="{{ 'You have already voted on this poll. Good job!' | translate }}">check_circle</mat-icon>
|
<mat-icon *ngIf="poll.user_has_voted_valid" color="accent" matTooltip="{{ 'You have already voted on this poll. Good job!' | translate }}">check_circle</mat-icon>
|
||||||
<mat-icon *ngIf="!poll.user_has_voted && poll.canBeVotedFor" color="warn" matTooltip="{{ 'You still have to vote on this poll.' | translate }}">warning</mat-icon>
|
<mat-icon *ngIf="!poll.user_has_voted_valid && poll.canBeVotedFor" color="warn" matTooltip="{{ 'You still have to vote on this poll.' | translate }}">warning</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</os-list-view-table>
|
</os-list-view-table>
|
||||||
|
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
<span> {{ 'Cast votes' }}: {{ poll.voted_id.length }} / {{ max }} </span>
|
<span>
|
||||||
|
{{ 'Casted votes' | translate }}: {{ poll.votescast }} / {{ max }},
|
||||||
|
{{ 'valid votes' | translate}}: {{ poll.votesvalid }} / {{ max }},
|
||||||
|
{{ 'invalid votes' | translate}}: {{ poll.votesinvalid }} / {{ max }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
||||||
|
@ -30,7 +30,7 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get valueInPercent(): number {
|
public get valueInPercent(): number {
|
||||||
return (this.poll.voted_id.length / this.max) * 100;
|
return (this.poll.votesvalid / this.max) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,6 +138,18 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
|||||||
return states;
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
@ -87,11 +87,6 @@ class Migration(migrations.Migration):
|
|||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
|
||||||
model_name="assignmentpoll",
|
|
||||||
name="voted",
|
|
||||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="assignmentpoll",
|
model_name="assignmentpoll",
|
||||||
name="allow_multiple_votes_per_candidate",
|
name="allow_multiple_votes_per_candidate",
|
||||||
@ -134,11 +129,38 @@ 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",
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.CharField(blank=True, max_length=255),
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentoption",
|
||||||
|
name="poll",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
|
related_name="options",
|
||||||
|
to="assignments.AssignmentPoll",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentvote",
|
||||||
|
name="option",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
|
related_name="votes",
|
||||||
|
to="assignments.AssignmentOption",
|
||||||
|
),
|
||||||
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="assignment",
|
model_name="assignment",
|
||||||
old_name="poll_description_default",
|
old_name="poll_description_default",
|
||||||
@ -166,6 +188,15 @@ class Migration(migrations.Migration):
|
|||||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="assignment",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
|
related_name="polls",
|
||||||
|
to="assignments.Assignment",
|
||||||
|
),
|
||||||
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="assignmentpoll", old_name="votescast", new_name="db_votescast"
|
model_name="assignmentpoll", old_name="votescast", new_name="db_votescast"
|
||||||
),
|
),
|
||||||
|
@ -267,7 +267,7 @@ class AssignmentVote(RESTModelMixin, BaseVote):
|
|||||||
objects = AssignmentVoteManager()
|
objects = AssignmentVoteManager()
|
||||||
|
|
||||||
option = models.ForeignKey(
|
option = models.ForeignKey(
|
||||||
"AssignmentOption", on_delete=models.CASCADE, related_name="votes"
|
"AssignmentOption", on_delete=CASCADE_AND_AUTOUPDATE, related_name="votes"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -279,11 +279,14 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
|||||||
vote_class = AssignmentVote
|
vote_class = AssignmentVote
|
||||||
|
|
||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey(
|
||||||
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
"AssignmentPoll", on_delete=CASCADE_AND_AUTOUPDATE, related_name="options"
|
||||||
)
|
)
|
||||||
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:
|
||||||
@ -304,9 +307,7 @@ class AssignmentPollManager(BaseManager):
|
|||||||
super()
|
super()
|
||||||
.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", "groups")
|
||||||
"options", "options__user", "options__votes", "groups", "voted"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -317,7 +318,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
option_class = AssignmentOption
|
option_class = AssignmentOption
|
||||||
|
|
||||||
assignment = models.ForeignKey(
|
assignment = models.ForeignKey(
|
||||||
Assignment, on_delete=models.CASCADE, related_name="polls"
|
Assignment, on_delete=CASCADE_AND_AUTOUPDATE, related_name="polls"
|
||||||
)
|
)
|
||||||
|
|
||||||
description = models.CharField(max_length=255, blank=True)
|
description = models.CharField(max_length=255, blank=True)
|
||||||
|
@ -403,11 +403,10 @@ 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 poll.votes_amount votes
|
- The sum of all amounts must be grater then 0 and <= poll.votes_amount
|
||||||
|
|
||||||
YN/YNA:
|
YN/YNA:
|
||||||
{<option_id>: 'Y' | 'N' [|'A']}
|
{<option_id>: 'Y' | 'N' [|'A']}
|
||||||
- all option_ids must be given TODO: No it must not be that way. Single Votes have to be accepted
|
|
||||||
- 'A' is only allowed in YNA pollmethod
|
- 'A' is only allowed in YNA pollmethod
|
||||||
|
|
||||||
Votes for all options have to be given
|
Votes for all options have to be given
|
||||||
@ -474,6 +473,11 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
)
|
)
|
||||||
amount_sum += amount
|
amount_sum += amount
|
||||||
|
|
||||||
|
if amount_sum <= 0:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You must give at least one vote"}
|
||||||
|
)
|
||||||
|
|
||||||
if amount_sum > poll.votes_amount:
|
if amount_sum > poll.votes_amount:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
@ -501,68 +505,118 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA
|
poll.pollmethod == AssignmentPoll.POLLMETHOD_YNA
|
||||||
and value not in ("Y", "N", "A",)
|
and value not in ("Y", "N", "A",)
|
||||||
):
|
):
|
||||||
raise ValidationError("Every value must be Y, N or A")
|
raise ValidationError(
|
||||||
|
{"detail": "Every value must be Y, N or A"}
|
||||||
|
)
|
||||||
elif (
|
elif (
|
||||||
poll.pollmethod == AssignmentPoll.POLLMETHOD_YN
|
poll.pollmethod == AssignmentPoll.POLLMETHOD_YN
|
||||||
and value not in ("Y", "N",)
|
and value not in ("Y", "N",)
|
||||||
):
|
):
|
||||||
raise ValidationError("Every value must be Y or N")
|
raise ValidationError({"detail": "Every value must be Y or N"})
|
||||||
|
|
||||||
options_data = data
|
options_data = data
|
||||||
|
|
||||||
# Check if all options were given
|
# Just for named/pseudoanonymous with YN/YNA skip the all-options-given check
|
||||||
db_option_ids = set(option.id for option in poll.get_options())
|
if poll.type not in (
|
||||||
data_option_ids = set(int(option_id) for option_id in options_data.keys())
|
AssignmentPoll.TYPE_NAMED,
|
||||||
if data_option_ids != db_option_ids:
|
AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
||||||
raise ValidationError(
|
) or poll.pollmethod not in (
|
||||||
{"error": "You have to provide values for all options"}
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
)
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
|
):
|
||||||
|
# Check if all options were given
|
||||||
|
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())
|
||||||
|
if data_option_ids != db_option_ids:
|
||||||
|
raise ValidationError(
|
||||||
|
{"error": "You have to provide values for all options"}
|
||||||
|
)
|
||||||
|
|
||||||
def create_votes(self, data, poll, user=None):
|
def create_votes_type_votes(self, data, poll, user):
|
||||||
"""
|
"""
|
||||||
Helper function for handle_(named|pseudoanonymous)_vote
|
Helper function for handle_(named|pseudoanonymous)_vote
|
||||||
Assumes data is already validated
|
Assumes data is already validated
|
||||||
"""
|
"""
|
||||||
options = poll.get_options()
|
options = poll.get_options()
|
||||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
if isinstance(data, dict):
|
||||||
if isinstance(data, dict):
|
for option_id, amount in data.items():
|
||||||
for option_id, amount in data.items():
|
# Add user to the option's voted array
|
||||||
# skip empty votes
|
option = options.get(pk=option_id)
|
||||||
if amount == 0:
|
option.voted.add(user)
|
||||||
continue
|
inform_changed_data(option)
|
||||||
option = options.get(pk=option_id)
|
|
||||||
vote = AssignmentVote.objects.create(
|
# skip creating votes with empty weights
|
||||||
option=option, user=user, weight=Decimal(amount), value="Y"
|
if amount == 0:
|
||||||
)
|
continue
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
|
||||||
else: # global_no or global_abstain
|
|
||||||
option = options.first()
|
|
||||||
vote = AssignmentVote.objects.create(
|
vote = AssignmentVote.objects.create(
|
||||||
option=option,
|
option=option, user=user, weight=Decimal(amount), value="Y"
|
||||||
user=user,
|
|
||||||
weight=Decimal(poll.votes_amount),
|
|
||||||
value=data,
|
|
||||||
)
|
)
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
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)
|
||||||
|
|
||||||
|
def create_votes_type_named_pseudoanonymous(
|
||||||
|
self, data, poll, check_user, vote_user
|
||||||
|
):
|
||||||
|
""" check_user is used for the voted-array, vote_user is the one put into the vote """
|
||||||
|
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)
|
||||||
|
|
||||||
|
def handle_named_vote(self, data, poll, user):
|
||||||
|
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 (
|
elif poll.pollmethod in (
|
||||||
AssignmentPoll.POLLMETHOD_YN,
|
AssignmentPoll.POLLMETHOD_YN,
|
||||||
AssignmentPoll.POLLMETHOD_YNA,
|
AssignmentPoll.POLLMETHOD_YNA,
|
||||||
):
|
):
|
||||||
for option_id, result in data.items():
|
# 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():
|
||||||
|
raise ValidationError({"detail": "You have already voted"})
|
||||||
|
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)
|
option = options.get(pk=option_id)
|
||||||
vote = AssignmentVote.objects.create(
|
if user in option.voted.all():
|
||||||
option=option, user=user, value=result
|
raise ValidationError(
|
||||||
)
|
{"detail": f"You have already voted for option {option.pk}"}
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
)
|
||||||
|
self.create_votes_type_named_pseudoanonymous(data, poll, user, None)
|
||||||
def handle_named_vote(self, data, poll, user):
|
|
||||||
# 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(data, poll, user)
|
|
||||||
|
|
||||||
def handle_pseudoanonymous_vote(self, data, poll):
|
|
||||||
self.create_votes(data, poll)
|
|
||||||
|
|
||||||
def convert_option_data(self, poll, data):
|
def convert_option_data(self, poll, data):
|
||||||
poll_options = poll.get_options()
|
poll_options = poll.get_options()
|
||||||
|
@ -76,11 +76,6 @@ 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="motionpoll",
|
model_name="motionpoll",
|
||||||
name="majority_method",
|
name="majority_method",
|
||||||
@ -112,11 +107,20 @@ 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",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
related_name="votes",
|
related_name="votes",
|
||||||
to="motions.MotionOption",
|
to="motions.MotionOption",
|
||||||
),
|
),
|
||||||
@ -142,11 +146,20 @@ class Migration(migrations.Migration):
|
|||||||
model_name="motionoption",
|
model_name="motionoption",
|
||||||
name="poll",
|
name="poll",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
related_name="options",
|
related_name="options",
|
||||||
to="motions.MotionPoll",
|
to="motions.MotionPoll",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="motion",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
|
related_name="polls",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="motionpoll", old_name="votescast", new_name="db_votescast"
|
model_name="motionpoll", old_name="votescast", new_name="db_votescast"
|
||||||
),
|
),
|
||||||
|
@ -873,7 +873,7 @@ class MotionVoteManager(BaseManager):
|
|||||||
class MotionVote(RESTModelMixin, BaseVote):
|
class MotionVote(RESTModelMixin, BaseVote):
|
||||||
access_permissions = MotionVoteAccessPermissions()
|
access_permissions = MotionVoteAccessPermissions()
|
||||||
option = models.ForeignKey(
|
option = models.ForeignKey(
|
||||||
"MotionOption", on_delete=models.CASCADE, related_name="votes"
|
"MotionOption", on_delete=CASCADE_AND_AUTOUPDATE, related_name="votes"
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = MotionVoteManager()
|
objects = MotionVoteManager()
|
||||||
@ -887,7 +887,10 @@ class MotionOption(RESTModelMixin, BaseOption):
|
|||||||
vote_class = MotionVote
|
vote_class = MotionVote
|
||||||
|
|
||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey(
|
||||||
"MotionPoll", related_name="options", on_delete=models.CASCADE
|
"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:
|
||||||
@ -908,7 +911,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", "groups", "voted")
|
.prefetch_related("options", "options__votes", "groups")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -918,7 +921,9 @@ class MotionPoll(RESTModelMixin, BasePoll):
|
|||||||
|
|
||||||
objects = MotionPollManager()
|
objects = MotionPollManager()
|
||||||
|
|
||||||
motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls")
|
motion = models.ForeignKey(
|
||||||
|
Motion, on_delete=CASCADE_AND_AUTOUPDATE, related_name="polls"
|
||||||
|
)
|
||||||
|
|
||||||
POLLMETHOD_YN = "YN"
|
POLLMETHOD_YN = "YN"
|
||||||
POLLMETHOD_YNA = "YNA"
|
POLLMETHOD_YNA = "YNA"
|
||||||
|
@ -20,13 +20,8 @@ class BasePollAccessPermissions(BaseAccessPermissions):
|
|||||||
Non-published polls will be restricted:
|
Non-published polls will be restricted:
|
||||||
- Remove votes* values from the poll
|
- Remove votes* values from the poll
|
||||||
- Remove yes/no/abstain fields from options
|
- Remove yes/no/abstain fields from options
|
||||||
- Remove voted_id field from the poll
|
|
||||||
- Remove fields given in self.assitional_fields from the poll
|
- Remove fields given in self.assitional_fields from the poll
|
||||||
"""
|
"""
|
||||||
# add hast_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):
|
if await async_has_perm(user_id, self.manage_permission):
|
||||||
data = full_data
|
data = full_data
|
||||||
else:
|
else:
|
||||||
@ -39,7 +34,6 @@ class BasePollAccessPermissions(BaseAccessPermissions):
|
|||||||
del poll["votesvalid"]
|
del poll["votesvalid"]
|
||||||
del poll["votesinvalid"]
|
del poll["votesinvalid"]
|
||||||
del poll["votescast"]
|
del poll["votescast"]
|
||||||
del poll["voted_id"]
|
|
||||||
for field in self.additional_fields:
|
for field in self.additional_fields:
|
||||||
del poll[field]
|
del poll[field]
|
||||||
data.append(poll)
|
data.append(poll)
|
||||||
@ -77,6 +71,10 @@ 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:
|
||||||
@ -89,5 +87,6 @@ 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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Iterable, Optional, Tuple, Type
|
from typing import Iterable, Optional, Set, 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,7 +35,8 @@ class BaseVote(models.Model):
|
|||||||
|
|
||||||
class BaseOption(models.Model):
|
class BaseOption(models.Model):
|
||||||
"""
|
"""
|
||||||
All subclasses must have poll attribute with the related name "options"
|
All subclasses must have poll attribute with the related name "options". Also
|
||||||
|
they must have a "voted" relation to users.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vote_class: Optional[Type["BaseVote"]] = None
|
vote_class: Optional[Type["BaseVote"]] = None
|
||||||
@ -73,6 +74,30 @@ class BaseOption(models.Model):
|
|||||||
)
|
)
|
||||||
return cls.vote_class
|
return cls.vote_class
|
||||||
|
|
||||||
|
def get_votes(self):
|
||||||
|
"""
|
||||||
|
Return a QuerySet with all vote objects related to this option.
|
||||||
|
"""
|
||||||
|
return self.get_vote_class().objects.filter(option=self)
|
||||||
|
|
||||||
|
def pseudoanonymize(self):
|
||||||
|
for vote in self.get_votes():
|
||||||
|
vote.user = None
|
||||||
|
vote.save()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.voted.clear()
|
||||||
|
|
||||||
|
# Delete votes
|
||||||
|
votes = self.get_votes()
|
||||||
|
votes_id = [vote.id for vote in votes]
|
||||||
|
votes.delete()
|
||||||
|
collection = self.get_vote_class().get_collection_string()
|
||||||
|
inform_deleted_data((collection, id) for id in votes_id)
|
||||||
|
|
||||||
|
# update self because the changed voted relation
|
||||||
|
inform_changed_data(self)
|
||||||
|
|
||||||
|
|
||||||
class BasePoll(models.Model):
|
class BasePoll(models.Model):
|
||||||
option_class: Optional[Type["BaseOption"]] = None
|
option_class: Optional[Type["BaseOption"]] = None
|
||||||
@ -101,7 +126,6 @@ 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,
|
||||||
@ -162,7 +186,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.count_users_voted())
|
return Decimal(self.amount_valid_votes())
|
||||||
|
|
||||||
def set_votesvalid(self, value):
|
def set_votesvalid(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -175,7 +199,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(0)
|
return Decimal(self.amount_invalid_votes())
|
||||||
|
|
||||||
def set_votesinvalid(self, value):
|
def set_votesinvalid(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -188,7 +212,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.count_users_voted())
|
return Decimal(self.amount_voted_users())
|
||||||
|
|
||||||
def set_votescast(self, value):
|
def set_votescast(self, value):
|
||||||
if self.type != self.TYPE_ANALOG:
|
if self.type != self.TYPE_ANALOG:
|
||||||
@ -197,14 +221,30 @@ class BasePoll(models.Model):
|
|||||||
|
|
||||||
votescast = property(get_votescast, set_votescast)
|
votescast = property(get_votescast, set_votescast)
|
||||||
|
|
||||||
def count_users_voted(self):
|
def get_user_ids_with_valid_votes(self):
|
||||||
return self.voted.all().count()
|
initial_option = self.get_options().first()
|
||||||
|
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)
|
||||||
|
|
||||||
def get_options(self):
|
def get_all_voted_user_ids(self):
|
||||||
"""
|
# TODO: This might be faster with only one DB query using distinct.
|
||||||
Returns the option objects for the poll.
|
user_ids: Set[int] = set()
|
||||||
"""
|
for option in self.get_options():
|
||||||
return self.get_option_class().objects.filter(poll=self)
|
user_ids.update(option.voted.all().values_list("pk", flat=True))
|
||||||
|
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. """
|
||||||
@ -218,6 +258,12 @@ class BasePoll(models.Model):
|
|||||||
)
|
)
|
||||||
return cls.option_class
|
return cls.option_class
|
||||||
|
|
||||||
|
def get_options(self):
|
||||||
|
"""
|
||||||
|
Returns the option objects for the poll.
|
||||||
|
"""
|
||||||
|
return self.get_option_class().objects.filter(poll=self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_vote_class(cls):
|
def get_vote_class(cls):
|
||||||
return cls.get_option_class().get_vote_class()
|
return cls.get_option_class().get_vote_class()
|
||||||
@ -229,22 +275,12 @@ class BasePoll(models.Model):
|
|||||||
return self.get_vote_class().objects.filter(option__poll__id=self.id)
|
return self.get_vote_class().objects.filter(option__poll__id=self.id)
|
||||||
|
|
||||||
def pseudoanonymize(self):
|
def pseudoanonymize(self):
|
||||||
for vote in self.get_votes():
|
for option in self.get_options():
|
||||||
vote.user = None
|
option.pseudoanonymize()
|
||||||
vote.save()
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.voted.clear()
|
for option in self.get_options():
|
||||||
|
option.reset()
|
||||||
# Delete votes
|
|
||||||
votes = self.get_votes()
|
|
||||||
votes_id = [vote.id for vote in votes]
|
|
||||||
votes.delete()
|
|
||||||
collection = self.get_vote_class().get_collection_string()
|
|
||||||
inform_deleted_data((collection, id) for id in votes_id)
|
|
||||||
|
|
||||||
# update options
|
|
||||||
inform_changed_data(self.get_options())
|
|
||||||
|
|
||||||
# Reset state
|
# Reset state
|
||||||
self.state = BasePoll.STATE_CREATED
|
self.state = BasePoll.STATE_CREATED
|
||||||
|
@ -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")
|
BASE_OPTION_FIELDS = ("id", "yes", "no", "abstain", "poll_id", "pollstate", "voted")
|
||||||
|
|
||||||
|
|
||||||
class BaseOptionSerializer(ModelSerializer):
|
class BaseOptionSerializer(ModelSerializer):
|
||||||
@ -31,6 +31,7 @@ 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()
|
||||||
|
|
||||||
@ -47,7 +48,6 @@ BASE_POLL_FIELDS = (
|
|||||||
"votesinvalid",
|
"votesinvalid",
|
||||||
"votescast",
|
"votescast",
|
||||||
"options",
|
"options",
|
||||||
"voted",
|
|
||||||
"id",
|
"id",
|
||||||
"onehundred_percent_base",
|
"onehundred_percent_base",
|
||||||
"majority_method",
|
"majority_method",
|
||||||
@ -59,7 +59,6 @@ class BasePollSerializer(ModelSerializer):
|
|||||||
groups = IdPrimaryKeyRelatedField(
|
groups = IdPrimaryKeyRelatedField(
|
||||||
many=True, required=False, queryset=get_group_model().objects.all()
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
)
|
)
|
||||||
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
|
||||||
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
votesvalid = DecimalField(
|
votesvalid = DecimalField(
|
||||||
|
@ -210,13 +210,12 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
|
|
||||||
elif poll.type == BasePoll.TYPE_NAMED:
|
elif poll.type == BasePoll.TYPE_NAMED:
|
||||||
self.handle_named_vote(data, poll, request.user)
|
self.handle_named_vote(data, poll, request.user)
|
||||||
poll.voted.add(request.user)
|
|
||||||
|
|
||||||
elif poll.type == BasePoll.TYPE_PSEUDOANONYMOUS:
|
elif poll.type == BasePoll.TYPE_PSEUDOANONYMOUS:
|
||||||
self.handle_pseudoanonymous_vote(data, poll)
|
self.handle_pseudoanonymous_vote(data, poll, request.user)
|
||||||
poll.voted.add(request.user)
|
|
||||||
|
inform_changed_data(poll)
|
||||||
|
|
||||||
inform_changed_data(poll) # needed for the changed voted relation
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
def assert_can_vote(self, poll, request):
|
def assert_can_vote(self, poll, request):
|
||||||
@ -224,7 +223,7 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
Raises a permission denied, if the user is not allowed to vote.
|
Raises a permission denied, if the user is not allowed to vote.
|
||||||
Analog: has to have manage permissions
|
Analog: has to have manage permissions
|
||||||
Named & Pseudoanonymous: has to be in a poll group and present
|
Named & Pseudoanonymous: has to be in a poll group and present
|
||||||
Only pseudoanonymous: has to not have voted yet
|
Note: For pseudoanonymous it is *not* tested, if the user has already voted!
|
||||||
"""
|
"""
|
||||||
if poll.type == BasePoll.TYPE_ANALOG:
|
if poll.type == BasePoll.TYPE_ANALOG:
|
||||||
if not self.has_manage_permissions():
|
if not self.has_manage_permissions():
|
||||||
@ -239,10 +238,6 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
):
|
):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
|
|
||||||
if poll.type == BasePoll.TYPE_PSEUDOANONYMOUS:
|
|
||||||
if request.user in poll.voted.all():
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
def parse_vote_value(self, obj, key):
|
def parse_vote_value(self, obj, key):
|
||||||
""" Raises a ValidationError on incorrect values, including None """
|
""" Raises a ValidationError on incorrect values, including None """
|
||||||
if key not in obj:
|
if key not in obj:
|
||||||
@ -278,13 +273,16 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
|
|
||||||
def handle_named_vote(self, data, poll, user):
|
def handle_named_vote(self, data, poll, user):
|
||||||
"""
|
"""
|
||||||
To be implemented by subclass. Handles the named vote. Assumes data is validated
|
To be implemented by subclass. Handles the named vote. Assumes data is validated.
|
||||||
|
Needs to manage the voted-array per option.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def handle_pseudoanonymous_vote(self, data, poll):
|
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||||
"""
|
"""
|
||||||
To be implemented by subclass. Handles the pseudoanonymous vote. Assumes data is validated
|
To be implemented by subclass. Handles the pseudoanonymous vote. Assumes data
|
||||||
|
is validated. Needs to check, if the vote is allowed by the voted-array per poll.
|
||||||
|
Needs to add the user to the voted-array.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -48,6 +48,15 @@ def test_assignment_vote_db_queries():
|
|||||||
assert count_queries(AssignmentVote.get_elements)() == 1
|
assert count_queries(AssignmentVote.get_elements)() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db(transaction=False)
|
||||||
|
def test_assignment_option_db_queries():
|
||||||
|
"""
|
||||||
|
Tests that only 1 query is done when fetching AssignmentOptions
|
||||||
|
"""
|
||||||
|
create_assignment_polls()
|
||||||
|
assert count_queries(AssignmentOption.get_elements)() == 1
|
||||||
|
|
||||||
|
|
||||||
def create_assignment_polls():
|
def create_assignment_polls():
|
||||||
"""
|
"""
|
||||||
Creates 1 assignment with 3 candidates which has 5 polls in which each candidate got a random amount of votes between 0 and 10 from 3 users
|
Creates 1 assignment with 3 candidates which has 5 polls in which each candidate got a random amount of votes between 0 and 10 from 3 users
|
||||||
|
@ -41,6 +41,15 @@ def test_motion_vote_db_queries():
|
|||||||
assert count_queries(MotionVote.get_elements)() == 1
|
assert count_queries(MotionVote.get_elements)() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db(transaction=False)
|
||||||
|
def test_motion_option_db_queries():
|
||||||
|
"""
|
||||||
|
Tests that only 1 query is done when fetching MotionOptions
|
||||||
|
"""
|
||||||
|
create_motion_polls()
|
||||||
|
assert count_queries(MotionOption.get_elements)() == 1
|
||||||
|
|
||||||
|
|
||||||
def create_motion_polls():
|
def create_motion_polls():
|
||||||
"""
|
"""
|
||||||
Creates 1 Motion with 5 polls with 5 options each which have 2 votes each
|
Creates 1 Motion with 5 polls with 5 options each which have 2 votes each
|
||||||
|
Loading…
Reference in New Issue
Block a user