Merge pull request #5541 from jsangmeister/vote-delegation

Add vote delegation
This commit is contained in:
Finn Stutzenstein 2020-10-02 14:15:06 +02:00 committed by GitHub
commit 8d2a7f1b12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1388 additions and 385 deletions

View File

@ -7,8 +7,8 @@ import { HttpService } from 'app/core/core-services/http.service';
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { RelationDefinition } from 'app/core/definitions/relations';
import { VotingService } from 'app/core/ui-services/voting.service';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { UserVote } from 'app/shared/models/poll/base-vote';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
@ -97,7 +97,6 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
viewModelStoreService: ViewModelStoreService,
translate: TranslateService,
relationManager: RelationManagerService,
votingService: VotingService,
http: HttpService
) {
super(
@ -110,7 +109,6 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
AssignmentPoll,
AssignmentPollRelations,
{},
votingService,
http
);
}
@ -123,14 +121,11 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
return this.translate.instant(plural ? 'Polls' : 'Poll');
};
public vote(data: VotingData, poll_id: number): Promise<void> {
let requestData;
if (data.global) {
requestData = `"${data.global}"`;
} else {
requestData = data.votes;
}
public vote(data: VotingData, poll_id: number, userId?: number): Promise<void> {
const requestData: UserVote = {
data: data.global ?? data.votes,
user_id: userId ?? undefined
};
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, requestData);
}
}

View File

@ -7,9 +7,8 @@ import { HttpService } from 'app/core/core-services/http.service';
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { RelationDefinition } from 'app/core/definitions/relations';
import { VotingService } from 'app/core/ui-services/voting.service';
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { VoteValue } from 'app/shared/models/poll/base-vote';
import { UserVote, VoteValue } from 'app/shared/models/poll/base-vote';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
@ -66,7 +65,6 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
viewModelStoreService: ViewModelStoreService,
translate: TranslateService,
relationManager: RelationManagerService,
votingService: VotingService,
http: HttpService
) {
super(
@ -79,7 +77,6 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
MotionPoll,
MotionPollRelations,
{},
votingService,
http
);
}
@ -92,7 +89,11 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
return this.translate.instant(plural ? 'Polls' : 'Poll');
};
public vote(vote: VoteValue, poll_id: number): Promise<void> {
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, JSON.stringify(vote));
public vote(vote: VoteValue, poll_id: number, userId?: number): Promise<void> {
const requestData: UserVote = {
data: vote,
user_id: userId ?? undefined
};
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, requestData);
}
}

View File

@ -43,6 +43,18 @@ const UserRelations: RelationDefinition[] = [
ownIdKey: 'groups_id',
ownKey: 'groups',
foreignViewModel: ViewGroup
},
{
type: 'M2O',
ownIdKey: 'vote_delegated_to_id',
ownKey: 'voteDelegatedTo',
foreignViewModel: ViewUser
},
{
type: 'M2M',
ownIdKey: 'vote_delegated_from_users_id',
ownKey: 'voteDelegationsFrom',
foreignViewModel: ViewUser
}
];
@ -255,6 +267,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
public async update(update: Partial<User>, viewModel: ViewUser): Promise<void> {
this.preventAlterationOnDemoUsers(viewModel);
console.log('update: ', update);
return super.update(update, viewModel);
}

View File

@ -1,7 +1,10 @@
import { Injectable } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
import { ViewUser } from 'app/site/users/models/view-user';
import { OperatorService } from '../core-services/operator.service';
export enum VotingError {
@ -9,19 +12,21 @@ export enum VotingError {
POLL_WRONG_TYPE,
USER_HAS_NO_PERMISSION,
USER_IS_ANONYMOUS,
USER_NOT_PRESENT
USER_NOT_PRESENT,
USER_HAS_DELEGATED_RIGHT
}
/**
* TODO: It appears that the only message that makes sense for the user to see it the last one.
*/
export const VotingErrorVerbose = {
1: "You can't vote on this poll right now because it's not in the 'Started' state.",
2: "You can't vote on this poll because its type is set to analog voting.",
3: "You don't have permission to vote on this poll.",
4: 'You have to be logged in to be able to vote.',
5: 'You have to be present to vote on a poll.',
6: "You have already voted on this poll. You can't change your vote in a pseudoanonymous poll."
const VotingErrorVerbose = {
1: _("You can not vote on this poll right now because it is not in the 'Started' state."),
2: _('You can not vote on this poll because its type is set to analog voting.'),
3: _('You do not not have the permission to vote on this poll.'),
4: _('You have to be logged in to be able to vote.'),
5: _('You have to be present to vote on a poll.'),
6: _('Your right to vote was delegated to another user.'),
7: _('You have already voted on this poll. You can not change your vote in a pseudoanonymous poll.')
};
@Injectable({
@ -33,8 +38,8 @@ export class VotingService {
/**
* checks whether the operator can vote on the given poll
*/
public canVote(poll: ViewBasePoll): boolean {
const error = this.getVotePermissionError(poll);
public canVote(poll: ViewBasePoll, user?: ViewUser): boolean {
const error = this.getVotePermissionError(poll, user);
return !error;
}
@ -42,12 +47,11 @@ export class VotingService {
* checks whether the operator can vote on the given poll
* @returns null if no errors exist (= user can vote) or else a VotingError
*/
public getVotePermissionError(poll: ViewBasePoll): VotingError | void {
public getVotePermissionError(poll: ViewBasePoll, user: ViewUser = this.operator.viewUser): VotingError | void {
if (this.operator.isAnonymous) {
return VotingError.USER_IS_ANONYMOUS;
}
const user = this.operator.user;
if (!poll.groups_id.intersect(user.groups_id).length) {
return VotingError.USER_HAS_NO_PERMISSION;
}
@ -57,13 +61,16 @@ export class VotingService {
if (poll.state !== PollState.Started) {
return VotingError.POLL_WRONG_STATE;
}
if (!user.is_present) {
if (!user.is_present && !this.operator.viewUser.canVoteFor(user)) {
return VotingError.USER_NOT_PRESENT;
}
if (user.isVoteRightDelegated && this.operator.user.id === user.id) {
return VotingError.USER_HAS_DELEGATED_RIGHT;
}
}
public getVotePermissionErrorVerbose(poll: ViewBasePoll): string | void {
const error = this.getVotePermissionError(poll);
public getVotePermissionErrorVerbose(poll: ViewBasePoll, user: ViewUser = this.operator.viewUser): string | void {
const error = this.getVotePermissionError(poll, user);
if (error) {
return VotingErrorVerbose[error];
}

View File

@ -29,7 +29,7 @@
</button>
</ng-container>
<ng-container *ngIf="!multiple && includeNone">
<mat-option>
<mat-option [value]="null">
{{ noneTitle | translate }}
</mat-option>
<mat-divider></mat-divider>

View File

@ -56,8 +56,9 @@ export abstract class BasePoll<
public votescast: number;
public groups_id: number[];
public majority_method: MajorityMethod;
public voted_id: number[];
public user_has_voted: boolean;
public user_has_voted_for_delegations: number[];
public pollmethod: PM;
public onehundred_percent_base: PB;

View File

@ -16,6 +16,13 @@ export const GeneralValueVerbose = {
votesabstain: 'Votes abstain'
};
export interface UserVote {
// the voting payload is hard to describe.
// Can be "VoteValue" or any userID-Number sequence in combination with any VoteValue
data: Object;
user_id?: number;
}
export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
public weight: number;
public value: VoteValue;

View File

@ -30,6 +30,8 @@ export class User extends BaseDecimalModel<User> {
public is_present: boolean;
public is_committee: boolean;
public email?: string;
public vote_delegated_to_id: number;
public vote_delegated_from_users_id: number[];
public last_email_send?: string; // ISO datetime string
public comment?: string;
public is_active?: boolean;
@ -41,6 +43,10 @@ export class User extends BaseDecimalModel<User> {
return this.vote_weight === 1;
}
public get isVoteRightDelegated(): boolean {
return !!this.vote_delegated_to_id;
}
public constructor(input?: Partial<User>) {
super(User.COLLECTIONSTRING, input);
}

View File

@ -1,5 +1,27 @@
<ng-container *ngIf="poll">
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted && !deliveringVote; else cannotVote">
<!-- own voting -->
<ng-container [ngTemplateOutlet]="votingArea"></ng-container>
<!-- Delegations -->
<ng-container *ngIf="user.is_present">
<div class="assignment-vote-delegation" *ngFor="let delegation of delegations">
<mat-divider></mat-divider>
<ng-container
[ngTemplateOutlet]="votingArea"
[ngTemplateOutletContext]="{ delegation: delegation }"
></ng-container>
</div>
</ng-container>
</ng-container>
<ng-template #votingArea let-delegation="delegation">
<h4 *ngIf="delegation" class="assignment-delegation-title">
<span>{{ 'Vote delegation for' | translate }}</span>
<span>&nbsp;{{ delegation.getFullName() }}</span>
</h4>
<ng-container *ngIf="canVote(delegation)">
<!-- Poll hint -->
<p *ngIf="pollHint">
<i>{{ pollHint }}</i>
@ -9,7 +31,7 @@
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1">
{{ 'Available votes' | translate }}:
<b> {{ getVotesAvailable() }}/{{ poll.votes_amount }} </b>
<b> {{ getVotesAvailable(delegation) }}/{{ poll.votes_amount }} </b>
</h4>
<!-- Options and Actions -->
@ -36,14 +58,9 @@
<button
class="vote-button"
mat-raised-button
(click)="saveSingleVote(option.id, action.vote)"
[disabled]="deliveringVote"
[ngClass]="
voteRequestData.votes[option.id] === action.vote ||
voteRequestData.votes[option.id] === 1
? action.css
: ''
"
(click)="saveSingleVote(option.id, action.vote, delegation)"
[disabled]="isDeliveringVote(delegation)"
[ngClass]="getActionButtonClass(action, option, delegation)"
>
<mat-icon> {{ action.icon }}</mat-icon>
</button>
@ -64,9 +81,9 @@
<button
class="vote-button"
mat-raised-button
(click)="saveGlobalVote('N')"
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
[disabled]="deliveringVote"
(click)="saveGlobalVote('N', delegation)"
[ngClass]="getGlobalNoClass(delegation)"
[disabled]="isDeliveringVote(delegation)"
>
<mat-icon> thumb_down </mat-icon>
</button>
@ -79,8 +96,9 @@
<button
class="vote-button"
mat-raised-button
(click)="saveGlobalVote('A')"
[ngClass]="voteRequestData.global === 'A' ? 'voted-abstain' : ''"
(click)="saveGlobalVote('A', delegation)"
[ngClass]="getGlobalAbstainClass(delegation)"
[disabled]="isDeliveringVote(delegation)"
>
<mat-icon> trip_origin</mat-icon>
</button>
@ -92,39 +110,47 @@
</ng-container>
<!-- Submit Vote -->
<ng-container [ngTemplateOutlet]="sendNow"></ng-container>
<ng-container
[ngTemplateOutlet]="sendNow"
[ngTemplateOutletContext]="{ delegation: delegation }"
></ng-container>
</ng-container>
<!-- Shows the permission error -->
<ng-container *ngIf="!vmanager.canVote(poll)">
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
<ng-container
*ngIf="!canVote(delegation)"
[ngTemplateOutlet]="cannotVote"
[ngTemplateOutletContext]="{ delegation: delegation }"
>
</ng-container>
</ng-container>
</ng-template>
<ng-template #cannotVote>
<ng-template #cannotVote let-delegation="delegation">
<div class="centered-button-wrapper">
<div *ngIf="!deliveringVote">
<mat-icon class="vote-submitted">
check_circle
</mat-icon>
<!-- Success -->
<div *ngIf="hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
<mat-icon class="vote-submitted"> check_circle </mat-icon>
<br />
<span>{{ 'Voting successful.' | translate }}</span>
</div>
<div *ngIf="deliveringVote" class="submit-vote-indicator">
<!-- Delivering -->
<div *ngIf="isDeliveringVote(delegation)" class="submit-vote-indicator">
<mat-spinner class="small-spinner"></mat-spinner>
<br />
<span>{{ 'Delivering vote... Please wait!' | translate }}</span>
</div>
<!-- Permission error error -->
<div *ngIf="!hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
<span>{{ getVotingError(delegation) | translate }}</span>
</div>
</div>
</ng-template>
<ng-template #sendNow>
<ng-template #sendNow let-delegation="delegation">
<div class="centered-button-wrapper">
<button mat-flat-button color="accent" (click)="submitVote()">
<mat-icon>
how_to_vote
</mat-icon>
<button mat-flat-button color="accent" (click)="submitVote(delegation)">
<mat-icon> how_to_vote </mat-icon>
<span>
{{ 'Submit vote now' | translate }}
</span>

View File

@ -7,6 +7,14 @@
margin: 20px 0;
}
.assignment-vote-delegation {
margin-top: 1em;
.assignment-delegation-title {
font-weight: 500;
}
}
.yn-grid {
@extend %vote-grid-base;
grid-template-areas:

View File

@ -7,24 +7,17 @@ import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import {
AssignmentPollRepositoryService,
GlobalVote,
VotingData
GlobalVote
} from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { VotingService } from 'app/core/ui-services/voting.service';
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { PollType } from 'app/shared/models/poll/base-poll';
import { VoteValue } from 'app/shared/models/poll/base-vote';
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BasePollVoteComponentDirective } from 'app/site/polls/components/base-poll-vote.component';
// TODO: Duplicate
interface VoteActions {
vote: VoteValue;
css: string;
icon: string;
label: string;
}
import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component';
import { ViewUser } from 'app/site/users/models/view-user';
@Component({
selector: 'os-assignment-poll-vote',
@ -35,37 +28,60 @@ interface VoteActions {
export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<ViewAssignmentPoll> implements OnInit {
public AssignmentPollMethod = AssignmentPollMethod;
public PollType = PollType;
public voteActions: VoteActions[] = [];
public voteRequestData: VotingData = {
votes: {}
};
public alreadyVoted: boolean;
public voteActions: VoteOption[] = [];
public get pollHint(): string {
return this.poll.assignment.default_poll_description;
}
public constructor(
title: Title,
protected translate: TranslateService,
matSnackbar: MatSnackBar,
operator: OperatorService,
public vmanager: VotingService,
votingService: VotingService,
private pollRepo: AssignmentPollRepositoryService,
private promptService: PromptService,
private cd: ChangeDetectorRef
) {
super(title, translate, matSnackbar, operator);
super(title, translate, matSnackbar, operator, votingService);
// observe user updates to refresh the view on dynamic changes
this.subscriptions.push(
operator.getViewUserObservable().subscribe(() => {
this.cd.markForCheck();
})
);
}
public ngOnInit(): void {
if (this.poll && !this.poll.user_has_voted) {
this.alreadyVoted = false;
this.defineVoteOptions();
} else {
this.alreadyVoted = true;
this.cd.markForCheck();
}
this.createVotingDataObjects();
this.defineVoteOptions();
this.cd.markForCheck();
}
public get pollHint(): string {
return this.poll.assignment.default_poll_description;
public getActionButtonClass(actions: VoteOption, option: ViewAssignmentOption, user: ViewUser = this.user): string {
if (
this.voteRequestData[user.id]?.votes[option.id] === actions.vote ||
this.voteRequestData[user.id]?.votes[option.id] === 1
) {
return actions.css;
}
return '';
}
public getGlobalAbstainClass(user: ViewUser = this.user): string {
if (this.voteRequestData[user.id]?.global === 'A') {
return 'voted-abstain';
}
return '';
}
public getGlobalNoClass(user: ViewUser = this.user): string {
if (this.voteRequestData[user.id]?.global === 'N') {
return 'voted-no';
}
return '';
}
private defineVoteOptions(): void {
@ -76,7 +92,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
label: 'Yes'
});
if (this.poll.pollmethod !== AssignmentPollMethod.Votes) {
if (this.poll?.pollmethod !== AssignmentPollMethod.Votes) {
this.voteActions.push({
vote: 'N',
css: 'voted-no',
@ -85,7 +101,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
});
}
if (this.poll.pollmethod === AssignmentPollMethod.YNA) {
if (this.poll?.pollmethod === AssignmentPollMethod.YNA) {
this.voteActions.push({
vote: 'A',
css: 'voted-abstain',
@ -95,40 +111,48 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
}
}
public getVotesCount(): number {
return Object.keys(this.voteRequestData.votes).filter(key => this.voteRequestData.votes[key]).length;
public getVotesCount(user: ViewUser = this.user): number {
if (this.voteRequestData[user.id]) {
return Object.keys(this.voteRequestData[user.id].votes).filter(
key => this.voteRequestData[user.id].votes[key]
).length;
}
}
public getVotesAvailable(): number {
return this.poll.votes_amount - this.getVotesCount();
public getVotesAvailable(user: ViewUser = this.user): number {
return this.poll.votes_amount - this.getVotesCount(user);
}
private isGlobalOptionSelected(): boolean {
return !!this.voteRequestData.global;
private isGlobalOptionSelected(user: ViewUser = this.user): boolean {
return !!this.voteRequestData[user.id]?.global;
}
public async submitVote(): Promise<void> {
public async submitVote(user: ViewUser = this.user): Promise<void> {
const title = this.translate.instant('Submit selection now?');
const content = this.translate.instant('Your decision cannot be changed afterwards.');
const confirmed = await this.promptService.open(title, content);
if (confirmed) {
this.deliveringVote = true;
this.deliveringVote[user.id] = true;
this.cd.markForCheck();
this.pollRepo
.vote(this.voteRequestData, this.poll.id)
.vote(this.voteRequestData[user.id], this.poll.id, user.id)
.then(() => {
this.alreadyVoted = true;
this.alreadyVoted[user.id] = true;
})
.catch(this.raiseError)
.finally(() => {
this.deliveringVote = false;
this.deliveringVote[user.id] = false;
});
}
}
public saveSingleVote(optionId: number, vote: VoteValue): void {
if (this.isGlobalOptionSelected()) {
delete this.voteRequestData.global;
public saveSingleVote(optionId: number, vote: VoteValue, user: ViewUser = this.user): void {
if (!this.voteRequestData[user.id]) {
throw new Error('The user for your voting request does not exist');
}
if (this.isGlobalOptionSelected(user)) {
delete this.voteRequestData[user.id].global;
}
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
@ -138,10 +162,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
.reduce((o, n) => {
o[n] = 0;
if (votesAmount === 1) {
if (n === optionId && this.voteRequestData.votes[n] !== 1) {
if (n === optionId && this.voteRequestData[user.id].votes[n] !== 1) {
o[n] = 1;
}
} else if ((n === optionId) !== (this.voteRequestData.votes[n] === 1)) {
} else if ((n === optionId) !== (this.voteRequestData[user.id].votes[n] === 1)) {
o[n] = 1;
}
@ -151,11 +175,11 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
// check if you can still vote
const countedVotes = Object.keys(tmpVoteRequest).filter(key => tmpVoteRequest[key]).length;
if (countedVotes <= votesAmount) {
this.voteRequestData.votes = tmpVoteRequest;
this.voteRequestData[user.id].votes = tmpVoteRequest;
// if you have no options anymore, try to send
if (this.getVotesCount() === votesAmount) {
this.submitVote();
if (this.getVotesCount(user) === votesAmount) {
this.submitVote(user);
}
} else {
this.raiseError(
@ -164,26 +188,29 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
}
} else {
// YN/YNA
if (this.voteRequestData.votes[optionId] && this.voteRequestData.votes[optionId] === vote) {
delete this.voteRequestData.votes[optionId];
if (
this.voteRequestData[user.id].votes[optionId] &&
this.voteRequestData[user.id].votes[optionId] === vote
) {
delete this.voteRequestData[user.id].votes[optionId];
} else {
this.voteRequestData.votes[optionId] = vote;
this.voteRequestData[user.id].votes[optionId] = vote;
}
// if you filled out every option, try to send
if (Object.keys(this.voteRequestData.votes).length === this.poll.options.length) {
this.submitVote();
if (Object.keys(this.voteRequestData[user.id].votes).length === this.poll.options.length) {
this.submitVote(user);
}
}
}
public saveGlobalVote(globalVote: GlobalVote): void {
this.voteRequestData.votes = {};
if (this.voteRequestData.global && this.voteRequestData.global === globalVote) {
delete this.voteRequestData.global;
public saveGlobalVote(globalVote: GlobalVote, user: ViewUser = this.user): void {
this.voteRequestData[user.id].votes = {};
if (this.voteRequestData[user.id].global && this.voteRequestData[user.id].global === globalVote) {
delete this.voteRequestData[user.id].global;
} else {
this.voteRequestData.global = globalVote;
this.submitVote();
this.voteRequestData[user.id].global = globalVote;
this.submitVote(user);
}
}
}

View File

@ -72,6 +72,8 @@ export class AssignmentPollComponent
this.descriptionForm = this.formBuilder.group({
description: this.poll ? this.poll.description : ''
});
console.log('the poll: ', this.poll);
}
/**

View File

@ -4,10 +4,10 @@
</p>
<div *ngIf="poll.pollClassType === 'motion'">
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
<os-motion-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-motion-poll-vote>
</div>
<div *ngIf="poll.pollClassType === 'assignment'">
<os-assignment-poll-vote [poll]="poll"></os-assignment-poll-vote>
<os-assignment-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-assignment-poll-vote>
</div>
</mat-card>

View File

@ -1,12 +1,34 @@
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid">
<ng-container *ngIf="poll">
<!-- own voting -->
<ng-container [ngTemplateOutlet]="votingArea"></ng-container>
<!-- Delegations -->
<ng-container *ngIf="user.is_present">
<div class="motion-vote-delegation" *ngFor="let delegation of delegations">
<mat-divider></mat-divider>
<ng-container
[ngTemplateOutlet]="votingArea"
[ngTemplateOutletContext]="{ delegation: delegation }"
></ng-container>
</div>
</ng-container>
</ng-container>
<ng-template #votingArea let-delegation="delegation">
<h4 *ngIf="delegation" class="motion-delegation-title">
<span>{{ 'Vote delegation for' | translate }}</span>
<span>&nbsp;{{ delegation.getFullName() }}</span>
</h4>
<div *ngIf="canVote(delegation)" class="vote-button-grid">
<!-- Voting -->
<div class="vote-button" *ngFor="let option of voteOptions">
<button
mat-raised-button
(click)="saveVote(option.vote)"
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
[disabled]="deliveringVote"
(click)="saveVote(option.vote, delegation)"
[ngClass]="getActionButtonClass(option, delegation)"
[disabled]="isDeliveringVote(delegation)"
>
<mat-icon> {{ option.icon }}</mat-icon>
</button>
@ -14,21 +36,33 @@
</div>
</div>
<div *ngIf="deliveringVote" class="submit-vote-indicator">
<ng-container
*ngIf="!canVote(delegation)"
[ngTemplateOutlet]="cannotVote"
[ngTemplateOutletContext]="{ delegation: delegation }"
>
</ng-container>
<!-- Delivering -->
<div *ngIf="isDeliveringVote(delegation)" class="submit-vote-indicator">
<mat-spinner class="small-spinner"></mat-spinner>
<br />
<span>{{ 'Delivering vote... Please wait!' | translate }}</span>
</div>
</ng-container>
</ng-template>
<ng-template #userHasVotes>
<div class="user-has-voted">
<ng-template #cannotVote let-delegation="delegation">
<!-- Success -->
<div *ngIf="hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)" class="user-has-voted">
<div>
<mat-icon class="vote-submitted">
check_circle
</mat-icon>
<mat-icon class="vote-submitted"> check_circle </mat-icon>
<br />
<span>{{ 'Voting successful.' | translate }}</span>
</div>
</div>
<!-- Error -->
<div *ngIf="!hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
<span>{{ getVotingError(delegation) | translate }}</span>
</div>
</ng-template>

View File

@ -8,6 +8,14 @@
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
.motion-vote-delegation {
margin-top: 1em;
.motion-delegation-title {
font-weight: 500;
}
}
.submit-vote-indicator {
margin-top: 1em;
text-align: center;
@ -26,6 +34,10 @@
}
}
.mat-divider-horizontal {
position: initial;
}
.user-has-voted {
display: flex;
text-align: center;

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
@ -10,14 +10,8 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { VotingService } from 'app/core/ui-services/voting.service';
import { VoteValue } from 'app/shared/models/poll/base-vote';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { BasePollVoteComponentDirective } from 'app/site/polls/components/base-poll-vote.component';
interface VoteOption {
vote?: VoteValue;
css?: string;
icon?: string;
label?: string;
}
import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component';
import { ViewUser } from 'app/site/users/models/view-user';
@Component({
selector: 'os-motion-poll-vote',
@ -25,8 +19,7 @@ interface VoteOption {
styleUrls: ['./motion-poll-vote.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MotionPollVoteComponent extends BasePollVoteComponentDirective<ViewMotionPoll> {
public currentVote: VoteOption = {};
export class MotionPollVoteComponent extends BasePollVoteComponentDirective<ViewMotionPoll> implements OnInit {
public voteOptions: VoteOption[] = [
{
vote: 'Y',
@ -53,30 +46,56 @@ export class MotionPollVoteComponent extends BasePollVoteComponentDirective<View
translate: TranslateService,
matSnackbar: MatSnackBar,
operator: OperatorService,
public vmanager: VotingService,
public votingService: VotingService,
private pollRepo: MotionPollRepositoryService,
private promptService: PromptService,
private cd: ChangeDetectorRef
) {
super(title, translate, matSnackbar, operator);
super(title, translate, matSnackbar, operator, votingService);
// observe user updates to refresh the view on dynamic changes
this.subscriptions.push(
operator.getViewUserObservable().subscribe(() => {
this.cd.markForCheck();
})
);
}
public async saveVote(vote: VoteValue): Promise<void> {
this.currentVote.vote = vote;
const title = this.translate.instant('Submit selection now?');
const content = this.translate.instant('Your decision cannot be changed afterwards.');
const confirmed = await this.promptService.open(title, content);
public ngOnInit(): void {
this.createVotingDataObjects();
this.cd.markForCheck();
}
if (confirmed) {
this.deliveringVote = true;
this.cd.markForCheck();
public getActionButtonClass(voteOption: VoteOption, user: ViewUser = this.user): string {
if (this.voteRequestData[user.id]?.vote === voteOption.vote) {
return voteOption.css;
}
return '';
}
this.pollRepo
.vote(vote, this.poll.id)
.catch(this.raiseError)
.finally(() => {
this.deliveringVote = false;
});
public async saveVote(vote: VoteValue, user: ViewUser = this.user): Promise<void> {
if (this.voteRequestData[user.id]) {
this.voteRequestData[user.id].vote = vote;
const title = this.translate.instant('Submit selection now?');
const content = this.translate.instant('Your decision cannot be changed afterwards.');
const confirmed = await this.promptService.open(title, content);
if (confirmed) {
this.deliveringVote[user.id] = true;
this.cd.markForCheck();
this.pollRepo
.vote(vote, this.poll.id, user.id)
.then(() => {
this.alreadyVoted[user.id] = true;
})
.catch(this.raiseError)
.finally(() => {
this.deliveringVote[user.id] = false;
this.cd.markForCheck();
});
}
}
}
}

View File

@ -60,7 +60,7 @@
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
<os-motion-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-motion-poll-vote>
</ng-container>
<!-- Detail link -->

View File

@ -5,11 +5,20 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import { VotingError } from 'app/core/ui-services/voting.service';
import { VotingData } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { VotingError, VotingService } from 'app/core/ui-services/voting.service';
import { VoteValue } from 'app/shared/models/poll/base-vote';
import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewBasePoll } from '../models/view-base-poll';
export interface VoteOption {
vote?: VoteValue;
css?: string;
icon?: string;
label?: string;
}
@Directive()
export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> extends BaseViewComponentDirective {
@Input()
@ -17,22 +26,71 @@ export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> ext
public votingErrors = VotingError;
public deliveringVote = false;
protected voteRequestData = {};
protected alreadyVoted = {};
protected deliveringVote = {};
protected user: ViewUser;
protected delegations: ViewUser[];
public constructor(
title: Title,
translate: TranslateService,
matSnackbar: MatSnackBar,
protected operator: OperatorService
operator: OperatorService,
protected votingService: VotingService
) {
super(title, translate, matSnackbar);
this.subscriptions.push(
this.operator.getViewUserObservable().subscribe(user => {
this.user = user;
operator.getViewUserObservable().subscribe(user => {
if (user) {
this.user = user;
this.delegations = user.voteDelegationsFrom;
}
})
);
}
protected createVotingDataObjects(): void {
if (this.user) {
this.voteRequestData[this.user.id] = {
votes: {}
} as VotingData;
this.alreadyVoted[this.user.id] = this.poll.user_has_voted;
this.deliveringVote[this.user.id] = false;
}
if (this.delegations) {
for (const delegation of this.delegations) {
this.voteRequestData[delegation.id] = {
votes: {}
} as VotingData;
this.alreadyVoted[delegation.id] = this.poll.hasVotedId(delegation.id);
this.deliveringVote[delegation.id] = false;
}
}
}
public isDeliveringVote(user: ViewUser = this.user): boolean {
return this.deliveringVote[user.id] === true;
}
public hasAlreadyVoted(user: ViewUser = this.user): boolean {
return this.alreadyVoted[user.id] === true;
}
public canVote(user: ViewUser = this.user): boolean {
return (
this.votingService.canVote(this.poll, user) && !this.isDeliveringVote(user) && !this.hasAlreadyVoted(user)
);
}
public getVotingError(user: ViewUser = this.user): string | void {
console.log('error ', this.votingService.getVotePermissionErrorVerbose(this.poll, user));
return this.votingService.getVotePermissionErrorVerbose(this.poll, user);
}
}

View File

@ -39,8 +39,15 @@ export class PollProgressComponent extends BaseViewComponentDirective implements
.getViewModelListObservable()
.pipe(
map(users =>
/**
* Filter the users who would be able to vote:
* They are present or have their right to vote delegated
* They are in one of the voting groups
*/
users.filter(
user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length
user =>
(user.is_present || user.isVoteRightDelegated) &&
this.poll.groups_id.intersect(user.groups_id).length
)
)
)

View File

@ -114,6 +114,10 @@ export abstract class ViewBasePoll<
public canBeVotedFor: () => boolean;
public hasVotedId(userId: number): boolean {
return this.user_has_voted_for_delegations?.includes(userId);
}
public abstract getSlide(): ProjectorElementBuildDeskriptor;
public abstract getContentObject(): BaseViewModel;

View File

@ -8,7 +8,6 @@ import { RelationManagerService } from 'app/core/core-services/relation-manager.
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { RelationDefinition } from 'app/core/definitions/relations';
import { BaseRepository, NestedModelDescriptors } from 'app/core/repositories/base-repository';
import { VotingService } from 'app/core/ui-services/voting.service';
import { ModelConstructor } from 'app/shared/models/base/base-model';
import { BasePoll, PollState } from 'app/shared/models/poll/base-poll';
import { BaseViewModel, TitleInformation } from 'app/site/base/base-view-model';
@ -30,7 +29,6 @@ export abstract class BasePollRepositoryService<
protected baseModelCtor: ModelConstructor<M>,
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [],
protected nestedModelDescriptors: NestedModelDescriptors = {},
private votingService: VotingService,
protected http: HttpService
) {
super(
@ -53,7 +51,7 @@ export abstract class BasePollRepositoryService<
protected createViewModelWithTitles(model: M): V {
const viewModel = super.createViewModelWithTitles(model);
Object.defineProperty(viewModel, 'canBeVotedFor', {
value: () => this.votingService.canVote(viewModel)
value: () => viewModel.isStarted
});
return viewModel;
}

View File

@ -164,6 +164,16 @@
[inputListValues]="groups"
></os-search-value-selector>
</mat-form-field>
<!-- Delegate Vote -->
<mat-form-field>
<os-search-value-selector
formControlName="vote_delegated_from_users_id"
[multiple]="true"
placeholder="{{ 'Transferred voting rights from' | translate }}"
[inputListValues]="users"
></os-search-value-selector>
</mat-form-field>
</div>
<div *ngIf="isAllowed('manage')">
@ -303,6 +313,18 @@
</div>
<div *ngIf="isAllowed('manage')">
<!-- Own Vote delegations -->
<div *ngIf="user.voteDelegatedTo">
<h4>{{ 'Vote right delegated to:' | translate }}</h4>
<span>{{ user.voteDelegatedTo }}</span>
</div>
<!-- Received Vote delegations -->
<div *ngIf="user.voteDelegationsFrom && user.voteDelegationsFrom.length">
<h4>{{ 'Vote delegations from' | translate }}</h4>
<span>{{ user.voteDelegationsFrom }}</span>
</div>
<!-- Vote weight -->
<div *ngIf="user.vote_weight && showVoteWeight">
<h4>{{ 'Vote weight' | translate }}</h4>

View File

@ -71,6 +71,8 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
*/
public readonly groups: BehaviorSubject<ViewGroup[]> = new BehaviorSubject<ViewGroup[]>([]);
public readonly users: BehaviorSubject<ViewUser[]> = new BehaviorSubject<ViewUser[]>([]);
/**
* Hold the list of genders (sexes) publicly to dynamically iterate in the view
*/
@ -124,6 +126,7 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
.subscribe(active => (this.isVoteWeightActive = active));
this.groupRepo.getViewModelListObservableWithoutDefaultGroup().subscribe(this.groups);
this.users = this.repo.getViewModelListBehaviorSubject();
}
/**
@ -173,6 +176,7 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
vote_weight: [],
about_me: [''],
groups_id: [''],
vote_delegated_from_users_id: [''],
is_present: [true],
is_committee: [false],
email: ['', Validators.email],

View File

@ -62,7 +62,7 @@
>
<div class="groupsCell">
<div *ngIf="user.groups && user.groups.length">
<os-icon-container icon="people">
<os-icon-container icon="people" noWrap="true">
<span *ngFor="let group of user.groups; let last = last">
{{ group.getTitle() | translate }}<span *ngIf="!last">,</span>
</span>
@ -74,6 +74,10 @@
<div *ngIf="user.number" class="spacer-top-5">
<os-icon-container icon="perm_identity">{{ user.number }}</os-icon-container>
</div>
<div *ngIf="user.vote_delegated_to_id" class="spacer-top-5">
<os-icon-container icon="forward" noWrap="true">{{ user.voteDelegatedTo }}</os-icon-container>
</div>
</div>
</div>
@ -90,9 +94,7 @@
</mat-icon>
<!-- Has comment indicator -->
<mat-icon inline *ngIf="!!user.comment" matTooltip="{{ user.comment }}">
comment
</mat-icon>
<mat-icon inline *ngIf="!!user.comment" matTooltip="{{ user.comment }}"> comment </mat-icon>
<os-icon-container *ngIf="user.isSamlUser" icon="device_hub"
><span>{{ 'Is SAML user' | translate }}</span></os-icon-container
@ -270,6 +272,14 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<os-search-value-selector
[(ngModel)]="infoDialog.vote_delegated_from_users_id"
[multiple]="true"
placeholder="{{ 'Transferred voting rights from' | translate }}"
[inputListValues]="users"
></os-search-value-selector>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="{{ 'Gender' | translate }}" [(ngModel)]="infoDialog.gender">
<mat-option>-</mat-option>
@ -300,6 +310,7 @@
color="accent"
[mat-dialog-close]="{
groups_id: infoDialog.groups_id,
vote_delegated_from_users_id: infoDialog.vote_delegated_from_users_id,
gender: infoDialog.gender,
number: infoDialog.number,
structure_level: infoDialog.structure_level

View File

@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { BehaviorSubject } from 'rxjs';
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service';
@ -55,6 +56,11 @@ interface InfoDialog {
* Structure level for one user.
*/
structure_level: string;
/**
* Transfer voting rights
*/
vote_delegated_from_users_id: number[];
}
/**
@ -82,6 +88,8 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
*/
public groups: ViewGroup[];
public readonly users: BehaviorSubject<ViewUser[]> = new BehaviorSubject<ViewUser[]>([]);
/**
* The list of all genders.
*/
@ -187,6 +195,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
// enable multiSelect for this listView
this.canMultiSelect = true;
this.users = this.repo.getViewModelListBehaviorSubject();
config.get<boolean>('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state));
config.get<boolean>('users_activate_vote_weight').subscribe(active => (this.isVoteWeightActive = active));
config.get<boolean>(this.selfPresentConfStr).subscribe(allowed => (this.allowSelfSetPresent = allowed));
@ -203,9 +212,12 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
// Initialize the groups
this.groups = this.groupRepo.getViewModelList().filter(group => group.id !== 1);
this.groupRepo
.getViewModelListObservable()
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
this.subscriptions.push(
this.groupRepo
.getViewModelListObservable()
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)))
);
}
/**
@ -242,7 +254,8 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
groups_id: user.groups_id,
gender: user.gender,
structure_level: user.structure_level,
number: user.number
number: user.number,
vote_delegated_from_users_id: user.vote_delegated_from_users_id
};
const dialogRef = this.dialog.open(this.userInfoDialog, infoDialogSettings);

View File

@ -82,9 +82,15 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
getDialogTitle: () => this.getTitle()
};
}
public canVoteFor(user: ViewUser): boolean {
return this.vote_delegated_from_users_id.includes(user.id);
}
}
interface IUserRelations {
groups: ViewGroup[];
voteDelegatedTo: ViewUser;
voteDelegationsFrom: ViewUser[];
}
export interface ViewUser extends User, IUserRelations {}

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.16 on 2020-09-10 11:02
from django.conf import settings
from django.db import migrations, models
import openslides.utils.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("assignments", "0014_remove_deprecated_slides"),
]
operations = [
migrations.AddField(
model_name="assignmentvote",
name="delegated_user",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
related_name="assignmentvote_delegated_votes",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -289,7 +289,7 @@ class AssignmentPollViewSet(BasePollViewSet):
poll.db_amount_global_no = Decimal(0)
poll.save()
def handle_analog_vote(self, data, poll, user):
def handle_analog_vote(self, data, poll):
for field in ["votesvalid", "votesinvalid", "votescast"]:
setattr(poll, field, data[field])
@ -336,7 +336,7 @@ class AssignmentPollViewSet(BasePollViewSet):
poll.save()
def validate_vote_data(self, data, poll, user):
def validate_vote_data(self, data, poll):
"""
Request data:
analog:
@ -478,10 +478,12 @@ class AssignmentPollViewSet(BasePollViewSet):
options_data = data
def create_votes_type_votes(self, data, poll, vote_weight, vote_user):
def create_votes_type_votes(self, data, poll, vote_weight, vote_user, request_user):
"""
Helper function for handle_(named|pseudoanonymous)_vote
Assumes data is already validated
vote_user is the user whose vote is given
request_user is the user who gives the vote, may be a delegate
"""
options = poll.get_options()
if isinstance(data, dict):
@ -495,30 +497,46 @@ class AssignmentPollViewSet(BasePollViewSet):
if config["users_activate_vote_weight"]:
weight *= vote_weight
vote = AssignmentVote.objects.create(
option=option, user=vote_user, weight=weight, value="Y"
option=option,
user=vote_user,
delegated_user=request_user,
weight=weight,
value="Y",
)
inform_changed_data(vote, no_delete_on_restriction=True)
else: # global_no or global_abstain
option = options[0]
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
vote = AssignmentVote.objects.create(
option=option, user=vote_user, weight=weight, value=data
option=option,
user=vote_user,
delegated_user=request_user,
weight=weight,
value=data,
)
inform_changed_data(vote, no_delete_on_restriction=True)
inform_changed_data(option)
inform_changed_data(poll)
def create_votes_types_yn_yna(self, data, poll, vote_weight, vote_user):
def create_votes_types_yn_yna(
self, data, poll, vote_weight, vote_user, request_user
):
"""
Helper function for handle_(named|pseudoanonymous)_vote
Assumes data is already validated
vote_user is the user whose vote is given
request_user is the user who gives the vote, may be a delegate
"""
options = poll.get_options()
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
for option_id, result in data.items():
option = options.get(pk=option_id)
vote = AssignmentVote.objects.create(
option=option, user=vote_user, value=result, weight=weight
option=option,
user=vote_user,
delegated_user=request_user,
value=result,
weight=weight,
)
inform_changed_data(vote, no_delete_on_restriction=True)
inform_changed_data(option, no_delete_on_restriction=True)
@ -527,24 +545,28 @@ class AssignmentPollViewSet(BasePollViewSet):
VotedModel = AssignmentPoll.voted.through
VotedModel.objects.create(assignmentpoll=poll, user=user)
def handle_named_vote(self, data, poll, user):
def handle_named_vote(self, data, poll, vote_user, request_user):
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
self.create_votes_type_votes(data, poll, user.vote_weight, user)
self.create_votes_type_votes(
data, poll, vote_user.vote_weight, vote_user, request_user
)
elif poll.pollmethod in (
AssignmentPoll.POLLMETHOD_YN,
AssignmentPoll.POLLMETHOD_YNA,
):
self.create_votes_types_yn_yna(data, poll, user.vote_weight, user)
self.create_votes_types_yn_yna(
data, poll, vote_user.vote_weight, vote_user, request_user
)
def handle_pseudoanonymous_vote(self, data, poll, user):
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
self.create_votes_type_votes(data, poll, user.vote_weight, None)
self.create_votes_type_votes(data, poll, user.vote_weight, None, None)
elif poll.pollmethod in (
AssignmentPoll.POLLMETHOD_YN,
AssignmentPoll.POLLMETHOD_YNA,
):
self.create_votes_types_yn_yna(data, poll, user.vote_weight, None)
self.create_votes_types_yn_yna(data, poll, user.vote_weight, None, None)
def convert_option_data(self, poll, data):
poll_options = poll.get_options()

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.16 on 2020-09-10 11:02
from django.conf import settings
from django.db import migrations, models
import openslides.utils.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("motions", "0036_rename_verbose_poll_types"),
]
operations = [
migrations.AddField(
model_name="motionvote",
name="delegated_user",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
related_name="motionvote_delegated_votes",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -1190,7 +1190,7 @@ class MotionPollViewSet(BasePollViewSet):
return result
def handle_analog_vote(self, data, poll, user):
def handle_analog_vote(self, data, poll):
option = poll.options.get()
vote, _ = MotionVote.objects.get_or_create(option=option, value="Y")
vote.weight = data["Y"]
@ -1209,7 +1209,7 @@ class MotionPollViewSet(BasePollViewSet):
poll.save()
def validate_vote_data(self, data, poll, user):
def validate_vote_data(self, data, poll):
"""
Request data for analog:
{ "Y": <amount>, "N": <amount>, ["A": <amount>],
@ -1240,15 +1240,25 @@ class MotionPollViewSet(BasePollViewSet):
VotedModel = MotionPoll.voted.through
VotedModel.objects.create(motionpoll=poll, user=user)
def handle_named_vote(self, data, poll, user):
self.handle_named_and_pseudoanonymous_vote(data, user, user, poll)
def handle_named_vote(self, data, poll, vote_user, request_user):
self.handle_named_and_pseudoanonymous_vote(
data,
poll,
weight_user=vote_user,
vote_user=vote_user,
request_user=request_user,
)
def handle_pseudoanonymous_vote(self, data, poll, user):
self.handle_named_and_pseudoanonymous_vote(data, user, None, poll)
self.handle_named_and_pseudoanonymous_vote(data, poll, user, None, None)
def handle_named_and_pseudoanonymous_vote(self, data, weight_user, vote_user, poll):
def handle_named_and_pseudoanonymous_vote(
self, data, poll, weight_user, vote_user, request_user
):
option = poll.options.get()
vote = MotionVote.objects.create(user=vote_user, option=option)
vote = MotionVote.objects.create(
user=vote_user, delegated_user=request_user, option=option
)
vote.value = data
vote.weight = (
weight_user.vote_weight

View File

@ -2,8 +2,13 @@ import json
from typing import Any, Dict, List
from ..poll.views import BasePoll
from ..utils import logging
from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import async_has_perm
from ..utils.auth import async_has_perm, user_collection_string
from ..utils.cache import element_cache
logger = logging.getLogger(__name__)
class BaseVoteAccessPermissions(BaseAccessPermissions):
@ -26,6 +31,7 @@ class BaseVoteAccessPermissions(BaseAccessPermissions):
for vote in full_data
if vote["pollstate"] == BasePoll.STATE_PUBLISHED
or vote["user_id"] == user_id
or vote["delegated_user_id"] == user_id
]
return data
@ -71,8 +77,24 @@ class BasePollAccessPermissions(BaseAccessPermissions):
"""
# add has_voted for all users to check whether op has voted
# also fill user_has_voted_for_delegations with all users for which he has
# already voted
user_data = await element_cache.get_element_data(
user_collection_string, user_id
)
if user_data is None:
logger.error(f"Could not find userdata for {user_id}")
vote_delegated_from_ids = set()
else:
vote_delegated_from_ids = set(user_data["vote_delegated_from_users_id"])
for poll in full_data:
poll["user_has_voted"] = user_id in poll["voted_id"]
voted_ids = set(poll["voted_id"])
voted_for_delegations = list(
vote_delegated_from_ids.intersection(voted_ids)
)
poll["user_has_voted_for_delegations"] = voted_for_delegations
if await async_has_perm(user_id, self.manage_permission):
data = full_data

View File

@ -29,6 +29,14 @@ class BaseVote(models.Model):
blank=True,
on_delete=SET_NULL_AND_AUTOUPDATE,
)
delegated_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
default=None,
null=True,
blank=True,
on_delete=SET_NULL_AND_AUTOUPDATE,
related_name="%(class)s_delegated_votes",
)
class Meta:
abstract = True

View File

@ -12,7 +12,15 @@ from ..utils.rest_api import (
from .models import BasePoll
BASE_VOTE_FIELDS = ("id", "weight", "value", "user", "option", "pollstate")
BASE_VOTE_FIELDS = (
"id",
"weight",
"value",
"user",
"delegated_user",
"option",
"pollstate",
)
class BaseVoteSerializer(ModelSerializer):

View File

@ -1,5 +1,6 @@
from textwrap import dedent
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.db import transaction
from django.db.utils import IntegrityError
@ -104,8 +105,8 @@ class BasePollViewSet(ModelViewSet):
# convert user ids to option ids
self.convert_option_data(poll, vote_data)
self.validate_vote_data(vote_data, poll, request.user)
self.handle_analog_vote(vote_data, poll, request.user)
self.validate_vote_data(vote_data, poll)
self.handle_analog_vote(vote_data, poll)
if request.data.get("publish_immediately"):
poll.state = BasePoll.STATE_PUBLISHED
@ -198,25 +199,41 @@ class BasePollViewSet(ModelViewSet):
if isinstance(request.user, AnonymousUser):
self.permission_denied(request)
# check permissions based on poll type and handle requests
self.assert_can_vote(poll, request)
# data format is:
# { data: <vote_data>, [user_id: int] }
# if user_id is given, the operator votes for this user instead of himself
# user_id is ignored for analog polls
data = request.data
self.validate_vote_data(data, poll, request.user)
if "data" not in data:
raise ValidationError({"detail": "No data provided."})
vote_data = data["data"]
if "user_id" in data and poll.type != BasePoll.TYPE_ANALOG:
try:
vote_user = get_user_model().objects.get(pk=data["user_id"])
except get_user_model().DoesNotExist:
raise ValidationError({"detail": "The given user does not exist."})
else:
vote_user = request.user
# check permissions based on poll type and user
self.assert_can_vote(poll, request, vote_user)
# validate the vote data
self.validate_vote_data(vote_data, poll)
if poll.type == BasePoll.TYPE_ANALOG:
self.handle_analog_vote(data, poll, request.user)
if request.data.get("publish_immediately") == "1":
self.handle_analog_vote(vote_data, poll)
if vote_data.get("publish_immediately") == "1":
poll.state = BasePoll.STATE_PUBLISHED
else:
poll.state = BasePoll.STATE_FINISHED
poll.save()
elif poll.type == BasePoll.TYPE_NAMED:
self.handle_named_vote(data, poll, request.user)
self.handle_named_vote(vote_data, poll, vote_user, request.user)
elif poll.type == BasePoll.TYPE_PSEUDOANONYMOUS:
self.handle_pseudoanonymous_vote(data, poll, request.user)
self.handle_pseudoanonymous_vote(vote_data, poll, vote_user)
inform_changed_data(poll)
@ -231,13 +248,16 @@ class BasePollViewSet(ModelViewSet):
inform_changed_data(poll.get_votes(), final_data=True)
return Response()
def assert_can_vote(self, poll, request):
def assert_can_vote(self, poll, request, vote_user):
"""
Raises a permission denied, if the user is not allowed to vote (or has already voted).
Adds the user to the voted array, so this needs to be reverted on error!
Adds the user to the voted array, so this needs to be reverted if a later error happens!
Analog: has to have manage permissions
Named & Pseudoanonymous: has to be in a poll group and present
"""
if request.user != vote_user and request.user != vote_user.vote_delegated_to:
self.permission_denied(request)
if poll.type == BasePoll.TYPE_ANALOG:
if not self.has_manage_permissions():
self.permission_denied(request)
@ -246,14 +266,14 @@ class BasePollViewSet(ModelViewSet):
raise ValidationError("You can only vote on a started poll.")
if not request.user.is_present or not in_some_groups(
request.user.id,
vote_user.id,
list(poll.groups.values_list("pk", flat=True)),
exact=True,
):
self.permission_denied(request)
try:
self.add_user_to_voted_array(request.user, poll)
self.add_user_to_voted_array(vote_user, poll)
inform_changed_data(poll)
except IntegrityError:
raise ValidationError({"detail": "You have already voted"})
@ -292,20 +312,20 @@ class BasePollViewSet(ModelViewSet):
"""
raise NotImplementedError()
def validate_vote_data(self, data, poll, user):
def validate_vote_data(self, data, poll):
"""
To be implemented by subclass. Validates the data according to poll type and method and fields by validated versions.
Raises ValidationError on failure
"""
raise NotImplementedError()
def handle_analog_vote(self, data, poll, user):
def handle_analog_vote(self, data, poll):
"""
To be implemented by subclass. Handles the analog vote. Assumes data is validated
"""
raise NotImplementedError()
def handle_named_vote(self, data, poll, user):
def handle_named_vote(self, data, poll, vote_user, request_user):
"""
To be implemented by subclass. Handles the named vote. Assumes data is validated.
Needs to manage the voted-array per option.

View File

@ -58,6 +58,8 @@ class UserAccessPermissions(BaseAccessPermissions):
own_data_fields = set(little_data_fields)
own_data_fields.add("email")
own_data_fields.add("gender")
own_data_fields.add("vote_delegated_to_id")
own_data_fields.add("vote_delegated_from_users_id")
# Check user permissions.
if await async_has_perm(user_id, "users.can_see_name"):

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.16 on 2020-09-03 11:13
from django.conf import settings
from django.db import migrations, models
import openslides.utils.models
class Migration(migrations.Migration):
dependencies = [
("users", "0014_user_rename_permission"),
]
operations = [
migrations.AddField(
model_name="user",
name="vote_delegated_to",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
related_name="vote_delegated_from_users",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -22,7 +22,11 @@ from openslides.utils.manager import BaseManager
from ..core.config import config
from ..utils.auth import GROUP_ADMIN_PK
from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin
from ..utils.models import (
CASCADE_AND_AUTOUPDATE,
SET_NULL_AND_AUTOUPDATE,
RESTModelMixin,
)
from .access_permissions import (
GroupAccessPermissions,
PersonalNoteAccessPermissions,
@ -54,7 +58,8 @@ class UserManager(BaseUserManager):
queryset=Permission.objects.select_related("content_type"),
)
),
)
),
"vote_delegated_from_users",
)
def create_user(self, username, password, skip_autoupdate=False, **kwargs):
@ -164,6 +169,14 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
default=Decimal("1"), max_digits=15, decimal_places=6, null=False, blank=True
)
vote_delegated_to = models.ForeignKey(
"self",
on_delete=SET_NULL_AND_AUTOUPDATE,
null=True,
blank=True,
related_name="vote_delegated_from_users",
)
objects = UserManager()
class Meta:

View File

@ -7,6 +7,7 @@ from ..utils.rest_api import (
JSONField,
ModelSerializer,
RelatedField,
SerializerMethodField,
ValidationError,
)
from ..utils.validate import validate_html_strict
@ -36,6 +37,8 @@ USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
"comment",
"is_active",
"auth_type",
"vote_delegated_to_id",
"vote_delegated_from_users_id",
)
@ -57,11 +60,14 @@ class UserSerializer(ModelSerializer):
),
)
vote_delegated_from_users_id = SerializerMethodField()
class Meta:
model = User
fields = USERCANSEEEXTRASERIALIZER_FIELDS + (
"default_password",
"session_auth_hash",
"vote_delegated_to",
)
read_only_fields = ("last_email_send", "auth_type")
@ -119,6 +125,13 @@ class UserSerializer(ModelSerializer):
inform_changed_data(user)
return user
def get_vote_delegated_from_users_id(self, user):
# check needed to prevent errors on import since we only give an OrderedDict there
if hasattr(user, "vote_delegated_from_users"):
return [delegator.id for delegator in user.vote_delegated_from_users.all()]
else:
return []
class PermissionRelatedField(RelatedField):
"""

View File

@ -174,9 +174,77 @@ class UserViewSet(ModelViewSet):
):
request.data["username"] = user.username
# check that no chains are created with vote delegation
delegate_id = request.data.get("vote_delegated_to_id")
if delegate_id:
try:
delegate = User.objects.get(id=delegate_id)
except User.DoesNotExist:
raise ValidationError(
{
"detail": f"Vote delegation: The user with id {delegate_id} does not exist"
}
)
self.assert_no_self_delegation(user, [delegate_id])
self.assert_vote_not_delegated(delegate)
self.assert_has_no_delegated_votes(user)
inform_changed_data(delegate)
if user.vote_delegated_to:
inform_changed_data(user.vote_delegated_to)
# handle delegated_from field seperately since its a SerializerMethodField
new_delegation_ids = request.data.get("vote_delegated_from_users_id")
if "vote_delegated_from_users_id" in request.data:
del request.data["vote_delegated_from_users_id"]
response = super().update(request, *args, **kwargs)
# after rest of the request succeeded, handle delegation changes
if new_delegation_ids:
self.assert_no_self_delegation(user, new_delegation_ids)
self.assert_vote_not_delegated(user)
for id in new_delegation_ids:
delegation_user = User.objects.get(id=id)
self.assert_has_no_delegated_votes(delegation_user)
delegation_user.vote_delegated_to = user
delegation_user.save()
delegations_to_remove = user.vote_delegated_from_users.exclude(
id__in=(new_delegation_ids or [])
)
for old_delegation_user in delegations_to_remove:
old_delegation_user.vote_delegated_to = None
old_delegation_user.save()
# if only delegated_from was changed, we need an autoupdate for the operator
if new_delegation_ids or delegations_to_remove:
inform_changed_data(user)
return response
def assert_vote_not_delegated(self, user):
if user.vote_delegated_to:
raise ValidationError(
{
"detail": "You cannot delegate a vote to a user who has already delegated his vote."
}
)
def assert_has_no_delegated_votes(self, user):
if user.vote_delegated_from_users and len(user.vote_delegated_from_users.all()):
raise ValidationError(
{
"detail": "You cannot delegate a vote of a user who is already a delegate of another user."
}
)
def assert_no_self_delegation(self, user, delegate_ids):
if user.id in delegate_ids:
raise ValidationError({"detail": "You cannot delegate a vote to yourself."})
def destroy(self, request, *args, **kwargs):
"""
Customized view endpoint to delete an user.
@ -391,6 +459,7 @@ class UserViewSet(ModelViewSet):
data = serializer.prepare_password(serializer.data)
groups = data["groups_id"]
del data["groups_id"]
del data["vote_delegated_from_users_id"]
db_user = User(**data)
try:

View File

@ -770,13 +770,15 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{
"options": {
"1": {"Y": "1", "N": "2.35", "A": "-1"},
"2": {"Y": "30", "N": "-2", "A": "8.93"},
"data": {
"options": {
"1": {"Y": "1", "N": "2.35", "A": "-1"},
"2": {"Y": "30", "N": "-2", "A": "8.93"},
},
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "-2",
},
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "-2",
},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -800,10 +802,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{
"options": {"1": {"Y": "1", "N": "1", "A": "1"}},
"votesvalid": "-1.5",
"votesinvalid": "-2",
"votescast": "-2",
"data": {
"options": {"1": {"Y": "1", "N": "1", "A": "1"}},
"votesvalid": "-1.5",
"votesinvalid": "-2",
"votescast": "-2",
},
},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -814,10 +818,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{
"options": {
"1": {"Y": "1", "N": "2.35", "A": "-1"},
"2": {"Y": "1", "N": "2.35", "A": "-1"},
}
"data": {
"options": {
"1": {"Y": "1", "N": "2.35", "A": "-1"},
"2": {"Y": "1", "N": "2.35", "A": "-1"},
}
},
},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -828,7 +834,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}},
{"data": {"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -839,9 +845,11 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{
"options": {
"1": {"Y": "1", "N": "2.35", "A": "-1"},
"3": {"Y": "1", "N": "2.35", "A": "-1"},
"data": {
"options": {
"1": {"Y": "1", "N": "2.35", "A": "-1"},
"3": {"Y": "1", "N": "2.35", "A": "-1"},
}
}
},
)
@ -851,25 +859,31 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
def test_no_permissions(self):
self.start_poll()
self.make_admin_delegate()
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentVote.objects.exists())
def test_wrong_state(self):
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
def test_missing_data(self):
self.start_poll()
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
def test_wrong_data_format(self):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), [1, 2, 5]
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -878,7 +892,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": [1, "string"]},
{"data": {"options": [1, "string"]}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -887,7 +901,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": {"string": "some_other_string"}},
{"data": {"options": {"string": "some_other_string"}}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -896,7 +910,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": {"1": [None]}},
{"data": {"options": {"1": [None]}}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -907,7 +921,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
data = {"options": {"1": {"Y": "1", "N": "3", "A": "-1"}}}
del data["options"]["1"][value]
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), data
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": data}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -917,10 +931,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{
"options": {"1": {"Y": 5, "N": 0, "A": 1}},
"votesvalid": "-2",
"votesinvalid": "1",
"votescast": "-1",
"data": {
"options": {"1": {"Y": 5, "N": 0, "A": 1}},
"votesvalid": "-2",
"votesinvalid": "1",
"votescast": "-1",
}
},
)
self.poll.state = 3
@ -928,10 +944,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{
"options": {"1": {"Y": 2, "N": 2, "A": 2}},
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "3",
"data": {
"options": {"1": {"Y": 2, "N": 2, "A": 2}},
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "3",
}
},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -973,7 +991,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "2": "N", "3": "A"},
{"data": {"1": "Y", "2": "N", "3": "A"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1007,7 +1025,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "2": "N", "3": "A"},
{"data": {"1": "Y", "2": "N", "3": "A"}},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 3)
@ -1039,12 +1057,12 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "N"},
{"data": {"1": "N"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1064,7 +1082,8 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
option2 = self.poll2.options.get()
# Do request to poll with option2 (which is wrong...)
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {str(option2.id): "Y"}
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {str(option2.id): "Y"}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(AssignmentVote.objects.count(), 0)
@ -1081,7 +1100,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "2": "N"},
{"data": {"1": "Y", "2": "N"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1092,7 +1111,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1103,7 +1122,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "3": "N"},
{"data": {"1": "Y", "3": "N"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1114,7 +1133,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.make_admin_delegate()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
@ -1125,7 +1144,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
gclient = self.create_guest_client()
response = gclient.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
@ -1137,20 +1156,24 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.admin.save()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
def test_wrong_state(self):
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
def test_missing_data(self):
self.start_poll()
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(
response, status.HTTP_200_OK
) # new "feature" because of partial requests: empty requests work!
@ -1160,7 +1183,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
[1, 2, 5],
{"data": [1, 2, 5]},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1170,7 +1193,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "string"},
{"data": {"1": "string"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1180,7 +1203,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"id": "Y"},
{"data": {"id": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1190,7 +1213,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": [None]},
{"data": {"1": [None]}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1229,7 +1252,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 1, "2": 0},
{"data": {"1": 1, "2": 0}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1254,12 +1277,12 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 1, "2": 0},
{"data": {"1": 1, "2": 0}},
format="json",
)
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 0, "2": 1},
{"data": {"1": 0, "2": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1278,7 +1301,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.poll.save()
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), "N"
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
@ -1294,7 +1317,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.poll.save()
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), "N"
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -1305,7 +1328,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.poll.save()
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), "A"
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
@ -1321,7 +1344,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.poll.save()
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), "A"
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -1331,7 +1354,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": -1},
{"data": {"1": -1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1342,7 +1365,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 2, "2": 1},
{"data": {"1": 2, "2": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1361,7 +1384,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 2, "2": 2},
{"data": {"1": 2, "2": 2}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1372,7 +1395,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 1, "2": 1, "3": 1},
{"data": {"1": 1, "2": 1, "3": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1381,7 +1404,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
def test_wrong_options(self):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"2": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -1390,7 +1415,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
self.make_admin_delegate()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentVote.objects.exists())
@ -1399,7 +1426,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
gclient = self.create_guest_client()
response = gclient.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentVote.objects.exists())
@ -1409,21 +1438,27 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.admin.is_present = False
self.admin.save()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
def test_wrong_state(self):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
def test_missing_data(self):
self.start_poll()
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertFalse(AssignmentVote.objects.exists())
@ -1431,7 +1466,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
[1, 2, 5],
{"data": [1, 2, 5]},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1441,7 +1476,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "string"},
{"data": {"1": "string"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1451,7 +1486,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"id": 1},
{"data": {"id": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1461,7 +1496,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": [None]},
{"data": {"1": [None]}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1495,7 +1530,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "2": "N", "3": "A"},
{"data": {"1": "Y", "2": "N", "3": "A"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1522,12 +1557,12 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "N"},
{"data": {"1": "N"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1541,7 +1576,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "2": "N"},
{"data": {"1": "Y", "2": "N"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1552,7 +1587,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1563,7 +1598,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y", "3": "N"},
{"data": {"1": "Y", "3": "N"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1574,7 +1609,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.make_admin_delegate()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
@ -1585,7 +1620,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
gclient = self.create_guest_client()
response = gclient.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
@ -1597,20 +1632,24 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.admin.save()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "Y"},
{"data": {"1": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
def test_wrong_state(self):
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
def test_missing_data(self):
self.start_poll()
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertFalse(AssignmentVote.objects.exists())
@ -1618,7 +1657,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
[1, 2, 5],
{"data": [1, 2, 5]},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1628,7 +1667,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "string"},
{"data": {"1": "string"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1638,7 +1677,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"id": "Y"},
{"data": {"id": "Y"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1648,7 +1687,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": [None]},
{"data": {"1": [None]}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1687,7 +1726,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 1, "2": 0},
{"data": {"1": 1, "2": 0}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1714,12 +1753,12 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 1, "2": 0},
{"data": {"1": 1, "2": 0}},
format="json",
)
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 0, "2": 1},
{"data": {"1": 0, "2": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1737,7 +1776,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": -1},
{"data": {"1": -1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1748,7 +1787,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 2, "2": 1},
{"data": {"1": 2, "2": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -1769,7 +1808,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 2, "2": 2},
{"data": {"1": 2, "2": 2}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1780,7 +1819,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": 1, "2": 1, "3": 1},
{"data": {"1": 1, "2": 1, "3": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1789,7 +1828,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
def test_wrong_options(self):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"2": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -1798,7 +1839,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
self.make_admin_delegate()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentVote.objects.exists())
@ -1807,7 +1850,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
gclient = self.create_guest_client()
response = gclient.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentVote.objects.exists())
@ -1817,21 +1862,27 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.admin.is_present = False
self.admin.save()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
def test_wrong_state(self):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"data": {"1": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
def test_missing_data(self):
self.start_poll()
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertFalse(AssignmentVote.objects.exists())
@ -1839,7 +1890,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
[1, 2, 5],
{"data": {"data": [1, 2, 5]}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1849,7 +1900,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": "string"},
{"data": {"1": "string"}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1859,7 +1910,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"id": 1},
{"data": {"id": 1}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1869,7 +1920,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"1": [None]},
{"data": {"1": [None]}},
format="json",
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
@ -1923,7 +1974,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
def test_vote(self):
response = self.user_client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"}
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
@ -1956,6 +2007,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"votesinvalid": "0.000000",
"votesvalid": "1.000000",
"user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id],
},
"assignments/assignment-option:1": {
@ -1973,6 +2025,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"option_id": 1,
"pollstate": AssignmentPoll.STATE_STARTED,
"user_id": self.user.id,
"delegated_user_id": self.user.id,
"value": "A",
"weight": "1.000000",
},
@ -1989,6 +2042,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"option_id": 1,
"pollstate": AssignmentPoll.STATE_STARTED,
"user_id": self.user.id,
"delegated_user_id": self.user.id,
"value": "A",
"weight": "1.000000",
},
@ -2018,6 +2072,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"id": 1,
"votes_amount": 1,
"user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
},
)
@ -2073,6 +2128,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"votesinvalid": "0.000000",
"votesvalid": "1.000000",
"user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id],
},
)
@ -2084,6 +2140,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"weight": "1.000000",
"value": "A",
"user_id": 3,
"delegated_user_id": None,
"option_id": 1,
},
)
@ -2108,9 +2165,9 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
):
poll_type = AssignmentPoll.TYPE_PSEUDOANONYMOUS
def test_vote(self):
def test_votex(self):
response = self.user_client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"}
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
@ -2136,6 +2193,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"description": self.description,
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
"user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id],
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
@ -2159,6 +2217,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"option_id": 1,
"pollstate": AssignmentPoll.STATE_STARTED,
"user_id": None,
"delegated_user_id": None,
"value": "A",
"weight": "1.000000",
},
@ -2190,6 +2249,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"id": 1,
"votes_amount": 1,
"user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
},
)
@ -2245,6 +2305,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"votesinvalid": "0.000000",
"votesvalid": "1.000000",
"user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id],
},
"assignments/assignment-vote:1": {
@ -2253,6 +2314,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"weight": "1.000000",
"value": "A",
"user_id": None,
"delegated_user_id": None,
"option_id": 1,
},
"assignments/assignment-option:1": {

View File

@ -167,6 +167,7 @@ class CreateMotionPoll(TestCase):
"id": 1,
"voted_id": [],
"user_has_voted": False,
"user_has_voted_for_delegations": [],
},
)
self.assertEqual(autoupdate[1], [])
@ -610,12 +611,14 @@ class VoteMotionPollAnalog(TestCase):
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{
"Y": "1",
"N": "2.35",
"A": "-1",
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "-2",
"data": {
"Y": "1",
"N": "2.35",
"A": "-1",
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "-2",
},
},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -634,14 +637,23 @@ class VoteMotionPollAnalog(TestCase):
def test_vote_no_permissions(self):
self.start_poll()
self.make_admin_delegate()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_no_data(self):
self.start_poll()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]), {})
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_missing_data(self):
self.start_poll()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"Y": "4", "N": "22.6"}
reverse("motionpoll-vote", args=[self.poll.pk]),
{"data": {"Y": "4", "N": "22.6"}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -649,7 +661,7 @@ class VoteMotionPollAnalog(TestCase):
def test_vote_wrong_data_format(self):
self.start_poll()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -658,7 +670,7 @@ class VoteMotionPollAnalog(TestCase):
self.start_poll()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"Y": "some string", "N": "-2", "A": "3"},
{"data": {"Y": "some string", "N": "-2", "A": "3"}},
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -668,12 +680,14 @@ class VoteMotionPollAnalog(TestCase):
self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{
"Y": "3",
"N": "1",
"A": "5",
"votesvalid": "-2",
"votesinvalid": "1",
"votescast": "-1",
"data": {
"Y": "3",
"N": "1",
"A": "5",
"votesvalid": "-2",
"votesinvalid": "1",
"votescast": "-1",
},
},
)
self.poll.state = 3
@ -681,12 +695,14 @@ class VoteMotionPollAnalog(TestCase):
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{
"Y": "1",
"N": "2.35",
"A": "-1",
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "3",
"data": {
"Y": "1",
"N": "2.35",
"A": "-1",
"votesvalid": "4.64",
"votesinvalid": "-2",
"votescast": "3",
},
},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -749,7 +765,7 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -773,7 +789,7 @@ class VoteMotionPollNamed(TestCase):
self.admin.vote_weight = weight = Decimal("3.5")
self.admin.save()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -799,11 +815,11 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get()
@ -824,38 +840,35 @@ class VoteMotionPollNamed(TestCase):
config["general_system_enable_anonymous"] = True
guest_client = APIClient()
response = guest_client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "Y"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
# TODO: Move to unit tests
def test_not_set_vote_values(self):
with self.assertRaises(ValueError):
self.poll.votesvalid = Decimal("1")
with self.assertRaises(ValueError):
self.poll.votesinvalid = Decimal("1")
with self.assertRaises(ValueError):
self.poll.votescast = Decimal("1")
def test_vote_wrong_state(self):
self.make_admin_present()
self.make_admin_delegate()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_wrong_group(self):
self.start_poll()
self.make_admin_present()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_not_present(self):
self.start_poll()
self.make_admin_delegate()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -863,7 +876,9 @@ class VoteMotionPollNamed(TestCase):
self.start_poll()
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -872,11 +887,118 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def setup_vote_delegation(self, with_delegation=True):
""" user -> admin """
self.start_poll()
self.make_admin_delegate()
self.make_admin_present()
user, _ = self.create_user()
user.groups.add(GROUP_DELEGATE_PK)
if with_delegation:
user.vote_delegated_to = self.admin
user.save()
inform_changed_data(self.admin) # put the admin into the cache to update
# its vote_delegated_to_id field
self.user = user
def test_vote_delegation(self):
self.setup_vote_delegation()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"data": "N", "user_id": self.user.pk}, # user not present
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("1"))
self.assertEqual(poll.get_votes().count(), 1)
option = poll.options.get()
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("1"))
self.assertEqual(option.abstain, Decimal("0"))
vote = option.votes.get()
self.assertEqual(vote.user, self.user)
self.assertEqual(vote.delegated_user, self.admin)
autoupdate = self.get_last_autoupdate(user=self.admin)
self.assertIn("motions/motion-poll:1", autoupdate[0])
self.assertEqual(
autoupdate[0]["motions/motion-poll:1"]["user_has_voted_for_delegations"],
[self.user.pk],
)
def test_vote_delegation_and_self_vote(self):
self.test_vote_delegation()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.votesvalid, Decimal("2"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
self.assertEqual(poll.votescast, Decimal("2"))
self.assertEqual(poll.get_votes().count(), 2)
option = poll.options.get()
self.assertEqual(option.yes, Decimal("1"))
self.assertEqual(option.no, Decimal("1"))
self.assertEqual(option.abstain, Decimal("0"))
vote = option.votes.get(user_id=self.admin.pk)
self.assertEqual(vote.user, self.admin)
self.assertEqual(vote.delegated_user, self.admin)
def test_vote_delegation_forbidden(self):
self.setup_vote_delegation(False)
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"data": "N", "user_id": self.user.pk},
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_delegation_not_present(self):
self.setup_vote_delegation()
self.admin.is_present = False
self.admin.save()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"data": "N", "user_id": self.user.pk},
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_delegation_delegatee_not_in_group(self):
self.setup_vote_delegation()
self.admin.groups.remove(GROUP_DELEGATE_PK)
self.admin.save()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"data": "N", "user_id": self.user.pk},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.get_votes().count(), 1)
vote = poll.get_votes()[0]
self.assertEqual(vote.value, "N")
self.assertEqual(vote.user, self.user)
self.assertEqual(vote.delegated_user, self.admin)
def test_vote_delegation_delegator_not_in_group(self):
self.setup_vote_delegation()
self.user.groups.remove(GROUP_DELEGATE_PK)
self.user.save()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"data": "N", "user_id": self.user.pk},
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
class VoteMotionPollNamedAutoupdates(TestCase):
"""3 important users:
@ -916,7 +1038,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
def test_vote(self):
response = self.user_client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -942,6 +1064,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"options_id": [1],
"id": 1,
"user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id],
},
"motions/motion-vote:1": {
@ -950,6 +1073,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"weight": "1.000000",
"value": "A",
"user_id": self.user.id,
"delegated_user_id": self.user.id,
"option_id": 1,
},
"motions/motion-option:1": {
@ -975,6 +1099,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"weight": "1.000000",
"value": "A",
"user_id": self.user.id,
"delegated_user_id": self.user.id,
},
)
self.assertEqual(
@ -1001,6 +1126,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"options_id": [1],
"id": 1,
"user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
},
)
self.assertEqual(
@ -1055,7 +1181,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
def test_vote(self):
response = self.user_client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -1081,6 +1207,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"options_id": [1],
"id": 1,
"user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id],
},
"motions/motion-vote:1": {
@ -1090,6 +1217,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"weight": "1.000000",
"value": "A",
"user_id": None,
"delegated_user_id": None,
},
"motions/motion-option:1": {
"abstain": "1.000000",
@ -1122,6 +1250,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"options_id": [1],
"id": 1,
"user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
},
)
@ -1177,7 +1306,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -1199,11 +1328,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
option = MotionPoll.objects.get().options.get()
@ -1219,7 +1348,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
config["general_system_enable_anonymous"] = True
guest_client = APIClient()
response = guest_client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "Y"
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1227,21 +1356,27 @@ class VoteMotionPollPseudoanonymous(TestCase):
def test_vote_wrong_state(self):
self.make_admin_present()
self.make_admin_delegate()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_wrong_group(self):
self.start_poll()
self.make_admin_present()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_not_present(self):
self.start_poll()
self.make_admin_delegate()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1249,7 +1384,9 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.start_poll()
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1258,7 +1395,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1344,6 +1481,7 @@ class PublishMotionPoll(TestCase):
"options_id": [1],
"id": 1,
"user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [],
},
"motions/motion-vote:1": {
@ -1353,6 +1491,7 @@ class PublishMotionPoll(TestCase):
"weight": "2.000000",
"value": "N",
"user_id": None,
"delegated_user_id": None,
},
"motions/motion-option:1": {
"abstain": "0.000000",
@ -1495,3 +1634,45 @@ class ResetMotionPoll(TestCase):
for user in (self.admin, self.user1, self.user2):
self.assertDeletedAutoupdate(self.vote1, user=user)
self.assertDeletedAutoupdate(self.vote2, user=user)
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
def advancedSetUp(self):
""" Set up user -> other_user delegation. """
self.motion = Motion(
title="test_title_dL91JqhMTiQuQLSDRItZ",
text="test_text_R7nURdXKVEfEnnJBXJYa",
)
self.motion.save()
self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
self.other_user, _ = self.create_user()
self.user, user_password = self.create_user()
self.user.groups.add(self.delegate_group)
self.user.is_present = True
self.user.vote_delegated_to = self.other_user
self.user.save()
self.user_client = APIClient()
self.user_client.login(username=self.user.username, password=user_password)
self.poll = MotionPoll.objects.create(
motion=self.motion,
title="test_title_Q3EuRaALSCCPJuQ2tMqj",
pollmethod="YNA",
type=BasePoll.TYPE_NAMED,
onehundred_percent_base="YN",
majority_method="simple",
)
self.poll.create_options()
self.poll.groups.add(self.delegate_group)
self.poll.save()
def test_start_poll(self):
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
# other_user has to receive an autoupdate because he was delegated
autoupdate = self.get_last_autoupdate(user=self.other_user)
assert "motions/motion-poll:1" in autoupdate[0]

View File

@ -24,12 +24,13 @@ def test_user_db_queries():
"""
Tests that only the following db queries are done:
* 2 requests to get the list of all users and
* 1 requests to get the list of all groups.
* 1 request to get all vote delegations
* 1 request to get the list of all groups.
"""
for index in range(10):
User.objects.create(username=f"user{index}")
assert count_queries(User.get_elements)() == 3
assert count_queries(User.get_elements)() == 4
@pytest.mark.django_db(transaction=False)
@ -232,6 +233,188 @@ class UserUpdate(TestCase):
# The user is not allowed to change some other fields (like last_name).
self.assertNotEqual(user.last_name, "New name fae1Bu1Eyeis9eRox4xu")
def test_update_vote_delegation(self):
user = User.objects.create_user(
username="non-admin Yd4ejrJXZi4Wn16ugHgY",
password="non-admin AQ4Dw2tN9byKpGD4f1gs",
)
response = self.client.patch(
reverse("user-detail", args=[user.pk]),
{"vote_delegated_to_id": self.admin.pk},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
user = User.objects.get(pk=user.pk)
self.assertEqual(user.vote_delegated_to_id, self.admin.pk)
admin = User.objects.get(username="admin")
self.assertEqual(
list(admin.vote_delegated_from_users.values_list("id", flat=True)),
[user.pk],
)
def test_update_vote_delegation_non_admin(self):
user = User.objects.create_user(
username="non-admin WpBQRSsCg6qNWNtN6bLP",
password="non-admin IzsDBt1uoqc2wo5BSUF1",
)
client = APIClient()
client.login(
username="non-admin WpBQRSsCg6qNWNtN6bLP",
password="non-admin IzsDBt1uoqc2wo5BSUF1",
)
response = client.patch(
reverse("user-detail", args=[user.pk]),
{"vote_delegated_to_id": self.admin.pk},
)
# self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_200_OK)
user = User.objects.get(pk=user.pk)
self.assertIsNone(user.vote_delegated_to_id)
def test_update_vote_delegated_to_self(self):
response = self.client.patch(
reverse("user-detail", args=[self.admin.pk]),
{"vote_delegated_to_id": self.admin.pk},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
admin = User.objects.get(pk=self.admin.pk)
self.assertIsNone(admin.vote_delegated_to_id)
def test_update_vote_delegation_invalid_id(self):
response = self.client.patch(
reverse("user-detail", args=[self.admin.pk]),
{"vote_delegated_to_id": 42},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
admin = User.objects.get(pk=self.admin.pk)
self.assertIsNone(admin.vote_delegated_to_id)
def test_update_vote_delegated_from_self(self):
response = self.client.patch(
reverse("user-detail", args=[self.admin.pk]),
{"vote_delegated_from_users_id": [self.admin.pk]},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
admin = User.objects.get(pk=self.admin.pk)
self.assertIsNone(admin.vote_delegated_to_id)
def setup_vote_delegation(self):
""" login and setup user -> user2 delegation """
self.user, _ = self.create_user()
self.user2, _ = self.create_user()
self.user.vote_delegated_to = self.user2
self.user.save()
self.assertEqual(self.user.vote_delegated_to_id, self.user2.pk)
def test_update_reset_vote_delegated_to(self):
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.user.pk]),
{"vote_delegated_to_id": None},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
user = User.objects.get(pk=self.user.pk)
self.assertEqual(user.vote_delegated_to_id, None)
def test_update_reset_vote_delegated_from(self):
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.user2.pk]),
{"vote_delegated_from_users_id": None},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
user = User.objects.get(pk=self.user.pk)
self.assertEqual(user.vote_delegated_to_id, None)
def test_update_nested_vote_delegation_1(self):
""" user -> user2 -> admin """
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.user2.pk]),
{"vote_delegated_to_id": self.admin.pk},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
user2 = User.objects.get(pk=self.user2.pk)
self.assertIsNone(user2.vote_delegated_to_id)
def test_update_nested_vote_delegation_2(self):
""" admin -> user -> user2 """
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.admin.pk]),
{"vote_delegated_to_id": self.user.pk},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
admin = User.objects.get(pk=self.admin.pk)
self.assertIsNone(admin.vote_delegated_to_id)
def test_update_vote_delegation_autoupdate(self):
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.user.pk]),
{"vote_delegated_to_id": self.admin.pk},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
autoupdate = self.get_last_autoupdate(user=self.admin)
user_au = autoupdate[0].get(f"users/user:{self.user.pk}")
self.assertIsNotNone(user_au)
self.assertEqual(user_au["vote_delegated_to_id"], self.admin.pk)
user2_au = autoupdate[0].get(f"users/user:{self.user2.pk}")
self.assertIsNotNone(user2_au)
self.assertEqual(user2_au["vote_delegated_from_users_id"], [])
admin_au = autoupdate[0].get(f"users/user:{self.admin.pk}")
self.assertIsNotNone(admin_au)
self.assertEqual(admin_au["vote_delegated_from_users_id"], [self.user.pk])
self.assertEqual(autoupdate[1], [])
def test_update_vote_delegated_from(self):
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.user2.pk]),
{"vote_delegated_from_users_id": [self.admin.pk]},
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
admin = User.objects.get(pk=self.admin.pk)
self.assertEqual(admin.vote_delegated_to_id, self.user2.id)
user = User.objects.get(pk=self.user.pk)
self.assertIsNone(user.vote_delegated_to_id)
def test_update_vote_delegated_from_nested_1(self):
""" admin -> user -> user2 """
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.user.pk]),
{"vote_delegated_from_users_id": [self.admin.pk]},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
admin = User.objects.get(pk=self.admin.pk)
self.assertIsNone(admin.vote_delegated_to_id)
def test_update_vote_delegated_from_nested_2(self):
""" user -> user2 -> admin """
self.setup_vote_delegation()
response = self.client.patch(
reverse("user-detail", args=[self.admin.pk]),
{"vote_delegated_from_users_id": [self.user2.pk]},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
user2 = User.objects.get(pk=self.user2.pk)
self.assertIsNone(user2.vote_delegated_to_id)
class UserDelete(TestCase):
"""

View File

@ -1,6 +1,7 @@
from decimal import Decimal
from unittest import TestCase
from openslides.motions.models import Motion, MotionChangeRecommendation
from openslides.motions.models import Motion, MotionChangeRecommendation, MotionPoll
# TODO: test for MotionPoll.set_options()
@ -50,3 +51,25 @@ class MotionChangeRecommendationTest(TestCase):
other_recommendations
)
self.assertFalse(collides)
class MotionPollAnalogFieldsTest(TestCase):
def setUp(self):
self.motion = Motion(
title="test_title_OoK9IeChe2Jeib9Deeji",
text="test_text_eichui1oobiSeit9aifo",
)
self.poll = MotionPoll(
motion=self.motion,
title="test_title_tho8PhiePh8upaex6phi",
pollmethod="YNA",
type=MotionPoll.TYPE_NAMED,
)
def test_not_set_vote_values(self):
with self.assertRaises(ValueError):
self.poll.votesvalid = Decimal("1")
with self.assertRaises(ValueError):
self.poll.votesinvalid = Decimal("1")
with self.assertRaises(ValueError):
self.poll.votescast = Decimal("1")