More voting UI improvements

For Motion poll:
- Overworked how motion poll chart displays the legend
- Added the vote counter to the motion detail
- Added a progress bar to the vote counter
- Fixed some perm errors with the chart
- Show a "Singe Votes" link as button for published named polls
- Replace the edit-button with a dot-menu
  - Having project, Edit, PDF and Delete

For Motion Poll detail:
- enhance search panel
- Remove the breadcrumbs
- Remove the vote counter
- Enhanced the single-vote grid, table and filter bar
- Enhance how the poll state enum was checkend

For the Motion Poll Create/Update Form:
- Remove the selection of poll-methode (whenever possible)
- only show "publish imediately" during creation
This commit is contained in:
Sean Engelhardt 2020-01-22 17:39:10 +01:00 committed by FinnStutzenstein
parent 682db96b7c
commit 84a39ccb62
41 changed files with 400 additions and 252 deletions

View File

@ -95,7 +95,6 @@ export class AppComponent {
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
// change default JS functions // change default JS functions
this.overloadArrayToString();
this.overloadArrayFunctions(); this.overloadArrayFunctions();
this.overloadModulo(); this.overloadModulo();
@ -118,7 +117,7 @@ export class AppComponent {
* *
* TODO: Should be renamed * TODO: Should be renamed
*/ */
private overloadArrayToString(): void { private overloadArrayFunctions(): void {
Object.defineProperty(Array.prototype, 'toString', { Object.defineProperty(Array.prototype, 'toString', {
value: function(): string { value: function(): string {
let string = ''; let string = '';
@ -139,12 +138,7 @@ export class AppComponent {
}, },
enumerable: false enumerable: false
}); });
}
/**
* Adds an implementation of flatMap and intersect.
*/
private overloadFlatMap(): void {
Object.defineProperty(Array.prototype, 'flatMap', { Object.defineProperty(Array.prototype, 'flatMap', {
value: function(o: any): any[] { value: function(o: any): any[] {
const concatFunction = (x: any, y: any[]) => x.concat(y); const concatFunction = (x: any, y: any[]) => x.concat(y);
@ -154,10 +148,11 @@ export class AppComponent {
enumerable: false enumerable: false
}); });
// intersect
Object.defineProperty(Array.prototype, 'intersect', { Object.defineProperty(Array.prototype, 'intersect', {
value: function<T>(other: T[]): T[] { value: function<T>(other: T[]): T[] {
let a = this, let a = this;
b = other; let b = other;
// indexOf to loop over shorter // indexOf to loop over shorter
if (b.length > a.length) { if (b.length > a.length) {
[a, b] = [b, a]; [a, b] = [b, a];
@ -167,6 +162,7 @@ export class AppComponent {
enumerable: false enumerable: false
}); });
// mapToObject
Object.defineProperty(Array.prototype, 'mapToObject', { Object.defineProperty(Array.prototype, 'mapToObject', {
value: function<T>(f: (item: T) => { [key: string]: any }): { [key: string]: any } { value: function<T>(f: (item: T) => { [key: string]: any }): { [key: string]: any } {
return this.reduce((aggr, item) => { return this.reduce((aggr, item) => {

View File

@ -71,6 +71,7 @@ export abstract class PollPdfService {
* @returns the amount of ballots, depending on the config settings * @returns the amount of ballots, depending on the config settings
*/ */
protected getBallotCount(): number { protected getBallotCount(): number {
// TODO: seems to be broken
switch (this.ballotCountSelection) { switch (this.ballotCountSelection) {
case 'NUMBER_OF_ALL_PARTICIPANTS': case 'NUMBER_OF_ALL_PARTICIPANTS':
return this.userRepo.getViewModelList().length; return this.userRepo.getViewModelList().length;

View File

@ -52,6 +52,22 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
public onehundred_percent_base: PercentBase; public onehundred_percent_base: PercentBase;
public user_has_voted: boolean; public user_has_voted: boolean;
public get isStateCreated(): boolean {
return this.state === PollState.Created;
}
public get isStateStarted(): boolean {
return this.state === PollState.Started;
}
public get isStateFinished(): boolean {
return this.state === PollState.Finished;
}
public get isStatePublished(): boolean {
return this.state === PollState.Published;
}
/** /**
* Determine if the state is finished or published * Determine if the state is finished or published
*/ */

View File

@ -9,9 +9,9 @@ export const VoteValueVerbose = {
}; };
export const GeneralValueVerbose = { export const GeneralValueVerbose = {
votesvalid: 'Votes valid', votesvalid: 'Valid votes',
votesinvalid: 'Votes invalid', votesinvalid: 'Invalid votes',
votescast: 'Votes cast', votescast: 'Total votes cast',
votesno: 'Votes No', votesno: 'Votes No',
votesabstain: 'Votes abstain' votesabstain: 'Votes abstain'
}; };

View File

@ -0,0 +1,20 @@
import { inject, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ParsePollNumberPipe } from './parse-poll-number.pipe';
describe('ParsePollNumberPipe', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [ParsePollNumberPipe]
});
TestBed.compileComponents();
});
it('create an instance', inject([TranslateService], (translate: TranslateService) => {
const pipe = new ParsePollNumberPipe(translate);
expect(pipe).toBeTruthy();
}));
});

View File

@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Pipe({
name: 'parsePollNumber'
})
export class ParsePollNumberPipe implements PipeTransform {
public constructor(private translate: TranslateService) {}
public transform(value: number): number | string {
const input = Math.trunc(value);
switch (input) {
case -1:
return this.translate.instant('majority');
case -2:
return this.translate.instant('undocumented');
default:
return input;
}
}
}

View File

@ -0,0 +1,8 @@
import { ReversePipe } from './reverse.pipe';
describe('ReversePipe', () => {
it('create an instance', () => {
const pipe = new ReversePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
/**
* Invert the order of arrays in templates
*
* @example
* ```html
* <li *ngFor="let user of users | reverse">
* {{ user.name }} has the id: {{ user.id }}
* </li>
* ```
*/
@Pipe({
name: 'reverse'
})
export class ReversePipe implements PipeTransform {
public transform(value: any[]): any[] {
return value.slice().reverse();
}
}

View File

@ -119,6 +119,8 @@ import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dia
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component'; import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
import { MotionPollDialogComponent } from 'app/site/motions/modules/motion-poll/motion-poll-dialog/motion-poll-dialog.component'; import { MotionPollDialogComponent } from 'app/site/motions/modules/motion-poll/motion-poll-dialog/motion-poll-dialog.component';
import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component'; import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
import { ReversePipe } from './pipes/reverse.pipe';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
@ -279,7 +281,9 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
BannerComponent, BannerComponent,
PollFormComponent, PollFormComponent,
MotionPollDialogComponent, MotionPollDialogComponent,
AssignmentPollDialogComponent AssignmentPollDialogComponent,
ParsePollNumberPipe,
ReversePipe
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -333,7 +337,9 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
BannerComponent, BannerComponent,
PollFormComponent, PollFormComponent,
MotionPollDialogComponent, MotionPollDialogComponent,
AssignmentPollDialogComponent AssignmentPollDialogComponent,
ParsePollNumberPipe,
ReversePipe
], ],
providers: [ providers: [
{ {
@ -349,7 +355,9 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
DecimalPipe, DecimalPipe,
ProgressSnackBarComponent, ProgressSnackBarComponent,
TrustPipe, TrustPipe,
LocalizedDatePipe LocalizedDatePipe,
ParsePollNumberPipe,
ReversePipe
], ],
entryComponents: [ entryComponents: [
SortBottomSheetComponent, SortBottomSheetComponent,

View File

@ -65,20 +65,12 @@
<div *ngIf="!editAssignment"> <div *ngIf="!editAssignment">
<!-- assignment meta infos--> <!-- assignment meta infos-->
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container> <ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
<!-- votable polls -->
<ng-container *ngIf="assignment">
<ng-container *ngFor="let poll of assignment.polls; trackBy: trackByIndex">
<mat-card class="os-card" *ngIf="poll.canBeVotedFor">
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
</mat-card>
</ng-container>
</ng-container>
<!-- candidates list --> <!-- candidates list -->
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container> <ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
<!-- closed polls --> <!-- closed polls -->
<ng-container *ngIf="assignment"> <ng-container *ngIf="assignment">
<ng-container *ngFor="let poll of assignment.polls; trackBy: trackByIndex"> <ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
<mat-card class="os-card" *ngIf="!poll.canBeVotedFor"> <mat-card class="os-card">
<os-assignment-poll [poll]="poll"> </os-assignment-poll> <os-assignment-poll [poll]="poll"> </os-assignment-poll>
</mat-card> </mat-card>
</ng-container> </ng-container>
@ -135,13 +127,17 @@
</ng-template> </ng-template>
<ng-template #candidatesTemplate> <ng-template #candidatesTemplate>
<mat-card class="os-card"> <mat-card class="os-card" *ngIf="assignment && !assignment.isFinished">
<ng-container *ngIf="assignment && !assignment.isFinished"> <ng-container>
<h3 translate>Candidates</h3> <h3 translate>Candidates</h3>
<div> <div>
<div <div
class="candidates-list" class="candidates-list"
*ngIf="assignment && assignment.assignment_related_users && assignment.assignment_related_users.length > 0" *ngIf="
assignment &&
assignment.assignment_related_users &&
assignment.assignment_related_users.length > 0
"
> >
<os-sorting-list <os-sorting-list
[input]="assignment.assignment_related_users" [input]="assignment.assignment_related_users"

View File

@ -33,16 +33,18 @@
{{ 'Groups' | translate }}: {{ 'Groups' | translate }}:
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span> <span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
</div> </div>
<div>{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}</div> <div>{{ 'Voting type' | translate }}: {{ poll.typeVerbose | translate }}</div>
<div>{{ 'Poll method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div> <div>{{ 'Election method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div> <div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div> <div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div> </div>
<!-- TODO Enum -->
<div *ngIf="poll.state === 2"> <div *ngIf="poll.state === 2">
<os-poll-progress [poll]="poll"></os-poll-progress> <os-poll-progress [poll]="poll"></os-poll-progress>
</div> </div>
<div *ngIf="poll.state === 3 || poll.state === 4"> <div *ngIf="poll.stateHasVotes">
<h2 translate>Result</h2> <h2 translate>Result</h2>
<div class="chart-wrapper"></div> <div class="chart-wrapper"></div>

View File

@ -8,14 +8,14 @@
<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">No user {{ option.candidate_id }}</span>
</div> </div>
<div> <div>
<div *ngFor="let value of analogPollValues" [formGroupName]="option.user_id"> <div *ngFor="let value of analogPollValues" [formGroupName]="option.user_id">
<os-check-input <os-check-input
[placeholder]="voteValueVerbose[value] | translate" [placeholder]="voteValueVerbose[value] | translate"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'Majority' | translate" [checkboxLabel]="'majority' | translate"
[formControlName]="value" [formControlName]="value"
></os-check-input> ></os-check-input>
</div> </div>
@ -28,7 +28,7 @@
[placeholder]="generalValueVerbose[value] | translate" [placeholder]="generalValueVerbose[value] | translate"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'Majority' | translate" [checkboxLabel]="'majority' | translate"
[formControlName]="value" [formControlName]="value"
></os-check-input> ></os-check-input>
</div> </div>

View File

@ -1,4 +1,10 @@
<div class="assignment-poll-wrapper" *ngIf="poll"> <div class="assignment-poll-wrapper" *ngIf="poll">
<div class="">
</div>
<div class="poll-menu"> <div class="poll-menu">
<!-- Buttons --> <!-- Buttons -->
<button <button
@ -9,30 +15,16 @@
> >
<mat-icon>more_horiz</mat-icon> <mat-icon>more_horiz</mat-icon>
</button> </button>
<mat-menu #pollItemMenu="matMenu">
<div *osPerms="'assignments.can_manage'">
<button mat-menu-item (click)="openDialog()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
</div>
<div *osPerms="'core.can_manage_projector'">
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
</div>
<div *osPerms="'assignments.can_manage'">
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
</mat-menu>
</div> </div>
<div> <div>
<div> <div>
<h3>
<a routerLink="/assignments/polls/{{ poll.id }}">
{{ poll.title }}
</a>
</h3>
<div class="poll-properties"> <div class="poll-properties">
<!-- <mat-chip *ngIf="pollService.isElectronicVotingEnabled">{{ poll.typeVerbose }}</mat-chip> -->
<mat-chip <mat-chip
class="poll-state active" class="poll-state active"
[matMenuTriggerFor]="triggerMenu" [matMenuTriggerFor]="triggerMenu"
@ -41,23 +33,14 @@
{{ poll.stateVerbose }} {{ poll.stateVerbose }}
</mat-chip> </mat-chip>
</div> </div>
<h3>
<a routerLink="/assignments/polls/{{ poll.id }}">
{{ poll.title }}
</a>
</h3>
</div> </div>
<div> <div *ngIf="poll.stateHasVotes">
<os-charts [type]="chartType" [labels]="candidatesLabels" [data]="chartDataSubject"></os-charts> <os-charts [type]="chartType" [labels]="candidatesLabels" [data]="chartDataSubject"></os-charts>
</div> </div>
</div> </div>
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote> <os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
<!-- <ng-container *ngIf="poll.state === pollStates.STATE_PUBLISHED" [ngTemplateOutlet]="resultsTemplate"></ng-container> -->
</div> </div>
<ng-template #resultsTemplate> </ng-template>
<mat-menu #triggerMenu="matMenu"> <mat-menu #triggerMenu="matMenu">
<ng-container *ngIf="poll"> <ng-container *ngIf="poll">
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue"> <button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue">
@ -65,3 +48,22 @@
</button> </button>
</ng-container> </ng-container>
</mat-menu> </mat-menu>
<mat-menu #pollItemMenu="matMenu">
<div *osPerms="'assignments.can_manage'">
<button mat-menu-item (click)="openDialog()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
</div>
<div *osPerms="'core.can_manage_projector'">
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
</div>
<div *osPerms="'assignments.can_manage'">
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
</mat-menu>

View File

@ -3,33 +3,10 @@
position: relative; position: relative;
padding: 0 15px; padding: 0 15px;
.poll-main-content { .poll-menu {
padding-top: 10px; position: absolute;
} top: 0;
right: 0;
.poll-grid {
display: grid;
grid-gap: 5px;
padding: 5px;
grid-template-columns: 30px auto 250px 150px;
.candidate-name {
word-wrap: break-word;
}
}
.right-align {
text-align: right;
}
.vote-input {
.mat-form-field-wrapper {
// padding-bottom: 0;
.mat-form-field-infix {
width: 60px;
border-top: 0;
}
}
} }
.poll-properties { .poll-properties {
@ -62,33 +39,4 @@
} }
} }
} }
.poll-menu {
position: absolute;
top: 0;
right: 0;
}
.poll-quorum {
text-align: right;
margin-right: 10px;
mat-icon {
vertical-align: middle;
font-size: 100%;
}
}
.top-aligned {
position: absolute;
top: 0;
left: 0;
}
.wide {
width: 90%;
}
.hint-form {
margin-top: 20px;
}
} }

View File

@ -49,7 +49,7 @@ export class AssignmentPollService extends PollService {
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id) const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
.length; .length;
poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`; poll.title = !length ? this.translate.instant('Ballot') : `${this.translate.instant('Ballot')} (${length + 1})`;
poll.pollmethod = AssignmentPollMethods.YN; poll.pollmethod = AssignmentPollMethods.YN;
poll.assignment_id = poll.assignment_id; poll.assignment_id = poll.assignment_id;
} }

View File

@ -25,6 +25,7 @@
<mat-icon *ngSwitchCase="'General'">home</mat-icon> <mat-icon *ngSwitchCase="'General'">home</mat-icon>
<mat-icon *ngSwitchCase="'Agenda'">today</mat-icon> <mat-icon *ngSwitchCase="'Agenda'">today</mat-icon>
<mat-icon *ngSwitchCase="'Motions'">assignment</mat-icon> <mat-icon *ngSwitchCase="'Motions'">assignment</mat-icon>
<mat-icon *ngSwitchCase="'Voting'">pie_chart</mat-icon>
<mat-icon *ngSwitchCase="'Elections'">how_to_vote</mat-icon> <mat-icon *ngSwitchCase="'Elections'">how_to_vote</mat-icon>
<mat-icon *ngSwitchCase="'Participants'">groups</mat-icon> <mat-icon *ngSwitchCase="'Participants'">groups</mat-icon>
<mat-icon *ngSwitchCase="'Custom translations'">language</mat-icon> <mat-icon *ngSwitchCase="'Custom translations'">language</mat-icon>

View File

@ -463,7 +463,7 @@
<div class="mat-card create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)"> <div class="mat-card create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
<button mat-button (click)="openDialog()"> <button mat-button (click)="openDialog()">
<mat-icon class="main-nav-color">poll</mat-icon> <mat-icon class="main-nav-color">poll</mat-icon>
<span translate>New poll</span> <span translate>New vote</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
<os-head-bar [goBack]="true" [nav]="false"> <os-head-bar [goBack]="true" [nav]="false">
<div class="title-slot"> <div class="title-slot">
<h2 *ngIf="poll">{{ 'Motion' | translate }} {{ poll.motion.id }}</h2> <h2 *ngIf="poll">{{ 'Motion' | translate }} {{ poll.motion.identifierOrTitle }}</h2>
</div> </div>
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'"> <div class="menu-slot" *osPerms="'motions.can_manage_polls'">
<button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu"> <button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
@ -18,14 +18,13 @@
<ng-template #viewTemplate> <ng-template #viewTemplate>
<ng-container *ngIf="poll"> <ng-container *ngIf="poll">
<h1>{{ poll.title }}</h1> <h1>{{ poll.title }}</h1>
<span *ngIf="poll.type !== 'analog'">{{ 'Polly type' | translate }}: {{ poll.type | translate }}</span> <span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
<div *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results to show' | translate }}</div>
<div *ngIf="poll.stateHasVotes"> <div *ngIf="poll.stateHasVotes">
<h2 translate>Result</h2> <h2 translate>Result</h2>
<div *ngIf="!poll.hasVotes">{{ 'No results to show' | translate }}</div>
<div class="result-wrapper" *ngIf="poll.hasVotes"> <div class="result-wrapper" *ngIf="poll.hasVotes">
<!-- Chart --> <!-- Chart -->
<os-charts <os-charts
@ -52,25 +51,28 @@
</mat-table> </mat-table>
<!-- Named table: only show if votes are present --> <!-- Named table: only show if votes are present -->
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data"> <div class="named-result-table" *ngIf="poll.type === 'named' && votesDataSource.data">
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter"/> <h3>{{ 'Single votes' | translate }}</h3>
<mat-form-field>
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
</mat-form-field>
<mat-table [dataSource]="votesDataSource"> <mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="key" sticky> <ng-container matColumnDef="key" sticky>
<mat-header-cell *matHeaderCellDef>{{ "User" | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote"> <mat-cell *matCellDef="let vote">
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div> <div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div> <div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="value" sticky> <ng-container matColumnDef="value" sticky>
<mat-header-cell *matHeaderCellDef>{{ "Vote" | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Vote' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell> <mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row> <mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row> <mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
</mat-table> </mat-table>
</ng-container> </div>
</div> </div>
</div> </div>
@ -82,29 +84,29 @@
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span> {{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
</span> </span>
</div> </div>
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div> <div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div> <div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div> </div>
<div *ngIf="poll.state === 2">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
</ng-container> </ng-container>
</ng-template> </ng-template>
<!-- More Menu --> <!-- More Menu -->
<mat-menu #pollDetailMenu="matMenu"> <mat-menu #pollDetailMenu="matMenu">
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button> <os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
<button mat-menu-item (click)="openDialog()"> <button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog()">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
<span translate>Edit</span> <span translate>Edit</span>
</button> </button>
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()"> <button
<mat-icon>polymer</mat-icon> mat-menu-item
<span translate>Pseudoanonymize</span> *osPerms="'motions.can_manage_polls'; and: poll && poll.type === 'named'"
(click)="pseudoanonymizePoll()"
>
<mat-icon>warning</mat-icon>
<span translate>Anonymize votes</span>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="deletePoll()"> <button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="deletePoll()">
<mat-icon color="warn">delete</mat-icon> <mat-icon color="warn">delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>

View File

@ -35,4 +35,8 @@
.named-result-table { .named-result-table {
grid-area: names; grid-area: names;
.mat-form-field {
font-size: 14px;
width: 100%;
}
} }

View File

@ -6,41 +6,43 @@
[placeholder]="'Yes' | translate" [placeholder]="'Yes' | translate"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'Majority' | translate" [checkboxLabel]="'majority' | translate"
formControlName="Y" formControlName="Y"
></os-check-input> ></os-check-input>
<os-check-input <os-check-input
[placeholder]="'No' | translate" [placeholder]="'No' | translate"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'Majority' | translate" [checkboxLabel]="'majority' | translate"
formControlName="N" formControlName="N"
></os-check-input> ></os-check-input>
<os-check-input <os-check-input
[placeholder]="'Abstain' | translate" [placeholder]="'Abstain' | translate"
[checkboxValue]="-1" [checkboxValue]="-1"
inputType="number" inputType="number"
[checkboxLabel]="'Majority' | translate" [checkboxLabel]="'majority' | translate"
formControlName="A" formControlName="A"
></os-check-input> ></os-check-input>
<os-check-input <os-check-input
[placeholder]="'Votes valid' | translate" [placeholder]="'Valid votes' | translate"
inputType="number" inputType="number"
formControlName="votesvalid" formControlName="votesvalid"
></os-check-input> ></os-check-input>
<os-check-input <os-check-input
[placeholder]="'Votes invalid' | translate" [placeholder]="'Invalid votes' | translate"
inputType="number" inputType="number"
formControlName="votesinvalid" formControlName="votesinvalid"
></os-check-input> ></os-check-input>
<os-check-input <os-check-input
[placeholder]="'Votes cast' | translate" [placeholder]="'Total votes cast' | translate"
inputType="number" inputType="number"
formControlName="votescast" formControlName="votescast"
></os-check-input> ></os-check-input>
</form> </form>
</div> </div>
<div>
<!-- Publish immediately button. Only show for new polls -->
<div *ngIf="!pollData.state">
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)"> <mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
<span translate>Publish immediately</span> <span translate>Publish immediately</span>
</mat-checkbox> </mat-checkbox>

View File

@ -42,9 +42,6 @@ export class MotionPollDialogComponent extends BasePollDialogComponent {
votesinvalid: data.votesinvalid, votesinvalid: data.votesinvalid,
votescast: data.votescast votescast: data.votescast
}; };
// if (data.pollmethod === 'YNA') {
// update.A = data.options[0].abstain;
// }
if (this.dialogVoteForm) { if (this.dialogVoteForm) {
const result = this.undoReplaceEmptyValues(update); const result = this.undoReplaceEmptyValues(update);
@ -64,9 +61,7 @@ export class MotionPollDialogComponent extends BasePollDialogComponent {
votesinvalid: ['', [Validators.min(-2)]], votesinvalid: ['', [Validators.min(-2)]],
votescast: ['', [Validators.min(-2)]] votescast: ['', [Validators.min(-2)]]
}); });
// if (this.pollData.pollmethod === MotionPollMethods.YNA) {
// this.dialogVoteForm.addControl('A', this.fb.control('', [Validators.min(-2)]));
// }
if (this.pollData.poll) { if (this.pollData.poll) {
this.updateDialogVoteForm(this.pollData); this.updateDialogVoteForm(this.pollData);
} }

View File

@ -1,6 +1,8 @@
<ng-container *ngIf="poll"> <ng-container *ngIf="poll">
<div *osPerms="'motions.can_manage_polls';and:poll.isStateStarted">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<ng-container *ngIf="vmanager.canVote(poll)"> <ng-container *ngIf="vmanager.canVote(poll)">
<!-- Voting --> <!-- Voting -->
<p *ngFor="let option of voteOptions"> <p *ngFor="let option of voteOptions">
<button <button

View File

@ -1,16 +1,15 @@
/** @import '~assets/styles/poll-colors.scss';
* These colors should be extracted from some global CSS Constants file
*/
.voted-yes { .voted-yes {
background-color: #9fd773; background-color: $votes-yes-color;
} }
.voted-no { .voted-no {
background-color: #cc6c5b; background-color: $votes-no-color;
} }
.voted-abstain { .voted-abstain {
background-color: #a6a6a6; background-color: $votes-abstain-color;
} }
.vote-label { .vote-label {

View File

@ -1,23 +1,23 @@
<div class="poll-preview-wrapper"> <div class="poll-preview-wrapper" *ngIf="poll && showPoll()">
<!-- Poll Infos --> <!-- Poll Infos -->
<div class="poll-title-wrapper" *ngIf="poll"> <div class="poll-title-wrapper">
<!-- Title --> <!-- Title -->
<a class="poll-title" routerLink="/motions/polls/{{ poll.id }}"> <a class="poll-title" [routerLink]="pollLink">
{{ poll.title }} {{ poll.title }}
</a> </a>
<!-- Edit button --> <!-- Dot Menu -->
<span class="poll-title-actions" *osPerms="'motions.can_manage_polls'"> <span class="poll-title-actions" *osPerms="'motions.can_manage_polls'">
<button mat-icon-button (click)="openDialog()"> <button mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
<mat-icon class="small-icon">edit</mat-icon> <mat-icon class="small-icon">more_horiz</mat-icon>
</button> </button>
</span> </span>
<!-- State chip --> <!-- State chip -->
<div class="poll-properties" *osPerms="'motions.can_manage_polls'"> <div class="poll-properties" *osPerms="'motions.can_manage_polls'">
<span *ngIf="pollService.isElectronicVotingEnabled && poll.typeVerbose !== 'Analog'"> <div *ngIf="pollService.isElectronicVotingEnabled && poll.type !== 'analog'">
{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }} {{ poll.typeVerbose | translate }}
</span> </div>
<mat-chip <mat-chip
disableRipple disableRipple
@ -37,35 +37,72 @@
</div> </div>
<ng-template #votingResult> <ng-template #votingResult>
<div (click)="openPoll()"> <div [routerLink]="pollLink">
<ng-container [ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"></ng-container> <ng-container
[ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"
></ng-container>
</div> </div>
</ng-template> </ng-template>
<ng-template #viewTemplate> <ng-template #viewTemplate>
<div class="poll-chart-wrapper"> <div class="poll-chart-wrapper">
<div class="votes-yes"> <!-- empty helper div to center the grid wrapper -->
<os-icon-container icon="check" size="large"> <div></div>
{{ voteYes }} <div class="doughnut-chart">
</os-icon-container> <os-charts *ngIf="showChart" [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
</div> </div>
<div *ngIf="showChart" class="doughnut-chart"> <div class="vote-legend">
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts> <div class="votes-yes" *ngIf="isVoteDocumented(voteYes)">
</div> <os-icon-container icon="thumb_up" size="large">
<div class="votes-no"> {{ voteYes | parsePollNumber }}
<os-icon-container icon="close" size="large"> </os-icon-container>
{{ voteNo }} </div>
</os-icon-container> <div class="votes-no" *ngIf="isVoteDocumented(voteNo)">
<os-icon-container icon="thumb_down" size="large">
{{ voteNo | parsePollNumber }}
</os-icon-container>
</div>
<div class="votes-abstain" *ngIf="isVoteDocumented(voteAbstain)">
<os-icon-container icon="trip_origin" size="large">
{{ voteAbstain | parsePollNumber }}
</os-icon-container>
</div>
</div> </div>
</div> </div>
<div class="poll-detail-button-wrapper" *ngIf="poll.type !== 'analog'">
<button mat-button [routerLink]="pollLink">
{{ 'Single Votes' | translate }}
</button>
</div>
</ng-template> </ng-template>
<ng-template #emptyTemplate> <ng-template #emptyTemplate>
<div> <div *osPerms="'motions.can_manage_polls'">
An empty poll - you have to enter votes. {{ 'An empty poll - you have to enter votes.' | translate }}
</div> </div>
</ng-template> </ng-template>
<!-- More Menu -->
<mat-menu #pollDetailMenu="matMenu">
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
<button mat-menu-item (click)="downloadPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>PDF</span>
</button>
<div *osPerms="'motions.can_manage_polls'">
<mat-divider></mat-divider>
<button mat-menu-item (click)="deletePoll()">
<mat-icon color="warn">delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
</mat-menu>
<!-- Select state menu -->
<mat-menu #triggerMenu="matMenu"> <mat-menu #triggerMenu="matMenu">
<ng-container *ngIf="poll"> <ng-container *ngIf="poll">
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue"> <button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue">

View File

@ -1,3 +1,5 @@
@import '~assets/styles/poll-colors.scss';
.poll-preview-wrapper { .poll-preview-wrapper {
padding: 8px; padding: 8px;
background: white; background: white;
@ -6,6 +8,7 @@
.poll-title { .poll-title {
color: black; color: black;
text-decoration: none; text-decoration: none;
font-weight: 500;
} }
.poll-title-actions { .poll-title-actions {
@ -48,20 +51,41 @@
display: grid; display: grid;
grid-template-columns: auto minmax(50px, 20%) auto; grid-template-columns: auto minmax(50px, 20%) auto;
.votes-no { .doughnut-chart {
color: #cc6c5b; margin-top: auto;
margin: auto 0; margin-bottom: auto;
width: fit-content;
} }
.votes-yes { .vote-legend {
color: #9fc773; margin: auto 10px;
margin: auto 0 auto auto;
width: fit-content; div + div {
margin-top: 10px;
}
.votes-yes {
color: $votes-yes-color;
}
.votes-no {
color: $votes-no-color;
}
.votes-abstain {
color: $votes-abstain-color;
}
} }
} }
} }
.poll-detail-button-wrapper {
display: flex;
margin: auto 0;
> button {
margin-left: auto;
}
}
.poll-preview-meta-info { .poll-preview-meta-info {
span { span {
padding: 0 5px; padding: 0 5px;

View File

@ -1,7 +1,6 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { MatDialog, MatSnackBar } from '@angular/material'; import { MatDialog, MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
@ -12,6 +11,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartData } from 'app/shared/components/charts/charts.component'; import { ChartData } from 'app/shared/components/charts/charts.component';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service'; import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.service';
import { BasePollComponent } from 'app/site/polls/components/base-poll.component'; import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
import { PollService } from 'app/site/polls/services/poll.service'; import { PollService } from 'app/site/polls/services/poll.service';
@ -40,6 +40,9 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
if (data.label === 'NO') { if (data.label === 'NO') {
this.voteNo = data.data[0]; this.voteNo = data.data[0];
} }
if (data.label === 'ABSTAIN') {
this.voteAbstain = data.data[0];
}
} }
this.chartDataSubject.next(chartData); this.chartDataSubject.next(chartData);
} }
@ -48,6 +51,10 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
return this._poll; return this._poll;
} }
public get pollLink(): string {
return `/motions/polls/${this.poll.id}`;
}
/** /**
* Subject to holding the data needed for the chart. * Subject to holding the data needed for the chart.
*/ */
@ -56,32 +63,45 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
/** /**
* Number of votes for `Yes`. * Number of votes for `Yes`.
*/ */
public set voteYes(n: number | string) { public set voteYes(n: number) {
this._voteYes = n; this._voteYes = n;
} }
public get voteYes(): number | string { public get voteYes(): number {
return this.verboseForNumber(this._voteYes as number); return this._voteYes;
} }
/** /**
* Number of votes for `No`. * Number of votes for `No`.
*/ */
public set voteNo(n: number | string) { public set voteNo(n: number) {
this._voteNo = n; this._voteNo = n;
} }
public get voteNo(): number | string { public get voteNo(): number {
return this.verboseForNumber(this._voteNo as number); return this._voteNo;
}
/**
* Number of votes for `Abstain`.
*/
public set voteAbstain(n: number) {
this._voteAbstain = n;
}
public get voteAbstain(): number {
return this._voteAbstain;
} }
public get showChart(): boolean { public get showChart(): boolean {
return this._voteYes >= 0 && this._voteNo >= 0; return this._voteYes >= 0 && this._voteNo >= 0;
} }
private _voteNo: number | string = 0; private _voteNo: number;
private _voteYes: number | string = 0; private _voteYes: number;
private _voteAbstain: number;
/** /**
* Constructor. * Constructor.
@ -101,27 +121,35 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
public pollRepo: MotionPollRepositoryService, public pollRepo: MotionPollRepositoryService,
pollDialog: MotionPollDialogService, pollDialog: MotionPollDialogService,
public pollService: PollService, public pollService: PollService,
private router: Router, private operator: OperatorService,
private operator: OperatorService private pdfService: MotionPollPdfService
) { ) {
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog); super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
} }
public openPoll(): void { public showPoll(): boolean {
if (this.operator.hasPerms('motions.can_manage_polls')) { return (
this.router.navigate(['motions', 'polls', this.poll.id]); this.operator.hasPerms('motions.can_manage_polls') ||
this.poll.isStatePublished ||
(this.poll.type !== 'analog' && this.poll.isStateStarted)
);
}
public downloadPdf(): void {
console.log('picture_as_pdf');
this.pdfService.printBallots(this.poll);
}
public async deletePoll(): Promise<void> {
const title = 'Delete poll';
const text = 'Do you really want to delete the selected poll?';
if (await this.promptService.open(title, text)) {
this.repo.delete(this.poll).catch(this.raiseError);
} }
} }
private verboseForNumber(input: number): number | string { public isVoteDocumented(vote: number): boolean {
input = Math.trunc(input); return vote !== null && vote !== undefined && vote !== -2;
switch (input) {
case -1:
return 'Majority';
case -2:
return 'Not documented';
default:
return input;
}
} }
} }

View File

@ -65,7 +65,7 @@ const routes: Routes = [
{ {
path: 'polls', path: 'polls',
loadChildren: () => import('./modules/motion-poll/motion-poll.module').then(m => m.MotionPollModule), loadChildren: () => import('./modules/motion-poll/motion-poll.module').then(m => m.MotionPollModule),
data: { basePerm: 'motions.can_manage_polls' } data: { basePerm: 'motions.can_see' }
}, },
{ {
path: ':id', path: ':id',

View File

@ -17,7 +17,7 @@ type BallotCountChoices = 'NUMBER_OF_DELEGATES' | 'NUMBER_OF_ALL_PARTICIPANTS' |
* *
* @example * @example
* ```ts * ```ts
* this.MotionPollPdfService.printBallos(this.poll); * this.MotionPollPdfService.printBallots(this.poll);
* ``` * ```
*/ */
@Injectable({ @Injectable({

View File

@ -75,7 +75,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
* @param fb * @param fb
* @param groupRepo * @param groupRepo
* @param location * @param location
* @param promptDialog * @param promptService
* @param dialog * @param dialog
*/ */
public constructor( public constructor(
@ -85,7 +85,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
protected repo: BasePollRepositoryService, protected repo: BasePollRepositoryService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected groupRepo: GroupRepositoryService, protected groupRepo: GroupRepositoryService,
protected promptDialog: PromptService, protected promptService: PromptService,
protected pollDialog: BasePollDialogService<V> protected pollDialog: BasePollDialogService<V>
) { ) {
super(title, translate, matSnackbar); super(title, translate, matSnackbar);
@ -108,16 +108,16 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
const title = 'Delete poll'; const title = 'Delete poll';
const text = 'Do you really want to delete the selected poll?'; const text = 'Do you really want to delete the selected poll?';
if (await this.promptDialog.open(title, text)) { if (await this.promptService.open(title, text)) {
this.repo.delete(this.poll).then(() => this.onDeleted(), this.raiseError); this.repo.delete(this.poll).then(() => this.onDeleted(), this.raiseError);
} }
} }
public async pseudoanonymizePoll(): Promise<void> { public async pseudoanonymizePoll(): Promise<void> {
const title = 'Pseudoanonymize poll'; const title = 'Anonymize single votes';
const text = 'Do you really want to pseudoanonymize the selected poll?'; const text = 'Do you really want to anonymize all votes? This cannot be undone.';
if (await this.promptDialog.open(title, text)) { if (await this.promptService.open(title, text)) {
this.repo.pseudoanonymize(this.poll).then(() => this.onPollLoaded(), this.raiseError); // votes have changed, but not the poll, so the components have to be informed about the update this.repo.pseudoanonymize(this.poll).then(() => this.onPollLoaded(), this.raiseError); // votes have changed, but not the poll, so the components have to be informed about the update
} }
} }

View File

@ -19,7 +19,7 @@
<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.state === 1">
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled"> <mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
<mat-select [placeholder]="'Poll type' | translate" formControlName="type" required> <mat-select [placeholder]="'Voting 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>
@ -56,7 +56,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-select placeholder="{{ 'Majority' | translate }}" formControlName="majority_method" required> <mat-select placeholder="{{ 'Required majority' | 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>

View File

@ -1 +1,3 @@
<span>{{ this.poll.voted_id.length }} von {{ this.max }} haben abgestimmt.</span> <span>{{ poll.voted_id.length }} von {{ max }} haben abgestimmt.</span>
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>

View File

@ -29,6 +29,10 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
super(title, translate, snackbar); super(title, translate, snackbar);
} }
public get valueInPercent(): number {
return (this.poll.voted_id.length / this.max) * 100;
}
/** /**
* OnInit. * OnInit.
* Sets the observable for groups. * Sets the observable for groups.

View File

@ -22,9 +22,9 @@ export const PollStateVerbose = {
}; };
export const PollTypeVerbose = { export const PollTypeVerbose = {
analog: 'Analog', analog: 'Analog voting',
named: 'Named', named: 'Named voting',
pseudoanonymous: 'Pseudoanonymous' pseudoanonymous: 'Pseudoanonymous voting'
}; };
export const PollPropertyVerbose = { export const PollPropertyVerbose = {
@ -37,9 +37,9 @@ export const PollPropertyVerbose = {
}; };
export const MajorityMethodVerbose = { export const MajorityMethodVerbose = {
simple: 'Simple', simple: 'Simple majority',
two_thirds: 'Two Thirds', two_thirds: 'Two-thirds majority',
three_quarters: 'Three Quarters', three_quarters: 'Three-quarters majority',
disabled: 'Disabled' disabled: 'Disabled'
}; };
@ -48,7 +48,7 @@ export const PercentBaseVerbose = {
YNA: 'Yes/No/Abstain', YNA: 'Yes/No/Abstain',
votes: 'All votes', votes: 'All votes',
valid: 'Valid votes', valid: 'Valid votes',
cast: 'Cast votes', cast: 'Total votes cast',
disabled: 'Disabled' disabled: 'Disabled'
}; };
@ -104,6 +104,7 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
} }
return states; return states;
} }
public abstract readonly pollClassType: 'motion' | 'assignment'; public abstract readonly pollClassType: 'motion' | 'assignment';
public canBeVotedFor: () => boolean; public canBeVotedFor: () => boolean;

View File

@ -0,0 +1,6 @@
/**
* Define the colors used for yes, no and abstain
*/
$votes-yes-color: #9fd773;
$votes-no-color: #cc6c5b;
$votes-abstain-color: #a6a6a6;

View File

@ -9,7 +9,7 @@ def get_config_variables():
They are grouped in 'Ballot and ballot papers' and 'PDF'. The generator has They are grouped in 'Ballot and ballot papers' and 'PDF'. The generator has
to be evaluated during app loading (see apps.py). to be evaluated during app loading (see apps.py).
""" """
# Polls # Voting
yield ConfigVariable( yield ConfigVariable(
name="assignment_poll_default_100_percent_base", name="assignment_poll_default_100_percent_base",
default_value="YNA", default_value="YNA",
@ -20,7 +20,7 @@ def get_config_variables():
for base in AssignmentPoll.PERCENT_BASES for base in AssignmentPoll.PERCENT_BASES
), ),
weight=400, weight=400,
group="Polls", group="Voting",
subgroup="Elections", subgroup="Elections",
) )
@ -35,7 +35,8 @@ def get_config_variables():
label="Required majority", label="Required majority",
help_text="Default method to check whether a candidate has reached the required majority.", help_text="Default method to check whether a candidate has reached the required majority.",
weight=405, weight=405,
group="Polls", hidden=True,
group="Voting",
subgroup="Elections", subgroup="Elections",
) )
@ -45,7 +46,7 @@ def get_config_variables():
input_type="boolean", input_type="boolean",
label="Put all candidates on the list of speakers", label="Put all candidates on the list of speakers",
weight=410, weight=410,
group="Polls", group="Voting",
subgroup="Elections", subgroup="Elections",
) )

View File

@ -117,7 +117,7 @@ def set_correct_state(apps, schema_editor):
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll") AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
AssignmentVote = apps.get_model("assignments", "AssignmentVote") AssignmentVote = apps.get_model("assignments", "AssignmentVote")
for poll in AssignmentPoll.objects.all(): for poll in AssignmentPoll.objects.all():
# Polls, that are published (old field) but have no votes, will be # Voting, that are published (old field) but have no votes, will be
# left at the created state... # left at the created state...
if AssignmentVote.objects.filter(option__poll__pk=poll.pk).exists(): if AssignmentVote.objects.filter(option__poll__pk=poll.pk).exists():
if poll.published: if poll.published:

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("core", "0026_remove_history_restricted"), ("core", "0025_projector_color"),
] ]
operations = [ operations = [

View File

@ -37,7 +37,7 @@ def calculate_aspect_ratios(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("core", "0027_projector_size_1"), ("core", "0026_projector_size_1"),
] ]
operations = [ operations = [

View File

@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("core", "0028_projector_size_2"), ("core", "0027_projector_size_2"),
] ]
operations = [ operations = [

View File

@ -5,6 +5,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("core", "0025_projector_color")] dependencies = [("core", "0028_projector_size_3")]
operations = [migrations.RemoveField(model_name="history", name="restricted")] operations = [migrations.RemoveField(model_name="history", name="restricted")]

View File

@ -388,18 +388,18 @@ def get_config_variables():
subgroup="PDF export", subgroup="PDF export",
) )
# Polls # Voting
yield ConfigVariable( yield ConfigVariable(
name="motion_poll_default_100_percent_base", name="motion_poll_default_100_percent_base",
default_value="YNA", default_value="YNA",
input_type="choice", input_type="choice",
label="The 100-%-base of an election result consists of", label="The 100 % base of a voting result consists of",
choices=tuple( choices=tuple(
{"value": base[0], "display_name": base[1]} {"value": base[0], "display_name": base[1]}
for base in MotionPoll.PERCENT_BASES for base in MotionPoll.PERCENT_BASES
), ),
weight=420, weight=420,
group="Polls", group="Voting",
subgroup="Motions", subgroup="Motions",
) )
@ -412,8 +412,9 @@ def get_config_variables():
for method in MotionPoll.MAJORITY_METHODS for method in MotionPoll.MAJORITY_METHODS
), ),
label="Required majority", label="Required majority",
help_text="Default method to check whether a candidate has reached the required majority.", help_text="Default method to check whether a motion has reached the required majority.",
weight=425, weight=425,
group="Polls", hidden=True,
group="Voting",
subgroup="Motions", subgroup="Motions",
) )