Rework assignment voting
- Remove "assignments.can_manage_polls" permission - Let the client handle some user errors - Add a send button to manually submit polls - Show a hint that the user already submitted a vote - will not (and should not) work for non-nominal voting - submitting a vote cannot be changed anymore - user will have to confirm sending - enable deselecting YNA-votings - nomainal voting will behace the same as non nominal voting - submitting empty votes should be possible Perhaps server side adjustments might still be required
This commit is contained in:
parent
61b7731073
commit
8fe5a0c9f4
@ -53,6 +53,13 @@ export interface AssignmentAnalogVoteData {
|
||||
global_abstain?: number;
|
||||
}
|
||||
|
||||
export interface VotingData {
|
||||
votes: Object;
|
||||
global?: GlobalVote;
|
||||
}
|
||||
|
||||
export type GlobalVote = 'A' | 'N';
|
||||
|
||||
/**
|
||||
* Repository Service for Assignments.
|
||||
*
|
||||
@ -109,8 +116,14 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||
};
|
||||
|
||||
// TODO: data must not be any
|
||||
public vote(data: any, poll_id: number): Promise<void> {
|
||||
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, data);
|
||||
public vote(data: VotingData, poll_id: number): Promise<void> {
|
||||
let requestData;
|
||||
if (data.global) {
|
||||
requestData = `"${data.global}"`;
|
||||
} else {
|
||||
requestData = data.votes;
|
||||
}
|
||||
|
||||
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, requestData);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h2 *ngIf="!!poll">{{ poll.title }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="menu-slot" *osPerms="'assignments.can_manage_polls'">
|
||||
<div class="menu-slot" *osPerms="'assignments.can_manage'">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
@ -150,20 +150,20 @@
|
||||
<!-- More Menu -->
|
||||
<mat-menu #pollDetailMenu="matMenu">
|
||||
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="openDialog(poll)">
|
||||
<button *osPerms="'assignments.can_manage'" mat-menu-item (click)="openDialog(poll)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span translate>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*osPerms="'assignments.can_manage_polls'; and: poll && poll.type === 'named'"
|
||||
*osPerms="'assignments.can_manage'; and: poll && poll.type === 'named'"
|
||||
(click)="pseudoanonymizePoll()"
|
||||
>
|
||||
<mat-icon>warning</mat-icon>
|
||||
<span translate>Anonymize votes</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="deletePoll()">
|
||||
<button *osPerms="'assignments.can_manage'" mat-menu-item (click)="deletePoll()">
|
||||
<mat-icon color="warn">delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<ng-container *ngIf="poll">
|
||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||
<!-- TODO: Someone should make this pretty -->
|
||||
<span *ngIf="poll.user_has_voted_valid">Your vote is valid!</span>
|
||||
<span *ngIf="poll.user_has_voted_invalid">DANGER: Your vote is invalid!</span>
|
||||
<span *ngIf="poll.user_has_not_voted">You have not give any voting here!</span>
|
||||
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted; else cannotVote">
|
||||
<!-- Submit Vote -->
|
||||
<ng-container [ngTemplateOutlet]="sendNow"></ng-container>
|
||||
|
||||
<!-- Leftover votes -->
|
||||
<h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1 && !currentVotes.global">
|
||||
<h4
|
||||
*ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1 && !isGlobalOptionSelected()"
|
||||
>
|
||||
{{ 'Votes for this poll' | translate }}: {{ getVotesCount() }}/{{ poll.votes_amount }}
|
||||
</h4>
|
||||
|
||||
@ -34,7 +34,11 @@
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveSingleVote(option.id, action.vote)"
|
||||
[ngClass]="currentVotes[option.id] === action.label ? action.css : ''"
|
||||
[ngClass]="
|
||||
voteRequestData.votes[option.id] === action.vote || voteRequestData.votes[option.id] === 1
|
||||
? action.css
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<mat-icon> {{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
@ -55,7 +59,7 @@
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveGlobalVote('N')"
|
||||
[ngClass]="currentVotes['global'] === 'No' ? 'voted-no' : ''"
|
||||
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
|
||||
>
|
||||
<mat-icon> thumb_down </mat-icon>
|
||||
</button>
|
||||
@ -68,7 +72,7 @@
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveGlobalVote('A')"
|
||||
[ngClass]="currentVotes['global'] === 'Abstain' ? 'voted-abstain' : ''"
|
||||
[ngClass]="voteRequestData.global === 'A' ? 'voted-abstain' : ''"
|
||||
>
|
||||
<mat-icon> trip_origin</mat-icon>
|
||||
</button>
|
||||
@ -78,6 +82,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Submit Vote -->
|
||||
<ng-container [ngTemplateOutlet]="sendNow"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Shows the permission error -->
|
||||
@ -85,3 +92,24 @@
|
||||
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #cannotVote>
|
||||
<div class="centered-button-wrapper">
|
||||
<os-icon-container icon="check">
|
||||
{{ 'You already voted on this poll' | translate}}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #sendNow>
|
||||
<div class="centered-button-wrapper">
|
||||
<button mat-flat-button color="accent" (click)="submitVote()">
|
||||
<mat-icon>
|
||||
how_to_vote
|
||||
</mat-icon>
|
||||
<span>
|
||||
{{ 'Submit vote now' | translate }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -40,6 +40,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.centered-button-wrapper {
|
||||
display: flex;
|
||||
> * {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Could be some more general component
|
||||
.voted-yes {
|
||||
background-color: $votes-yes-color;
|
||||
}
|
||||
|
@ -5,23 +5,28 @@ import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
|
||||
import {
|
||||
AssignmentPollRepositoryService,
|
||||
GlobalVote,
|
||||
VotingData
|
||||
} 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 { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
|
||||
|
||||
// TODO: Duplicate
|
||||
interface VoteActions {
|
||||
vote: 'Y' | 'N' | 'A';
|
||||
vote: Vote;
|
||||
css: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type Vote = 'Y' | 'N' | 'A';
|
||||
|
||||
@Component({
|
||||
selector: 'os-assignment-poll-vote',
|
||||
templateUrl: './assignment-poll-vote.component.html',
|
||||
@ -31,11 +36,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
public AssignmentPollMethod = AssignmentPollMethod;
|
||||
public PollType = PollType;
|
||||
public voteActions: VoteActions[] = [];
|
||||
|
||||
/** holds the currently saved votes */
|
||||
public currentVotes: { [key: number]: string | null; global?: string } = {};
|
||||
|
||||
private votes: ViewAssignmentVote[];
|
||||
public voteRequestData: VotingData = {
|
||||
votes: {}
|
||||
};
|
||||
public alreadyVoted: boolean;
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
@ -43,23 +47,19 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
matSnackbar: MatSnackBar,
|
||||
vmanager: VotingService,
|
||||
operator: OperatorService,
|
||||
private voteRepo: AssignmentVoteRepositoryService,
|
||||
private pollRepo: AssignmentPollRepositoryService
|
||||
private pollRepo: AssignmentPollRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(title, translate, matSnackbar, vmanager, operator);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
if (this.poll) {
|
||||
if (this.poll && this.poll.user_has_not_voted) {
|
||||
this.alreadyVoted = false;
|
||||
this.defineVoteOptions();
|
||||
} else {
|
||||
this.alreadyVoted = true;
|
||||
}
|
||||
|
||||
this.subscriptions.push(
|
||||
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
||||
this.votes = votes;
|
||||
this.updateVotes();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private defineVoteOptions(): void {
|
||||
@ -90,63 +90,82 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
}
|
||||
|
||||
public getVotesCount(): number {
|
||||
return Object.keys(this.currentVotes).filter(key => this.currentVotes[key]).length;
|
||||
return Object.keys(this.voteRequestData.votes).filter(key => this.voteRequestData.votes[key]).length;
|
||||
}
|
||||
|
||||
protected updateVotes(): void {
|
||||
if (this.user && this.votes && this.poll) {
|
||||
const filtered = this.votes.filter(
|
||||
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
|
||||
);
|
||||
public isGlobalOptionSelected(): boolean {
|
||||
return !!this.voteRequestData.global;
|
||||
}
|
||||
|
||||
for (const option of this.poll.options) {
|
||||
let curr_vote = filtered.find(vote => vote.option.id === option.id);
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.Votes && curr_vote) {
|
||||
if (curr_vote.value !== 'Y') {
|
||||
this.currentVotes.global = curr_vote.valueVerbose;
|
||||
curr_vote = null;
|
||||
} else {
|
||||
this.currentVotes.global = null;
|
||||
public submitVote(): void {
|
||||
const title = this.translate.instant('Are you sure?');
|
||||
const content = this.translate.instant('Your decision cannot be changed afterwards');
|
||||
this.promptService.open(title, content).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.pollRepo
|
||||
.vote(this.voteRequestData, this.poll.id)
|
||||
.then(() => {
|
||||
this.alreadyVoted = true;
|
||||
})
|
||||
.catch(this.raiseError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public saveSingleVote(optionId: number, vote: Vote): void {
|
||||
if (this.isGlobalOptionSelected()) {
|
||||
delete this.voteRequestData.global;
|
||||
}
|
||||
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
|
||||
const votesAmount = this.poll.votes_amount;
|
||||
const tmpVoteRequest = this.poll.options
|
||||
.map(option => option.id)
|
||||
.reduce((o, n) => {
|
||||
o[n] = 0;
|
||||
if (votesAmount === 1) {
|
||||
if (n === optionId && this.voteRequestData.votes[n] !== 1) {
|
||||
o[n] = 1;
|
||||
}
|
||||
} else if ((n === optionId) !== (this.voteRequestData.votes[n] === 1)) {
|
||||
o[n] = 1;
|
||||
}
|
||||
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
// check if you can still vote
|
||||
const countedVotes = Object.keys(tmpVoteRequest).filter(key => tmpVoteRequest[key]).length;
|
||||
if (countedVotes <= votesAmount) {
|
||||
this.voteRequestData.votes = tmpVoteRequest;
|
||||
|
||||
// if you have no options anymore, try to send
|
||||
if (this.getVotesCount() === votesAmount) {
|
||||
this.submitVote();
|
||||
}
|
||||
this.currentVotes[option.id] = curr_vote && curr_vote.valueVerbose;
|
||||
} else {
|
||||
this.raiseError(
|
||||
this.translate.instant('You reached the maximum amount of votes. Deselect somebody first')
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// YN/YNA
|
||||
if (this.voteRequestData.votes[optionId] && this.voteRequestData.votes[optionId] === vote) {
|
||||
delete this.voteRequestData.votes[optionId];
|
||||
} else {
|
||||
this.voteRequestData.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPollOptionIds(): number[] {
|
||||
return this.poll.options.map(option => option.id);
|
||||
}
|
||||
|
||||
public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void {
|
||||
let requestData;
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
|
||||
const pollOptionIds = this.getPollOptionIds();
|
||||
|
||||
requestData = pollOptionIds.reduce((o, n) => {
|
||||
o[n] = 0;
|
||||
if (this.poll.votes_amount === 1) {
|
||||
if (n === optionId && this.currentVotes[n] !== 'Yes') {
|
||||
o[n] = 1;
|
||||
}
|
||||
} else if ((n === optionId) !== (this.currentVotes[n] === 'Yes')) {
|
||||
o[n] = 1;
|
||||
}
|
||||
|
||||
return o;
|
||||
}, {});
|
||||
} else {
|
||||
// YN/YNA
|
||||
requestData = {};
|
||||
requestData[optionId] = vote;
|
||||
}
|
||||
|
||||
this.pollRepo.vote(requestData, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
|
||||
public saveGlobalVote(globalVote: 'N' | 'A'): void {
|
||||
// This may be a bug in angulars HTTP client: A string is not quoted to be valid json.
|
||||
// Maybe they expect a string to be alrady a jsonified object.
|
||||
this.pollRepo.vote(`"${globalVote}"`, this.poll.id).catch(this.raiseError);
|
||||
public saveGlobalVote(globalVote: GlobalVote): void {
|
||||
this.voteRequestData.votes = {};
|
||||
this.voteRequestData.global = globalVote;
|
||||
this.submitVote();
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
<!-- Buttons -->
|
||||
<button
|
||||
mat-icon-button
|
||||
*osPerms="['core.can_manage_projector', 'assignments.can_manage_polls']"
|
||||
*osPerms="['core.can_manage_projector', 'assignments.can_manage']"
|
||||
[matMenuTriggerFor]="pollItemMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
@ -30,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Change state button -->
|
||||
<div *osPerms="'assignments.can_manage_polls'">
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngIf="!poll.isPublished"
|
||||
@ -67,7 +67,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Poll progress bar -->
|
||||
<div *osPerms="'assignments.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<div *osPerms="'assignments.can_manage'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
||||
|
@ -30,10 +30,10 @@ export abstract class BasePollVoteComponent<V extends ViewBasePoll> extends Base
|
||||
this.subscriptions.push(
|
||||
this.operator.getViewUserObservable().subscribe(user => {
|
||||
this.user = user;
|
||||
this.updateVotes();
|
||||
// this.updateVotes();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract updateVotes(): void;
|
||||
// protected abstract updateVotes(): void;
|
||||
}
|
||||
|
@ -86,7 +86,6 @@
|
||||
required
|
||||
|
||||
/>
|
||||
<mat-hint *ngIf="showSingleAmountHint"> {{ 'Multiple votes are disabled due to security reasons' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
|
||||
<mat-checkbox formControlName="global_abstain">{{
|
||||
|
@ -85,8 +85,6 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
*/
|
||||
public publishImmediately = true;
|
||||
|
||||
public showSingleAmountHint = false;
|
||||
|
||||
public showNonNominalWarning = false;
|
||||
|
||||
/**
|
||||
@ -210,29 +208,11 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
* TODO: Enabling this requires at least another layout and some rework
|
||||
*/
|
||||
private setVotesAmountCtrl(): void {
|
||||
// Disable "Amounts of votes" if anonymous and yes-method
|
||||
const votesAmountCtrl = this.contentForm.get('votes_amount');
|
||||
|
||||
if (this.contentForm.get('type').value === PollType.Pseudoanonymous) {
|
||||
this.showNonNominalWarning = true;
|
||||
} else {
|
||||
this.showNonNominalWarning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Not required when batch sending works again
|
||||
*/
|
||||
if (
|
||||
this.contentForm.get('type').value === PollType.Pseudoanonymous &&
|
||||
this.contentForm.get('pollmethod').value === 'votes'
|
||||
) {
|
||||
votesAmountCtrl.disable();
|
||||
votesAmountCtrl.setValue(1);
|
||||
this.showSingleAmountHint = true;
|
||||
} else {
|
||||
votesAmountCtrl.enable();
|
||||
this.showSingleAmountHint = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getValues<V extends ViewBasePoll>(): Partial<V> {
|
||||
|
@ -16,7 +16,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
||||
|
||||
class AssignmentPollAccessPermissions(BasePollAccessPermissions):
|
||||
base_permission = "assignments.can_see"
|
||||
manage_permission = "assignments.can_manage_polls"
|
||||
manage_permission = "assignments.can_manage"
|
||||
additional_fields = ["amount_global_no", "amount_global_abstain"]
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user