Enhance Assignment Voting

- repaired the PDF Service for ballots
- fixed some permission errors
- analog voting has no "started" option anymore
- more-link as button
- named voting has a progress bar
- Shows the poll type for eVoting
- Moves and declutters meta info
- Enhance the grid and the layout in detail view
- declutter and enhance the dot-menus
- some other layout changes
- remove breadcrumbs in assignment detail
- other cleanups refinements
- Voting in Assignment over instead of forms
(requires more server changes)
This commit is contained in:
Sean Engelhardt 2020-02-11 11:24:43 +01:00 committed by FinnStutzenstein
parent 524a97cdcc
commit 6044c63c28
23 changed files with 383 additions and 319 deletions

View File

@ -116,6 +116,7 @@ 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);
}

View File

@ -51,19 +51,19 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
public onehundred_percent_base: PercentBase;
public user_has_voted: boolean;
public get isStateCreated(): boolean {
public get isCreated(): boolean {
return this.state === PollState.Created;
}
public get isStateStarted(): boolean {
public get isStarted(): boolean {
return this.state === PollState.Started;
}
public get isStateFinished(): boolean {
public get isFinished(): boolean {
return this.state === PollState.Finished;
}
public get isStatePublished(): boolean {
public get isPublished(): boolean {
return this.state === PollState.Published;
}
@ -71,20 +71,6 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
return this.onehundred_percent_base === PercentBase.Valid || this.onehundred_percent_base === PercentBase.Cast;
}
/**
* If the state is finished.
*/
public get isFinished(): boolean {
return this.state === PollState.Finished;
}
/**
* If the state is published.
*/
public get isPublished(): boolean {
return this.state === PollState.Published;
}
/**
* Determine if the state is finished or published
*/

View File

@ -6,7 +6,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
// MaterialUI modules
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonModule, MatAnchor } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';

View File

@ -1,16 +1,9 @@
<os-head-bar
[goBack]="true"
[nav]="false"
[hasMainButton]="poll ? poll.type === 'analog' && (poll.state === 2 || poll.state === 3) : false"
[mainButtonIcon]="'edit'"
[mainActionTooltip]="'Edit' | translate"
(mainEvent)="openDialog()"
>
<os-head-bar [goBack]="true" [nav]="false">
<div class="title-slot">
<h2 *ngIf="!!poll">{{ poll.title }}</h2>
</div>
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
<div class="menu-slot" *osPerms="'assignments.can_manage_polls'">
<button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
<mat-icon>more_vert</mat-icon>
</button>
@ -25,30 +18,14 @@
<ng-template #viewTemplate>
<ng-container *ngIf="isReady">
<h1>{{ poll.title }}</h1>
<mat-divider></mat-divider>
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
<div class="poll-content">
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
{{ 'Groups' | translate }}:
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
</div>
<div>{{ 'Voting type' | translate }}: {{ poll.typeVerbose | translate }}</div>
<div>{{ 'Election method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div>
<!-- TODO Enum -->
<div *ngIf="poll.state === 2">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
<div *ngIf="poll.stateHasVotes">
<h2 translate>Result</h2>
<div class="chart-wrapper" [ngClass]="{ flex: isVotedPoll }">
<mat-table [dataSource]="poll.tableData">
<div class="result-wrapper">
<!-- Result Table -->
<mat-table class="result-table" [dataSource]="poll.tableData">
<ng-container matColumnDef="user" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
@ -81,43 +58,64 @@
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
</mat-table>
<div class="chart-inner-wrapper">
<os-charts
*ngIf="chartDataSubject.value"
[type]="chartType"
[labels]="candidatesLabels"
[size]="isVotedPoll ? 70 : 100"
[legendPosition]="isVotedPoll ? 'right' : 'top'"
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>
<!-- Result Chart -->
<os-charts
class="result-chart"
*ngIf="chartDataSubject.value"
[type]="chartType"
[labels]="candidatesLabels"
[data]="chartDataSubject"
[hasPadding]="false"
[legendPosition]="isVotedPoll ? 'right' : 'top'"
></os-charts>
<!-- Named Result -->
<div class="named-result-table" *ngIf="poll.type === 'named' && votesDataSource.data">
<h3>{{ 'Single votes' | translate }}</h3>
<mat-form-field>
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
</mat-form-field>
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="users" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
<div *ngIf="!row.user">{{ 'Unknown user' | translate }}</div>
</mat-cell>
</ng-container>
<ng-container
[matColumnDef]="'votes-' + option.user_id"
*ngFor="let option of poll.options"
sticky
>
<mat-header-cell *matHeaderCellDef>
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row.votes[option.user_id] }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinitionPerName"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinitionPerName"></mat-row>
</mat-table>
</div>
</div>
</div>
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data">
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="users" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
<div *ngIf="!row.user">{{ 'Unknown user' | translate }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'votes-' + option.user_id" *ngFor="let option of poll.options" sticky>
<mat-header-cell *matHeaderCellDef>
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row.votes[option.user_id] }}
</mat-cell>
</ng-container>
<!-- Meta Infos -->
<div class="poll-content small">
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
{{ 'Groups' | translate }}:
<mat-header-row *matHeaderRowDef="columnDefinitionPerName"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinitionPerName"></mat-row>
</mat-table>
</ng-container>
<span *ngFor="let group of poll.groups; let i = index">
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
</span>
</div>
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div>
</ng-container>
</ng-template>
@ -125,13 +123,21 @@
<!-- More Menu -->
<mat-menu #pollDetailMenu="matMenu">
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
<mat-icon>polymer</mat-icon>
<span translate>Pseudoanonymize</span>
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="openDialog()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
<button
mat-menu-item
*osPerms="'assignments.can_manage_polls'; and: poll && poll.type === 'named'"
(click)="pseudoanonymizePoll()"
>
<mat-icon>warning</mat-icon>
<span translate>Anonymize votes</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="deletePoll()">
<mat-icon>delete</mat-icon>
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="deletePoll()">
<mat-icon color="warn">delete</mat-icon>
<span translate>Delete</span>
</button>
</mat-menu>

View File

@ -1,15 +1,40 @@
.chart-wrapper {
&.flex {
display: flex;
@import '~assets/styles/variables.scss';
.mat-table {
flex: 2;
.mat-column-votes {
justify-content: center;
}
}
.chart-inner-wrapper {
flex: 3;
}
.result-wrapper {
display: grid;
grid-gap: 10px;
grid-template-areas:
'chart'
'results'
'names';
}
@include desktop {
.result-wrapper {
grid-template-areas:
'results chart'
'names names';
grid-template-columns: 2fr 1fr;
}
}
.result-table {
grid-area: results;
}
.result-chart {
grid-area: chart;
max-width: 300px;
}
.named-result-table {
grid-area: names;
.mat-form-field {
font-size: 14px;
width: 100%;
}
}
.poll-content {
padding-top: 20px;
}

View File

@ -91,6 +91,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
if (this.isVotedPoll) {
this._chartType = 'doughnut';
this.chartDataSubject.next(this.poll.generateCircleChartData());
} else {
super.initChartData();
}
}

View File

@ -1,79 +1,80 @@
<ng-container *ngIf="poll">
<!-- Poll progress bar -->
<div *osPerms="'assignments.can_manage_polls'; and: poll.isStarted">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<ng-container *ngIf="vmanager.canVote(poll)">
<form *ngIf="voteForm" [formGroup]="voteForm" class="voting-grid">
<!-- 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>
<!-- Leftover votes -->
<h4 *ngIf="poll.pollmethod === pollMethods.Votes">
{{ 'Votes for this poll' | translate }}: {{ poll.votes_amount }}
<!-- ({{ getVotesCount() }}/{{ poll.votes_amount }} {{ 'Votes' | translate }}) -->
</h4>
<!-- 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">{{ "Unknown user" | translate }}</span>
</div>
<div class="current-vote">
<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"
>
<mat-radio-button value="Y" (click)="yesButtonClicked($event, option.id.toString())">
<span translate>Yes</span>
</mat-radio-button>
<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 === pollMethods.YNA">
<span translate>Abstain</span>
</mat-radio-button>
</mat-radio-group>
</ng-container>
<!-- 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" *ngIf="poll.type !== PollType.Named || poll.pollmethod !== pollMethods.Votes">
<button
mat-button
mat-button-default
(click)="saveVotes()"
[disabled]="isSaveButtonDisabled()"
<!-- Options and Actions -->
<div *ngFor="let option of poll.options; let i = index">
<div
[ngClass]="{
'yna-grid': poll.pollmethod === pollMethods.YNA,
'yn-grid': poll.pollmethod === pollMethods.YN,
'single-vote-grid': poll.pollmethod === pollMethods.Votes
}"
>
<span translate>Save</span>
</button>
<div class="vote-candidate-name">
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
<span *ngIf="!option.user">{{ 'Unknown user' | translate }}</span>
</div>
<div *ngFor="let action of voteActions">
<button
mat-raised-button
(click)="saveSingleVote(option.id, action.vote)"
[ngClass]="currentVotes[option.id] ? action.css : ''"
>
<mat-icon> {{ action.icon }}</mat-icon>
</button>
<span *ngIf="poll.pollmethod !== pollMethods.Votes" class="vote-label">
{{ action.label | translate }}
</span>
</div>
</div>
<mat-divider *ngIf="poll.options.length - 1 > i"></mat-divider>
</div>
<!-- global no/abstain -->
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && (poll.global_no || poll.global_abstain)">
<mat-divider></mat-divider>
<div class="global-option-grid">
<div *ngIf="poll.global_no">
<button
mat-raised-button
(click)="saveGlobalVote('N')"
[ngClass]="currentVotes['global'] === 'No' ? 'voted-no' : ''"
>
<mat-icon> thumb_down </mat-icon>
</button>
<span class="vote-label">
{{ 'No to all' | translate }}
</span>
</div>
<div *ngIf="poll.global_abstain">
<button
mat-raised-button
(click)="saveGlobalVote('A')"
[ngClass]="currentVotes['global'] === 'Abstain' ? 'voted-abstain' : ''"
>
<mat-icon> trip_origin</mat-icon>
</button>
<span class="vote-label">
{{ 'Abstain' | translate }}
</span>
</div>
</div>
</ng-container>
</ng-container>
<!-- Shows the permission error -->
<ng-container *ngIf="!vmanager.canVote(poll)">
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
</ng-container>

View File

@ -1,12 +1,61 @@
.current-vote {
color: #777;
margin-right: 10px;
@import '~assets/styles/poll-colors.scss';
%vote-grid-base {
display: grid;
grid-gap: 10px;
margin: 20px 0;
}
.voting-grid {
display: grid;
grid-gap: 5px;
padding: 5px;
align-items: baseline;
grid-template-columns: auto max-content max-content;
.yn-grid {
@extend %vote-grid-base;
grid-template-areas:
'name name'
'yes no';
}
.yna-grid {
@extend %vote-grid-base;
grid-template-areas:
'name name name'
'yes no abstain';
}
.single-vote-grid {
@extend %vote-grid-base;
grid-template-areas: 'yes name';
grid-template-columns: min-content auto;
}
.global-option-grid {
@extend %vote-grid-base;
grid-template-columns: auto auto;
}
.vote-candidate-name {
grid-area: name;
display: flex;
span {
margin-top: auto;
margin-bottom: auto;
}
}
.voted-yes {
background-color: $votes-yes-color;
}
.voted-no {
background-color: $votes-no-color;
}
.voted-abstain {
background-color: $votes-abstain-color;
}
.vote-label {
margin-left: 10px;
}
.mat-divider-horizontal {
position: initial;
}

View File

@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
@ -15,6 +14,14 @@ import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
// TODO: Duplicate
interface VoteActions {
vote: 'Y' | 'N' | 'A';
css: string;
icon: string;
label: string;
}
@Component({
selector: 'os-assignment-poll-vote',
templateUrl: './assignment-poll-vote.component.html',
@ -23,8 +30,7 @@ import { ViewAssignmentVote } from '../../models/view-assignment-vote';
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
public pollMethods = AssignmentPollMethods;
public PollType = PollType;
public voteForm: FormGroup;
public voteActions: VoteActions[] = [];
/** holds the currently saved votes */
public currentVotes: { [key: number]: string | null; global?: string } = {};
@ -38,13 +44,13 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
vmanager: VotingService,
operator: OperatorService,
private voteRepo: AssignmentVoteRepositoryService,
private pollRepo: AssignmentPollRepositoryService,
private formBuilder: FormBuilder
private pollRepo: AssignmentPollRepositoryService
) {
super(title, translate, matSnackbar, vmanager, operator);
}
public ngOnInit(): void {
this.defineVoteOptions();
this.subscriptions.push(
this.voteRepo.getViewModelListObservable().subscribe(votes => {
this.votes = votes;
@ -53,119 +59,74 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
);
}
private defineVoteOptions(): void {
this.voteActions.push({
vote: 'Y',
css: 'voted-yes',
icon: 'thumb_up',
label: 'Yes'
});
if (this.poll.pollmethod !== AssignmentPollMethods.Votes) {
this.voteActions.push({
vote: 'N',
css: 'voted-no',
icon: 'thumb_down',
label: 'No'
});
}
if (this.poll.pollmethod === AssignmentPollMethods.YNA) {
this.voteActions.push({
vote: 'A',
css: 'voted-abstain',
icon: 'trip_origin',
label: 'Abstain'
});
}
}
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
);
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) {
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();
}
});
this.currentVotes[option.id] = curr_vote && curr_vote.valueVerbose;
}
}
}
private saveVotesIfNamed(): void {
if (this.poll.type === PollType.Named && !this.isSaveButtonDisabled()) {
this.saveVotes();
}
private getPollOptionIds(): number[] {
return this.poll.options.map(option => option.id);
}
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);
public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void {
const pollOptionIds = this.getPollOptionIds();
const requestMap = pollOptionIds.reduce((o, n) => {
if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) {
o[n] = 1;
} else {
this.poll.options.forEach(option => {
values[option.id] = this.voteForm.value.votes[option.id] === 'Y' ? 1 : 0;
});
o[n] = 0;
}
}
this.pollRepo.vote(values, this.poll.id).catch(this.raiseError);
return o;
}, {});
this.pollRepo.vote(JSON.stringify(requestMap), 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();
}
}
}
public saveGlobalVote(globalVote: 'N' | 'A'): void {
this.pollRepo.vote(`"${globalVote}"`, this.poll.id).catch(this.raiseError);
}
}

View File

@ -1,4 +1,4 @@
<mat-card class="os-card" *ngIf="poll">
<mat-card class="os-card" *ngIf="poll && showPoll()">
<div class="assignment-poll-wrapper">
<div class="assignment-poll-title-header">
<mat-card-title>
@ -8,28 +8,24 @@
</mat-card-title>
<div class="poll-properties">
<mat-chip
*osPerms="'assignments.can_manage'"
*osPerms="'assignments.can_manage_polls'"
class="poll-state active"
[disableRipple]="true"
[matMenuTriggerFor]="triggerMenu"
[ngClass]="poll.stateVerbose.toLowerCase()"
>
{{ poll.stateVerbose | translate }}
</mat-chip>
<mat-chip
*ngIf="!canManage && poll.isPublished"
[disableRipple]="true"
class="poll-state active"
[ngClass]="poll.stateVerbose.toLowerCase()"
[class]="poll.stateVerbose.toLowerCase()"
[ngClass]="{ disabled: !poll.getNextStates() }"
>
{{ poll.stateVerbose | translate }}
</mat-chip>
<span *ngIf="poll.type !== 'analog'">
{{ poll.typeVerbose | translate }}
</span>
</div>
<div class="poll-menu">
<!-- Buttons -->
<button
mat-icon-button
*osPerms="'assignments.can_manage'; &quot;core.can_manage_projector&quot;"
*osPerms="'assignments.motions.can_manage_polls';or: 'core.can_manage_projector'"
[matMenuTriggerFor]="pollItemMenu"
(click)="$event.stopPropagation()"
>
@ -39,9 +35,19 @@
</div>
<div *ngIf="hasVotes">
<os-charts [type]="chartType" [labels]="candidatesLabels" [data]="chartDataSubject"></os-charts>
<os-charts
[type]="chartType"
[labels]="candidatesLabels"
[data]="chartDataSubject"
[hasPadding]="false"
></os-charts>
</div>
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
<div class="poll-detail-button-wrapper">
<a mat-button routerLink="/assignments/polls/{{ poll.id }}">
{{ 'More' | translate }}
</a>
</div>
</div>
</mat-card>
@ -64,6 +70,10 @@
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
</div>
<div *osPerms="'assignments.can_manage'">
<button mat-menu-item (click)="printBallot()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Ballot paper</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
<mat-icon>delete</mat-icon>

View File

@ -1,7 +1,7 @@
.assignment-poll-wrapper {
@import '~assets/styles/poll-common-styles.scss';
position: relative;
padding: 0 15px;
margin: 0 15px;
.poll-menu {
position: absolute;
@ -39,4 +39,12 @@
}
}
}
.poll-detail-button-wrapper {
display: flex;
margin: auto 0;
> a {
margin-left: auto;
}
}
}

View File

@ -15,6 +15,7 @@ import { PollState } from 'app/shared/models/poll/base-poll';
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
import { PollService } from 'app/site/polls/services/poll.service';
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
import { ViewAssignmentOption } from '../../models/view-assignment-option';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
@ -89,7 +90,8 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
pollDialog: AssignmentPollDialogService,
public pollService: PollService,
private operator: OperatorService,
private formBuilder: FormBuilder
private formBuilder: FormBuilder,
private pdfService: AssignmentPollPdfService
) {
super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog);
}
@ -105,11 +107,17 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
/**
* Print the PDF of this poll with the corresponding options and numbers
*
*/
public printBallot(): void {
throw new Error('TODO');
// this.pdfService.printBallots(this.poll);
this.pdfService.printBallots(this.poll);
}
public showPoll(): boolean {
return (
this.operator.hasPerms('assignments.can_manage_polls') ||
this.poll.isPublished ||
(this.poll.type !== 'analog' && this.poll.isStarted)
);
}
/**

View File

@ -2,7 +2,7 @@ import { BehaviorSubject } from 'rxjs';
import { ChartData } from 'app/shared/components/charts/charts.component';
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
import { PollColor } from 'app/shared/models/poll/base-poll';
import { PollColor, PollState } from 'app/shared/models/poll/base-poll';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { PollData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
@ -87,6 +87,16 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
return data;
}
/**
* Override from base poll to skip started state in analog poll type
*/
public getNextStates(): { [key: number]: string } {
if (this.poll.type === 'analog' && this.state === PollState.Created) {
return null;
}
return super.getNextStates();
}
public getPercentBase(): number {
return 0;
}

View File

@ -7,6 +7,7 @@ import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
/**
@ -138,7 +139,7 @@ export class AssignmentPollPdfService extends PollPdfService {
// TODO: typing of result
private createCandidateFields(poll: ViewAssignmentPoll): object {
/*const candidates = poll.options.sort((a, b) => {
const candidates = poll.options.sort((a, b) => {
return a.weight - b.weight;
});
const resultObject = candidates.map(cand => {
@ -151,13 +152,12 @@ export class AssignmentPollPdfService extends PollPdfService {
noEntry.margin[1] = 25;
resultObject.push(noEntry);
}
return resultObject;*/
throw new Error('TODO');
return resultObject;
}
// TODO: typing of result
/*private createYNBallotEntry(option: string, method: AssignmentPollmethods): object {
const choices = method === 'yna' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
private createYNBallotEntry(option: string, method: AssignmentPollMethods): object {
const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
const columnstack = choices.map(choice => {
return {
width: 'auto',
@ -174,7 +174,7 @@ export class AssignmentPollPdfService extends PollPdfService {
columns: columnstack
}
];
}*/
}
/**
* Generates the poll description
@ -184,10 +184,9 @@ export class AssignmentPollPdfService extends PollPdfService {
*/
// TODO: typing of result
private createPollHint(poll: ViewAssignmentPoll): object {
/*return {
return {
text: poll.description || '',
style: 'description'
};*/
throw new Error('TODO');
};
}
}

View File

@ -31,7 +31,6 @@
class="result-chart"
*ngIf="chartDataSubject.value"
[type]="chartType"
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>

View File

@ -42,7 +42,7 @@
</div>
<!-- Publish immediately button. Only show for new polls -->
<div *ngIf="!pollData.isStatePublished">
<div *ngIf="!pollData.isPublished">
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
<span translate>Publish immediately</span>
</mat-checkbox>

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="poll">
<div *osPerms="'motions.can_manage_polls';and:poll.isStateStarted">
<div *osPerms="'motions.can_manage_polls';and:poll.isStarted">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<ng-container *ngIf="vmanager.canVote(poll)">

View File

@ -20,19 +20,11 @@
</div>
<mat-chip
*ngIf="poll.getNextStates()"
disableRipple
class="poll-state active"
[matMenuTriggerFor]="triggerMenu"
[ngClass]="poll.stateVerbose.toLowerCase()"
>
{{ poll.stateVerbose }}
</mat-chip>
<mat-chip
*ngIf="!poll.getNextStates()"
disableRipple
class="poll-state active"
[ngClass]="poll.stateVerbose.toLowerCase()"
[class]="poll.stateVerbose.toLowerCase()"
[ngClass]="{ 'disabled': !poll.getNextStates() }"
>
{{ poll.stateVerbose }}
</mat-chip>
@ -87,9 +79,9 @@
</div>
</div>
<div class="poll-detail-button-wrapper">
<button mat-button [routerLink]="pollLink">
<a mat-button [routerLink]="pollLink">
{{ 'More' | translate }}
</button>
</a>
</div>
</ng-template>

View File

@ -88,7 +88,7 @@
.poll-detail-button-wrapper {
display: flex;
margin: auto 0;
> button {
> a {
margin-left: auto;
}
}

View File

@ -130,8 +130,8 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
public showPoll(): boolean {
return (
this.operator.hasPerms('motions.can_manage_polls') ||
this.poll.isStatePublished ||
(this.poll.type !== 'analog' && this.poll.isStateStarted)
this.poll.isPublished ||
(this.poll.type !== 'analog' && this.poll.isStarted)
);
}

View File

@ -18,7 +18,7 @@
</span>
</div>
<form [formGroup]="contentForm" class="poll-preview-meta-info-form">
<ng-container *ngIf="!data || !data.state || data.isStateCreated">
<ng-container *ngIf="!data || !data.state || data.isCreated">
<!-- Poll Type -->
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
@ -71,7 +71,7 @@
<!-- Amount of Votes -->
<ng-container
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isStateCreated)"
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isCreated)"
>
<mat-form-field>
<input

View File

@ -856,6 +856,13 @@ button.mat-menu-item.selected {
}
}
/**
* Use to disable events on (i.e) matMenuTriggerFor
*/
.disabled {
pointer-events: none;
}
// custom horrizontal scroll-bar
.h-scroller {

View File

@ -407,7 +407,7 @@ class AssignmentPollViewSet(BasePollViewSet):
YN/YNA:
{<option_id>: 'Y' | 'N' [|'A']}
- all option_ids must be given
- all option_ids must be given TODO: No it must not be that way. Single Votes have to be accepted
- 'A' is only allowed in YNA pollmethod
Votes for all options have to be given