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 { RelationManagerService } from 'app/core/core-services/relation-manager.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { RelationDefinition } from 'app/core/definitions/relations'; 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 { 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 { ViewAssignment } from 'app/site/assignments/models/view-assignment';
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option'; import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
@ -97,7 +97,6 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService,
relationManager: RelationManagerService, relationManager: RelationManagerService,
votingService: VotingService,
http: HttpService http: HttpService
) { ) {
super( super(
@ -110,7 +109,6 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
AssignmentPoll, AssignmentPoll,
AssignmentPollRelations, AssignmentPollRelations,
{}, {},
votingService,
http http
); );
} }
@ -123,14 +121,11 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
return this.translate.instant(plural ? 'Polls' : 'Poll'); return this.translate.instant(plural ? 'Polls' : 'Poll');
}; };
public vote(data: VotingData, poll_id: number): Promise<void> { public vote(data: VotingData, poll_id: number, userId?: number): Promise<void> {
let requestData; const requestData: UserVote = {
if (data.global) { data: data.global ?? data.votes,
requestData = `"${data.global}"`; user_id: userId ?? undefined
} else { };
requestData = data.votes;
}
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, requestData); 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 { RelationManagerService } from 'app/core/core-services/relation-manager.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { RelationDefinition } from 'app/core/definitions/relations'; 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 { 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 { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option'; import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
@ -66,7 +65,6 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService,
relationManager: RelationManagerService, relationManager: RelationManagerService,
votingService: VotingService,
http: HttpService http: HttpService
) { ) {
super( super(
@ -79,7 +77,6 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
MotionPoll, MotionPoll,
MotionPollRelations, MotionPollRelations,
{}, {},
votingService,
http http
); );
} }
@ -92,7 +89,11 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
return this.translate.instant(plural ? 'Polls' : 'Poll'); return this.translate.instant(plural ? 'Polls' : 'Poll');
}; };
public vote(vote: VoteValue, poll_id: number): Promise<void> { public vote(vote: VoteValue, poll_id: number, userId?: number): Promise<void> {
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, JSON.stringify(vote)); 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', ownIdKey: 'groups_id',
ownKey: 'groups', ownKey: 'groups',
foreignViewModel: ViewGroup 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> { public async update(update: Partial<User>, viewModel: ViewUser): Promise<void> {
this.preventAlterationOnDemoUsers(viewModel); this.preventAlterationOnDemoUsers(viewModel);
console.log('update: ', update);
return super.update(update, viewModel); return super.update(update, viewModel);
} }

View File

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

View File

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

View File

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

View File

@ -16,6 +16,13 @@ export const GeneralValueVerbose = {
votesabstain: 'Votes abstain' 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> { export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
public weight: number; public weight: number;
public value: VoteValue; public value: VoteValue;

View File

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

View File

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

View File

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

View File

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

View File

@ -4,10 +4,10 @@
</p> </p>
<div *ngIf="poll.pollClassType === 'motion'"> <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>
<div *ngIf="poll.pollClassType === 'assignment'"> <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> </div>
</mat-card> </mat-card>

View File

@ -1,12 +1,34 @@
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes"> <ng-container *ngIf="poll">
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid"> <!-- 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 --> <!-- Voting -->
<div class="vote-button" *ngFor="let option of voteOptions"> <div class="vote-button" *ngFor="let option of voteOptions">
<button <button
mat-raised-button mat-raised-button
(click)="saveVote(option.vote)" (click)="saveVote(option.vote, delegation)"
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''" [ngClass]="getActionButtonClass(option, delegation)"
[disabled]="deliveringVote" [disabled]="isDeliveringVote(delegation)"
> >
<mat-icon> {{ option.icon }}</mat-icon> <mat-icon> {{ option.icon }}</mat-icon>
</button> </button>
@ -14,21 +36,33 @@
</div> </div>
</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> <mat-spinner class="small-spinner"></mat-spinner>
<br /> <br />
<span>{{ 'Delivering vote... Please wait!' | translate }}</span> <span>{{ 'Delivering vote... Please wait!' | translate }}</span>
</div> </div>
</ng-container> </ng-template>
<ng-template #userHasVotes> <ng-template #cannotVote let-delegation="delegation">
<div class="user-has-voted"> <!-- Success -->
<div *ngIf="hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)" class="user-has-voted">
<div> <div>
<mat-icon class="vote-submitted"> <mat-icon class="vote-submitted"> check_circle </mat-icon>
check_circle
</mat-icon>
<br /> <br />
<span>{{ 'Voting successful.' | translate }}</span> <span>{{ 'Voting successful.' | translate }}</span>
</div> </div>
</div> </div>
<!-- Error -->
<div *ngIf="!hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
<span>{{ getVotingError(delegation) | translate }}</span>
</div>
</ng-template> </ng-template>

View File

@ -8,6 +8,14 @@
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
} }
.motion-vote-delegation {
margin-top: 1em;
.motion-delegation-title {
font-weight: 500;
}
}
.submit-vote-indicator { .submit-vote-indicator {
margin-top: 1em; margin-top: 1em;
text-align: center; text-align: center;
@ -26,6 +34,10 @@
} }
} }
.mat-divider-horizontal {
position: initial;
}
.user-has-voted { .user-has-voted {
display: flex; display: flex;
text-align: center; 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 { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser'; 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 { VotingService } from 'app/core/ui-services/voting.service';
import { VoteValue } from 'app/shared/models/poll/base-vote'; import { VoteValue } from 'app/shared/models/poll/base-vote';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { BasePollVoteComponentDirective } from 'app/site/polls/components/base-poll-vote.component'; import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component';
import { ViewUser } from 'app/site/users/models/view-user';
interface VoteOption {
vote?: VoteValue;
css?: string;
icon?: string;
label?: string;
}
@Component({ @Component({
selector: 'os-motion-poll-vote', selector: 'os-motion-poll-vote',
@ -25,8 +19,7 @@ interface VoteOption {
styleUrls: ['./motion-poll-vote.component.scss'], styleUrls: ['./motion-poll-vote.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MotionPollVoteComponent extends BasePollVoteComponentDirective<ViewMotionPoll> { export class MotionPollVoteComponent extends BasePollVoteComponentDirective<ViewMotionPoll> implements OnInit {
public currentVote: VoteOption = {};
public voteOptions: VoteOption[] = [ public voteOptions: VoteOption[] = [
{ {
vote: 'Y', vote: 'Y',
@ -53,30 +46,56 @@ export class MotionPollVoteComponent extends BasePollVoteComponentDirective<View
translate: TranslateService, translate: TranslateService,
matSnackbar: MatSnackBar, matSnackbar: MatSnackBar,
operator: OperatorService, operator: OperatorService,
public vmanager: VotingService, public votingService: VotingService,
private pollRepo: MotionPollRepositoryService, private pollRepo: MotionPollRepositoryService,
private promptService: PromptService, private promptService: PromptService,
private cd: ChangeDetectorRef 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> { public ngOnInit(): void {
this.currentVote.vote = vote; this.createVotingDataObjects();
const title = this.translate.instant('Submit selection now?'); this.cd.markForCheck();
const content = this.translate.instant('Your decision cannot be changed afterwards.'); }
const confirmed = await this.promptService.open(title, content);
if (confirmed) { public getActionButtonClass(voteOption: VoteOption, user: ViewUser = this.user): string {
this.deliveringVote = true; if (this.voteRequestData[user.id]?.vote === voteOption.vote) {
this.cd.markForCheck(); return voteOption.css;
}
return '';
}
this.pollRepo public async saveVote(vote: VoteValue, user: ViewUser = this.user): Promise<void> {
.vote(vote, this.poll.id) if (this.voteRequestData[user.id]) {
.catch(this.raiseError) this.voteRequestData[user.id].vote = vote;
.finally(() => {
this.deliveringVote = false; 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"> <div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
<os-poll-progress [poll]="poll"></os-poll-progress> <os-poll-progress [poll]="poll"></os-poll-progress>
</div> </div>
<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> </ng-container>
<!-- Detail link --> <!-- Detail link -->

View File

@ -5,11 +5,20 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service'; 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 { BaseViewComponentDirective } from 'app/site/base/base-view';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewBasePoll } from '../models/view-base-poll'; import { ViewBasePoll } from '../models/view-base-poll';
export interface VoteOption {
vote?: VoteValue;
css?: string;
icon?: string;
label?: string;
}
@Directive() @Directive()
export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> extends BaseViewComponentDirective { export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> extends BaseViewComponentDirective {
@Input() @Input()
@ -17,22 +26,71 @@ export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> ext
public votingErrors = VotingError; public votingErrors = VotingError;
public deliveringVote = false; protected voteRequestData = {};
protected alreadyVoted = {};
protected deliveringVote = {};
protected user: ViewUser; protected user: ViewUser;
protected delegations: ViewUser[];
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
matSnackbar: MatSnackBar, matSnackbar: MatSnackBar,
protected operator: OperatorService operator: OperatorService,
protected votingService: VotingService
) { ) {
super(title, translate, matSnackbar); super(title, translate, matSnackbar);
this.subscriptions.push( this.subscriptions.push(
this.operator.getViewUserObservable().subscribe(user => { operator.getViewUserObservable().subscribe(user => {
this.user = 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() .getViewModelListObservable()
.pipe( .pipe(
map(users => 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( 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 canBeVotedFor: () => boolean;
public hasVotedId(userId: number): boolean {
return this.user_has_voted_for_delegations?.includes(userId);
}
public abstract getSlide(): ProjectorElementBuildDeskriptor; public abstract getSlide(): ProjectorElementBuildDeskriptor;
public abstract getContentObject(): BaseViewModel; 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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { RelationDefinition } from 'app/core/definitions/relations'; import { RelationDefinition } from 'app/core/definitions/relations';
import { BaseRepository, NestedModelDescriptors } from 'app/core/repositories/base-repository'; 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 { ModelConstructor } from 'app/shared/models/base/base-model';
import { BasePoll, PollState } from 'app/shared/models/poll/base-poll'; import { BasePoll, PollState } from 'app/shared/models/poll/base-poll';
import { BaseViewModel, TitleInformation } from 'app/site/base/base-view-model'; import { BaseViewModel, TitleInformation } from 'app/site/base/base-view-model';
@ -30,7 +29,6 @@ export abstract class BasePollRepositoryService<
protected baseModelCtor: ModelConstructor<M>, protected baseModelCtor: ModelConstructor<M>,
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [], protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [],
protected nestedModelDescriptors: NestedModelDescriptors = {}, protected nestedModelDescriptors: NestedModelDescriptors = {},
private votingService: VotingService,
protected http: HttpService protected http: HttpService
) { ) {
super( super(
@ -53,7 +51,7 @@ export abstract class BasePollRepositoryService<
protected createViewModelWithTitles(model: M): V { protected createViewModelWithTitles(model: M): V {
const viewModel = super.createViewModelWithTitles(model); const viewModel = super.createViewModelWithTitles(model);
Object.defineProperty(viewModel, 'canBeVotedFor', { Object.defineProperty(viewModel, 'canBeVotedFor', {
value: () => this.votingService.canVote(viewModel) value: () => viewModel.isStarted
}); });
return viewModel; return viewModel;
} }

View File

@ -164,6 +164,16 @@
[inputListValues]="groups" [inputListValues]="groups"
></os-search-value-selector> ></os-search-value-selector>
</mat-form-field> </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>
<div *ngIf="isAllowed('manage')"> <div *ngIf="isAllowed('manage')">
@ -303,6 +313,18 @@
</div> </div>
<div *ngIf="isAllowed('manage')"> <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 --> <!-- Vote weight -->
<div *ngIf="user.vote_weight && showVoteWeight"> <div *ngIf="user.vote_weight && showVoteWeight">
<h4>{{ 'Vote weight' | translate }}</h4> <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 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 * 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)); .subscribe(active => (this.isVoteWeightActive = active));
this.groupRepo.getViewModelListObservableWithoutDefaultGroup().subscribe(this.groups); this.groupRepo.getViewModelListObservableWithoutDefaultGroup().subscribe(this.groups);
this.users = this.repo.getViewModelListBehaviorSubject();
} }
/** /**
@ -173,6 +176,7 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
vote_weight: [], vote_weight: [],
about_me: [''], about_me: [''],
groups_id: [''], groups_id: [''],
vote_delegated_from_users_id: [''],
is_present: [true], is_present: [true],
is_committee: [false], is_committee: [false],
email: ['', Validators.email], email: ['', Validators.email],

View File

@ -62,7 +62,7 @@
> >
<div class="groupsCell"> <div class="groupsCell">
<div *ngIf="user.groups && user.groups.length"> <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"> <span *ngFor="let group of user.groups; let last = last">
{{ group.getTitle() | translate }}<span *ngIf="!last">,</span> {{ group.getTitle() | translate }}<span *ngIf="!last">,</span>
</span> </span>
@ -74,6 +74,10 @@
<div *ngIf="user.number" class="spacer-top-5"> <div *ngIf="user.number" class="spacer-top-5">
<os-icon-container icon="perm_identity">{{ user.number }}</os-icon-container> <os-icon-container icon="perm_identity">{{ user.number }}</os-icon-container>
</div> </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>
</div> </div>
@ -90,9 +94,7 @@
</mat-icon> </mat-icon>
<!-- Has comment indicator --> <!-- Has comment indicator -->
<mat-icon inline *ngIf="!!user.comment" matTooltip="{{ user.comment }}"> <mat-icon inline *ngIf="!!user.comment" matTooltip="{{ user.comment }}"> comment </mat-icon>
comment
</mat-icon>
<os-icon-container *ngIf="user.isSamlUser" icon="device_hub" <os-icon-container *ngIf="user.isSamlUser" icon="device_hub"
><span>{{ 'Is SAML user' | translate }}</span></os-icon-container ><span>{{ 'Is SAML user' | translate }}</span></os-icon-container
@ -270,6 +272,14 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </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-form-field>
<mat-select placeholder="{{ 'Gender' | translate }}" [(ngModel)]="infoDialog.gender"> <mat-select placeholder="{{ 'Gender' | translate }}" [(ngModel)]="infoDialog.gender">
<mat-option>-</mat-option> <mat-option>-</mat-option>
@ -300,6 +310,7 @@
color="accent" color="accent"
[mat-dialog-close]="{ [mat-dialog-close]="{
groups_id: infoDialog.groups_id, groups_id: infoDialog.groups_id,
vote_delegated_from_users_id: infoDialog.vote_delegated_from_users_id,
gender: infoDialog.gender, gender: infoDialog.gender,
number: infoDialog.number, number: infoDialog.number,
structure_level: infoDialog.structure_level 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 { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid'; import { PblColumnDefinition } from '@pebula/ngrid';
import { BehaviorSubject } from 'rxjs';
import { OperatorService, Permission } from 'app/core/core-services/operator.service'; import { OperatorService, Permission } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
@ -55,6 +56,11 @@ interface InfoDialog {
* Structure level for one user. * Structure level for one user.
*/ */
structure_level: string; 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 groups: ViewGroup[];
public readonly users: BehaviorSubject<ViewUser[]> = new BehaviorSubject<ViewUser[]>([]);
/** /**
* The list of all genders. * The list of all genders.
*/ */
@ -187,6 +195,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
// enable multiSelect for this listView // enable multiSelect for this listView
this.canMultiSelect = true; this.canMultiSelect = true;
this.users = this.repo.getViewModelListBehaviorSubject();
config.get<boolean>('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state)); 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>('users_activate_vote_weight').subscribe(active => (this.isVoteWeightActive = active));
config.get<boolean>(this.selfPresentConfStr).subscribe(allowed => (this.allowSelfSetPresent = allowed)); config.get<boolean>(this.selfPresentConfStr).subscribe(allowed => (this.allowSelfSetPresent = allowed));
@ -203,9 +212,12 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
// Initialize the groups // Initialize the groups
this.groups = this.groupRepo.getViewModelList().filter(group => group.id !== 1); this.groups = this.groupRepo.getViewModelList().filter(group => group.id !== 1);
this.groupRepo
.getViewModelListObservable() this.subscriptions.push(
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1))); 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, groups_id: user.groups_id,
gender: user.gender, gender: user.gender,
structure_level: user.structure_level, 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); 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() getDialogTitle: () => this.getTitle()
}; };
} }
public canVoteFor(user: ViewUser): boolean {
return this.vote_delegated_from_users_id.includes(user.id);
}
} }
interface IUserRelations { interface IUserRelations {
groups: ViewGroup[]; groups: ViewGroup[];
voteDelegatedTo: ViewUser;
voteDelegationsFrom: ViewUser[];
} }
export interface ViewUser extends User, IUserRelations {} 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.db_amount_global_no = Decimal(0)
poll.save() poll.save()
def handle_analog_vote(self, data, poll, user): def handle_analog_vote(self, data, poll):
for field in ["votesvalid", "votesinvalid", "votescast"]: for field in ["votesvalid", "votesinvalid", "votescast"]:
setattr(poll, field, data[field]) setattr(poll, field, data[field])
@ -336,7 +336,7 @@ class AssignmentPollViewSet(BasePollViewSet):
poll.save() poll.save()
def validate_vote_data(self, data, poll, user): def validate_vote_data(self, data, poll):
""" """
Request data: Request data:
analog: analog:
@ -478,10 +478,12 @@ class AssignmentPollViewSet(BasePollViewSet):
options_data = data 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 Helper function for handle_(named|pseudoanonymous)_vote
Assumes data is already validated 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() options = poll.get_options()
if isinstance(data, dict): if isinstance(data, dict):
@ -495,30 +497,46 @@ class AssignmentPollViewSet(BasePollViewSet):
if config["users_activate_vote_weight"]: if config["users_activate_vote_weight"]:
weight *= vote_weight weight *= vote_weight
vote = AssignmentVote.objects.create( 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) inform_changed_data(vote, no_delete_on_restriction=True)
else: # global_no or global_abstain else: # global_no or global_abstain
option = options[0] option = options[0]
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1) weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
vote = AssignmentVote.objects.create( 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(vote, no_delete_on_restriction=True)
inform_changed_data(option) inform_changed_data(option)
inform_changed_data(poll) 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 Helper function for handle_(named|pseudoanonymous)_vote
Assumes data is already validated 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() options = poll.get_options()
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1) weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
for option_id, result in data.items(): for option_id, result in data.items():
option = options.get(pk=option_id) option = options.get(pk=option_id)
vote = AssignmentVote.objects.create( 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(vote, no_delete_on_restriction=True)
inform_changed_data(option, 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 = AssignmentPoll.voted.through
VotedModel.objects.create(assignmentpoll=poll, user=user) 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: 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 ( elif poll.pollmethod in (
AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YN,
AssignmentPoll.POLLMETHOD_YNA, AssignmentPoll.POLLMETHOD_YNA,
): ):
self.create_votes_types_yn_yna(data, poll, user.vote_weight, 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): def handle_pseudoanonymous_vote(self, data, poll, user):
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES: 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 ( elif poll.pollmethod in (
AssignmentPoll.POLLMETHOD_YN, AssignmentPoll.POLLMETHOD_YN,
AssignmentPoll.POLLMETHOD_YNA, AssignmentPoll.POLLMETHOD_YNA,
): ):
self.create_votes_types_yn_yna(data, poll, user.vote_weight, None) self.create_votes_types_yn_yna(data, poll, user.vote_weight, None, None)
def convert_option_data(self, poll, data): def convert_option_data(self, poll, data):
poll_options = poll.get_options() poll_options = poll.get_options()

View File

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

View File

@ -2,8 +2,13 @@ import json
from typing import Any, Dict, List from typing import Any, Dict, List
from ..poll.views import BasePoll from ..poll.views import BasePoll
from ..utils import logging
from ..utils.access_permissions import BaseAccessPermissions 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): class BaseVoteAccessPermissions(BaseAccessPermissions):
@ -26,6 +31,7 @@ class BaseVoteAccessPermissions(BaseAccessPermissions):
for vote in full_data for vote in full_data
if vote["pollstate"] == BasePoll.STATE_PUBLISHED if vote["pollstate"] == BasePoll.STATE_PUBLISHED
or vote["user_id"] == user_id or vote["user_id"] == user_id
or vote["delegated_user_id"] == user_id
] ]
return data return data
@ -71,8 +77,24 @@ class BasePollAccessPermissions(BaseAccessPermissions):
""" """
# add has_voted for all users to check whether op has voted # 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: for poll in full_data:
poll["user_has_voted"] = user_id in poll["voted_id"] 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): if await async_has_perm(user_id, self.manage_permission):
data = full_data data = full_data

View File

@ -29,6 +29,14 @@ class BaseVote(models.Model):
blank=True, blank=True,
on_delete=SET_NULL_AND_AUTOUPDATE, 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: class Meta:
abstract = True abstract = True

View File

@ -12,7 +12,15 @@ from ..utils.rest_api import (
from .models import BasePoll 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): class BaseVoteSerializer(ModelSerializer):

View File

@ -1,5 +1,6 @@
from textwrap import dedent from textwrap import dedent
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.db import transaction from django.db import transaction
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
@ -104,8 +105,8 @@ class BasePollViewSet(ModelViewSet):
# convert user ids to option ids # convert user ids to option ids
self.convert_option_data(poll, vote_data) self.convert_option_data(poll, vote_data)
self.validate_vote_data(vote_data, poll, request.user) self.validate_vote_data(vote_data, poll)
self.handle_analog_vote(vote_data, poll, request.user) self.handle_analog_vote(vote_data, poll)
if request.data.get("publish_immediately"): if request.data.get("publish_immediately"):
poll.state = BasePoll.STATE_PUBLISHED poll.state = BasePoll.STATE_PUBLISHED
@ -198,25 +199,41 @@ class BasePollViewSet(ModelViewSet):
if isinstance(request.user, AnonymousUser): if isinstance(request.user, AnonymousUser):
self.permission_denied(request) self.permission_denied(request)
# check permissions based on poll type and handle requests # data format is:
self.assert_can_vote(poll, request) # { 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 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: if poll.type == BasePoll.TYPE_ANALOG:
self.handle_analog_vote(data, poll, request.user) self.handle_analog_vote(vote_data, poll)
if request.data.get("publish_immediately") == "1": if vote_data.get("publish_immediately") == "1":
poll.state = BasePoll.STATE_PUBLISHED poll.state = BasePoll.STATE_PUBLISHED
else: else:
poll.state = BasePoll.STATE_FINISHED poll.state = BasePoll.STATE_FINISHED
poll.save() poll.save()
elif poll.type == BasePoll.TYPE_NAMED: 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: 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) inform_changed_data(poll)
@ -231,13 +248,16 @@ class BasePollViewSet(ModelViewSet):
inform_changed_data(poll.get_votes(), final_data=True) inform_changed_data(poll.get_votes(), final_data=True)
return Response() 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). 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 Analog: has to have manage permissions
Named & Pseudoanonymous: has to be in a poll group and present Named & Pseudoanonymous: has to be in a poll group and present
""" """
if request.user != vote_user and request.user != vote_user.vote_delegated_to:
self.permission_denied(request)
if poll.type == BasePoll.TYPE_ANALOG: if poll.type == BasePoll.TYPE_ANALOG:
if not self.has_manage_permissions(): if not self.has_manage_permissions():
self.permission_denied(request) self.permission_denied(request)
@ -246,14 +266,14 @@ class BasePollViewSet(ModelViewSet):
raise ValidationError("You can only vote on a started poll.") raise ValidationError("You can only vote on a started poll.")
if not request.user.is_present or not in_some_groups( 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)), list(poll.groups.values_list("pk", flat=True)),
exact=True, exact=True,
): ):
self.permission_denied(request) self.permission_denied(request)
try: try:
self.add_user_to_voted_array(request.user, poll) self.add_user_to_voted_array(vote_user, poll)
inform_changed_data(poll) inform_changed_data(poll)
except IntegrityError: except IntegrityError:
raise ValidationError({"detail": "You have already voted"}) raise ValidationError({"detail": "You have already voted"})
@ -292,20 +312,20 @@ class BasePollViewSet(ModelViewSet):
""" """
raise NotImplementedError() 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. To be implemented by subclass. Validates the data according to poll type and method and fields by validated versions.
Raises ValidationError on failure Raises ValidationError on failure
""" """
raise NotImplementedError() 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 To be implemented by subclass. Handles the analog vote. Assumes data is validated
""" """
raise NotImplementedError() 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. To be implemented by subclass. Handles the named vote. Assumes data is validated.
Needs to manage the voted-array per option. 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 = set(little_data_fields)
own_data_fields.add("email") own_data_fields.add("email")
own_data_fields.add("gender") 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. # Check user permissions.
if await async_has_perm(user_id, "users.can_see_name"): 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 ..core.config import config
from ..utils.auth import GROUP_ADMIN_PK 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 ( from .access_permissions import (
GroupAccessPermissions, GroupAccessPermissions,
PersonalNoteAccessPermissions, PersonalNoteAccessPermissions,
@ -54,7 +58,8 @@ class UserManager(BaseUserManager):
queryset=Permission.objects.select_related("content_type"), queryset=Permission.objects.select_related("content_type"),
) )
), ),
) ),
"vote_delegated_from_users",
) )
def create_user(self, username, password, skip_autoupdate=False, **kwargs): 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 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() objects = UserManager()
class Meta: class Meta:

View File

@ -7,6 +7,7 @@ from ..utils.rest_api import (
JSONField, JSONField,
ModelSerializer, ModelSerializer,
RelatedField, RelatedField,
SerializerMethodField,
ValidationError, ValidationError,
) )
from ..utils.validate import validate_html_strict from ..utils.validate import validate_html_strict
@ -36,6 +37,8 @@ USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
"comment", "comment",
"is_active", "is_active",
"auth_type", "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: class Meta:
model = User model = User
fields = USERCANSEEEXTRASERIALIZER_FIELDS + ( fields = USERCANSEEEXTRASERIALIZER_FIELDS + (
"default_password", "default_password",
"session_auth_hash", "session_auth_hash",
"vote_delegated_to",
) )
read_only_fields = ("last_email_send", "auth_type") read_only_fields = ("last_email_send", "auth_type")
@ -119,6 +125,13 @@ class UserSerializer(ModelSerializer):
inform_changed_data(user) inform_changed_data(user)
return 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): class PermissionRelatedField(RelatedField):
""" """

View File

@ -174,9 +174,77 @@ class UserViewSet(ModelViewSet):
): ):
request.data["username"] = user.username 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) 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 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): def destroy(self, request, *args, **kwargs):
""" """
Customized view endpoint to delete an user. Customized view endpoint to delete an user.
@ -391,6 +459,7 @@ class UserViewSet(ModelViewSet):
data = serializer.prepare_password(serializer.data) data = serializer.prepare_password(serializer.data)
groups = data["groups_id"] groups = data["groups_id"]
del data["groups_id"] del data["groups_id"]
del data["vote_delegated_from_users_id"]
db_user = User(**data) db_user = User(**data)
try: try:

View File

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

View File

@ -167,6 +167,7 @@ class CreateMotionPoll(TestCase):
"id": 1, "id": 1,
"voted_id": [], "voted_id": [],
"user_has_voted": False, "user_has_voted": False,
"user_has_voted_for_delegations": [],
}, },
) )
self.assertEqual(autoupdate[1], []) self.assertEqual(autoupdate[1], [])
@ -610,12 +611,14 @@ class VoteMotionPollAnalog(TestCase):
response = self.client.post( response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), reverse("motionpoll-vote", args=[self.poll.pk]),
{ {
"Y": "1", "data": {
"N": "2.35", "Y": "1",
"A": "-1", "N": "2.35",
"votesvalid": "4.64", "A": "-1",
"votesinvalid": "-2", "votesvalid": "4.64",
"votescast": "-2", "votesinvalid": "-2",
"votescast": "-2",
},
}, },
) )
self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -634,14 +637,23 @@ class VoteMotionPollAnalog(TestCase):
def test_vote_no_permissions(self): def test_vote_no_permissions(self):
self.start_poll() self.start_poll()
self.make_admin_delegate() 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) 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): def test_vote_missing_data(self):
self.start_poll() self.start_poll()
response = self.client.post( 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -649,7 +661,7 @@ class VoteMotionPollAnalog(TestCase):
def test_vote_wrong_data_format(self): def test_vote_wrong_data_format(self):
self.start_poll() self.start_poll()
response = self.client.post( 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -658,7 +670,7 @@ class VoteMotionPollAnalog(TestCase):
self.start_poll() self.start_poll()
response = self.client.post( response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -668,12 +680,14 @@ class VoteMotionPollAnalog(TestCase):
self.client.post( self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), reverse("motionpoll-vote", args=[self.poll.pk]),
{ {
"Y": "3", "data": {
"N": "1", "Y": "3",
"A": "5", "N": "1",
"votesvalid": "-2", "A": "5",
"votesinvalid": "1", "votesvalid": "-2",
"votescast": "-1", "votesinvalid": "1",
"votescast": "-1",
},
}, },
) )
self.poll.state = 3 self.poll.state = 3
@ -681,12 +695,14 @@ class VoteMotionPollAnalog(TestCase):
response = self.client.post( response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), reverse("motionpoll-vote", args=[self.poll.pk]),
{ {
"Y": "1", "data": {
"N": "2.35", "Y": "1",
"A": "-1", "N": "2.35",
"votesvalid": "4.64", "A": "-1",
"votesinvalid": "-2", "votesvalid": "4.64",
"votescast": "3", "votesinvalid": "-2",
"votescast": "3",
},
}, },
) )
self.assertHttpStatusVerbose(response, status.HTTP_200_OK) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
@ -749,7 +765,7 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() self.make_admin_present()
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
@ -773,7 +789,7 @@ class VoteMotionPollNamed(TestCase):
self.admin.vote_weight = weight = Decimal("3.5") self.admin.vote_weight = weight = Decimal("3.5")
self.admin.save() self.admin.save()
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
@ -799,11 +815,11 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() self.make_admin_present()
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
@ -824,38 +840,35 @@ class VoteMotionPollNamed(TestCase):
config["general_system_enable_anonymous"] = True config["general_system_enable_anonymous"] = True
guest_client = APIClient() guest_client = APIClient()
response = guest_client.post( 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) 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): def test_vote_wrong_state(self):
self.make_admin_present() self.make_admin_present()
self.make_admin_delegate() 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_wrong_group(self): def test_vote_wrong_group(self):
self.start_poll() self.start_poll()
self.make_admin_present() 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_not_present(self): def test_vote_not_present(self):
self.start_poll() self.start_poll()
self.make_admin_delegate() 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -863,7 +876,9 @@ class VoteMotionPollNamed(TestCase):
self.start_poll() self.start_poll()
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -872,11 +887,118 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() self.make_admin_present()
response = self.client.post( 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) 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): class VoteMotionPollNamedAutoupdates(TestCase):
"""3 important users: """3 important users:
@ -916,7 +1038,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
def test_vote(self): def test_vote(self):
response = self.user_client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
@ -942,6 +1064,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"options_id": [1], "options_id": [1],
"id": 1, "id": 1,
"user_has_voted": False, "user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id], "voted_id": [self.user.id],
}, },
"motions/motion-vote:1": { "motions/motion-vote:1": {
@ -950,6 +1073,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"weight": "1.000000", "weight": "1.000000",
"value": "A", "value": "A",
"user_id": self.user.id, "user_id": self.user.id,
"delegated_user_id": self.user.id,
"option_id": 1, "option_id": 1,
}, },
"motions/motion-option:1": { "motions/motion-option:1": {
@ -975,6 +1099,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"weight": "1.000000", "weight": "1.000000",
"value": "A", "value": "A",
"user_id": self.user.id, "user_id": self.user.id,
"delegated_user_id": self.user.id,
}, },
) )
self.assertEqual( self.assertEqual(
@ -1001,6 +1126,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"options_id": [1], "options_id": [1],
"id": 1, "id": 1,
"user_has_voted": user == self.user, "user_has_voted": user == self.user,
"user_has_voted_for_delegations": [],
}, },
) )
self.assertEqual( self.assertEqual(
@ -1055,7 +1181,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
def test_vote(self): def test_vote(self):
response = self.user_client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
@ -1081,6 +1207,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"options_id": [1], "options_id": [1],
"id": 1, "id": 1,
"user_has_voted": False, "user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [self.user.id], "voted_id": [self.user.id],
}, },
"motions/motion-vote:1": { "motions/motion-vote:1": {
@ -1090,6 +1217,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"weight": "1.000000", "weight": "1.000000",
"value": "A", "value": "A",
"user_id": None, "user_id": None,
"delegated_user_id": None,
}, },
"motions/motion-option:1": { "motions/motion-option:1": {
"abstain": "1.000000", "abstain": "1.000000",
@ -1122,6 +1250,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"options_id": [1], "options_id": [1],
"id": 1, "id": 1,
"user_has_voted": user == self.user, "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_delegate()
self.make_admin_present() self.make_admin_present()
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get() poll = MotionPoll.objects.get()
@ -1199,11 +1328,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() self.make_admin_present()
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
response = self.client.post( 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) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
option = MotionPoll.objects.get().options.get() option = MotionPoll.objects.get().options.get()
@ -1219,7 +1348,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
config["general_system_enable_anonymous"] = True config["general_system_enable_anonymous"] = True
guest_client = APIClient() guest_client = APIClient()
response = guest_client.post( 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1227,21 +1356,27 @@ class VoteMotionPollPseudoanonymous(TestCase):
def test_vote_wrong_state(self): def test_vote_wrong_state(self):
self.make_admin_present() self.make_admin_present()
self.make_admin_delegate() 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_wrong_group(self): def test_vote_wrong_group(self):
self.start_poll() self.start_poll()
self.make_admin_present() 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
def test_vote_not_present(self): def test_vote_not_present(self):
self.start_poll() self.start_poll()
self.make_admin_delegate() 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.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1249,7 +1384,9 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.start_poll() self.start_poll()
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1258,7 +1395,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate() self.make_admin_delegate()
self.make_admin_present() self.make_admin_present()
response = self.client.post( 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.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists()) self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -1344,6 +1481,7 @@ class PublishMotionPoll(TestCase):
"options_id": [1], "options_id": [1],
"id": 1, "id": 1,
"user_has_voted": False, "user_has_voted": False,
"user_has_voted_for_delegations": [],
"voted_id": [], "voted_id": [],
}, },
"motions/motion-vote:1": { "motions/motion-vote:1": {
@ -1353,6 +1491,7 @@ class PublishMotionPoll(TestCase):
"weight": "2.000000", "weight": "2.000000",
"value": "N", "value": "N",
"user_id": None, "user_id": None,
"delegated_user_id": None,
}, },
"motions/motion-option:1": { "motions/motion-option:1": {
"abstain": "0.000000", "abstain": "0.000000",
@ -1495,3 +1634,45 @@ class ResetMotionPoll(TestCase):
for user in (self.admin, self.user1, self.user2): for user in (self.admin, self.user1, self.user2):
self.assertDeletedAutoupdate(self.vote1, user=user) self.assertDeletedAutoupdate(self.vote1, user=user)
self.assertDeletedAutoupdate(self.vote2, user=user) self.assertDeletedAutoupdate(self.vote2, user=user)
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: Tests that only the following db queries are done:
* 2 requests to get the list of all users and * 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): for index in range(10):
User.objects.create(username=f"user{index}") 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) @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). # The user is not allowed to change some other fields (like last_name).
self.assertNotEqual(user.last_name, "New name fae1Bu1Eyeis9eRox4xu") 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): class UserDelete(TestCase):
""" """

View File

@ -1,6 +1,7 @@
from decimal import Decimal
from unittest import TestCase 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() # TODO: test for MotionPoll.set_options()
@ -50,3 +51,25 @@ class MotionChangeRecommendationTest(TestCase):
other_recommendations other_recommendations
) )
self.assertFalse(collides) 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")