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 *ngFor="let option of options" class="votes-grid">
<div> <div>
<span *ngIf="option.user">{{ option.user.getFullName() }}</span> <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>
<div> <div>

View File

@ -1,48 +1,74 @@
<ng-container *ngIf="poll"> <ng-container *ngIf="poll">
<ng-container *ngIf="vmanager.canVote(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"> <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> <div>
<span *ngIf="option.user">{{ option.user.getFullName() }}</span> <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>
<div class="current-vote"> <div class="current-vote">
<ng-container *ngIf="currentVotes[option.user_id] !== null"> <ng-container *ngIf="poll.pollmethod !== pollMethods.Votes && currentVotes[option.user_id]">
({{ 'Current' | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }}) ({{ '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> </ng-container>
</div> </div>
<mat-radio-group <mat-radio-group
name="votes-{{ poll.id }}-{{ option.id }}" name="votes-{{ poll.id }}-{{ option.id }}"
[formControlName]="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> <span translate>Yes</span>
</mat-radio-button> </mat-radio-button>
<mat-radio-button value="N"> <mat-radio-button value="N" *ngIf="poll.pollmethod !== pollMethods.Votes">
<span translate>No</span> <span translate>No</span>
</mat-radio-button> </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> <span translate>Abstain</span>
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
</ng-container>
<mat-form-field *ngIf="poll.pollmethod === 'votes'" class="vote-input"> <!-- global no/abstain -->
<input matInput type="number" min="0" [formControlName]="option.id" /> <ng-container *ngIf="poll.pollmethod === pollMethods.Votes && (poll.global_no || poll.global_abstain)">
</mat-form-field> <!-- 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> </ng-container>
</form> </form>
<div class="right-align"> <div class="right-align" *ngIf="poll.type !== PollType.Named || poll.pollmethod !== pollMethods.Votes">
<button <button
mat-button mat-button
mat-button-default mat-button-default
(click)="saveVotes()" (click)="saveVotes()"
[disabled]="!voteForm || voteForm.invalid || voteForm.pristine" [disabled]="isSaveButtonDisabled()"
> >
<span translate>Save</span> <span translate>Save</span>
</button> </button>

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; 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 { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser'; 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 { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
import { VotingService } from 'app/core/ui-services/voting.service'; import { VotingService } from 'app/core/ui-services/voting.service';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; 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 { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll'; import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
import { ViewAssignmentVote } from '../../models/view-assignment-vote'; 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 { export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
public pollMethods = AssignmentPollMethods; public pollMethods = AssignmentPollMethods;
public PollType = PollType;
public voteForm: FormGroup; public voteForm: FormGroup;
/** holds the currently saved votes */ /** holds the currently saved votes */
public currentVotes: { [key: number]: string | number | null } = {}; public currentVotes: { [key: number]: string | null; global?: string } = {};
private votes: ViewAssignmentVote[]; private votes: ViewAssignmentVote[];
@ -57,30 +58,114 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
const filtered = this.votes.filter( const filtered = this.votes.filter(
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
); );
this.voteForm = this.formBuilder.group( this.voteForm = this.formBuilder.group({
this.poll.options.reduce((obj, option) => { votes: this.formBuilder.group(
obj[option.id] = ['', [Validators.required]]; this.poll.options.mapToObject(option => ({ [option.id]: ['', [Validators.required]] }))
return obj; )
}, {}) });
); if (
for (const option of this.poll.options) { this.poll.pollmethod === AssignmentPollMethods.Votes &&
const curr_vote = filtered.find(vote => vote.option.id === option.id); (this.poll.global_no || this.poll.global_abstain)
this.currentVotes[option.user_id] = curr_vote ) {
? this.poll.pollmethod === AssignmentPollMethods.Votes this.voteForm.addControl('global', new FormControl('', Validators.required));
? curr_vote.weight
: curr_vote.value
: null;
this.voteForm.get(option.id.toString()).setValue(this.currentVotes[option.user_id]);
} }
for (const option of this.poll.options) {
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();
}
});
}
}
}
private saveVotesIfNamed(): void {
if (this.poll.type === PollType.Named && !this.isSaveButtonDisabled()) {
this.saveVotes();
} }
} }
public saveVotes(): void { public saveVotes(): void {
this.pollRepo.vote(this.voteForm.value, this.poll.id).catch(this.raiseError); 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 getCurrentVoteVerbose(user_id: number): string { public isSaveButtonDisabled(): boolean {
const curr_vote = this.currentVotes[user_id]; return (
return this.poll.pollmethod === AssignmentPollMethods.Votes ? curr_vote : VoteValueVerbose[curr_vote]; !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> </span>
</div> </div>
<form [formGroup]="contentForm" class="poll-preview--meta-info-form"> <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-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"> <mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">
{{ option.value | translate }} {{ option.value | translate }}
</mat-option> </mat-option>
@ -32,12 +32,12 @@
[multiple]="true" [multiple]="true"
[showChips]="false" [showChips]="false"
[includeNone]="false" [includeNone]="false"
[placeholder]="'Entitled to vote' | translate" [placeholder]="PollPropertyVerbose.groups | translate"
[inputListValues]="groupObservable" [inputListValues]="groupObservable"
></os-search-value-selector> ></os-search-value-selector>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="pollMethods"> <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"> <mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
{{ option.value }} {{ option.value }}
</mat-option> </mat-option>
@ -47,20 +47,28 @@
</ng-container> </ng-container>
<mat-form-field> <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"> <ng-container *ngFor="let option of percentBases | keyvalue">
<mat-option *ngIf="isValidPercentBaseWithMethod(option.key)" [value]="option.key"> <mat-option [value]="option.key">{{ option.value | translate }}</mat-option>
{{ option.value | translate }}
</mat-option>
</ng-container> </ng-container>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<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"> <mat-option *ngFor="let option of majorityMethods | keyvalue" [value]="option.key">
{{ option.value | translate }} {{ option.value | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </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> </form>
</div> </div>

View File

@ -8,10 +8,13 @@ import { Observable } from 'rxjs';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { PercentBase } from 'app/shared/models/poll/base-poll'; 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 { BaseViewComponent } from 'app/site/base/base-view';
import { import {
MajorityMethodVerbose, MajorityMethodVerbose,
PercentBaseVerbose, PercentBaseVerbose,
PollPropertyVerbose,
PollTypeVerbose, PollTypeVerbose,
ViewBasePoll ViewBasePoll
} from 'app/site/polls/models/view-base-poll'; } from 'app/site/polls/models/view-base-poll';
@ -29,6 +32,9 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
*/ */
public contentForm: FormGroup; public contentForm: FormGroup;
public PollType = PollType;
public PollPropertyVerbose = PollPropertyVerbose;
/** /**
* The different methods for this poll. * The different methods for this poll.
*/ */
@ -92,6 +98,11 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
public ngOnInit(): void { public ngOnInit(): void {
this.groupObservable = this.groupRepo.getViewModelListObservable(); 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) { if (this.data) {
Object.keys(this.contentForm.controls).forEach(key => { Object.keys(this.contentForm.controls).forEach(key => {
if (this.data[key]) { if (this.data[key]) {
@ -118,12 +129,14 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
forbiddenBases = [PercentBase.YN, PercentBase.YNA]; forbiddenBases = [PercentBase.YN, PercentBase.YNA];
} }
this.percentBases = {}; const percentBases = {};
for (const [key, value] of Object.entries(PercentBaseVerbose)) { for (const [key, value] of Object.entries(PercentBaseVerbose)) {
if (!forbiddenBases.includes(key)) { 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 }; 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. * 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.getVerboseNameForKey(key),
this.pollService.getVerboseNameForValue(key, value as string) this.pollService.getVerboseNameForValue(key, value as string)
]); ]);
if (data.type === 'named') { if (data.type !== 'analog') {
this.pollValues.push([ this.pollValues.push([
this.pollService.getVerboseNameForKey('groups'), 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 { private initContentForm(): void {
@ -162,7 +176,10 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
pollmethod: ['', Validators.required], pollmethod: ['', Validators.required],
onehundred_percent_base: ['', Validators.required], onehundred_percent_base: ['', Validators.required],
majority_method: ['', 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', type: 'Poll type',
pollmethod: 'Poll method', pollmethod: 'Poll method',
state: 'State', 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 = { export const MajorityMethodVerbose = {

View File

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