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:
parent
682db96b7c
commit
84a39ccb62
@ -95,7 +95,6 @@ export class AppComponent {
|
||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||
|
||||
// change default JS functions
|
||||
this.overloadArrayToString();
|
||||
this.overloadArrayFunctions();
|
||||
this.overloadModulo();
|
||||
|
||||
@ -118,7 +117,7 @@ export class AppComponent {
|
||||
*
|
||||
* TODO: Should be renamed
|
||||
*/
|
||||
private overloadArrayToString(): void {
|
||||
private overloadArrayFunctions(): void {
|
||||
Object.defineProperty(Array.prototype, 'toString', {
|
||||
value: function(): string {
|
||||
let string = '';
|
||||
@ -139,12 +138,7 @@ export class AppComponent {
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an implementation of flatMap and intersect.
|
||||
*/
|
||||
private overloadFlatMap(): void {
|
||||
Object.defineProperty(Array.prototype, 'flatMap', {
|
||||
value: function(o: any): any[] {
|
||||
const concatFunction = (x: any, y: any[]) => x.concat(y);
|
||||
@ -154,10 +148,11 @@ export class AppComponent {
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
// intersect
|
||||
Object.defineProperty(Array.prototype, 'intersect', {
|
||||
value: function<T>(other: T[]): T[] {
|
||||
let a = this,
|
||||
b = other;
|
||||
let a = this;
|
||||
let b = other;
|
||||
// indexOf to loop over shorter
|
||||
if (b.length > a.length) {
|
||||
[a, b] = [b, a];
|
||||
@ -167,6 +162,7 @@ export class AppComponent {
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
// mapToObject
|
||||
Object.defineProperty(Array.prototype, 'mapToObject', {
|
||||
value: function<T>(f: (item: T) => { [key: string]: any }): { [key: string]: any } {
|
||||
return this.reduce((aggr, item) => {
|
||||
|
@ -71,6 +71,7 @@ export abstract class PollPdfService {
|
||||
* @returns the amount of ballots, depending on the config settings
|
||||
*/
|
||||
protected getBallotCount(): number {
|
||||
// TODO: seems to be broken
|
||||
switch (this.ballotCountSelection) {
|
||||
case 'NUMBER_OF_ALL_PARTICIPANTS':
|
||||
return this.userRepo.getViewModelList().length;
|
||||
|
@ -52,6 +52,22 @@ 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 {
|
||||
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
|
||||
*/
|
||||
|
@ -9,9 +9,9 @@ export const VoteValueVerbose = {
|
||||
};
|
||||
|
||||
export const GeneralValueVerbose = {
|
||||
votesvalid: 'Votes valid',
|
||||
votesinvalid: 'Votes invalid',
|
||||
votescast: 'Votes cast',
|
||||
votesvalid: 'Valid votes',
|
||||
votesinvalid: 'Invalid votes',
|
||||
votescast: 'Total votes cast',
|
||||
votesno: 'Votes No',
|
||||
votesabstain: 'Votes abstain'
|
||||
};
|
||||
|
20
client/src/app/shared/pipes/parse-poll-number.pipe.spec.ts
Normal file
20
client/src/app/shared/pipes/parse-poll-number.pipe.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
22
client/src/app/shared/pipes/parse-poll-number.pipe.ts
Normal file
22
client/src/app/shared/pipes/parse-poll-number.pipe.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
8
client/src/app/shared/pipes/reverse.pipe.spec.ts
Normal file
8
client/src/app/shared/pipes/reverse.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ReversePipe } from './reverse.pipe';
|
||||
|
||||
describe('ReversePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new ReversePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
20
client/src/app/shared/pipes/reverse.pipe.ts
Normal file
20
client/src/app/shared/pipes/reverse.pipe.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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 { 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 { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
||||
import { ReversePipe } from './pipes/reverse.pipe';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -279,7 +281,9 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
|
||||
BannerComponent,
|
||||
PollFormComponent,
|
||||
MotionPollDialogComponent,
|
||||
AssignmentPollDialogComponent
|
||||
AssignmentPollDialogComponent,
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -333,7 +337,9 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
|
||||
BannerComponent,
|
||||
PollFormComponent,
|
||||
MotionPollDialogComponent,
|
||||
AssignmentPollDialogComponent
|
||||
AssignmentPollDialogComponent,
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@ -349,7 +355,9 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
|
||||
DecimalPipe,
|
||||
ProgressSnackBarComponent,
|
||||
TrustPipe,
|
||||
LocalizedDatePipe
|
||||
LocalizedDatePipe,
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe
|
||||
],
|
||||
entryComponents: [
|
||||
SortBottomSheetComponent,
|
||||
|
@ -65,20 +65,12 @@
|
||||
<div *ngIf="!editAssignment">
|
||||
<!-- assignment meta infos-->
|
||||
<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 -->
|
||||
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
||||
<!-- closed polls -->
|
||||
<ng-container *ngIf="assignment">
|
||||
<ng-container *ngFor="let poll of assignment.polls; trackBy: trackByIndex">
|
||||
<mat-card class="os-card" *ngIf="!poll.canBeVotedFor">
|
||||
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
||||
<mat-card class="os-card">
|
||||
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
@ -135,13 +127,17 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #candidatesTemplate>
|
||||
<mat-card class="os-card">
|
||||
<ng-container *ngIf="assignment && !assignment.isFinished">
|
||||
<mat-card class="os-card" *ngIf="assignment && !assignment.isFinished">
|
||||
<ng-container>
|
||||
<h3 translate>Candidates</h3>
|
||||
<div>
|
||||
<div
|
||||
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
|
||||
[input]="assignment.assignment_related_users"
|
||||
|
@ -33,16 +33,18 @@
|
||||
{{ 'Groups' | translate }}:
|
||||
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
|
||||
</div>
|
||||
<div>{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}</div>
|
||||
<div>{{ 'Poll method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
|
||||
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</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>
|
||||
|
||||
<div *ngIf="poll.state === 3 || poll.state === 4">
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<h2 translate>Result</h2>
|
||||
|
||||
<div class="chart-wrapper"></div>
|
||||
|
@ -8,14 +8,14 @@
|
||||
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
|
||||
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<div *ngFor="let value of analogPollValues" [formGroupName]="option.user_id">
|
||||
<os-check-input
|
||||
[placeholder]="voteValueVerbose[value] | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
[checkboxLabel]="'majority' | translate"
|
||||
[formControlName]="value"
|
||||
></os-check-input>
|
||||
</div>
|
||||
@ -28,7 +28,7 @@
|
||||
[placeholder]="generalValueVerbose[value] | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
[checkboxLabel]="'majority' | translate"
|
||||
[formControlName]="value"
|
||||
></os-check-input>
|
||||
</div>
|
||||
|
@ -1,4 +1,10 @@
|
||||
<div class="assignment-poll-wrapper" *ngIf="poll">
|
||||
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="poll-menu">
|
||||
<!-- Buttons -->
|
||||
<button
|
||||
@ -9,30 +15,16 @@
|
||||
>
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</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>
|
||||
<h3>
|
||||
<a routerLink="/assignments/polls/{{ poll.id }}">
|
||||
{{ poll.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="poll-properties">
|
||||
<!-- <mat-chip *ngIf="pollService.isElectronicVotingEnabled">{{ poll.typeVerbose }}</mat-chip> -->
|
||||
<mat-chip
|
||||
class="poll-state active"
|
||||
[matMenuTriggerFor]="triggerMenu"
|
||||
@ -41,23 +33,14 @@
|
||||
{{ poll.stateVerbose }}
|
||||
</mat-chip>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<a routerLink="/assignments/polls/{{ poll.id }}">
|
||||
{{ poll.title }}
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<os-charts [type]="chartType" [labels]="candidatesLabels" [data]="chartDataSubject"></os-charts>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<ng-template #resultsTemplate> </ng-template>
|
||||
|
||||
<mat-menu #triggerMenu="matMenu">
|
||||
<ng-container *ngIf="poll">
|
||||
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue">
|
||||
@ -65,3 +48,22 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
</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>
|
||||
|
@ -3,33 +3,10 @@
|
||||
position: relative;
|
||||
padding: 0 15px;
|
||||
|
||||
.poll-main-content {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.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-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export class AssignmentPollService extends PollService {
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
|
||||
.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.assignment_id = poll.assignment_id;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
<mat-icon *ngSwitchCase="'General'">home</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Agenda'">today</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="'Participants'">groups</mat-icon>
|
||||
<mat-icon *ngSwitchCase="'Custom translations'">language</mat-icon>
|
||||
|
@ -463,7 +463,7 @@
|
||||
<div class="mat-card create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
|
||||
<button mat-button (click)="openDialog()">
|
||||
<mat-icon class="main-nav-color">poll</mat-icon>
|
||||
<span translate>New poll</span>
|
||||
<span translate>New vote</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<os-head-bar [goBack]="true" [nav]="false">
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="poll">{{ 'Motion' | translate }} {{ poll.motion.id }}</h2>
|
||||
<h2 *ngIf="poll">{{ 'Motion' | translate }} {{ poll.motion.identifierOrTitle }}</h2>
|
||||
</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">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
@ -18,14 +18,13 @@
|
||||
<ng-template #viewTemplate>
|
||||
<ng-container *ngIf="poll">
|
||||
<h1>{{ poll.title }}</h1>
|
||||
<span *ngIf="poll.type !== 'analog'">{{ 'Polly type' | translate }}: {{ poll.type | translate }}</span>
|
||||
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
|
||||
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
||||
|
||||
<div *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results to show' | translate }}</div>
|
||||
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<h2 translate>Result</h2>
|
||||
|
||||
<div *ngIf="!poll.hasVotes">{{ 'No results to show' | translate }}</div>
|
||||
|
||||
<div class="result-wrapper" *ngIf="poll.hasVotes">
|
||||
<!-- Chart -->
|
||||
<os-charts
|
||||
@ -52,25 +51,28 @@
|
||||
</mat-table>
|
||||
|
||||
<!-- Named table: only show if votes are present -->
|
||||
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data">
|
||||
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter"/>
|
||||
<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="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">
|
||||
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
||||
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
|
||||
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
|
||||
</mat-table>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -82,29 +84,29 @@
|
||||
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<div *ngIf="poll.state === 2">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<!-- 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 (click)="openDialog()">
|
||||
<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 *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
|
||||
<mat-icon>polymer</mat-icon>
|
||||
<span translate>Pseudoanonymize</span>
|
||||
<button
|
||||
mat-menu-item
|
||||
*osPerms="'motions.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()">
|
||||
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="deletePoll()">
|
||||
<mat-icon color="warn">delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
|
@ -35,4 +35,8 @@
|
||||
|
||||
.named-result-table {
|
||||
grid-area: names;
|
||||
.mat-form-field {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -6,41 +6,43 @@
|
||||
[placeholder]="'Yes' | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
[checkboxLabel]="'majority' | translate"
|
||||
formControlName="Y"
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
[placeholder]="'No' | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
[checkboxLabel]="'majority' | translate"
|
||||
formControlName="N"
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
[placeholder]="'Abstain' | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
[checkboxLabel]="'majority' | translate"
|
||||
formControlName="A"
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
[placeholder]="'Votes valid' | translate"
|
||||
[placeholder]="'Valid votes' | translate"
|
||||
inputType="number"
|
||||
formControlName="votesvalid"
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
[placeholder]="'Votes invalid' | translate"
|
||||
[placeholder]="'Invalid votes' | translate"
|
||||
inputType="number"
|
||||
formControlName="votesinvalid"
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
[placeholder]="'Votes cast' | translate"
|
||||
[placeholder]="'Total votes cast' | translate"
|
||||
inputType="number"
|
||||
formControlName="votescast"
|
||||
></os-check-input>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<!-- Publish immediately button. Only show for new polls -->
|
||||
<div *ngIf="!pollData.state">
|
||||
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
|
||||
<span translate>Publish immediately</span>
|
||||
</mat-checkbox>
|
||||
|
@ -42,9 +42,6 @@ export class MotionPollDialogComponent extends BasePollDialogComponent {
|
||||
votesinvalid: data.votesinvalid,
|
||||
votescast: data.votescast
|
||||
};
|
||||
// if (data.pollmethod === 'YNA') {
|
||||
// update.A = data.options[0].abstain;
|
||||
// }
|
||||
|
||||
if (this.dialogVoteForm) {
|
||||
const result = this.undoReplaceEmptyValues(update);
|
||||
@ -64,9 +61,7 @@ export class MotionPollDialogComponent extends BasePollDialogComponent {
|
||||
votesinvalid: ['', [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) {
|
||||
this.updateDialogVoteForm(this.pollData);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<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)">
|
||||
|
||||
<!-- Voting -->
|
||||
<p *ngFor="let option of voteOptions">
|
||||
<button
|
||||
|
@ -1,16 +1,15 @@
|
||||
/**
|
||||
* These colors should be extracted from some global CSS Constants file
|
||||
*/
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.voted-yes {
|
||||
background-color: #9fd773;
|
||||
background-color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
background-color: #cc6c5b;
|
||||
background-color: $votes-no-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
background-color: #a6a6a6;
|
||||
background-color: $votes-abstain-color;
|
||||
}
|
||||
|
||||
.vote-label {
|
||||
|
@ -1,23 +1,23 @@
|
||||
<div class="poll-preview-wrapper">
|
||||
<div class="poll-preview-wrapper" *ngIf="poll && showPoll()">
|
||||
<!-- Poll Infos -->
|
||||
<div class="poll-title-wrapper" *ngIf="poll">
|
||||
<div class="poll-title-wrapper">
|
||||
<!-- Title -->
|
||||
<a class="poll-title" routerLink="/motions/polls/{{ poll.id }}">
|
||||
<a class="poll-title" [routerLink]="pollLink">
|
||||
{{ poll.title }}
|
||||
</a>
|
||||
|
||||
<!-- Edit button -->
|
||||
<!-- Dot Menu -->
|
||||
<span class="poll-title-actions" *osPerms="'motions.can_manage_polls'">
|
||||
<button mat-icon-button (click)="openDialog()">
|
||||
<mat-icon class="small-icon">edit</mat-icon>
|
||||
<button mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
||||
<mat-icon class="small-icon">more_horiz</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<!-- State chip -->
|
||||
<div class="poll-properties" *osPerms="'motions.can_manage_polls'">
|
||||
<span *ngIf="pollService.isElectronicVotingEnabled && poll.typeVerbose !== 'Analog'">
|
||||
{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}
|
||||
</span>
|
||||
<div *ngIf="pollService.isElectronicVotingEnabled && poll.type !== 'analog'">
|
||||
{{ poll.typeVerbose | translate }}
|
||||
</div>
|
||||
|
||||
<mat-chip
|
||||
disableRipple
|
||||
@ -37,35 +37,72 @@
|
||||
</div>
|
||||
|
||||
<ng-template #votingResult>
|
||||
<div (click)="openPoll()">
|
||||
<ng-container [ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"></ng-container>
|
||||
<div [routerLink]="pollLink">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #viewTemplate>
|
||||
<div class="poll-chart-wrapper">
|
||||
<div class="votes-yes">
|
||||
<os-icon-container icon="check" size="large">
|
||||
{{ voteYes }}
|
||||
</os-icon-container>
|
||||
<!-- empty helper div to center the grid wrapper -->
|
||||
<div></div>
|
||||
<div class="doughnut-chart">
|
||||
<os-charts *ngIf="showChart" [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
|
||||
</div>
|
||||
<div *ngIf="showChart" class="doughnut-chart">
|
||||
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
|
||||
</div>
|
||||
<div class="votes-no">
|
||||
<os-icon-container icon="close" size="large">
|
||||
{{ voteNo }}
|
||||
</os-icon-container>
|
||||
<div class="vote-legend">
|
||||
<div class="votes-yes" *ngIf="isVoteDocumented(voteYes)">
|
||||
<os-icon-container icon="thumb_up" size="large">
|
||||
{{ voteYes | parsePollNumber }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<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 class="poll-detail-button-wrapper" *ngIf="poll.type !== 'analog'">
|
||||
<button mat-button [routerLink]="pollLink">
|
||||
{{ 'Single Votes' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #emptyTemplate>
|
||||
<div>
|
||||
An empty poll - you have to enter votes.
|
||||
<div *osPerms="'motions.can_manage_polls'">
|
||||
{{ 'An empty poll - you have to enter votes.' | translate }}
|
||||
</div>
|
||||
</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">
|
||||
<ng-container *ngIf="poll">
|
||||
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue">
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.poll-preview-wrapper {
|
||||
padding: 8px;
|
||||
background: white;
|
||||
@ -6,6 +8,7 @@
|
||||
.poll-title {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.poll-title-actions {
|
||||
@ -48,20 +51,41 @@
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(50px, 20%) auto;
|
||||
|
||||
.votes-no {
|
||||
color: #cc6c5b;
|
||||
margin: auto 0;
|
||||
width: fit-content;
|
||||
.doughnut-chart {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.votes-yes {
|
||||
color: #9fc773;
|
||||
margin: auto 0 auto auto;
|
||||
width: fit-content;
|
||||
.vote-legend {
|
||||
margin: auto 10px;
|
||||
|
||||
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 {
|
||||
span {
|
||||
padding: 0 5px;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
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 { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
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 { PollService } from 'app/site/polls/services/poll.service';
|
||||
|
||||
@ -40,6 +40,9 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
if (data.label === 'NO') {
|
||||
this.voteNo = data.data[0];
|
||||
}
|
||||
if (data.label === 'ABSTAIN') {
|
||||
this.voteAbstain = data.data[0];
|
||||
}
|
||||
}
|
||||
this.chartDataSubject.next(chartData);
|
||||
}
|
||||
@ -48,6 +51,10 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
return this._poll;
|
||||
}
|
||||
|
||||
public get pollLink(): string {
|
||||
return `/motions/polls/${this.poll.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subject to holding the data needed for the chart.
|
||||
*/
|
||||
@ -56,32 +63,45 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
/**
|
||||
* Number of votes for `Yes`.
|
||||
*/
|
||||
public set voteYes(n: number | string) {
|
||||
public set voteYes(n: number) {
|
||||
this._voteYes = n;
|
||||
}
|
||||
|
||||
public get voteYes(): number | string {
|
||||
return this.verboseForNumber(this._voteYes as number);
|
||||
public get voteYes(): number {
|
||||
return this._voteYes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of votes for `No`.
|
||||
*/
|
||||
public set voteNo(n: number | string) {
|
||||
public set voteNo(n: number) {
|
||||
this._voteNo = n;
|
||||
}
|
||||
|
||||
public get voteNo(): number | string {
|
||||
return this.verboseForNumber(this._voteNo as number);
|
||||
public get voteNo(): 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 {
|
||||
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.
|
||||
@ -101,27 +121,35 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
public pollRepo: MotionPollRepositoryService,
|
||||
pollDialog: MotionPollDialogService,
|
||||
public pollService: PollService,
|
||||
private router: Router,
|
||||
private operator: OperatorService
|
||||
private operator: OperatorService,
|
||||
private pdfService: MotionPollPdfService
|
||||
) {
|
||||
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
|
||||
}
|
||||
|
||||
public openPoll(): void {
|
||||
if (this.operator.hasPerms('motions.can_manage_polls')) {
|
||||
this.router.navigate(['motions', 'polls', this.poll.id]);
|
||||
public showPoll(): boolean {
|
||||
return (
|
||||
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 {
|
||||
input = Math.trunc(input);
|
||||
switch (input) {
|
||||
case -1:
|
||||
return 'Majority';
|
||||
case -2:
|
||||
return 'Not documented';
|
||||
default:
|
||||
return input;
|
||||
}
|
||||
public isVoteDocumented(vote: number): boolean {
|
||||
return vote !== null && vote !== undefined && vote !== -2;
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'polls',
|
||||
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',
|
||||
|
@ -17,7 +17,7 @@ type BallotCountChoices = 'NUMBER_OF_DELEGATES' | 'NUMBER_OF_ALL_PARTICIPANTS' |
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* this.MotionPollPdfService.printBallos(this.poll);
|
||||
* this.MotionPollPdfService.printBallots(this.poll);
|
||||
* ```
|
||||
*/
|
||||
@Injectable({
|
||||
|
@ -75,7 +75,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
* @param fb
|
||||
* @param groupRepo
|
||||
* @param location
|
||||
* @param promptDialog
|
||||
* @param promptService
|
||||
* @param dialog
|
||||
*/
|
||||
public constructor(
|
||||
@ -85,7 +85,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
protected repo: BasePollRepositoryService,
|
||||
protected route: ActivatedRoute,
|
||||
protected groupRepo: GroupRepositoryService,
|
||||
protected promptDialog: PromptService,
|
||||
protected promptService: PromptService,
|
||||
protected pollDialog: BasePollDialogService<V>
|
||||
) {
|
||||
super(title, translate, matSnackbar);
|
||||
@ -108,16 +108,16 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
const title = 'Delete 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);
|
||||
}
|
||||
}
|
||||
|
||||
public async pseudoanonymizePoll(): Promise<void> {
|
||||
const title = 'Pseudoanonymize poll';
|
||||
const text = 'Do you really want to pseudoanonymize the selected poll?';
|
||||
const title = 'Anonymize single votes';
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
<form [formGroup]="contentForm" class="poll-preview--meta-info-form">
|
||||
<ng-container *ngIf="!data || !data.state || data.state === 1">
|
||||
<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">
|
||||
{{ option.value | translate }}
|
||||
</mat-option>
|
||||
@ -56,7 +56,7 @@
|
||||
</mat-select>
|
||||
</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">
|
||||
{{ option.value | translate }}
|
||||
</mat-option>
|
||||
|
@ -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>
|
||||
|
@ -29,6 +29,10 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
|
||||
super(title, translate, snackbar);
|
||||
}
|
||||
|
||||
public get valueInPercent(): number {
|
||||
return (this.poll.voted_id.length / this.max) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* OnInit.
|
||||
* Sets the observable for groups.
|
||||
|
@ -22,9 +22,9 @@ export const PollStateVerbose = {
|
||||
};
|
||||
|
||||
export const PollTypeVerbose = {
|
||||
analog: 'Analog',
|
||||
named: 'Named',
|
||||
pseudoanonymous: 'Pseudoanonymous'
|
||||
analog: 'Analog voting',
|
||||
named: 'Named voting',
|
||||
pseudoanonymous: 'Pseudoanonymous voting'
|
||||
};
|
||||
|
||||
export const PollPropertyVerbose = {
|
||||
@ -37,9 +37,9 @@ export const PollPropertyVerbose = {
|
||||
};
|
||||
|
||||
export const MajorityMethodVerbose = {
|
||||
simple: 'Simple',
|
||||
two_thirds: 'Two Thirds',
|
||||
three_quarters: 'Three Quarters',
|
||||
simple: 'Simple majority',
|
||||
two_thirds: 'Two-thirds majority',
|
||||
three_quarters: 'Three-quarters majority',
|
||||
disabled: 'Disabled'
|
||||
};
|
||||
|
||||
@ -48,7 +48,7 @@ export const PercentBaseVerbose = {
|
||||
YNA: 'Yes/No/Abstain',
|
||||
votes: 'All votes',
|
||||
valid: 'Valid votes',
|
||||
cast: 'Cast votes',
|
||||
cast: 'Total votes cast',
|
||||
disabled: 'Disabled'
|
||||
};
|
||||
|
||||
@ -104,6 +104,7 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
public abstract readonly pollClassType: 'motion' | 'assignment';
|
||||
|
||||
public canBeVotedFor: () => boolean;
|
||||
|
6
client/src/assets/styles/poll-colors.scss
Normal file
6
client/src/assets/styles/poll-colors.scss
Normal 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;
|
@ -9,7 +9,7 @@ def get_config_variables():
|
||||
They are grouped in 'Ballot and ballot papers' and 'PDF'. The generator has
|
||||
to be evaluated during app loading (see apps.py).
|
||||
"""
|
||||
# Polls
|
||||
# Voting
|
||||
yield ConfigVariable(
|
||||
name="assignment_poll_default_100_percent_base",
|
||||
default_value="YNA",
|
||||
@ -20,7 +20,7 @@ def get_config_variables():
|
||||
for base in AssignmentPoll.PERCENT_BASES
|
||||
),
|
||||
weight=400,
|
||||
group="Polls",
|
||||
group="Voting",
|
||||
subgroup="Elections",
|
||||
)
|
||||
|
||||
@ -35,7 +35,8 @@ def get_config_variables():
|
||||
label="Required majority",
|
||||
help_text="Default method to check whether a candidate has reached the required majority.",
|
||||
weight=405,
|
||||
group="Polls",
|
||||
hidden=True,
|
||||
group="Voting",
|
||||
subgroup="Elections",
|
||||
)
|
||||
|
||||
@ -45,7 +46,7 @@ def get_config_variables():
|
||||
input_type="boolean",
|
||||
label="Put all candidates on the list of speakers",
|
||||
weight=410,
|
||||
group="Polls",
|
||||
group="Voting",
|
||||
subgroup="Elections",
|
||||
)
|
||||
|
||||
|
@ -117,7 +117,7 @@ def set_correct_state(apps, schema_editor):
|
||||
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
|
||||
AssignmentVote = apps.get_model("assignments", "AssignmentVote")
|
||||
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...
|
||||
if AssignmentVote.objects.filter(option__poll__pk=poll.pk).exists():
|
||||
if poll.published:
|
||||
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0026_remove_history_restricted"),
|
||||
("core", "0025_projector_color"),
|
||||
]
|
||||
|
||||
operations = [
|
@ -37,7 +37,7 @@ def calculate_aspect_ratios(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0027_projector_size_1"),
|
||||
("core", "0026_projector_size_1"),
|
||||
]
|
||||
|
||||
operations = [
|
@ -6,7 +6,7 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0028_projector_size_2"),
|
||||
("core", "0027_projector_size_2"),
|
||||
]
|
||||
|
||||
operations = [
|
@ -5,6 +5,6 @@ from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("core", "0025_projector_color")]
|
||||
dependencies = [("core", "0028_projector_size_3")]
|
||||
|
||||
operations = [migrations.RemoveField(model_name="history", name="restricted")]
|
@ -388,18 +388,18 @@ def get_config_variables():
|
||||
subgroup="PDF export",
|
||||
)
|
||||
|
||||
# Polls
|
||||
# Voting
|
||||
yield ConfigVariable(
|
||||
name="motion_poll_default_100_percent_base",
|
||||
default_value="YNA",
|
||||
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(
|
||||
{"value": base[0], "display_name": base[1]}
|
||||
for base in MotionPoll.PERCENT_BASES
|
||||
),
|
||||
weight=420,
|
||||
group="Polls",
|
||||
group="Voting",
|
||||
subgroup="Motions",
|
||||
)
|
||||
|
||||
@ -412,8 +412,9 @@ def get_config_variables():
|
||||
for method in MotionPoll.MAJORITY_METHODS
|
||||
),
|
||||
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,
|
||||
group="Polls",
|
||||
hidden=True,
|
||||
group="Voting",
|
||||
subgroup="Motions",
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user