Merge pull request #5541 from jsangmeister/vote-delegation
Add vote delegation
This commit is contained in:
commit
8d2a7f1b12
@ -7,8 +7,8 @@ import { HttpService } from 'app/core/core-services/http.service';
|
||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { UserVote } from 'app/shared/models/poll/base-vote';
|
||||
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
@ -97,7 +97,6 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
translate: TranslateService,
|
||||
relationManager: RelationManagerService,
|
||||
votingService: VotingService,
|
||||
http: HttpService
|
||||
) {
|
||||
super(
|
||||
@ -110,7 +109,6 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||
AssignmentPoll,
|
||||
AssignmentPollRelations,
|
||||
{},
|
||||
votingService,
|
||||
http
|
||||
);
|
||||
}
|
||||
@ -123,14 +121,11 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||
};
|
||||
|
||||
public vote(data: VotingData, poll_id: number): Promise<void> {
|
||||
let requestData;
|
||||
if (data.global) {
|
||||
requestData = `"${data.global}"`;
|
||||
} else {
|
||||
requestData = data.votes;
|
||||
}
|
||||
|
||||
public vote(data: VotingData, poll_id: number, userId?: number): Promise<void> {
|
||||
const requestData: UserVote = {
|
||||
data: data.global ?? data.votes,
|
||||
user_id: userId ?? undefined
|
||||
};
|
||||
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, requestData);
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,8 @@ import { HttpService } from 'app/core/core-services/http.service';
|
||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { UserVote, VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
@ -66,7 +65,6 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
translate: TranslateService,
|
||||
relationManager: RelationManagerService,
|
||||
votingService: VotingService,
|
||||
http: HttpService
|
||||
) {
|
||||
super(
|
||||
@ -79,7 +77,6 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
|
||||
MotionPoll,
|
||||
MotionPollRelations,
|
||||
{},
|
||||
votingService,
|
||||
http
|
||||
);
|
||||
}
|
||||
@ -92,7 +89,11 @@ export class MotionPollRepositoryService extends BasePollRepositoryService<
|
||||
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||
};
|
||||
|
||||
public vote(vote: VoteValue, poll_id: number): Promise<void> {
|
||||
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, JSON.stringify(vote));
|
||||
public vote(vote: VoteValue, poll_id: number, userId?: number): Promise<void> {
|
||||
const requestData: UserVote = {
|
||||
data: vote,
|
||||
user_id: userId ?? undefined
|
||||
};
|
||||
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, requestData);
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,18 @@ const UserRelations: RelationDefinition[] = [
|
||||
ownIdKey: 'groups_id',
|
||||
ownKey: 'groups',
|
||||
foreignViewModel: ViewGroup
|
||||
},
|
||||
{
|
||||
type: 'M2O',
|
||||
ownIdKey: 'vote_delegated_to_id',
|
||||
ownKey: 'voteDelegatedTo',
|
||||
foreignViewModel: ViewUser
|
||||
},
|
||||
{
|
||||
type: 'M2M',
|
||||
ownIdKey: 'vote_delegated_from_users_id',
|
||||
ownKey: 'voteDelegationsFrom',
|
||||
foreignViewModel: ViewUser
|
||||
}
|
||||
];
|
||||
|
||||
@ -255,6 +267,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
|
||||
public async update(update: Partial<User>, viewModel: ViewUser): Promise<void> {
|
||||
this.preventAlterationOnDemoUsers(viewModel);
|
||||
console.log('update: ', update);
|
||||
|
||||
return super.update(update, viewModel);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { OperatorService } from '../core-services/operator.service';
|
||||
|
||||
export enum VotingError {
|
||||
@ -9,19 +12,21 @@ export enum VotingError {
|
||||
POLL_WRONG_TYPE,
|
||||
USER_HAS_NO_PERMISSION,
|
||||
USER_IS_ANONYMOUS,
|
||||
USER_NOT_PRESENT
|
||||
USER_NOT_PRESENT,
|
||||
USER_HAS_DELEGATED_RIGHT
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: It appears that the only message that makes sense for the user to see it the last one.
|
||||
*/
|
||||
export const VotingErrorVerbose = {
|
||||
1: "You can't vote on this poll right now because it's not in the 'Started' state.",
|
||||
2: "You can't vote on this poll because its type is set to analog voting.",
|
||||
3: "You don't have permission to vote on this poll.",
|
||||
4: 'You have to be logged in to be able to vote.',
|
||||
5: 'You have to be present to vote on a poll.',
|
||||
6: "You have already voted on this poll. You can't change your vote in a pseudoanonymous poll."
|
||||
const VotingErrorVerbose = {
|
||||
1: _("You can not vote on this poll right now because it is not in the 'Started' state."),
|
||||
2: _('You can not vote on this poll because its type is set to analog voting.'),
|
||||
3: _('You do not not have the permission to vote on this poll.'),
|
||||
4: _('You have to be logged in to be able to vote.'),
|
||||
5: _('You have to be present to vote on a poll.'),
|
||||
6: _('Your right to vote was delegated to another user.'),
|
||||
7: _('You have already voted on this poll. You can not change your vote in a pseudoanonymous poll.')
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
@ -33,8 +38,8 @@ export class VotingService {
|
||||
/**
|
||||
* checks whether the operator can vote on the given poll
|
||||
*/
|
||||
public canVote(poll: ViewBasePoll): boolean {
|
||||
const error = this.getVotePermissionError(poll);
|
||||
public canVote(poll: ViewBasePoll, user?: ViewUser): boolean {
|
||||
const error = this.getVotePermissionError(poll, user);
|
||||
return !error;
|
||||
}
|
||||
|
||||
@ -42,12 +47,11 @@ export class VotingService {
|
||||
* checks whether the operator can vote on the given poll
|
||||
* @returns null if no errors exist (= user can vote) or else a VotingError
|
||||
*/
|
||||
public getVotePermissionError(poll: ViewBasePoll): VotingError | void {
|
||||
public getVotePermissionError(poll: ViewBasePoll, user: ViewUser = this.operator.viewUser): VotingError | void {
|
||||
if (this.operator.isAnonymous) {
|
||||
return VotingError.USER_IS_ANONYMOUS;
|
||||
}
|
||||
|
||||
const user = this.operator.user;
|
||||
if (!poll.groups_id.intersect(user.groups_id).length) {
|
||||
return VotingError.USER_HAS_NO_PERMISSION;
|
||||
}
|
||||
@ -57,13 +61,16 @@ export class VotingService {
|
||||
if (poll.state !== PollState.Started) {
|
||||
return VotingError.POLL_WRONG_STATE;
|
||||
}
|
||||
if (!user.is_present) {
|
||||
if (!user.is_present && !this.operator.viewUser.canVoteFor(user)) {
|
||||
return VotingError.USER_NOT_PRESENT;
|
||||
}
|
||||
if (user.isVoteRightDelegated && this.operator.user.id === user.id) {
|
||||
return VotingError.USER_HAS_DELEGATED_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
public getVotePermissionErrorVerbose(poll: ViewBasePoll): string | void {
|
||||
const error = this.getVotePermissionError(poll);
|
||||
public getVotePermissionErrorVerbose(poll: ViewBasePoll, user: ViewUser = this.operator.viewUser): string | void {
|
||||
const error = this.getVotePermissionError(poll, user);
|
||||
if (error) {
|
||||
return VotingErrorVerbose[error];
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!multiple && includeNone">
|
||||
<mat-option>
|
||||
<mat-option [value]="null">
|
||||
{{ noneTitle | translate }}
|
||||
</mat-option>
|
||||
<mat-divider></mat-divider>
|
||||
|
@ -56,8 +56,9 @@ export abstract class BasePoll<
|
||||
public votescast: number;
|
||||
public groups_id: number[];
|
||||
public majority_method: MajorityMethod;
|
||||
public voted_id: number[];
|
||||
public user_has_voted: boolean;
|
||||
|
||||
public user_has_voted_for_delegations: number[];
|
||||
public pollmethod: PM;
|
||||
public onehundred_percent_base: PB;
|
||||
|
||||
|
@ -16,6 +16,13 @@ export const GeneralValueVerbose = {
|
||||
votesabstain: 'Votes abstain'
|
||||
};
|
||||
|
||||
export interface UserVote {
|
||||
// the voting payload is hard to describe.
|
||||
// Can be "VoteValue" or any userID-Number sequence in combination with any VoteValue
|
||||
data: Object;
|
||||
user_id?: number;
|
||||
}
|
||||
|
||||
export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
|
||||
public weight: number;
|
||||
public value: VoteValue;
|
||||
|
@ -30,6 +30,8 @@ export class User extends BaseDecimalModel<User> {
|
||||
public is_present: boolean;
|
||||
public is_committee: boolean;
|
||||
public email?: string;
|
||||
public vote_delegated_to_id: number;
|
||||
public vote_delegated_from_users_id: number[];
|
||||
public last_email_send?: string; // ISO datetime string
|
||||
public comment?: string;
|
||||
public is_active?: boolean;
|
||||
@ -41,6 +43,10 @@ export class User extends BaseDecimalModel<User> {
|
||||
return this.vote_weight === 1;
|
||||
}
|
||||
|
||||
public get isVoteRightDelegated(): boolean {
|
||||
return !!this.vote_delegated_to_id;
|
||||
}
|
||||
|
||||
public constructor(input?: Partial<User>) {
|
||||
super(User.COLLECTIONSTRING, input);
|
||||
}
|
||||
|
@ -1,5 +1,27 @@
|
||||
<ng-container *ngIf="poll">
|
||||
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted && !deliveringVote; else cannotVote">
|
||||
<!-- own voting -->
|
||||
<ng-container [ngTemplateOutlet]="votingArea"></ng-container>
|
||||
|
||||
<!-- Delegations -->
|
||||
<ng-container *ngIf="user.is_present">
|
||||
<div class="assignment-vote-delegation" *ngFor="let delegation of delegations">
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="votingArea"
|
||||
[ngTemplateOutletContext]="{ delegation: delegation }"
|
||||
></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #votingArea let-delegation="delegation">
|
||||
<h4 *ngIf="delegation" class="assignment-delegation-title">
|
||||
<span>{{ 'Vote delegation for' | translate }}</span>
|
||||
<span> {{ delegation.getFullName() }}</span>
|
||||
</h4>
|
||||
|
||||
<ng-container *ngIf="canVote(delegation)">
|
||||
<!-- Poll hint -->
|
||||
<p *ngIf="pollHint">
|
||||
<i>{{ pollHint }}</i>
|
||||
@ -9,7 +31,7 @@
|
||||
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1">
|
||||
{{ 'Available votes' | translate }}:
|
||||
|
||||
<b> {{ getVotesAvailable() }}/{{ poll.votes_amount }} </b>
|
||||
<b> {{ getVotesAvailable(delegation) }}/{{ poll.votes_amount }} </b>
|
||||
</h4>
|
||||
|
||||
<!-- Options and Actions -->
|
||||
@ -36,14 +58,9 @@
|
||||
<button
|
||||
class="vote-button"
|
||||
mat-raised-button
|
||||
(click)="saveSingleVote(option.id, action.vote)"
|
||||
[disabled]="deliveringVote"
|
||||
[ngClass]="
|
||||
voteRequestData.votes[option.id] === action.vote ||
|
||||
voteRequestData.votes[option.id] === 1
|
||||
? action.css
|
||||
: ''
|
||||
"
|
||||
(click)="saveSingleVote(option.id, action.vote, delegation)"
|
||||
[disabled]="isDeliveringVote(delegation)"
|
||||
[ngClass]="getActionButtonClass(action, option, delegation)"
|
||||
>
|
||||
<mat-icon> {{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
@ -64,9 +81,9 @@
|
||||
<button
|
||||
class="vote-button"
|
||||
mat-raised-button
|
||||
(click)="saveGlobalVote('N')"
|
||||
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
|
||||
[disabled]="deliveringVote"
|
||||
(click)="saveGlobalVote('N', delegation)"
|
||||
[ngClass]="getGlobalNoClass(delegation)"
|
||||
[disabled]="isDeliveringVote(delegation)"
|
||||
>
|
||||
<mat-icon> thumb_down </mat-icon>
|
||||
</button>
|
||||
@ -79,8 +96,9 @@
|
||||
<button
|
||||
class="vote-button"
|
||||
mat-raised-button
|
||||
(click)="saveGlobalVote('A')"
|
||||
[ngClass]="voteRequestData.global === 'A' ? 'voted-abstain' : ''"
|
||||
(click)="saveGlobalVote('A', delegation)"
|
||||
[ngClass]="getGlobalAbstainClass(delegation)"
|
||||
[disabled]="isDeliveringVote(delegation)"
|
||||
>
|
||||
<mat-icon> trip_origin</mat-icon>
|
||||
</button>
|
||||
@ -92,39 +110,47 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- Submit Vote -->
|
||||
<ng-container [ngTemplateOutlet]="sendNow"></ng-container>
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="sendNow"
|
||||
[ngTemplateOutletContext]="{ delegation: delegation }"
|
||||
></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Shows the permission error -->
|
||||
<ng-container *ngIf="!vmanager.canVote(poll)">
|
||||
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
||||
<ng-container
|
||||
*ngIf="!canVote(delegation)"
|
||||
[ngTemplateOutlet]="cannotVote"
|
||||
[ngTemplateOutletContext]="{ delegation: delegation }"
|
||||
>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #cannotVote>
|
||||
<ng-template #cannotVote let-delegation="delegation">
|
||||
<div class="centered-button-wrapper">
|
||||
<div *ngIf="!deliveringVote">
|
||||
<mat-icon class="vote-submitted">
|
||||
check_circle
|
||||
</mat-icon>
|
||||
<!-- Success -->
|
||||
<div *ngIf="hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
|
||||
<mat-icon class="vote-submitted"> check_circle </mat-icon>
|
||||
<br />
|
||||
<span>{{ 'Voting successful.' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="deliveringVote" class="submit-vote-indicator">
|
||||
<!-- Delivering -->
|
||||
<div *ngIf="isDeliveringVote(delegation)" class="submit-vote-indicator">
|
||||
<mat-spinner class="small-spinner"></mat-spinner>
|
||||
<br />
|
||||
<span>{{ 'Delivering vote... Please wait!' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Permission error error -->
|
||||
<div *ngIf="!hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
|
||||
<span>{{ getVotingError(delegation) | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #sendNow>
|
||||
<ng-template #sendNow let-delegation="delegation">
|
||||
<div class="centered-button-wrapper">
|
||||
<button mat-flat-button color="accent" (click)="submitVote()">
|
||||
<mat-icon>
|
||||
how_to_vote
|
||||
</mat-icon>
|
||||
<button mat-flat-button color="accent" (click)="submitVote(delegation)">
|
||||
<mat-icon> how_to_vote </mat-icon>
|
||||
<span>
|
||||
{{ 'Submit vote now' | translate }}
|
||||
</span>
|
||||
|
@ -7,6 +7,14 @@
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.assignment-vote-delegation {
|
||||
margin-top: 1em;
|
||||
|
||||
.assignment-delegation-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.yn-grid {
|
||||
@extend %vote-grid-base;
|
||||
grid-template-areas:
|
||||
|
@ -7,24 +7,17 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import {
|
||||
AssignmentPollRepositoryService,
|
||||
GlobalVote,
|
||||
VotingData
|
||||
GlobalVote
|
||||
} from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BasePollVoteComponentDirective } from 'app/site/polls/components/base-poll-vote.component';
|
||||
|
||||
// TODO: Duplicate
|
||||
interface VoteActions {
|
||||
vote: VoteValue;
|
||||
css: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
|
||||
@Component({
|
||||
selector: 'os-assignment-poll-vote',
|
||||
@ -35,37 +28,60 @@ interface VoteActions {
|
||||
export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<ViewAssignmentPoll> implements OnInit {
|
||||
public AssignmentPollMethod = AssignmentPollMethod;
|
||||
public PollType = PollType;
|
||||
public voteActions: VoteActions[] = [];
|
||||
public voteRequestData: VotingData = {
|
||||
votes: {}
|
||||
};
|
||||
public alreadyVoted: boolean;
|
||||
public voteActions: VoteOption[] = [];
|
||||
|
||||
public get pollHint(): string {
|
||||
return this.poll.assignment.default_poll_description;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
operator: OperatorService,
|
||||
public vmanager: VotingService,
|
||||
votingService: VotingService,
|
||||
private pollRepo: AssignmentPollRepositoryService,
|
||||
private promptService: PromptService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
super(title, translate, matSnackbar, operator);
|
||||
super(title, translate, matSnackbar, operator, votingService);
|
||||
|
||||
// observe user updates to refresh the view on dynamic changes
|
||||
this.subscriptions.push(
|
||||
operator.getViewUserObservable().subscribe(() => {
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
if (this.poll && !this.poll.user_has_voted) {
|
||||
this.alreadyVoted = false;
|
||||
this.createVotingDataObjects();
|
||||
this.defineVoteOptions();
|
||||
} else {
|
||||
this.alreadyVoted = true;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public getActionButtonClass(actions: VoteOption, option: ViewAssignmentOption, user: ViewUser = this.user): string {
|
||||
if (
|
||||
this.voteRequestData[user.id]?.votes[option.id] === actions.vote ||
|
||||
this.voteRequestData[user.id]?.votes[option.id] === 1
|
||||
) {
|
||||
return actions.css;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public get pollHint(): string {
|
||||
return this.poll.assignment.default_poll_description;
|
||||
public getGlobalAbstainClass(user: ViewUser = this.user): string {
|
||||
if (this.voteRequestData[user.id]?.global === 'A') {
|
||||
return 'voted-abstain';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public getGlobalNoClass(user: ViewUser = this.user): string {
|
||||
if (this.voteRequestData[user.id]?.global === 'N') {
|
||||
return 'voted-no';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private defineVoteOptions(): void {
|
||||
@ -76,7 +92,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
|
||||
label: 'Yes'
|
||||
});
|
||||
|
||||
if (this.poll.pollmethod !== AssignmentPollMethod.Votes) {
|
||||
if (this.poll?.pollmethod !== AssignmentPollMethod.Votes) {
|
||||
this.voteActions.push({
|
||||
vote: 'N',
|
||||
css: 'voted-no',
|
||||
@ -85,7 +101,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
|
||||
});
|
||||
}
|
||||
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.YNA) {
|
||||
if (this.poll?.pollmethod === AssignmentPollMethod.YNA) {
|
||||
this.voteActions.push({
|
||||
vote: 'A',
|
||||
css: 'voted-abstain',
|
||||
@ -95,40 +111,48 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
|
||||
}
|
||||
}
|
||||
|
||||
public getVotesCount(): number {
|
||||
return Object.keys(this.voteRequestData.votes).filter(key => this.voteRequestData.votes[key]).length;
|
||||
public getVotesCount(user: ViewUser = this.user): number {
|
||||
if (this.voteRequestData[user.id]) {
|
||||
return Object.keys(this.voteRequestData[user.id].votes).filter(
|
||||
key => this.voteRequestData[user.id].votes[key]
|
||||
).length;
|
||||
}
|
||||
}
|
||||
|
||||
public getVotesAvailable(): number {
|
||||
return this.poll.votes_amount - this.getVotesCount();
|
||||
public getVotesAvailable(user: ViewUser = this.user): number {
|
||||
return this.poll.votes_amount - this.getVotesCount(user);
|
||||
}
|
||||
|
||||
private isGlobalOptionSelected(): boolean {
|
||||
return !!this.voteRequestData.global;
|
||||
private isGlobalOptionSelected(user: ViewUser = this.user): boolean {
|
||||
return !!this.voteRequestData[user.id]?.global;
|
||||
}
|
||||
|
||||
public async submitVote(): Promise<void> {
|
||||
public async submitVote(user: ViewUser = this.user): Promise<void> {
|
||||
const title = this.translate.instant('Submit selection now?');
|
||||
const content = this.translate.instant('Your decision cannot be changed afterwards.');
|
||||
const confirmed = await this.promptService.open(title, content);
|
||||
if (confirmed) {
|
||||
this.deliveringVote = true;
|
||||
this.deliveringVote[user.id] = true;
|
||||
this.cd.markForCheck();
|
||||
this.pollRepo
|
||||
.vote(this.voteRequestData, this.poll.id)
|
||||
.vote(this.voteRequestData[user.id], this.poll.id, user.id)
|
||||
.then(() => {
|
||||
this.alreadyVoted = true;
|
||||
this.alreadyVoted[user.id] = true;
|
||||
})
|
||||
.catch(this.raiseError)
|
||||
.finally(() => {
|
||||
this.deliveringVote = false;
|
||||
this.deliveringVote[user.id] = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public saveSingleVote(optionId: number, vote: VoteValue): void {
|
||||
if (this.isGlobalOptionSelected()) {
|
||||
delete this.voteRequestData.global;
|
||||
public saveSingleVote(optionId: number, vote: VoteValue, user: ViewUser = this.user): void {
|
||||
if (!this.voteRequestData[user.id]) {
|
||||
throw new Error('The user for your voting request does not exist');
|
||||
}
|
||||
|
||||
if (this.isGlobalOptionSelected(user)) {
|
||||
delete this.voteRequestData[user.id].global;
|
||||
}
|
||||
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
|
||||
@ -138,10 +162,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
|
||||
.reduce((o, n) => {
|
||||
o[n] = 0;
|
||||
if (votesAmount === 1) {
|
||||
if (n === optionId && this.voteRequestData.votes[n] !== 1) {
|
||||
if (n === optionId && this.voteRequestData[user.id].votes[n] !== 1) {
|
||||
o[n] = 1;
|
||||
}
|
||||
} else if ((n === optionId) !== (this.voteRequestData.votes[n] === 1)) {
|
||||
} else if ((n === optionId) !== (this.voteRequestData[user.id].votes[n] === 1)) {
|
||||
o[n] = 1;
|
||||
}
|
||||
|
||||
@ -151,11 +175,11 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
|
||||
// check if you can still vote
|
||||
const countedVotes = Object.keys(tmpVoteRequest).filter(key => tmpVoteRequest[key]).length;
|
||||
if (countedVotes <= votesAmount) {
|
||||
this.voteRequestData.votes = tmpVoteRequest;
|
||||
this.voteRequestData[user.id].votes = tmpVoteRequest;
|
||||
|
||||
// if you have no options anymore, try to send
|
||||
if (this.getVotesCount() === votesAmount) {
|
||||
this.submitVote();
|
||||
if (this.getVotesCount(user) === votesAmount) {
|
||||
this.submitVote(user);
|
||||
}
|
||||
} else {
|
||||
this.raiseError(
|
||||
@ -164,26 +188,29 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponentDirective<
|
||||
}
|
||||
} else {
|
||||
// YN/YNA
|
||||
if (this.voteRequestData.votes[optionId] && this.voteRequestData.votes[optionId] === vote) {
|
||||
delete this.voteRequestData.votes[optionId];
|
||||
if (
|
||||
this.voteRequestData[user.id].votes[optionId] &&
|
||||
this.voteRequestData[user.id].votes[optionId] === vote
|
||||
) {
|
||||
delete this.voteRequestData[user.id].votes[optionId];
|
||||
} else {
|
||||
this.voteRequestData.votes[optionId] = vote;
|
||||
this.voteRequestData[user.id].votes[optionId] = vote;
|
||||
}
|
||||
|
||||
// if you filled out every option, try to send
|
||||
if (Object.keys(this.voteRequestData.votes).length === this.poll.options.length) {
|
||||
this.submitVote();
|
||||
if (Object.keys(this.voteRequestData[user.id].votes).length === this.poll.options.length) {
|
||||
this.submitVote(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public saveGlobalVote(globalVote: GlobalVote): void {
|
||||
this.voteRequestData.votes = {};
|
||||
if (this.voteRequestData.global && this.voteRequestData.global === globalVote) {
|
||||
delete this.voteRequestData.global;
|
||||
public saveGlobalVote(globalVote: GlobalVote, user: ViewUser = this.user): void {
|
||||
this.voteRequestData[user.id].votes = {};
|
||||
if (this.voteRequestData[user.id].global && this.voteRequestData[user.id].global === globalVote) {
|
||||
delete this.voteRequestData[user.id].global;
|
||||
} else {
|
||||
this.voteRequestData.global = globalVote;
|
||||
this.submitVote();
|
||||
this.voteRequestData[user.id].global = globalVote;
|
||||
this.submitVote(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,8 @@ export class AssignmentPollComponent
|
||||
this.descriptionForm = this.formBuilder.group({
|
||||
description: this.poll ? this.poll.description : ''
|
||||
});
|
||||
|
||||
console.log('the poll: ', this.poll);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,10 +4,10 @@
|
||||
</p>
|
||||
|
||||
<div *ngIf="poll.pollClassType === 'motion'">
|
||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||
<os-motion-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-motion-poll-vote>
|
||||
</div>
|
||||
|
||||
<div *ngIf="poll.pollClassType === 'assignment'">
|
||||
<os-assignment-poll-vote [poll]="poll"></os-assignment-poll-vote>
|
||||
<os-assignment-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-assignment-poll-vote>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
@ -1,12 +1,34 @@
|
||||
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
|
||||
<div *ngIf="vmanager.canVote(poll) && !deliveringVote" class="vote-button-grid">
|
||||
<ng-container *ngIf="poll">
|
||||
<!-- own voting -->
|
||||
<ng-container [ngTemplateOutlet]="votingArea"></ng-container>
|
||||
|
||||
<!-- Delegations -->
|
||||
<ng-container *ngIf="user.is_present">
|
||||
<div class="motion-vote-delegation" *ngFor="let delegation of delegations">
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="votingArea"
|
||||
[ngTemplateOutletContext]="{ delegation: delegation }"
|
||||
></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #votingArea let-delegation="delegation">
|
||||
<h4 *ngIf="delegation" class="motion-delegation-title">
|
||||
<span>{{ 'Vote delegation for' | translate }}</span>
|
||||
<span> {{ delegation.getFullName() }}</span>
|
||||
</h4>
|
||||
|
||||
<div *ngIf="canVote(delegation)" class="vote-button-grid">
|
||||
<!-- Voting -->
|
||||
<div class="vote-button" *ngFor="let option of voteOptions">
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveVote(option.vote)"
|
||||
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
|
||||
[disabled]="deliveringVote"
|
||||
(click)="saveVote(option.vote, delegation)"
|
||||
[ngClass]="getActionButtonClass(option, delegation)"
|
||||
[disabled]="isDeliveringVote(delegation)"
|
||||
>
|
||||
<mat-icon> {{ option.icon }}</mat-icon>
|
||||
</button>
|
||||
@ -14,21 +36,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="deliveringVote" class="submit-vote-indicator">
|
||||
<ng-container
|
||||
*ngIf="!canVote(delegation)"
|
||||
[ngTemplateOutlet]="cannotVote"
|
||||
[ngTemplateOutletContext]="{ delegation: delegation }"
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<!-- Delivering -->
|
||||
<div *ngIf="isDeliveringVote(delegation)" class="submit-vote-indicator">
|
||||
<mat-spinner class="small-spinner"></mat-spinner>
|
||||
<br />
|
||||
<span>{{ 'Delivering vote... Please wait!' | translate }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #userHasVotes>
|
||||
<div class="user-has-voted">
|
||||
<ng-template #cannotVote let-delegation="delegation">
|
||||
<!-- Success -->
|
||||
<div *ngIf="hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)" class="user-has-voted">
|
||||
<div>
|
||||
<mat-icon class="vote-submitted">
|
||||
check_circle
|
||||
</mat-icon>
|
||||
<mat-icon class="vote-submitted"> check_circle </mat-icon>
|
||||
<br />
|
||||
<span>{{ 'Voting successful.' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div *ngIf="!hasAlreadyVoted(delegation) && !isDeliveringVote(delegation)">
|
||||
<span>{{ getVotingError(delegation) | translate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -8,6 +8,14 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
}
|
||||
|
||||
.motion-vote-delegation {
|
||||
margin-top: 1em;
|
||||
|
||||
.motion-delegation-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-vote-indicator {
|
||||
margin-top: 1em;
|
||||
text-align: center;
|
||||
@ -26,6 +34,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mat-divider-horizontal {
|
||||
position: initial;
|
||||
}
|
||||
|
||||
.user-has-voted {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@ -10,14 +10,8 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { BasePollVoteComponentDirective } from 'app/site/polls/components/base-poll-vote.component';
|
||||
|
||||
interface VoteOption {
|
||||
vote?: VoteValue;
|
||||
css?: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
}
|
||||
import { BasePollVoteComponentDirective, VoteOption } from 'app/site/polls/components/base-poll-vote.component';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
|
||||
@Component({
|
||||
selector: 'os-motion-poll-vote',
|
||||
@ -25,8 +19,7 @@ interface VoteOption {
|
||||
styleUrls: ['./motion-poll-vote.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MotionPollVoteComponent extends BasePollVoteComponentDirective<ViewMotionPoll> {
|
||||
public currentVote: VoteOption = {};
|
||||
export class MotionPollVoteComponent extends BasePollVoteComponentDirective<ViewMotionPoll> implements OnInit {
|
||||
public voteOptions: VoteOption[] = [
|
||||
{
|
||||
vote: 'Y',
|
||||
@ -53,30 +46,56 @@ export class MotionPollVoteComponent extends BasePollVoteComponentDirective<View
|
||||
translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
operator: OperatorService,
|
||||
public vmanager: VotingService,
|
||||
public votingService: VotingService,
|
||||
private pollRepo: MotionPollRepositoryService,
|
||||
private promptService: PromptService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
super(title, translate, matSnackbar, operator);
|
||||
super(title, translate, matSnackbar, operator, votingService);
|
||||
|
||||
// observe user updates to refresh the view on dynamic changes
|
||||
this.subscriptions.push(
|
||||
operator.getViewUserObservable().subscribe(() => {
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async saveVote(vote: VoteValue): Promise<void> {
|
||||
this.currentVote.vote = vote;
|
||||
public ngOnInit(): void {
|
||||
this.createVotingDataObjects();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public getActionButtonClass(voteOption: VoteOption, user: ViewUser = this.user): string {
|
||||
if (this.voteRequestData[user.id]?.vote === voteOption.vote) {
|
||||
return voteOption.css;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public async saveVote(vote: VoteValue, user: ViewUser = this.user): Promise<void> {
|
||||
if (this.voteRequestData[user.id]) {
|
||||
this.voteRequestData[user.id].vote = vote;
|
||||
|
||||
const title = this.translate.instant('Submit selection now?');
|
||||
const content = this.translate.instant('Your decision cannot be changed afterwards.');
|
||||
const confirmed = await this.promptService.open(title, content);
|
||||
|
||||
if (confirmed) {
|
||||
this.deliveringVote = true;
|
||||
this.deliveringVote[user.id] = true;
|
||||
this.cd.markForCheck();
|
||||
|
||||
this.pollRepo
|
||||
.vote(vote, this.poll.id)
|
||||
.vote(vote, this.poll.id, user.id)
|
||||
.then(() => {
|
||||
this.alreadyVoted[user.id] = true;
|
||||
})
|
||||
.catch(this.raiseError)
|
||||
.finally(() => {
|
||||
this.deliveringVote = false;
|
||||
this.deliveringVote[user.id] = false;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
||||
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||
<os-motion-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-motion-poll-vote>
|
||||
</ng-container>
|
||||
|
||||
<!-- Detail link -->
|
||||
|
@ -5,11 +5,20 @@ import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { VotingError } from 'app/core/ui-services/voting.service';
|
||||
import { VotingData } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||
import { VotingError, VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { ViewBasePoll } from '../models/view-base-poll';
|
||||
|
||||
export interface VoteOption {
|
||||
vote?: VoteValue;
|
||||
css?: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> extends BaseViewComponentDirective {
|
||||
@Input()
|
||||
@ -17,22 +26,71 @@ export abstract class BasePollVoteComponentDirective<V extends ViewBasePoll> ext
|
||||
|
||||
public votingErrors = VotingError;
|
||||
|
||||
public deliveringVote = false;
|
||||
protected voteRequestData = {};
|
||||
|
||||
protected alreadyVoted = {};
|
||||
|
||||
protected deliveringVote = {};
|
||||
|
||||
protected user: ViewUser;
|
||||
|
||||
protected delegations: ViewUser[];
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
protected operator: OperatorService
|
||||
operator: OperatorService,
|
||||
protected votingService: VotingService
|
||||
) {
|
||||
super(title, translate, matSnackbar);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.operator.getViewUserObservable().subscribe(user => {
|
||||
operator.getViewUserObservable().subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
this.delegations = user.voteDelegationsFrom;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected createVotingDataObjects(): void {
|
||||
if (this.user) {
|
||||
this.voteRequestData[this.user.id] = {
|
||||
votes: {}
|
||||
} as VotingData;
|
||||
this.alreadyVoted[this.user.id] = this.poll.user_has_voted;
|
||||
this.deliveringVote[this.user.id] = false;
|
||||
}
|
||||
|
||||
if (this.delegations) {
|
||||
for (const delegation of this.delegations) {
|
||||
this.voteRequestData[delegation.id] = {
|
||||
votes: {}
|
||||
} as VotingData;
|
||||
this.alreadyVoted[delegation.id] = this.poll.hasVotedId(delegation.id);
|
||||
this.deliveringVote[delegation.id] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isDeliveringVote(user: ViewUser = this.user): boolean {
|
||||
return this.deliveringVote[user.id] === true;
|
||||
}
|
||||
|
||||
public hasAlreadyVoted(user: ViewUser = this.user): boolean {
|
||||
return this.alreadyVoted[user.id] === true;
|
||||
}
|
||||
|
||||
public canVote(user: ViewUser = this.user): boolean {
|
||||
return (
|
||||
this.votingService.canVote(this.poll, user) && !this.isDeliveringVote(user) && !this.hasAlreadyVoted(user)
|
||||
);
|
||||
}
|
||||
|
||||
public getVotingError(user: ViewUser = this.user): string | void {
|
||||
console.log('error ', this.votingService.getVotePermissionErrorVerbose(this.poll, user));
|
||||
return this.votingService.getVotePermissionErrorVerbose(this.poll, user);
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +39,15 @@ export class PollProgressComponent extends BaseViewComponentDirective implements
|
||||
.getViewModelListObservable()
|
||||
.pipe(
|
||||
map(users =>
|
||||
/**
|
||||
* Filter the users who would be able to vote:
|
||||
* They are present or have their right to vote delegated
|
||||
* They are in one of the voting groups
|
||||
*/
|
||||
users.filter(
|
||||
user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length
|
||||
user =>
|
||||
(user.is_present || user.isVoteRightDelegated) &&
|
||||
this.poll.groups_id.intersect(user.groups_id).length
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -114,6 +114,10 @@ export abstract class ViewBasePoll<
|
||||
|
||||
public canBeVotedFor: () => boolean;
|
||||
|
||||
public hasVotedId(userId: number): boolean {
|
||||
return this.user_has_voted_for_delegations?.includes(userId);
|
||||
}
|
||||
|
||||
public abstract getSlide(): ProjectorElementBuildDeskriptor;
|
||||
|
||||
public abstract getContentObject(): BaseViewModel;
|
||||
|
@ -8,7 +8,6 @@ import { RelationManagerService } from 'app/core/core-services/relation-manager.
|
||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||
import { BaseRepository, NestedModelDescriptors } from 'app/core/repositories/base-repository';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { ModelConstructor } from 'app/shared/models/base/base-model';
|
||||
import { BasePoll, PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { BaseViewModel, TitleInformation } from 'app/site/base/base-view-model';
|
||||
@ -30,7 +29,6 @@ export abstract class BasePollRepositoryService<
|
||||
protected baseModelCtor: ModelConstructor<M>,
|
||||
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [],
|
||||
protected nestedModelDescriptors: NestedModelDescriptors = {},
|
||||
private votingService: VotingService,
|
||||
protected http: HttpService
|
||||
) {
|
||||
super(
|
||||
@ -53,7 +51,7 @@ export abstract class BasePollRepositoryService<
|
||||
protected createViewModelWithTitles(model: M): V {
|
||||
const viewModel = super.createViewModelWithTitles(model);
|
||||
Object.defineProperty(viewModel, 'canBeVotedFor', {
|
||||
value: () => this.votingService.canVote(viewModel)
|
||||
value: () => viewModel.isStarted
|
||||
});
|
||||
return viewModel;
|
||||
}
|
||||
|
@ -164,6 +164,16 @@
|
||||
[inputListValues]="groups"
|
||||
></os-search-value-selector>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Delegate Vote -->
|
||||
<mat-form-field>
|
||||
<os-search-value-selector
|
||||
formControlName="vote_delegated_from_users_id"
|
||||
[multiple]="true"
|
||||
placeholder="{{ 'Transferred voting rights from' | translate }}"
|
||||
[inputListValues]="users"
|
||||
></os-search-value-selector>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isAllowed('manage')">
|
||||
@ -303,6 +313,18 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="isAllowed('manage')">
|
||||
<!-- Own Vote delegations -->
|
||||
<div *ngIf="user.voteDelegatedTo">
|
||||
<h4>{{ 'Vote right delegated to:' | translate }}</h4>
|
||||
<span>{{ user.voteDelegatedTo }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Received Vote delegations -->
|
||||
<div *ngIf="user.voteDelegationsFrom && user.voteDelegationsFrom.length">
|
||||
<h4>{{ 'Vote delegations from' | translate }}</h4>
|
||||
<span>{{ user.voteDelegationsFrom }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Vote weight -->
|
||||
<div *ngIf="user.vote_weight && showVoteWeight">
|
||||
<h4>{{ 'Vote weight' | translate }}</h4>
|
||||
|
@ -71,6 +71,8 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
|
||||
*/
|
||||
public readonly groups: BehaviorSubject<ViewGroup[]> = new BehaviorSubject<ViewGroup[]>([]);
|
||||
|
||||
public readonly users: BehaviorSubject<ViewUser[]> = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
/**
|
||||
* Hold the list of genders (sexes) publicly to dynamically iterate in the view
|
||||
*/
|
||||
@ -124,6 +126,7 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
|
||||
.subscribe(active => (this.isVoteWeightActive = active));
|
||||
|
||||
this.groupRepo.getViewModelListObservableWithoutDefaultGroup().subscribe(this.groups);
|
||||
this.users = this.repo.getViewModelListBehaviorSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,6 +176,7 @@ export class UserDetailComponent extends BaseViewComponentDirective implements O
|
||||
vote_weight: [],
|
||||
about_me: [''],
|
||||
groups_id: [''],
|
||||
vote_delegated_from_users_id: [''],
|
||||
is_present: [true],
|
||||
is_committee: [false],
|
||||
email: ['', Validators.email],
|
||||
|
@ -62,7 +62,7 @@
|
||||
>
|
||||
<div class="groupsCell">
|
||||
<div *ngIf="user.groups && user.groups.length">
|
||||
<os-icon-container icon="people">
|
||||
<os-icon-container icon="people" noWrap="true">
|
||||
<span *ngFor="let group of user.groups; let last = last">
|
||||
{{ group.getTitle() | translate }}<span *ngIf="!last">,</span>
|
||||
</span>
|
||||
@ -74,6 +74,10 @@
|
||||
<div *ngIf="user.number" class="spacer-top-5">
|
||||
<os-icon-container icon="perm_identity">{{ user.number }}</os-icon-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="user.vote_delegated_to_id" class="spacer-top-5">
|
||||
<os-icon-container icon="forward" noWrap="true">{{ user.voteDelegatedTo }}</os-icon-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -90,9 +94,7 @@
|
||||
</mat-icon>
|
||||
|
||||
<!-- Has comment indicator -->
|
||||
<mat-icon inline *ngIf="!!user.comment" matTooltip="{{ user.comment }}">
|
||||
comment
|
||||
</mat-icon>
|
||||
<mat-icon inline *ngIf="!!user.comment" matTooltip="{{ user.comment }}"> comment </mat-icon>
|
||||
|
||||
<os-icon-container *ngIf="user.isSamlUser" icon="device_hub"
|
||||
><span>{{ 'Is SAML user' | translate }}</span></os-icon-container
|
||||
@ -270,6 +272,14 @@
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<os-search-value-selector
|
||||
[(ngModel)]="infoDialog.vote_delegated_from_users_id"
|
||||
[multiple]="true"
|
||||
placeholder="{{ 'Transferred voting rights from' | translate }}"
|
||||
[inputListValues]="users"
|
||||
></os-search-value-selector>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="{{ 'Gender' | translate }}" [(ngModel)]="infoDialog.gender">
|
||||
<mat-option>-</mat-option>
|
||||
@ -300,6 +310,7 @@
|
||||
color="accent"
|
||||
[mat-dialog-close]="{
|
||||
groups_id: infoDialog.groups_id,
|
||||
vote_delegated_from_users_id: infoDialog.vote_delegated_from_users_id,
|
||||
gender: infoDialog.gender,
|
||||
number: infoDialog.number,
|
||||
structure_level: infoDialog.structure_level
|
||||
|
@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PblColumnDefinition } from '@pebula/ngrid';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
@ -55,6 +56,11 @@ interface InfoDialog {
|
||||
* Structure level for one user.
|
||||
*/
|
||||
structure_level: string;
|
||||
|
||||
/**
|
||||
* Transfer voting rights
|
||||
*/
|
||||
vote_delegated_from_users_id: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,6 +88,8 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
||||
*/
|
||||
public groups: ViewGroup[];
|
||||
|
||||
public readonly users: BehaviorSubject<ViewUser[]> = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
/**
|
||||
* The list of all genders.
|
||||
*/
|
||||
@ -187,6 +195,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
||||
|
||||
// enable multiSelect for this listView
|
||||
this.canMultiSelect = true;
|
||||
this.users = this.repo.getViewModelListBehaviorSubject();
|
||||
config.get<boolean>('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state));
|
||||
config.get<boolean>('users_activate_vote_weight').subscribe(active => (this.isVoteWeightActive = active));
|
||||
config.get<boolean>(this.selfPresentConfStr).subscribe(allowed => (this.allowSelfSetPresent = allowed));
|
||||
@ -203,9 +212,12 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
||||
|
||||
// Initialize the groups
|
||||
this.groups = this.groupRepo.getViewModelList().filter(group => group.id !== 1);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.groupRepo
|
||||
.getViewModelListObservable()
|
||||
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
|
||||
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,7 +254,8 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
||||
groups_id: user.groups_id,
|
||||
gender: user.gender,
|
||||
structure_level: user.structure_level,
|
||||
number: user.number
|
||||
number: user.number,
|
||||
vote_delegated_from_users_id: user.vote_delegated_from_users_id
|
||||
};
|
||||
|
||||
const dialogRef = this.dialog.open(this.userInfoDialog, infoDialogSettings);
|
||||
|
@ -82,9 +82,15 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
|
||||
getDialogTitle: () => this.getTitle()
|
||||
};
|
||||
}
|
||||
|
||||
public canVoteFor(user: ViewUser): boolean {
|
||||
return this.vote_delegated_from_users_id.includes(user.id);
|
||||
}
|
||||
}
|
||||
interface IUserRelations {
|
||||
groups: ViewGroup[];
|
||||
voteDelegatedTo: ViewUser;
|
||||
voteDelegationsFrom: ViewUser[];
|
||||
}
|
||||
|
||||
export interface ViewUser extends User, IUserRelations {}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
@ -289,7 +289,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
poll.db_amount_global_no = Decimal(0)
|
||||
poll.save()
|
||||
|
||||
def handle_analog_vote(self, data, poll, user):
|
||||
def handle_analog_vote(self, data, poll):
|
||||
for field in ["votesvalid", "votesinvalid", "votescast"]:
|
||||
setattr(poll, field, data[field])
|
||||
|
||||
@ -336,7 +336,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
|
||||
poll.save()
|
||||
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
def validate_vote_data(self, data, poll):
|
||||
"""
|
||||
Request data:
|
||||
analog:
|
||||
@ -478,10 +478,12 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
|
||||
options_data = data
|
||||
|
||||
def create_votes_type_votes(self, data, poll, vote_weight, vote_user):
|
||||
def create_votes_type_votes(self, data, poll, vote_weight, vote_user, request_user):
|
||||
"""
|
||||
Helper function for handle_(named|pseudoanonymous)_vote
|
||||
Assumes data is already validated
|
||||
vote_user is the user whose vote is given
|
||||
request_user is the user who gives the vote, may be a delegate
|
||||
"""
|
||||
options = poll.get_options()
|
||||
if isinstance(data, dict):
|
||||
@ -495,30 +497,46 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
if config["users_activate_vote_weight"]:
|
||||
weight *= vote_weight
|
||||
vote = AssignmentVote.objects.create(
|
||||
option=option, user=vote_user, weight=weight, value="Y"
|
||||
option=option,
|
||||
user=vote_user,
|
||||
delegated_user=request_user,
|
||||
weight=weight,
|
||||
value="Y",
|
||||
)
|
||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||
else: # global_no or global_abstain
|
||||
option = options[0]
|
||||
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
||||
vote = AssignmentVote.objects.create(
|
||||
option=option, user=vote_user, weight=weight, value=data
|
||||
option=option,
|
||||
user=vote_user,
|
||||
delegated_user=request_user,
|
||||
weight=weight,
|
||||
value=data,
|
||||
)
|
||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||
inform_changed_data(option)
|
||||
inform_changed_data(poll)
|
||||
|
||||
def create_votes_types_yn_yna(self, data, poll, vote_weight, vote_user):
|
||||
def create_votes_types_yn_yna(
|
||||
self, data, poll, vote_weight, vote_user, request_user
|
||||
):
|
||||
"""
|
||||
Helper function for handle_(named|pseudoanonymous)_vote
|
||||
Assumes data is already validated
|
||||
vote_user is the user whose vote is given
|
||||
request_user is the user who gives the vote, may be a delegate
|
||||
"""
|
||||
options = poll.get_options()
|
||||
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
||||
for option_id, result in data.items():
|
||||
option = options.get(pk=option_id)
|
||||
vote = AssignmentVote.objects.create(
|
||||
option=option, user=vote_user, value=result, weight=weight
|
||||
option=option,
|
||||
user=vote_user,
|
||||
delegated_user=request_user,
|
||||
value=result,
|
||||
weight=weight,
|
||||
)
|
||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
||||
inform_changed_data(option, no_delete_on_restriction=True)
|
||||
@ -527,24 +545,28 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
VotedModel = AssignmentPoll.voted.through
|
||||
VotedModel.objects.create(assignmentpoll=poll, user=user)
|
||||
|
||||
def handle_named_vote(self, data, poll, user):
|
||||
def handle_named_vote(self, data, poll, vote_user, request_user):
|
||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||
self.create_votes_type_votes(data, poll, user.vote_weight, user)
|
||||
self.create_votes_type_votes(
|
||||
data, poll, vote_user.vote_weight, vote_user, request_user
|
||||
)
|
||||
elif poll.pollmethod in (
|
||||
AssignmentPoll.POLLMETHOD_YN,
|
||||
AssignmentPoll.POLLMETHOD_YNA,
|
||||
):
|
||||
self.create_votes_types_yn_yna(data, poll, user.vote_weight, user)
|
||||
self.create_votes_types_yn_yna(
|
||||
data, poll, vote_user.vote_weight, vote_user, request_user
|
||||
)
|
||||
|
||||
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||
if poll.pollmethod == AssignmentPoll.POLLMETHOD_VOTES:
|
||||
self.create_votes_type_votes(data, poll, user.vote_weight, None)
|
||||
self.create_votes_type_votes(data, poll, user.vote_weight, None, None)
|
||||
|
||||
elif poll.pollmethod in (
|
||||
AssignmentPoll.POLLMETHOD_YN,
|
||||
AssignmentPoll.POLLMETHOD_YNA,
|
||||
):
|
||||
self.create_votes_types_yn_yna(data, poll, user.vote_weight, None)
|
||||
self.create_votes_types_yn_yna(data, poll, user.vote_weight, None, None)
|
||||
|
||||
def convert_option_data(self, poll, data):
|
||||
poll_options = poll.get_options()
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
@ -1190,7 +1190,7 @@ class MotionPollViewSet(BasePollViewSet):
|
||||
|
||||
return result
|
||||
|
||||
def handle_analog_vote(self, data, poll, user):
|
||||
def handle_analog_vote(self, data, poll):
|
||||
option = poll.options.get()
|
||||
vote, _ = MotionVote.objects.get_or_create(option=option, value="Y")
|
||||
vote.weight = data["Y"]
|
||||
@ -1209,7 +1209,7 @@ class MotionPollViewSet(BasePollViewSet):
|
||||
|
||||
poll.save()
|
||||
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
def validate_vote_data(self, data, poll):
|
||||
"""
|
||||
Request data for analog:
|
||||
{ "Y": <amount>, "N": <amount>, ["A": <amount>],
|
||||
@ -1240,15 +1240,25 @@ class MotionPollViewSet(BasePollViewSet):
|
||||
VotedModel = MotionPoll.voted.through
|
||||
VotedModel.objects.create(motionpoll=poll, user=user)
|
||||
|
||||
def handle_named_vote(self, data, poll, user):
|
||||
self.handle_named_and_pseudoanonymous_vote(data, user, user, poll)
|
||||
def handle_named_vote(self, data, poll, vote_user, request_user):
|
||||
self.handle_named_and_pseudoanonymous_vote(
|
||||
data,
|
||||
poll,
|
||||
weight_user=vote_user,
|
||||
vote_user=vote_user,
|
||||
request_user=request_user,
|
||||
)
|
||||
|
||||
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||
self.handle_named_and_pseudoanonymous_vote(data, user, None, poll)
|
||||
self.handle_named_and_pseudoanonymous_vote(data, poll, user, None, None)
|
||||
|
||||
def handle_named_and_pseudoanonymous_vote(self, data, weight_user, vote_user, poll):
|
||||
def handle_named_and_pseudoanonymous_vote(
|
||||
self, data, poll, weight_user, vote_user, request_user
|
||||
):
|
||||
option = poll.options.get()
|
||||
vote = MotionVote.objects.create(user=vote_user, option=option)
|
||||
vote = MotionVote.objects.create(
|
||||
user=vote_user, delegated_user=request_user, option=option
|
||||
)
|
||||
vote.value = data
|
||||
vote.weight = (
|
||||
weight_user.vote_weight
|
||||
|
@ -2,8 +2,13 @@ import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from ..poll.views import BasePoll
|
||||
from ..utils import logging
|
||||
from ..utils.access_permissions import BaseAccessPermissions
|
||||
from ..utils.auth import async_has_perm
|
||||
from ..utils.auth import async_has_perm, user_collection_string
|
||||
from ..utils.cache import element_cache
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseVoteAccessPermissions(BaseAccessPermissions):
|
||||
@ -26,6 +31,7 @@ class BaseVoteAccessPermissions(BaseAccessPermissions):
|
||||
for vote in full_data
|
||||
if vote["pollstate"] == BasePoll.STATE_PUBLISHED
|
||||
or vote["user_id"] == user_id
|
||||
or vote["delegated_user_id"] == user_id
|
||||
]
|
||||
return data
|
||||
|
||||
@ -71,8 +77,24 @@ class BasePollAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
|
||||
# add has_voted for all users to check whether op has voted
|
||||
# also fill user_has_voted_for_delegations with all users for which he has
|
||||
# already voted
|
||||
user_data = await element_cache.get_element_data(
|
||||
user_collection_string, user_id
|
||||
)
|
||||
if user_data is None:
|
||||
logger.error(f"Could not find userdata for {user_id}")
|
||||
vote_delegated_from_ids = set()
|
||||
else:
|
||||
vote_delegated_from_ids = set(user_data["vote_delegated_from_users_id"])
|
||||
|
||||
for poll in full_data:
|
||||
poll["user_has_voted"] = user_id in poll["voted_id"]
|
||||
voted_ids = set(poll["voted_id"])
|
||||
voted_for_delegations = list(
|
||||
vote_delegated_from_ids.intersection(voted_ids)
|
||||
)
|
||||
poll["user_has_voted_for_delegations"] = voted_for_delegations
|
||||
|
||||
if await async_has_perm(user_id, self.manage_permission):
|
||||
data = full_data
|
||||
|
@ -29,6 +29,14 @@ class BaseVote(models.Model):
|
||||
blank=True,
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
)
|
||||
delegated_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
related_name="%(class)s_delegated_votes",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
@ -12,7 +12,15 @@ from ..utils.rest_api import (
|
||||
from .models import BasePoll
|
||||
|
||||
|
||||
BASE_VOTE_FIELDS = ("id", "weight", "value", "user", "option", "pollstate")
|
||||
BASE_VOTE_FIELDS = (
|
||||
"id",
|
||||
"weight",
|
||||
"value",
|
||||
"user",
|
||||
"delegated_user",
|
||||
"option",
|
||||
"pollstate",
|
||||
)
|
||||
|
||||
|
||||
class BaseVoteSerializer(ModelSerializer):
|
||||
|
@ -1,5 +1,6 @@
|
||||
from textwrap import dedent
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
@ -104,8 +105,8 @@ class BasePollViewSet(ModelViewSet):
|
||||
# convert user ids to option ids
|
||||
self.convert_option_data(poll, vote_data)
|
||||
|
||||
self.validate_vote_data(vote_data, poll, request.user)
|
||||
self.handle_analog_vote(vote_data, poll, request.user)
|
||||
self.validate_vote_data(vote_data, poll)
|
||||
self.handle_analog_vote(vote_data, poll)
|
||||
|
||||
if request.data.get("publish_immediately"):
|
||||
poll.state = BasePoll.STATE_PUBLISHED
|
||||
@ -198,25 +199,41 @@ class BasePollViewSet(ModelViewSet):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
self.permission_denied(request)
|
||||
|
||||
# check permissions based on poll type and handle requests
|
||||
self.assert_can_vote(poll, request)
|
||||
|
||||
# data format is:
|
||||
# { data: <vote_data>, [user_id: int] }
|
||||
# if user_id is given, the operator votes for this user instead of himself
|
||||
# user_id is ignored for analog polls
|
||||
data = request.data
|
||||
self.validate_vote_data(data, poll, request.user)
|
||||
if "data" not in data:
|
||||
raise ValidationError({"detail": "No data provided."})
|
||||
vote_data = data["data"]
|
||||
if "user_id" in data and poll.type != BasePoll.TYPE_ANALOG:
|
||||
try:
|
||||
vote_user = get_user_model().objects.get(pk=data["user_id"])
|
||||
except get_user_model().DoesNotExist:
|
||||
raise ValidationError({"detail": "The given user does not exist."})
|
||||
else:
|
||||
vote_user = request.user
|
||||
|
||||
# check permissions based on poll type and user
|
||||
self.assert_can_vote(poll, request, vote_user)
|
||||
|
||||
# validate the vote data
|
||||
self.validate_vote_data(vote_data, poll)
|
||||
|
||||
if poll.type == BasePoll.TYPE_ANALOG:
|
||||
self.handle_analog_vote(data, poll, request.user)
|
||||
if request.data.get("publish_immediately") == "1":
|
||||
self.handle_analog_vote(vote_data, poll)
|
||||
if vote_data.get("publish_immediately") == "1":
|
||||
poll.state = BasePoll.STATE_PUBLISHED
|
||||
else:
|
||||
poll.state = BasePoll.STATE_FINISHED
|
||||
poll.save()
|
||||
|
||||
elif poll.type == BasePoll.TYPE_NAMED:
|
||||
self.handle_named_vote(data, poll, request.user)
|
||||
self.handle_named_vote(vote_data, poll, vote_user, request.user)
|
||||
|
||||
elif poll.type == BasePoll.TYPE_PSEUDOANONYMOUS:
|
||||
self.handle_pseudoanonymous_vote(data, poll, request.user)
|
||||
self.handle_pseudoanonymous_vote(vote_data, poll, vote_user)
|
||||
|
||||
inform_changed_data(poll)
|
||||
|
||||
@ -231,13 +248,16 @@ class BasePollViewSet(ModelViewSet):
|
||||
inform_changed_data(poll.get_votes(), final_data=True)
|
||||
return Response()
|
||||
|
||||
def assert_can_vote(self, poll, request):
|
||||
def assert_can_vote(self, poll, request, vote_user):
|
||||
"""
|
||||
Raises a permission denied, if the user is not allowed to vote (or has already voted).
|
||||
Adds the user to the voted array, so this needs to be reverted on error!
|
||||
Adds the user to the voted array, so this needs to be reverted if a later error happens!
|
||||
Analog: has to have manage permissions
|
||||
Named & Pseudoanonymous: has to be in a poll group and present
|
||||
"""
|
||||
if request.user != vote_user and request.user != vote_user.vote_delegated_to:
|
||||
self.permission_denied(request)
|
||||
|
||||
if poll.type == BasePoll.TYPE_ANALOG:
|
||||
if not self.has_manage_permissions():
|
||||
self.permission_denied(request)
|
||||
@ -246,14 +266,14 @@ class BasePollViewSet(ModelViewSet):
|
||||
raise ValidationError("You can only vote on a started poll.")
|
||||
|
||||
if not request.user.is_present or not in_some_groups(
|
||||
request.user.id,
|
||||
vote_user.id,
|
||||
list(poll.groups.values_list("pk", flat=True)),
|
||||
exact=True,
|
||||
):
|
||||
self.permission_denied(request)
|
||||
|
||||
try:
|
||||
self.add_user_to_voted_array(request.user, poll)
|
||||
self.add_user_to_voted_array(vote_user, poll)
|
||||
inform_changed_data(poll)
|
||||
except IntegrityError:
|
||||
raise ValidationError({"detail": "You have already voted"})
|
||||
@ -292,20 +312,20 @@ class BasePollViewSet(ModelViewSet):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
def validate_vote_data(self, data, poll):
|
||||
"""
|
||||
To be implemented by subclass. Validates the data according to poll type and method and fields by validated versions.
|
||||
Raises ValidationError on failure
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def handle_analog_vote(self, data, poll, user):
|
||||
def handle_analog_vote(self, data, poll):
|
||||
"""
|
||||
To be implemented by subclass. Handles the analog vote. Assumes data is validated
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def handle_named_vote(self, data, poll, user):
|
||||
def handle_named_vote(self, data, poll, vote_user, request_user):
|
||||
"""
|
||||
To be implemented by subclass. Handles the named vote. Assumes data is validated.
|
||||
Needs to manage the voted-array per option.
|
||||
|
@ -58,6 +58,8 @@ class UserAccessPermissions(BaseAccessPermissions):
|
||||
own_data_fields = set(little_data_fields)
|
||||
own_data_fields.add("email")
|
||||
own_data_fields.add("gender")
|
||||
own_data_fields.add("vote_delegated_to_id")
|
||||
own_data_fields.add("vote_delegated_from_users_id")
|
||||
|
||||
# Check user permissions.
|
||||
if await async_has_perm(user_id, "users.can_see_name"):
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
@ -22,7 +22,11 @@ from openslides.utils.manager import BaseManager
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.auth import GROUP_ADMIN_PK
|
||||
from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin
|
||||
from ..utils.models import (
|
||||
CASCADE_AND_AUTOUPDATE,
|
||||
SET_NULL_AND_AUTOUPDATE,
|
||||
RESTModelMixin,
|
||||
)
|
||||
from .access_permissions import (
|
||||
GroupAccessPermissions,
|
||||
PersonalNoteAccessPermissions,
|
||||
@ -54,7 +58,8 @@ class UserManager(BaseUserManager):
|
||||
queryset=Permission.objects.select_related("content_type"),
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
"vote_delegated_from_users",
|
||||
)
|
||||
|
||||
def create_user(self, username, password, skip_autoupdate=False, **kwargs):
|
||||
@ -164,6 +169,14 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||
default=Decimal("1"), max_digits=15, decimal_places=6, null=False, blank=True
|
||||
)
|
||||
|
||||
vote_delegated_to = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="vote_delegated_from_users",
|
||||
)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
class Meta:
|
||||
|
@ -7,6 +7,7 @@ from ..utils.rest_api import (
|
||||
JSONField,
|
||||
ModelSerializer,
|
||||
RelatedField,
|
||||
SerializerMethodField,
|
||||
ValidationError,
|
||||
)
|
||||
from ..utils.validate import validate_html_strict
|
||||
@ -36,6 +37,8 @@ USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
|
||||
"comment",
|
||||
"is_active",
|
||||
"auth_type",
|
||||
"vote_delegated_to_id",
|
||||
"vote_delegated_from_users_id",
|
||||
)
|
||||
|
||||
|
||||
@ -57,11 +60,14 @@ class UserSerializer(ModelSerializer):
|
||||
),
|
||||
)
|
||||
|
||||
vote_delegated_from_users_id = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = USERCANSEEEXTRASERIALIZER_FIELDS + (
|
||||
"default_password",
|
||||
"session_auth_hash",
|
||||
"vote_delegated_to",
|
||||
)
|
||||
read_only_fields = ("last_email_send", "auth_type")
|
||||
|
||||
@ -119,6 +125,13 @@ class UserSerializer(ModelSerializer):
|
||||
inform_changed_data(user)
|
||||
return user
|
||||
|
||||
def get_vote_delegated_from_users_id(self, user):
|
||||
# check needed to prevent errors on import since we only give an OrderedDict there
|
||||
if hasattr(user, "vote_delegated_from_users"):
|
||||
return [delegator.id for delegator in user.vote_delegated_from_users.all()]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class PermissionRelatedField(RelatedField):
|
||||
"""
|
||||
|
@ -174,9 +174,77 @@ class UserViewSet(ModelViewSet):
|
||||
):
|
||||
request.data["username"] = user.username
|
||||
|
||||
# check that no chains are created with vote delegation
|
||||
delegate_id = request.data.get("vote_delegated_to_id")
|
||||
if delegate_id:
|
||||
try:
|
||||
delegate = User.objects.get(id=delegate_id)
|
||||
except User.DoesNotExist:
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": f"Vote delegation: The user with id {delegate_id} does not exist"
|
||||
}
|
||||
)
|
||||
|
||||
self.assert_no_self_delegation(user, [delegate_id])
|
||||
self.assert_vote_not_delegated(delegate)
|
||||
self.assert_has_no_delegated_votes(user)
|
||||
|
||||
inform_changed_data(delegate)
|
||||
if user.vote_delegated_to:
|
||||
inform_changed_data(user.vote_delegated_to)
|
||||
|
||||
# handle delegated_from field seperately since its a SerializerMethodField
|
||||
new_delegation_ids = request.data.get("vote_delegated_from_users_id")
|
||||
if "vote_delegated_from_users_id" in request.data:
|
||||
del request.data["vote_delegated_from_users_id"]
|
||||
|
||||
response = super().update(request, *args, **kwargs)
|
||||
|
||||
# after rest of the request succeeded, handle delegation changes
|
||||
if new_delegation_ids:
|
||||
self.assert_no_self_delegation(user, new_delegation_ids)
|
||||
self.assert_vote_not_delegated(user)
|
||||
|
||||
for id in new_delegation_ids:
|
||||
delegation_user = User.objects.get(id=id)
|
||||
self.assert_has_no_delegated_votes(delegation_user)
|
||||
delegation_user.vote_delegated_to = user
|
||||
delegation_user.save()
|
||||
|
||||
delegations_to_remove = user.vote_delegated_from_users.exclude(
|
||||
id__in=(new_delegation_ids or [])
|
||||
)
|
||||
for old_delegation_user in delegations_to_remove:
|
||||
old_delegation_user.vote_delegated_to = None
|
||||
old_delegation_user.save()
|
||||
|
||||
# if only delegated_from was changed, we need an autoupdate for the operator
|
||||
if new_delegation_ids or delegations_to_remove:
|
||||
inform_changed_data(user)
|
||||
|
||||
return response
|
||||
|
||||
def assert_vote_not_delegated(self, user):
|
||||
if user.vote_delegated_to:
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "You cannot delegate a vote to a user who has already delegated his vote."
|
||||
}
|
||||
)
|
||||
|
||||
def assert_has_no_delegated_votes(self, user):
|
||||
if user.vote_delegated_from_users and len(user.vote_delegated_from_users.all()):
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "You cannot delegate a vote of a user who is already a delegate of another user."
|
||||
}
|
||||
)
|
||||
|
||||
def assert_no_self_delegation(self, user, delegate_ids):
|
||||
if user.id in delegate_ids:
|
||||
raise ValidationError({"detail": "You cannot delegate a vote to yourself."})
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to delete an user.
|
||||
@ -391,6 +459,7 @@ class UserViewSet(ModelViewSet):
|
||||
data = serializer.prepare_password(serializer.data)
|
||||
groups = data["groups_id"]
|
||||
del data["groups_id"]
|
||||
del data["vote_delegated_from_users_id"]
|
||||
|
||||
db_user = User(**data)
|
||||
try:
|
||||
|
@ -770,6 +770,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"options": {
|
||||
"1": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||
"2": {"Y": "30", "N": "-2", "A": "8.93"},
|
||||
@ -778,6 +779,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
"votesinvalid": "-2",
|
||||
"votescast": "-2",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 6)
|
||||
@ -800,11 +802,13 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"options": {"1": {"Y": "1", "N": "1", "A": "1"}},
|
||||
"votesvalid": "-1.5",
|
||||
"votesinvalid": "-2",
|
||||
"votescast": "-2",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -814,11 +818,13 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"options": {
|
||||
"1": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||
"2": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -828,7 +834,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}},
|
||||
{"data": {"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -839,10 +845,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"options": {
|
||||
"1": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||
"3": {"Y": "1", "N": "2.35", "A": "-1"},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -851,25 +859,31 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
def test_no_permissions(self):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_wrong_state(self):
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_wrong_data_format(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), [1, 2, 5]
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -878,7 +892,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"options": [1, "string"]},
|
||||
{"data": {"options": [1, "string"]}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -887,7 +901,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"options": {"string": "some_other_string"}},
|
||||
{"data": {"options": {"string": "some_other_string"}}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -896,7 +910,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"options": {"1": [None]}},
|
||||
{"data": {"options": {"1": [None]}}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -907,7 +921,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
data = {"options": {"1": {"Y": "1", "N": "3", "A": "-1"}}}
|
||||
del data["options"]["1"][value]
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), data
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": data}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -917,10 +931,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"options": {"1": {"Y": 5, "N": 0, "A": 1}},
|
||||
"votesvalid": "-2",
|
||||
"votesinvalid": "1",
|
||||
"votescast": "-1",
|
||||
}
|
||||
},
|
||||
)
|
||||
self.poll.state = 3
|
||||
@ -928,10 +944,12 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"options": {"1": {"Y": 2, "N": 2, "A": 2}},
|
||||
"votesvalid": "4.64",
|
||||
"votesinvalid": "-2",
|
||||
"votescast": "3",
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -973,7 +991,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "2": "N", "3": "A"},
|
||||
{"data": {"1": "Y", "2": "N", "3": "A"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1007,7 +1025,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "2": "N", "3": "A"},
|
||||
{"data": {"1": "Y", "2": "N", "3": "A"}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||
@ -1039,12 +1057,12 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "N"},
|
||||
{"data": {"1": "N"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1064,7 +1082,8 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
option2 = self.poll2.options.get()
|
||||
# Do request to poll with option2 (which is wrong...)
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {str(option2.id): "Y"}
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {str(option2.id): "Y"}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 0)
|
||||
@ -1081,7 +1100,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "2": "N"},
|
||||
{"data": {"1": "Y", "2": "N"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1092,7 +1111,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1103,7 +1122,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "3": "N"},
|
||||
{"data": {"1": "Y", "3": "N"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1114,7 +1133,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
@ -1125,7 +1144,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
gclient = self.create_guest_client()
|
||||
response = gclient.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
@ -1137,20 +1156,24 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_wrong_state(self):
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(
|
||||
response, status.HTTP_200_OK
|
||||
) # new "feature" because of partial requests: empty requests work!
|
||||
@ -1160,7 +1183,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
[1, 2, 5],
|
||||
{"data": [1, 2, 5]},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1170,7 +1193,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "string"},
|
||||
{"data": {"1": "string"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1180,7 +1203,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"id": "Y"},
|
||||
{"data": {"id": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1190,7 +1213,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": [None]},
|
||||
{"data": {"1": [None]}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1229,7 +1252,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 1, "2": 0},
|
||||
{"data": {"1": 1, "2": 0}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1254,12 +1277,12 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 1, "2": 0},
|
||||
{"data": {"1": 1, "2": 0}},
|
||||
format="json",
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 0, "2": 1},
|
||||
{"data": {"1": 0, "2": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1278,7 +1301,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.poll.save()
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), "N"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
@ -1294,7 +1317,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.poll.save()
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), "N"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "N"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -1305,7 +1328,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.poll.save()
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
@ -1321,7 +1344,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.poll.save()
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -1331,7 +1354,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": -1},
|
||||
{"data": {"1": -1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1342,7 +1365,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 2, "2": 1},
|
||||
{"data": {"1": 2, "2": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1361,7 +1384,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 2, "2": 2},
|
||||
{"data": {"1": 2, "2": 2}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1372,7 +1395,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 1, "2": 1, "3": 1},
|
||||
{"data": {"1": 1, "2": 1, "3": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1381,7 +1404,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
def test_wrong_options(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"2": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -1390,7 +1415,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -1399,7 +1426,9 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
gclient = self.create_guest_client()
|
||||
response = gclient.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -1409,21 +1438,27 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.admin.is_present = False
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_wrong_state(self):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
@ -1431,7 +1466,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
[1, 2, 5],
|
||||
{"data": [1, 2, 5]},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1441,7 +1476,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "string"},
|
||||
{"data": {"1": "string"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1451,7 +1486,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"id": 1},
|
||||
{"data": {"id": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1461,7 +1496,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": [None]},
|
||||
{"data": {"1": [None]}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1495,7 +1530,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "2": "N", "3": "A"},
|
||||
{"data": {"1": "Y", "2": "N", "3": "A"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1522,12 +1557,12 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "N"},
|
||||
{"data": {"1": "N"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1541,7 +1576,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "2": "N"},
|
||||
{"data": {"1": "Y", "2": "N"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1552,7 +1587,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1563,7 +1598,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y", "3": "N"},
|
||||
{"data": {"1": "Y", "3": "N"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1574,7 +1609,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
@ -1585,7 +1620,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
gclient = self.create_guest_client()
|
||||
response = gclient.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
@ -1597,20 +1632,24 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "Y"},
|
||||
{"data": {"1": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_wrong_state(self):
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
@ -1618,7 +1657,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
[1, 2, 5],
|
||||
{"data": [1, 2, 5]},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1628,7 +1667,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "string"},
|
||||
{"data": {"1": "string"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1638,7 +1677,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"id": "Y"},
|
||||
{"data": {"id": "Y"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1648,7 +1687,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": [None]},
|
||||
{"data": {"1": [None]}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1687,7 +1726,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 1, "2": 0},
|
||||
{"data": {"1": 1, "2": 0}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1714,12 +1753,12 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 1, "2": 0},
|
||||
{"data": {"1": 1, "2": 0}},
|
||||
format="json",
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 0, "2": 1},
|
||||
{"data": {"1": 0, "2": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1737,7 +1776,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": -1},
|
||||
{"data": {"1": -1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1748,7 +1787,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 2, "2": 1},
|
||||
{"data": {"1": 2, "2": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
@ -1769,7 +1808,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 2, "2": 2},
|
||||
{"data": {"1": 2, "2": 2}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1780,7 +1819,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": 1, "2": 1, "3": 1},
|
||||
{"data": {"1": 1, "2": 1, "3": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1789,7 +1828,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
def test_wrong_options(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"2": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"2": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
@ -1798,7 +1839,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -1807,7 +1850,9 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
gclient = self.create_guest_client()
|
||||
response = gclient.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
@ -1817,21 +1862,27 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.admin.is_present = False
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_wrong_state(self):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": 1}, format="json"
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"1": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
@ -1839,7 +1890,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
[1, 2, 5],
|
||||
{"data": {"data": [1, 2, 5]}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1849,7 +1900,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": "string"},
|
||||
{"data": {"1": "string"}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1859,7 +1910,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"id": 1},
|
||||
{"data": {"id": 1}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1869,7 +1920,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||
{"1": [None]},
|
||||
{"data": {"1": [None]}},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
@ -1923,7 +1974,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
|
||||
def test_vote(self):
|
||||
response = self.user_client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"}
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
@ -1956,6 +2007,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
"user_has_voted": False,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-option:1": {
|
||||
@ -1973,6 +2025,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"option_id": 1,
|
||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
||||
"user_id": self.user.id,
|
||||
"delegated_user_id": self.user.id,
|
||||
"value": "A",
|
||||
"weight": "1.000000",
|
||||
},
|
||||
@ -1989,6 +2042,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"option_id": 1,
|
||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
||||
"user_id": self.user.id,
|
||||
"delegated_user_id": self.user.id,
|
||||
"value": "A",
|
||||
"weight": "1.000000",
|
||||
},
|
||||
@ -2018,6 +2072,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"id": 1,
|
||||
"votes_amount": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"user_has_voted_for_delegations": [],
|
||||
},
|
||||
)
|
||||
|
||||
@ -2073,6 +2128,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
"user_has_voted": user == self.user,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
)
|
||||
@ -2084,6 +2140,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"weight": "1.000000",
|
||||
"value": "A",
|
||||
"user_id": 3,
|
||||
"delegated_user_id": None,
|
||||
"option_id": 1,
|
||||
},
|
||||
)
|
||||
@ -2108,9 +2165,9 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
):
|
||||
poll_type = AssignmentPoll.TYPE_PSEUDOANONYMOUS
|
||||
|
||||
def test_vote(self):
|
||||
def test_votex(self):
|
||||
response = self.user_client.post(
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"1": "A"}
|
||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
@ -2136,6 +2193,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"description": self.description,
|
||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
||||
"user_has_voted": False,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [self.user.id],
|
||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
||||
@ -2159,6 +2217,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"option_id": 1,
|
||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
||||
"user_id": None,
|
||||
"delegated_user_id": None,
|
||||
"value": "A",
|
||||
"weight": "1.000000",
|
||||
},
|
||||
@ -2190,6 +2249,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"id": 1,
|
||||
"votes_amount": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"user_has_voted_for_delegations": [],
|
||||
},
|
||||
)
|
||||
|
||||
@ -2245,6 +2305,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
"user_has_voted": user == self.user,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
@ -2253,6 +2314,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"weight": "1.000000",
|
||||
"value": "A",
|
||||
"user_id": None,
|
||||
"delegated_user_id": None,
|
||||
"option_id": 1,
|
||||
},
|
||||
"assignments/assignment-option:1": {
|
||||
|
@ -167,6 +167,7 @@ class CreateMotionPoll(TestCase):
|
||||
"id": 1,
|
||||
"voted_id": [],
|
||||
"user_has_voted": False,
|
||||
"user_has_voted_for_delegations": [],
|
||||
},
|
||||
)
|
||||
self.assertEqual(autoupdate[1], [])
|
||||
@ -610,6 +611,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"Y": "1",
|
||||
"N": "2.35",
|
||||
"A": "-1",
|
||||
@ -617,6 +619,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
"votesinvalid": "-2",
|
||||
"votescast": "-2",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -634,14 +637,23 @@ class VoteMotionPollAnalog(TestCase):
|
||||
def test_vote_no_permissions(self):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_no_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]), {})
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"Y": "4", "N": "22.6"}
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"data": {"Y": "4", "N": "22.6"}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
@ -649,7 +661,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
def test_vote_wrong_data_format(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
@ -658,7 +670,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"Y": "some string", "N": "-2", "A": "3"},
|
||||
{"data": {"Y": "some string", "N": "-2", "A": "3"}},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
@ -668,6 +680,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"Y": "3",
|
||||
"N": "1",
|
||||
"A": "5",
|
||||
@ -675,12 +688,14 @@ class VoteMotionPollAnalog(TestCase):
|
||||
"votesinvalid": "1",
|
||||
"votescast": "-1",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.poll.state = 3
|
||||
self.poll.save()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{
|
||||
"data": {
|
||||
"Y": "1",
|
||||
"N": "2.35",
|
||||
"A": "-1",
|
||||
@ -688,6 +703,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
"votesinvalid": "-2",
|
||||
"votescast": "3",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -749,7 +765,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -773,7 +789,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.admin.vote_weight = weight = Decimal("3.5")
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -799,11 +815,11 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -824,38 +840,35 @@ class VoteMotionPollNamed(TestCase):
|
||||
config["general_system_enable_anonymous"] = True
|
||||
guest_client = APIClient()
|
||||
response = guest_client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "Y"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
# TODO: Move to unit tests
|
||||
def test_not_set_vote_values(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.poll.votesvalid = Decimal("1")
|
||||
with self.assertRaises(ValueError):
|
||||
self.poll.votesinvalid = Decimal("1")
|
||||
with self.assertRaises(ValueError):
|
||||
self.poll.votescast = Decimal("1")
|
||||
|
||||
def test_vote_wrong_state(self):
|
||||
self.make_admin_present()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_wrong_group(self):
|
||||
self.start_poll()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_not_present(self):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
@ -863,7 +876,9 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
@ -872,11 +887,118 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def setup_vote_delegation(self, with_delegation=True):
|
||||
""" user -> admin """
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
user, _ = self.create_user()
|
||||
user.groups.add(GROUP_DELEGATE_PK)
|
||||
if with_delegation:
|
||||
user.vote_delegated_to = self.admin
|
||||
user.save()
|
||||
inform_changed_data(self.admin) # put the admin into the cache to update
|
||||
# its vote_delegated_to_id field
|
||||
self.user = user
|
||||
|
||||
def test_vote_delegation(self):
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"data": "N", "user_id": self.user.pk}, # user not present
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
vote = option.votes.get()
|
||||
self.assertEqual(vote.user, self.user)
|
||||
self.assertEqual(vote.delegated_user, self.admin)
|
||||
|
||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
||||
self.assertIn("motions/motion-poll:1", autoupdate[0])
|
||||
self.assertEqual(
|
||||
autoupdate[0]["motions/motion-poll:1"]["user_has_voted_for_delegations"],
|
||||
[self.user.pk],
|
||||
)
|
||||
|
||||
def test_vote_delegation_and_self_vote(self):
|
||||
self.test_vote_delegation()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
self.assertEqual(poll.get_votes().count(), 2)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("1"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
vote = option.votes.get(user_id=self.admin.pk)
|
||||
self.assertEqual(vote.user, self.admin)
|
||||
self.assertEqual(vote.delegated_user, self.admin)
|
||||
|
||||
def test_vote_delegation_forbidden(self):
|
||||
self.setup_vote_delegation(False)
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"data": "N", "user_id": self.user.pk},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_delegation_not_present(self):
|
||||
self.setup_vote_delegation()
|
||||
self.admin.is_present = False
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"data": "N", "user_id": self.user.pk},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_delegation_delegatee_not_in_group(self):
|
||||
self.setup_vote_delegation()
|
||||
self.admin.groups.remove(GROUP_DELEGATE_PK)
|
||||
self.admin.save()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"data": "N", "user_id": self.user.pk},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
vote = poll.get_votes()[0]
|
||||
self.assertEqual(vote.value, "N")
|
||||
self.assertEqual(vote.user, self.user)
|
||||
self.assertEqual(vote.delegated_user, self.admin)
|
||||
|
||||
def test_vote_delegation_delegator_not_in_group(self):
|
||||
self.setup_vote_delegation()
|
||||
self.user.groups.remove(GROUP_DELEGATE_PK)
|
||||
self.user.save()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]),
|
||||
{"data": "N", "user_id": self.user.pk},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
|
||||
class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"""3 important users:
|
||||
@ -916,7 +1038,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
|
||||
def test_vote(self):
|
||||
response = self.user_client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -942,6 +1064,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": False,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
@ -950,6 +1073,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"weight": "1.000000",
|
||||
"value": "A",
|
||||
"user_id": self.user.id,
|
||||
"delegated_user_id": self.user.id,
|
||||
"option_id": 1,
|
||||
},
|
||||
"motions/motion-option:1": {
|
||||
@ -975,6 +1099,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"weight": "1.000000",
|
||||
"value": "A",
|
||||
"user_id": self.user.id,
|
||||
"delegated_user_id": self.user.id,
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
@ -1001,6 +1126,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"user_has_voted_for_delegations": [],
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
@ -1055,7 +1181,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
|
||||
def test_vote(self):
|
||||
response = self.user_client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -1081,6 +1207,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": False,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
@ -1090,6 +1217,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"weight": "1.000000",
|
||||
"value": "A",
|
||||
"user_id": None,
|
||||
"delegated_user_id": None,
|
||||
},
|
||||
"motions/motion-option:1": {
|
||||
"abstain": "1.000000",
|
||||
@ -1122,6 +1250,7 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"user_has_voted_for_delegations": [],
|
||||
},
|
||||
)
|
||||
|
||||
@ -1177,7 +1306,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
@ -1199,11 +1328,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "N"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
option = MotionPoll.objects.get().options.get()
|
||||
@ -1219,7 +1348,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
config["general_system_enable_anonymous"] = True
|
||||
guest_client = APIClient()
|
||||
response = guest_client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "Y"
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "Y"}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
@ -1227,21 +1356,27 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
def test_vote_wrong_state(self):
|
||||
self.make_admin_present()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_wrong_group(self):
|
||||
self.start_poll()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_vote_not_present(self):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
@ -1249,7 +1384,9 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
self.start_poll()
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(reverse("motionpoll-vote", args=[self.poll.pk]))
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": {}}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
|
||||
@ -1258,7 +1395,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
self.make_admin_delegate()
|
||||
self.make_admin_present()
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": [1, 2, 5]}
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||
@ -1344,6 +1481,7 @@ class PublishMotionPoll(TestCase):
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": False,
|
||||
"user_has_voted_for_delegations": [],
|
||||
"voted_id": [],
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
@ -1353,6 +1491,7 @@ class PublishMotionPoll(TestCase):
|
||||
"weight": "2.000000",
|
||||
"value": "N",
|
||||
"user_id": None,
|
||||
"delegated_user_id": None,
|
||||
},
|
||||
"motions/motion-option:1": {
|
||||
"abstain": "0.000000",
|
||||
@ -1495,3 +1634,45 @@ class ResetMotionPoll(TestCase):
|
||||
for user in (self.admin, self.user1, self.user2):
|
||||
self.assertDeletedAutoupdate(self.vote1, user=user)
|
||||
self.assertDeletedAutoupdate(self.vote2, user=user)
|
||||
|
||||
|
||||
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
|
||||
def advancedSetUp(self):
|
||||
""" Set up user -> other_user delegation. """
|
||||
self.motion = Motion(
|
||||
title="test_title_dL91JqhMTiQuQLSDRItZ",
|
||||
text="test_text_R7nURdXKVEfEnnJBXJYa",
|
||||
)
|
||||
self.motion.save()
|
||||
|
||||
self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
|
||||
|
||||
self.other_user, _ = self.create_user()
|
||||
self.user, user_password = self.create_user()
|
||||
self.user.groups.add(self.delegate_group)
|
||||
self.user.is_present = True
|
||||
self.user.vote_delegated_to = self.other_user
|
||||
self.user.save()
|
||||
|
||||
self.user_client = APIClient()
|
||||
self.user_client.login(username=self.user.username, password=user_password)
|
||||
|
||||
self.poll = MotionPoll.objects.create(
|
||||
motion=self.motion,
|
||||
title="test_title_Q3EuRaALSCCPJuQ2tMqj",
|
||||
pollmethod="YNA",
|
||||
type=BasePoll.TYPE_NAMED,
|
||||
onehundred_percent_base="YN",
|
||||
majority_method="simple",
|
||||
)
|
||||
self.poll.create_options()
|
||||
self.poll.groups.add(self.delegate_group)
|
||||
self.poll.save()
|
||||
|
||||
def test_start_poll(self):
|
||||
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
|
||||
# other_user has to receive an autoupdate because he was delegated
|
||||
autoupdate = self.get_last_autoupdate(user=self.other_user)
|
||||
assert "motions/motion-poll:1" in autoupdate[0]
|
||||
|
@ -24,12 +24,13 @@ def test_user_db_queries():
|
||||
"""
|
||||
Tests that only the following db queries are done:
|
||||
* 2 requests to get the list of all users and
|
||||
* 1 requests to get the list of all groups.
|
||||
* 1 request to get all vote delegations
|
||||
* 1 request to get the list of all groups.
|
||||
"""
|
||||
for index in range(10):
|
||||
User.objects.create(username=f"user{index}")
|
||||
|
||||
assert count_queries(User.get_elements)() == 3
|
||||
assert count_queries(User.get_elements)() == 4
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=False)
|
||||
@ -232,6 +233,188 @@ class UserUpdate(TestCase):
|
||||
# The user is not allowed to change some other fields (like last_name).
|
||||
self.assertNotEqual(user.last_name, "New name fae1Bu1Eyeis9eRox4xu")
|
||||
|
||||
def test_update_vote_delegation(self):
|
||||
user = User.objects.create_user(
|
||||
username="non-admin Yd4ejrJXZi4Wn16ugHgY",
|
||||
password="non-admin AQ4Dw2tN9byKpGD4f1gs",
|
||||
)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[user.pk]),
|
||||
{"vote_delegated_to_id": self.admin.pk},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
user = User.objects.get(pk=user.pk)
|
||||
self.assertEqual(user.vote_delegated_to_id, self.admin.pk)
|
||||
admin = User.objects.get(username="admin")
|
||||
self.assertEqual(
|
||||
list(admin.vote_delegated_from_users.values_list("id", flat=True)),
|
||||
[user.pk],
|
||||
)
|
||||
|
||||
def test_update_vote_delegation_non_admin(self):
|
||||
user = User.objects.create_user(
|
||||
username="non-admin WpBQRSsCg6qNWNtN6bLP",
|
||||
password="non-admin IzsDBt1uoqc2wo5BSUF1",
|
||||
)
|
||||
client = APIClient()
|
||||
client.login(
|
||||
username="non-admin WpBQRSsCg6qNWNtN6bLP",
|
||||
password="non-admin IzsDBt1uoqc2wo5BSUF1",
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
reverse("user-detail", args=[user.pk]),
|
||||
{"vote_delegated_to_id": self.admin.pk},
|
||||
)
|
||||
|
||||
# self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
user = User.objects.get(pk=user.pk)
|
||||
self.assertIsNone(user.vote_delegated_to_id)
|
||||
|
||||
def test_update_vote_delegated_to_self(self):
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.admin.pk]),
|
||||
{"vote_delegated_to_id": self.admin.pk},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
admin = User.objects.get(pk=self.admin.pk)
|
||||
self.assertIsNone(admin.vote_delegated_to_id)
|
||||
|
||||
def test_update_vote_delegation_invalid_id(self):
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.admin.pk]),
|
||||
{"vote_delegated_to_id": 42},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
admin = User.objects.get(pk=self.admin.pk)
|
||||
self.assertIsNone(admin.vote_delegated_to_id)
|
||||
|
||||
def test_update_vote_delegated_from_self(self):
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.admin.pk]),
|
||||
{"vote_delegated_from_users_id": [self.admin.pk]},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
admin = User.objects.get(pk=self.admin.pk)
|
||||
self.assertIsNone(admin.vote_delegated_to_id)
|
||||
|
||||
def setup_vote_delegation(self):
|
||||
""" login and setup user -> user2 delegation """
|
||||
self.user, _ = self.create_user()
|
||||
self.user2, _ = self.create_user()
|
||||
self.user.vote_delegated_to = self.user2
|
||||
self.user.save()
|
||||
self.assertEqual(self.user.vote_delegated_to_id, self.user2.pk)
|
||||
|
||||
def test_update_reset_vote_delegated_to(self):
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.user.pk]),
|
||||
{"vote_delegated_to_id": None},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
user = User.objects.get(pk=self.user.pk)
|
||||
self.assertEqual(user.vote_delegated_to_id, None)
|
||||
|
||||
def test_update_reset_vote_delegated_from(self):
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.user2.pk]),
|
||||
{"vote_delegated_from_users_id": None},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
user = User.objects.get(pk=self.user.pk)
|
||||
self.assertEqual(user.vote_delegated_to_id, None)
|
||||
|
||||
def test_update_nested_vote_delegation_1(self):
|
||||
""" user -> user2 -> admin """
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.user2.pk]),
|
||||
{"vote_delegated_to_id": self.admin.pk},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
user2 = User.objects.get(pk=self.user2.pk)
|
||||
self.assertIsNone(user2.vote_delegated_to_id)
|
||||
|
||||
def test_update_nested_vote_delegation_2(self):
|
||||
""" admin -> user -> user2 """
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.admin.pk]),
|
||||
{"vote_delegated_to_id": self.user.pk},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
admin = User.objects.get(pk=self.admin.pk)
|
||||
self.assertIsNone(admin.vote_delegated_to_id)
|
||||
|
||||
def test_update_vote_delegation_autoupdate(self):
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.user.pk]),
|
||||
{"vote_delegated_to_id": self.admin.pk},
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
|
||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
||||
user_au = autoupdate[0].get(f"users/user:{self.user.pk}")
|
||||
self.assertIsNotNone(user_au)
|
||||
self.assertEqual(user_au["vote_delegated_to_id"], self.admin.pk)
|
||||
user2_au = autoupdate[0].get(f"users/user:{self.user2.pk}")
|
||||
self.assertIsNotNone(user2_au)
|
||||
self.assertEqual(user2_au["vote_delegated_from_users_id"], [])
|
||||
admin_au = autoupdate[0].get(f"users/user:{self.admin.pk}")
|
||||
self.assertIsNotNone(admin_au)
|
||||
self.assertEqual(admin_au["vote_delegated_from_users_id"], [self.user.pk])
|
||||
self.assertEqual(autoupdate[1], [])
|
||||
|
||||
def test_update_vote_delegated_from(self):
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.user2.pk]),
|
||||
{"vote_delegated_from_users_id": [self.admin.pk]},
|
||||
)
|
||||
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
admin = User.objects.get(pk=self.admin.pk)
|
||||
self.assertEqual(admin.vote_delegated_to_id, self.user2.id)
|
||||
user = User.objects.get(pk=self.user.pk)
|
||||
self.assertIsNone(user.vote_delegated_to_id)
|
||||
|
||||
def test_update_vote_delegated_from_nested_1(self):
|
||||
""" admin -> user -> user2 """
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.user.pk]),
|
||||
{"vote_delegated_from_users_id": [self.admin.pk]},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
admin = User.objects.get(pk=self.admin.pk)
|
||||
self.assertIsNone(admin.vote_delegated_to_id)
|
||||
|
||||
def test_update_vote_delegated_from_nested_2(self):
|
||||
""" user -> user2 -> admin """
|
||||
self.setup_vote_delegation()
|
||||
response = self.client.patch(
|
||||
reverse("user-detail", args=[self.admin.pk]),
|
||||
{"vote_delegated_from_users_id": [self.user2.pk]},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
user2 = User.objects.get(pk=self.user2.pk)
|
||||
self.assertIsNone(user2.vote_delegated_to_id)
|
||||
|
||||
|
||||
class UserDelete(TestCase):
|
||||
"""
|
||||
|
@ -1,6 +1,7 @@
|
||||
from decimal import Decimal
|
||||
from unittest import TestCase
|
||||
|
||||
from openslides.motions.models import Motion, MotionChangeRecommendation
|
||||
from openslides.motions.models import Motion, MotionChangeRecommendation, MotionPoll
|
||||
|
||||
|
||||
# TODO: test for MotionPoll.set_options()
|
||||
@ -50,3 +51,25 @@ class MotionChangeRecommendationTest(TestCase):
|
||||
other_recommendations
|
||||
)
|
||||
self.assertFalse(collides)
|
||||
|
||||
|
||||
class MotionPollAnalogFieldsTest(TestCase):
|
||||
def setUp(self):
|
||||
self.motion = Motion(
|
||||
title="test_title_OoK9IeChe2Jeib9Deeji",
|
||||
text="test_text_eichui1oobiSeit9aifo",
|
||||
)
|
||||
self.poll = MotionPoll(
|
||||
motion=self.motion,
|
||||
title="test_title_tho8PhiePh8upaex6phi",
|
||||
pollmethod="YNA",
|
||||
type=MotionPoll.TYPE_NAMED,
|
||||
)
|
||||
|
||||
def test_not_set_vote_values(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.poll.votesvalid = Decimal("1")
|
||||
with self.assertRaises(ValueError):
|
||||
self.poll.votesinvalid = Decimal("1")
|
||||
with self.assertRaises(ValueError):
|
||||
self.poll.votescast = Decimal("1")
|
||||
|
Loading…
Reference in New Issue
Block a user