diff --git a/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.html b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.html index f67ea8518..79de2161e 100644 --- a/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -6,7 +6,7 @@
{{ option.user.getFullName() }} - No user {{ option.candidate_id }} + {{ 'Unknown User' | translate }}
diff --git a/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.html b/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.html index 86b367c4c..9dfc2c126 100644 --- a/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.html +++ b/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.html @@ -1,48 +1,74 @@ - {{ 'You can distribute' | translate }} {{ poll.votes_amount }} {{ 'votes' | translate }}.
- + +
+
+ + ({{ getVotesCount() }}/{{ poll.votes_amount }} {{ 'Votes' | translate }}) + +
+ + +
{{ option.user.getFullName() }} - No user {{ option.candidate_id }} + {{ "Unknown user" | translate }}
- - ({{ 'Current' | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }}) + + ({{ 'Current' | translate }}: {{ currentVotes[option.user_id] | translate }}) + + + ({{ 'Current choice' | translate }})
- + Yes - + No - + Abstain +
- - - + + + +
+
+ + ({{ 'Current' | translate }}: {{ currentVotes.global | translate }}) + +
+ + + Global no + + + Global abstain + +
-
+
diff --git a/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.ts b/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.ts index f5b0fb3cc..e90c4da91 100644 --- a/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll-vote/assignment-poll-vote.component.ts @@ -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 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 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(); + } + } + } } } diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.html b/client/src/app/site/polls/components/poll-form/poll-form.component.html index 3f8213039..e6966d0dd 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.html +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.html @@ -17,9 +17,9 @@
- + - + {{ option.value | translate }} @@ -32,12 +32,12 @@ [multiple]="true" [showChips]="false" [includeNone]="false" - [placeholder]="'Entitled to vote' | translate" + [placeholder]="PollPropertyVerbose.groups | translate" [inputListValues]="groupObservable" > - + {{ option.value }} @@ -47,20 +47,28 @@ - + - - {{ option.value | translate }} - + {{ option.value | translate }} - + {{ option.value | translate }} + + + + + + + {{ PollPropertyVerbose.global_no | translate }} + {{ PollPropertyVerbose.global_abstain | translate }} + +
diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.ts b/client/src/app/site/polls/components/poll-form/poll-form.component.ts index d975844da..fbc99ccca 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.ts @@ -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 = 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: [] }); } } diff --git a/client/src/app/site/polls/models/view-base-poll.ts b/client/src/app/site/polls/models/view-base-poll.ts index 0b28c742d..ea2275220 100644 --- a/client/src/app/site/polls/models/view-base-poll.ts +++ b/client/src/app/site/polls/models/view-base-poll.ts @@ -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 = { diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index dd552d1e7..10197b63b 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -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], } )