improved 'votes' pollmethod

This commit is contained in:
Joshua Sangmeister 2020-01-22 17:31:10 +01:00 committed by FinnStutzenstein
parent 1de73d5701
commit bc54a6eb46
7 changed files with 198 additions and 59 deletions

View File

@ -6,7 +6,7 @@
<div *ngFor="let option of options" class="votes-grid">
<div>
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
<span *ngIf="!option.user">{{ 'Unknown User' | translate }}</span>
</div>
<div>

View File

@ -1,48 +1,74 @@
<ng-container *ngIf="poll">
<ng-container *ngIf="vmanager.canVote(poll)">
<span *ngIf="poll.pollmethod === 'votes'"
>{{ 'You can distribute' | translate }} {{ poll.votes_amount }} {{ 'votes' | translate }}.</span
>
<form *ngIf="voteForm" [formGroup]="voteForm" class="voting-grid">
<ng-container *ngFor="let option of poll.options">
<!-- empty divs to fit the grid -->
<div></div><div></div>
<div>
<span *ngIf="poll.pollmethod === pollMethods.Votes">
({{ getVotesCount() }}/{{ poll.votes_amount }} {{ 'Votes' | translate }})
</span>
</div>
<!-- candidate votes -->
<ng-container *ngFor="let option of poll.options" formGroupName="votes">
<div>
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
<span *ngIf="!option.user">{{ "Unknown user" | translate }}</span>
</div>
<div class="current-vote">
<ng-container *ngIf="currentVotes[option.user_id] !== null">
({{ 'Current' | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }})
<ng-container *ngIf="poll.pollmethod !== pollMethods.Votes && currentVotes[option.user_id]">
({{ 'Current' | translate }}: {{ currentVotes[option.user_id] | translate }})
</ng-container>
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && currentVotes[option.user_id]">
({{ 'Current choice' | translate }})
</ng-container>
</div>
<mat-radio-group
name="votes-{{ poll.id }}-{{ option.id }}"
[formControlName]="option.id"
*ngIf="poll.pollmethod !== 'votes'"
>
<mat-radio-button value="Y">
<mat-radio-button value="Y" (click)="yesButtonClicked($event, option.id.toString())">
<span translate>Yes</span>
</mat-radio-button>
<mat-radio-button value="N">
<mat-radio-button value="N" *ngIf="poll.pollmethod !== pollMethods.Votes">
<span translate>No</span>
</mat-radio-button>
<mat-radio-button value="A" *ngIf="poll.pollmethod === 'YNA'">
<mat-radio-button value="A" *ngIf="poll.pollmethod === pollMethods.YNA">
<span translate>Abstain</span>
</mat-radio-button>
</mat-radio-group>
</ng-container>
<mat-form-field *ngIf="poll.pollmethod === 'votes'" class="vote-input">
<input matInput type="number" min="0" [formControlName]="option.id" />
</mat-form-field>
<!-- global no/abstain -->
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && (poll.global_no || poll.global_abstain)">
<!-- empty div to fit the grid -->
<div></div>
<div class="current-vote">
<ng-container *ngIf="currentVotes.global">
({{ 'Current' | translate }}: {{ currentVotes.global | translate }})
</ng-container>
</div>
<mat-radio-group
name="votes-{{ poll.id }}-global"
formControlName="global"
>
<mat-radio-button value="N" *ngIf="poll.global_no">
<span translate>Global no</span>
</mat-radio-button>
<mat-radio-button value="A" *ngIf="poll.global_abstain">
<span translate>Global abstain</span>
</mat-radio-button>
</mat-radio-group>
</ng-container>
</form>
<div class="right-align">
<div class="right-align" *ngIf="poll.type !== PollType.Named || poll.pollmethod !== pollMethods.Votes">
<button
mat-button
mat-button-default
(click)="saveVotes()"
[disabled]="!voteForm || voteForm.invalid || voteForm.pristine"
[disabled]="isSaveButtonDisabled()"
>
<span translate>Save</span>
</button>

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
@ -10,7 +10,7 @@ import { AssignmentPollRepositoryService } from 'app/core/repositories/assignmen
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
import { VotingService } from 'app/core/ui-services/voting.service';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
import { VoteValueVerbose } from 'app/shared/models/poll/base-vote';
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';
@ -22,11 +22,12 @@ import { ViewAssignmentVote } from '../../models/view-assignment-vote';
})
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
public pollMethods = AssignmentPollMethods;
public PollType = PollType;
public voteForm: FormGroup;
/** holds the currently saved votes */
public currentVotes: { [key: number]: string | number | null } = {};
public currentVotes: { [key: number]: string | null; global?: string } = {};
private votes: ViewAssignmentVote[];
@ -57,30 +58,114 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
const filtered = this.votes.filter(
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
);
this.voteForm = this.formBuilder.group(
this.poll.options.reduce((obj, option) => {
obj[option.id] = ['', [Validators.required]];
return obj;
}, {})
);
this.voteForm = this.formBuilder.group({
votes: this.formBuilder.group(
this.poll.options.mapToObject(option => ({ [option.id]: ['', [Validators.required]] }))
)
});
if (
this.poll.pollmethod === AssignmentPollMethods.Votes &&
(this.poll.global_no || this.poll.global_abstain)
) {
this.voteForm.addControl('global', new FormControl('', Validators.required));
}
for (const option of this.poll.options) {
const curr_vote = filtered.find(vote => vote.option.id === option.id);
this.currentVotes[option.user_id] = curr_vote
? this.poll.pollmethod === AssignmentPollMethods.Votes
? curr_vote.weight
: curr_vote.value
: null;
this.voteForm.get(option.id.toString()).setValue(this.currentVotes[option.user_id]);
let curr_vote = filtered.find(vote => vote.option.id === option.id);
if (this.poll.pollmethod === AssignmentPollMethods.Votes && curr_vote) {
if (curr_vote.value !== 'Y') {
this.currentVotes.global = curr_vote.valueVerbose;
this.voteForm.controls.global.setValue(curr_vote.value);
curr_vote = null;
} else {
this.currentVotes.global = null;
}
}
this.currentVotes[option.user_id] = curr_vote && curr_vote.valueVerbose;
this.voteForm.get(['votes', option.id]).setValue(curr_vote && curr_vote.value);
}
if (this.poll.pollmethod === AssignmentPollMethods.Votes) {
this.voteForm.controls.votes.valueChanges.subscribe(value => {
if (Object.values(value).some(vote => vote)) {
const ctrl = this.voteForm.controls.global;
if (ctrl) {
ctrl.reset();
}
this.saveVotesIfNamed();
}
});
this.voteForm.controls.global.valueChanges.subscribe(value => {
if (value) {
this.voteForm.controls.votes.reset();
this.saveVotesIfNamed();
}
});
}
}
}
public saveVotes(): void {
this.pollRepo.vote(this.voteForm.value, this.poll.id).catch(this.raiseError);
private saveVotesIfNamed(): void {
if (this.poll.type === PollType.Named && !this.isSaveButtonDisabled()) {
this.saveVotes();
}
}
public getCurrentVoteVerbose(user_id: number): string {
const curr_vote = this.currentVotes[user_id];
return this.poll.pollmethod === AssignmentPollMethods.Votes ? curr_vote : VoteValueVerbose[curr_vote];
public saveVotes(): void {
let values = this.voteForm.value.votes;
// convert Y to 1 and null to 0 for votes method
if (this.poll.pollmethod === this.pollMethods.Votes) {
if (this.voteForm.value.global) {
values = JSON.stringify(this.voteForm.value.global);
} else {
this.poll.options.forEach(option => {
values[option.id] = this.voteForm.value.votes[option.id] === 'Y' ? 1 : 0;
});
}
}
this.pollRepo.vote(values, this.poll.id).catch(this.raiseError);
}
public isSaveButtonDisabled(): boolean {
return (
!this.voteForm ||
this.voteForm.pristine ||
(this.poll.pollmethod === AssignmentPollMethods.Votes
? !this.getAllFormControls().some(control => control.valid)
: this.voteForm.invalid)
);
}
public getVotesCount(): number {
return Object.values(this.voteForm.value.votes).filter(vote => vote).length;
}
private getAllFormControls(): AbstractControl[] {
if (this.voteForm) {
const votesFormGroup = this.voteForm.controls.votes as FormGroup;
return [...Object.values(votesFormGroup.controls), this.voteForm.controls.global];
} else {
return [];
}
}
public yesButtonClicked($event: MouseEvent, optionId: string): void {
if (this.poll.pollmethod === AssignmentPollMethods.Votes) {
// check current value (before click)
if (this.voteForm.value.votes[optionId] === 'Y') {
// this handler is executed before the mat-radio-button handler, so we have to set a timeout or else the other handler will just set the value again
setTimeout(() => {
this.voteForm.get(['votes', optionId]).setValue(null);
this.voteForm.markAsDirty();
this.saveVotesIfNamed();
});
} else {
// check if by clicking this button, the amount of votes would succeed the permitted amount
if (this.getVotesCount() >= this.poll.votes_amount) {
$event.preventDefault();
}
}
}
}
}

View File

@ -17,9 +17,9 @@
</span>
</div>
<form [formGroup]="contentForm" class="poll-preview--meta-info-form">
<ng-container *ngIf="!data || !data.state || data.state === 1">
<ng-container *ngIf="!data || !data.state || data.isStateCreated">
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
<mat-select [placeholder]="'Voting type' | translate" formControlName="type" required>
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
<mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">
{{ option.value | translate }}
</mat-option>
@ -32,12 +32,12 @@
[multiple]="true"
[showChips]="false"
[includeNone]="false"
[placeholder]="'Entitled to vote' | translate"
[placeholder]="PollPropertyVerbose.groups | translate"
[inputListValues]="groupObservable"
></os-search-value-selector>
</mat-form-field>
<mat-form-field *ngIf="pollMethods">
<mat-select [placeholder]="'Poll method' | translate" formControlName="pollmethod" required>
<mat-select [placeholder]="PollPropertyVerbose.pollmethod | translate" formControlName="pollmethod" required>
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
{{ option.value }}
</mat-option>
@ -47,20 +47,28 @@
</ng-container>
<mat-form-field>
<mat-select placeholder="{{ '100% base' | translate }}" formControlName="onehundred_percent_base" required>
<mat-select placeholder="{{ PollPropertyVerbose.onehundred_percent_base | translate }}" formControlName="onehundred_percent_base" required>
<ng-container *ngFor="let option of percentBases | keyvalue">
<mat-option *ngIf="isValidPercentBaseWithMethod(option.key)" [value]="option.key">
{{ option.value | translate }}
</mat-option>
<mat-option [value]="option.key">{{ option.value | translate }}</mat-option>
</ng-container>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="{{ 'Required majority' | translate }}" formControlName="majority_method" required>
<mat-select placeholder="{{ PollPropertyVerbose.majority_method | translate }}" formControlName="majority_method" required>
<mat-option *ngFor="let option of majorityMethods | keyvalue" [value]="option.key">
{{ option.value | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngIf="!data || !data.state || data.state === 1">
<ng-container *ngIf="contentForm.get('pollmethod').value === 'votes'">
<mat-form-field>
<input type="number" matInput placeholder="{{ PollPropertyVerbose.votes_amount | translate }}" formControlName="votes_amount" min="1" required>
</mat-form-field>
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
<mat-checkbox formControlName="global_abstain">{{ PollPropertyVerbose.global_abstain | translate }}</mat-checkbox>
</ng-container>
</ng-container>
</form>
</div>

View File

@ -8,10 +8,13 @@ import { Observable } from 'rxjs';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { PercentBase } from 'app/shared/models/poll/base-poll';
import { PollType } from 'app/shared/models/poll/base-poll';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BaseViewComponent } from 'app/site/base/base-view';
import {
MajorityMethodVerbose,
PercentBaseVerbose,
PollPropertyVerbose,
PollTypeVerbose,
ViewBasePoll
} from 'app/site/polls/models/view-base-poll';
@ -29,6 +32,9 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
*/
public contentForm: FormGroup;
public PollType = PollType;
public PollPropertyVerbose = PollPropertyVerbose;
/**
* The different methods for this poll.
*/
@ -92,6 +98,11 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
public ngOnInit(): void {
this.groupObservable = this.groupRepo.getViewModelListObservable();
const cast = <ViewAssignmentPoll>this.data;
if (cast.assignment && !cast.votes_amount) {
cast.votes_amount = cast.assignment.open_posts;
}
if (this.data) {
Object.keys(this.contentForm.controls).forEach(key => {
if (this.data[key]) {
@ -118,12 +129,14 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
forbiddenBases = [PercentBase.YN, PercentBase.YNA];
}
this.percentBases = {};
const percentBases = {};
for (const [key, value] of Object.entries(PercentBaseVerbose)) {
if (!forbiddenBases.includes(key)) {
this.percentBases[key] = value;
percentBases[key] = value;
}
}
this.percentBases = percentBases;
// TODO: update selected base
});
}
@ -131,10 +144,6 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
return { ...this.data, ...this.contentForm.value };
}
public isValidPercentBaseWithMethod(base: PercentBase): boolean {
return !(base === PercentBase.YNA && this.contentForm.get('pollmethod').value === 'YN');
}
/**
* This updates the poll-values to get correct data in the view.
*
@ -147,12 +156,17 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
this.pollService.getVerboseNameForKey(key),
this.pollService.getVerboseNameForValue(key, value as string)
]);
if (data.type === 'named') {
if (data.type !== 'analog') {
this.pollValues.push([
this.pollService.getVerboseNameForKey('groups'),
this.groupRepo.getNameForIds(...data.groups_id)
this.groupRepo.getNameForIds(...([] || (data && data.groups_id)))
]);
}
if (data.pollmethod === 'votes') {
this.pollValues.push([this.pollService.getVerboseNameForKey('votes_amount'), data.votes_amount]);
this.pollValues.push([this.pollService.getVerboseNameForKey('global_no'), data.global_no]);
this.pollValues.push([this.pollService.getVerboseNameForKey('global_abstain'), data.global_abstain]);
}
}
private initContentForm(): void {
@ -162,7 +176,10 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
pollmethod: ['', Validators.required],
onehundred_percent_base: ['', Validators.required],
majority_method: ['', Validators.required],
groups_id: [[]]
votes_amount: [1, [Validators.required, Validators.min(1)]],
groups_id: [],
global_no: [],
global_abstain: []
});
}
}

View File

@ -32,7 +32,10 @@ export const PollPropertyVerbose = {
type: 'Poll type',
pollmethod: 'Poll method',
state: 'State',
groups: 'Entitled to vote'
groups: 'Entitled to vote',
votes_amount: 'Amount of votes',
global_no: 'Enable global no',
global_abstain: 'Enable global abstain'
};
export const MajorityMethodVerbose = {

View File

@ -474,10 +474,10 @@ class AssignmentPollViewSet(BasePollViewSet):
)
amount_sum += amount
if amount_sum != poll.votes_amount:
if amount_sum > poll.votes_amount:
raise ValidationError(
{
"detail": "You have to give exactly {0} votes",
"detail": "You can give a maximum of {0} votes",
"args": [poll.votes_amount],
}
)