Enhance charts and tables for assignments
Also some various improvements
This commit is contained in:
parent
e2feeb4b65
commit
61b7731073
@ -173,6 +173,7 @@ _('Number of all participants');
|
||||
_('Use the following custom number');
|
||||
_('Custom number of ballot papers');
|
||||
_('Voting');
|
||||
_('Click here to vote');
|
||||
// subgroup PDF export
|
||||
_('PDF export');
|
||||
_('Title for PDF documents of motions');
|
||||
|
@ -7,8 +7,7 @@ export interface BannerDefinition {
|
||||
class?: string;
|
||||
icon?: string;
|
||||
text?: string;
|
||||
bgColor?: string;
|
||||
color?: string;
|
||||
subText?: string;
|
||||
link?: string;
|
||||
largerOnMobileView?: boolean;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollListObservableService } from 'app/site/polls/services/poll-list-observable.service';
|
||||
@ -15,6 +16,8 @@ import { VotingService } from './voting.service';
|
||||
export class VotingBannerService {
|
||||
private currentBanner: BannerDefinition;
|
||||
|
||||
private subText = 'Click here to vote';
|
||||
|
||||
public constructor(
|
||||
pollListObservableService: PollListObservableService,
|
||||
private banner: BannerService,
|
||||
@ -58,6 +61,7 @@ export class VotingBannerService {
|
||||
private createBanner(text: string, link: string): BannerDefinition {
|
||||
return {
|
||||
text: text,
|
||||
subText: this.subText,
|
||||
link: link,
|
||||
icon: 'how_to_vote',
|
||||
largerOnMobileView: true
|
||||
@ -72,11 +76,13 @@ export class VotingBannerService {
|
||||
* @returns The title.
|
||||
*/
|
||||
private getTextForPoll(poll: ViewBasePoll): string {
|
||||
return poll instanceof ViewMotionPoll
|
||||
? `${this.translate.instant('Motion') + ' ' + poll.motion.getIdentifierOrTitle()}: ${this.translate.instant(
|
||||
'Voting is open'
|
||||
)}`
|
||||
: `${poll.getTitle()}: ${this.translate.instant('Ballot is open')}`;
|
||||
if (poll instanceof ViewMotionPoll) {
|
||||
return `${this.translate.instant('Motion')} ${poll.motion.getIdentifierOrTitle()}: ${this.translate.instant(
|
||||
'Voting opened'
|
||||
)}`;
|
||||
} else if (poll instanceof ViewAssignmentPoll) {
|
||||
return `${poll.assignment.getTitle()}: ${this.translate.instant('Ballot openened!')}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,6 +16,9 @@
|
||||
<ng-container *ngSwitchDefault>
|
||||
<a class="banner-link" [routerLink]="banner.link" [style.cursor]="banner.link ? 'pointer' : 'default'">
|
||||
<mat-icon>{{ banner.icon }}</mat-icon> <span>{{ banner.text }}</span>
|
||||
<div *ngIf="banner.subText">
|
||||
{{ banner.subText | translate }}
|
||||
</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -3,12 +3,12 @@
|
||||
.banner {
|
||||
&.larger-on-mobile {
|
||||
@include set-breakpoint-lower(sm) {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
position: relative; // was fixed before to prevent the overflow
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@ -18,7 +18,6 @@
|
||||
border-bottom: 1px solid white;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
/** Custom component theme. Only lives in a specific scope */
|
||||
@mixin os-banner-style($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
|
||||
/** style for the offline-banner */
|
||||
.banner {
|
||||
background: mat-color($primary, 900);
|
||||
background: mat-color($accent, 500);
|
||||
}
|
||||
}
|
||||
|
@ -199,14 +199,24 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
labels: {}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{ ticks: { beginAtZero: true, stepSize: 1 } }],
|
||||
yAxes: [{ ticks: { beginAtZero: true } }]
|
||||
xAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
drawOnChartArea: false
|
||||
},
|
||||
plugins: {
|
||||
datalabels: {
|
||||
anchor: 'end',
|
||||
align: 'end'
|
||||
ticks: { beginAtZero: true, stepSize: 1 },
|
||||
stacked: true
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
drawOnChartArea: false
|
||||
},
|
||||
ticks: { beginAtZero: true, mirror: true, labelOffset: -20 },
|
||||
stacked: true
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@ -251,16 +261,6 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
super(title, translate, matSnackbar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the chart-options, if the `stackedBar` is used.
|
||||
*/
|
||||
private setupStackedBar(): void {
|
||||
this.chartOptions.scales = Object.assign(this.chartOptions.scales, {
|
||||
xAxes: [{ stacked: true }],
|
||||
yAxes: [{ stacked: true }]
|
||||
});
|
||||
}
|
||||
|
||||
private setupBar(): void {
|
||||
if (!this.chartData.every(date => date.barThickness && date.maxBarThickness)) {
|
||||
this.chartData = this.chartData.map(chartDate => ({
|
||||
@ -284,7 +284,6 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
fontSize: 14,
|
||||
boxWidth: 40
|
||||
};
|
||||
break;
|
||||
}
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
@ -292,7 +291,6 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
private checkChartType(chartType?: ChartType): void {
|
||||
let type = chartType || this._type;
|
||||
if (type === 'stackedBar') {
|
||||
this.setupStackedBar();
|
||||
this.setupBar();
|
||||
type = 'horizontalBar';
|
||||
}
|
||||
|
@ -88,14 +88,12 @@ export class CheckInputComponent extends BaseViewComponent implements OnInit, Co
|
||||
* @param obj the value from the parent form. Type "any" is required by the interface
|
||||
*/
|
||||
public writeValue(obj: string | number): void {
|
||||
if (obj) {
|
||||
if (obj === this.checkboxValue) {
|
||||
if (obj && obj === this.checkboxValue) {
|
||||
this.checkboxStateChanged(true);
|
||||
} else {
|
||||
this.contentForm.patchValue(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hands changes back to the parent form
|
||||
|
@ -0,0 +1,18 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span translate>Online voting is impossible to secure</span>
|
||||
</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<span translate>
|
||||
During voting, OpenSlides does not store the individual user ID of the voter. This in no way means that a
|
||||
non-nominal vote is completely anonymous and secure. You cannot track the decisions of your voters after the
|
||||
data has been submitted. The validity of the data cannot always be guaranteed, especially if you use OpenSlides
|
||||
in a distributed online setup. You are responsible for your own actions.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions>
|
||||
<button type="button" mat-button [mat-dialog-close]="null">
|
||||
<span translate>I know the risk</span>
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { VotingPrivacyWarningComponent } from './voting-privacy-warning.component';
|
||||
|
||||
describe('VotingPrivacyWarningComponent', () => {
|
||||
let component: VotingPrivacyWarningComponent;
|
||||
let fixture: ComponentFixture<VotingPrivacyWarningComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(VotingPrivacyWarningComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'os-voting-privacy-warning',
|
||||
templateUrl: './voting-privacy-warning.component.html',
|
||||
styleUrls: ['./voting-privacy-warning.component.scss']
|
||||
})
|
||||
export class VotingPrivacyWarningComponent implements OnInit {
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit(): void {}
|
||||
}
|
@ -76,6 +76,10 @@ export abstract class BasePoll<
|
||||
return this.onehundred_percent_base === PercentBase.Valid || this.onehundred_percent_base === PercentBase.Cast;
|
||||
}
|
||||
|
||||
public get isPercentBaseCast(): boolean {
|
||||
return this.onehundred_percent_base === PercentBase.Cast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the state is finished or published
|
||||
*/
|
||||
|
@ -114,7 +114,6 @@ import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
import { ChartsComponent } from './components/charts/charts.component';
|
||||
import { CheckInputComponent } from './components/check-input/check-input.component';
|
||||
import { BannerComponent } from './components/banner/banner.component';
|
||||
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.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 { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||
@ -122,6 +121,7 @@ import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
||||
import { ReversePipe } from './pipes/reverse.pipe';
|
||||
import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
|
||||
import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
||||
import { VotingPrivacyWarningComponent } from './components/voting-privacy-warning/voting-privacy-warning.component';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -285,7 +285,8 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe,
|
||||
PollKeyVerbosePipe,
|
||||
PollPercentBasePipe
|
||||
PollPercentBasePipe,
|
||||
VotingPrivacyWarningComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -342,7 +343,8 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe,
|
||||
PollKeyVerbosePipe,
|
||||
PollPercentBasePipe
|
||||
PollPercentBasePipe,
|
||||
VotingPrivacyWarningComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@ -373,7 +375,8 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
||||
ProgressSnackBarComponent,
|
||||
SuperSearchComponent,
|
||||
MotionPollDialogComponent,
|
||||
AssignmentPollDialogComponent
|
||||
AssignmentPollDialogComponent,
|
||||
VotingPrivacyWarningComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
@ -313,7 +313,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
collectionString: ViewAssignmentPoll.COLLECTIONSTRING,
|
||||
assignment_id: this.assignment.id,
|
||||
assignment: this.assignment,
|
||||
...this.assignmentPollService.getDefaultPollData()
|
||||
...this.assignmentPollService.getDefaultPollData(this.assignment.id)
|
||||
};
|
||||
|
||||
this.pollDialog.openDialog(dialogData);
|
||||
|
@ -20,14 +20,14 @@
|
||||
<h1>{{ poll.title }}</h1>
|
||||
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
||||
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<div class="assignment-result-wrapper">
|
||||
<div class="assignment-result-wrapper" *ngIf="poll.stateHasVotes">
|
||||
<!-- Result Table -->
|
||||
<div>
|
||||
<table class="assignment-result-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th translate>Candidates</th>
|
||||
<th>
|
||||
<th class="voting-option" translate>Candidates</th>
|
||||
<th class="result voted-yes">
|
||||
<span *ngIf="!poll.isMethodY" translate>
|
||||
Yes
|
||||
</span>
|
||||
@ -35,11 +35,12 @@
|
||||
Votes
|
||||
</span>
|
||||
</th>
|
||||
<th translate *ngIf="!poll.isMethodY">No</th>
|
||||
<th translate *ngIf="poll.isMethodYNA">Abstain</th>
|
||||
<th class="result voted-no" translate *ngIf="!poll.isMethodY">No</th>
|
||||
<th class="result voted-abstain" translate *ngIf="poll.isMethodYNA">Abstain</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of poll.tableData" [class]="row.class">
|
||||
<td>
|
||||
<td class="voting-option">
|
||||
<div>
|
||||
<span>
|
||||
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||
</span>
|
||||
@ -47,30 +48,37 @@
|
||||
<br />
|
||||
{{ row.votingOptionSubtitle }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td *ngFor="let vote of row.value">
|
||||
<div class="single-result" *ngIf="vote && voteFitsMethod(vote)">
|
||||
<td class="result" *ngFor="let vote of row.value">
|
||||
<div
|
||||
class="single-result"
|
||||
[ngClass]="getVoteClass(vote)"
|
||||
*ngIf="vote && voteFitsMethod(vote)"
|
||||
>
|
||||
<span>
|
||||
{{ vote.amount | parsePollNumber }}
|
||||
<span *ngIf="vote.showPercent">
|
||||
{{ vote.amount | pollPercentBase: poll }}
|
||||
</span>
|
||||
{{ vote.amount | parsePollNumber }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Result Chart -->
|
||||
<div class="chart-wrapper">
|
||||
<os-charts
|
||||
class="assignment-result-chart"
|
||||
[ngClass]="chartType === 'doughnut' ? 'pie-chart' : ''"
|
||||
*ngIf="chartDataSubject.value"
|
||||
[type]="chartType"
|
||||
[labels]="candidatesLabels"
|
||||
[data]="chartDataSubject"
|
||||
[hasPadding]="false"
|
||||
[showLegend]="false"
|
||||
legendPosition="right"
|
||||
></os-charts>
|
||||
</div>
|
||||
@ -121,9 +129,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meta Infos -->
|
||||
<div class="assignment-poll-meta">
|
||||
<div *ngIf="poll" class="assignment-poll-meta">
|
||||
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||
{{ 'Groups' | translate }}:
|
||||
|
||||
@ -136,7 +145,6 @@
|
||||
{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- More Menu -->
|
||||
|
@ -2,24 +2,22 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.assignment-result-wrapper {
|
||||
margin-top: 2em;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
|
||||
.assignment-result-table {
|
||||
margin-top: 2em;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
tr {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
border-bottom: none;
|
||||
td:first-child {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
tr.sums {
|
||||
@ -30,13 +28,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
text-align: right;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.voting-option {
|
||||
min-width: 200px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.user + .sums {
|
||||
td {
|
||||
padding-top: 4em;
|
||||
}
|
||||
padding-top: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.single-result {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
margin-top: 2em;
|
||||
.pie-chart {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@ -44,10 +59,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.single-vote-result + .single-vote-result {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.named-result-table {
|
||||
.mat-form-field {
|
||||
font-size: 14px;
|
||||
@ -57,6 +68,10 @@
|
||||
.single-votes-table {
|
||||
display: block;
|
||||
height: 500px;
|
||||
|
||||
.single-vote-result + .single-vote-result {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.vote-field {
|
||||
@ -65,6 +80,7 @@
|
||||
padding-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.assignment-poll-meta {
|
||||
display: grid;
|
||||
|
@ -34,11 +34,9 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
public candidatesLabels: string[] = [];
|
||||
|
||||
public get chartType(): ChartType {
|
||||
return this._chartType;
|
||||
return 'stackedBar';
|
||||
}
|
||||
|
||||
private _chartType: ChartType = 'horizontalBar';
|
||||
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
@ -114,19 +112,15 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
}
|
||||
}
|
||||
|
||||
protected initChartData(): void {
|
||||
if (this.poll.isMethodY) {
|
||||
this._chartType = 'doughnut';
|
||||
this.chartDataSubject.next(this.pollService.generateCircleChartData(this.poll));
|
||||
} else {
|
||||
super.initChartData();
|
||||
}
|
||||
}
|
||||
|
||||
protected hasPerms(): boolean {
|
||||
return this.operator.hasPerms('assignments.can_manage');
|
||||
}
|
||||
|
||||
public getVoteClass(votingResult: VotingResult): string {
|
||||
const cssPrefix = 'voted-';
|
||||
return `${cssPrefix}${votingResult.vote}`;
|
||||
}
|
||||
|
||||
public voteFitsMethod(result: VotingResult): boolean {
|
||||
if (this.poll.isMethodY) {
|
||||
if (result.vote === 'abstain' || result.vote === 'no') {
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { AssignmentPollDialogComponent } from './assignment-poll-dialog.component';
|
||||
|
||||
describe('AssignmentPollDialogComponent', () => {
|
||||
let component: AssignmentPollDialogComponent;
|
||||
let fixture: ComponentFixture<AssignmentPollDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AssignmentPollDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -85,15 +85,23 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
|
||||
public ngOnInit(): void {
|
||||
// TODO: not solid.
|
||||
// on new poll creation, poll.options does not exist, so we have to build a substitute from the assignment candidates
|
||||
this.options = this.pollData.options
|
||||
? this.pollData.options
|
||||
: this.pollData.assignment.candidates.map(
|
||||
if (this.pollData) {
|
||||
if (this.pollData.options) {
|
||||
this.options = this.pollData.options;
|
||||
} else if (
|
||||
this.pollData.assignment &&
|
||||
this.pollData.assignment.candidates &&
|
||||
this.pollData.assignment.candidates.length
|
||||
) {
|
||||
this.options = this.pollData.assignment.candidates.map(
|
||||
user => ({
|
||||
user_id: user.id,
|
||||
user: user
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.subscriptions.push(
|
||||
this.pollForm.contentForm.get('pollmethod').valueChanges.subscribe(() => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
<ng-container *ngIf="poll">
|
||||
|
||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||
<!-- TODO: Someone should make this pretty -->
|
||||
<span *ngIf="poll.user_has_voted_valid">Your vote is valid!</span>
|
||||
@ -22,7 +21,12 @@
|
||||
}"
|
||||
>
|
||||
<div class="vote-candidate-name">
|
||||
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
|
||||
<span *ngIf="option.user">
|
||||
<span>{{ option.user.short_name }}</span>
|
||||
<div class="user-subtitle" *ngIf="option.user.getLevelAndNumber()">
|
||||
{{ option.user.getLevelAndNumber() }}
|
||||
</div>
|
||||
</span>
|
||||
<span *ngIf="!option.user">{{ 'Unknown user' | translate }}</span>
|
||||
</div>
|
||||
|
||||
|
@ -122,12 +122,17 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
let requestData;
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
|
||||
const pollOptionIds = this.getPollOptionIds();
|
||||
|
||||
requestData = pollOptionIds.reduce((o, n) => {
|
||||
if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) {
|
||||
o[n] = 1; // TODO: allow multiple votes per candidate
|
||||
} else {
|
||||
o[n] = 0;
|
||||
if (this.poll.votes_amount === 1) {
|
||||
if (n === optionId && this.currentVotes[n] !== 'Yes') {
|
||||
o[n] = 1;
|
||||
}
|
||||
} else if ((n === optionId) !== (this.currentVotes[n] === 'Yes')) {
|
||||
o[n] = 1;
|
||||
}
|
||||
|
||||
return o;
|
||||
}, {});
|
||||
} else {
|
||||
@ -135,6 +140,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
requestData = {};
|
||||
requestData[optionId] = vote;
|
||||
}
|
||||
|
||||
this.pollRepo.vote(requestData, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<mat-card class="os-card" *ngIf="poll && showPoll()">
|
||||
<mat-card class="os-card" *ngIf="poll">
|
||||
<div class="assignment-poll-wrapper">
|
||||
<div>
|
||||
<!-- Title -->
|
||||
@ -21,7 +21,7 @@
|
||||
<!-- Buttons -->
|
||||
<button
|
||||
mat-icon-button
|
||||
*osPerms="'assignments.motions.can_manage_polls'; or: 'core.can_manage_projector'"
|
||||
*osPerms="['core.can_manage_projector', 'assignments.can_manage_polls']"
|
||||
[matMenuTriggerFor]="pollItemMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
@ -45,16 +45,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="canSeeVotes">
|
||||
<!-- Chart -->
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<div *osPerms="'assignments.can_manage'; or: poll.isPublished">
|
||||
<os-charts
|
||||
[class]="chartType === 'doughnut' ? 'doughnut-chart' : 'bar-chart'"
|
||||
[type]="chartType"
|
||||
[labels]="candidatesLabels"
|
||||
[data]="chartDataSubject"
|
||||
[hasPadding]="false"
|
||||
[showLegend]="!poll.isMethodY"
|
||||
legendPosition="right"
|
||||
></os-charts>
|
||||
</div>
|
||||
|
||||
<!-- Cannot see unpublished -->
|
||||
<div *osPerms="'assignments.can_manage'; complement: true">
|
||||
<span *ngIf="poll.isFinished">
|
||||
{{ 'Counting is in progress' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Poll progress bar -->
|
||||
<div *osPerms="'assignments.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
|
@ -29,11 +29,4 @@
|
||||
.publish-poll-button {
|
||||
color: $poll-publish-color;
|
||||
}
|
||||
|
||||
.doughnut-chart {
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||
@ -39,7 +38,7 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
}
|
||||
|
||||
public get chartType(): ChartType {
|
||||
return this.pollService.getChartType(this.poll);
|
||||
return 'stackedBar';
|
||||
}
|
||||
|
||||
public candidatesLabels: string[] = [];
|
||||
@ -49,21 +48,6 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
*/
|
||||
public descriptionForm: FormGroup;
|
||||
|
||||
/**
|
||||
* @returns true if the user is permitted to do operations
|
||||
*/
|
||||
public get canManage(): boolean {
|
||||
return this.operator.hasPerms('assignments.can_manage');
|
||||
}
|
||||
|
||||
public get canSee(): boolean {
|
||||
return this.operator.hasPerms('assignments.can_see');
|
||||
}
|
||||
|
||||
public get canSeeVotes(): boolean {
|
||||
return (this.canManage && this.poll.isFinished) || this.poll.isPublished;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the description on the form differs from the poll's description
|
||||
*/
|
||||
@ -80,7 +64,6 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
repo: AssignmentPollRepositoryService,
|
||||
pollDialog: AssignmentPollDialogService,
|
||||
public pollService: PollService,
|
||||
private operator: OperatorService,
|
||||
private formBuilder: FormBuilder,
|
||||
private pdfService: AssignmentPollPdfService
|
||||
) {
|
||||
@ -99,12 +82,4 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
public printBallot(): void {
|
||||
this.pdfService.printBallots(this.poll);
|
||||
}
|
||||
|
||||
public showPoll(): boolean {
|
||||
return (
|
||||
this.operator.hasPerms('assignments.can_manage_polls') ||
|
||||
this.poll.isPublished ||
|
||||
(this.poll.type !== 'analog' && this.poll.isStarted)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,11 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
||||
}));
|
||||
|
||||
tableData.push(
|
||||
...this.sumTableKeys.map(key => ({
|
||||
...this.sumTableKeys
|
||||
.filter(key => {
|
||||
return !key.hide;
|
||||
})
|
||||
.map(key => ({
|
||||
votingOption: key.vote,
|
||||
class: 'sums',
|
||||
value: [
|
||||
|
@ -54,14 +54,21 @@ export class AssignmentPollService extends PollService {
|
||||
.subscribe(method => (this.defaultPollMethod = method));
|
||||
}
|
||||
|
||||
public getDefaultPollData(): AssignmentPoll {
|
||||
const poll = new AssignmentPoll(super.getDefaultPollData());
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
|
||||
.length;
|
||||
public getDefaultPollData(contextId?: number): AssignmentPoll {
|
||||
const poll = new AssignmentPoll({
|
||||
...super.getDefaultPollData()
|
||||
});
|
||||
|
||||
poll.title = !length ? this.translate.instant('Ballot') : `${this.translate.instant('Ballot')} (${length + 1})`;
|
||||
poll.title = this.translate.instant('Ballot');
|
||||
poll.pollmethod = this.defaultPollMethod;
|
||||
|
||||
if (contextId) {
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === contextId).length;
|
||||
if (length) {
|
||||
poll.title += ` (${length + 1})`;
|
||||
}
|
||||
}
|
||||
|
||||
return poll;
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,10 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll, MotionPollMethod, P
|
||||
return this.options[0];
|
||||
}
|
||||
|
||||
public get hasPresentableValues(): boolean {
|
||||
return this.result.hasPresentableValues;
|
||||
}
|
||||
|
||||
public get hasVotes(): boolean {
|
||||
return this.result && !!this.result.votes.length;
|
||||
}
|
||||
|
@ -133,7 +133,12 @@
|
||||
|
||||
{{ getTitleWithChanges() }}
|
||||
</h1>
|
||||
<button mat-icon-button color="primary" (click)="toggleFavorite()">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
(click)="toggleFavorite()"
|
||||
matTooltip="{{ 'Mark as personal favorite' | translate }}"
|
||||
>
|
||||
<mat-icon>{{ motion.star ? 'star' : 'star_border' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -460,14 +465,18 @@
|
||||
<!-- motion polls -->
|
||||
<div *ngIf="!editMotion" class="spacer-top-20 spacer-bottom-20">
|
||||
<os-motion-poll *ngFor="let poll of motion.polls; trackBy: trackByIndex" [poll]="poll"></os-motion-poll>
|
||||
<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>
|
||||
<button
|
||||
class="create-poll-button"
|
||||
create-poll-button
|
||||
mat-stroked-button
|
||||
(click)="openDialog()"
|
||||
*ngIf="perms.isAllowed('createpoll', motion)"
|
||||
>
|
||||
<mat-icon class="main-nav-color">add</mat-icon>
|
||||
<span translate>New vote</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #contentTemplate>
|
||||
|
@ -5,12 +5,7 @@ span {
|
||||
}
|
||||
|
||||
.create-poll-button {
|
||||
margin-top: 10px;
|
||||
padding: 0px !important;
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.extra-controls-slot {
|
||||
|
@ -1633,7 +1633,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
collectionString: ViewMotionPoll.COLLECTIONSTRING,
|
||||
motion_id: this.motion.id,
|
||||
motion: this.motion,
|
||||
...this.motionPollService.getDefaultPollData()
|
||||
...this.motionPollService.getDefaultPollData(this.motion.id)
|
||||
};
|
||||
|
||||
this.pollDialog.openDialog(dialogData);
|
||||
|
@ -67,9 +67,8 @@
|
||||
</table>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="doughnut-chart">
|
||||
<div class="doughnut-chart" *ngIf="poll.hasPresentableValues && chartDataSubject.value">
|
||||
<os-charts
|
||||
*ngIf="chartDataSubject.value"
|
||||
[type]="chartType"
|
||||
[data]="chartDataSubject"
|
||||
[showLegend]="false"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Inject, ViewChild } from '@angular/core';
|
||||
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
@ -15,7 +15,7 @@ import { PercentBaseVerbose } from 'app/site/polls/models/view-base-poll';
|
||||
templateUrl: './motion-poll-dialog.component.html',
|
||||
styleUrls: ['./motion-poll-dialog.component.scss']
|
||||
})
|
||||
export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotionPoll> {
|
||||
export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotionPoll> implements OnInit {
|
||||
public PercentBaseVerbose = PercentBaseVerbose;
|
||||
|
||||
@ViewChild('pollForm', { static: false })
|
||||
@ -30,6 +30,9 @@ export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotio
|
||||
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewMotionPoll>
|
||||
) {
|
||||
super(title, translate, matSnackbar, dialogRef);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.createDialog();
|
||||
}
|
||||
|
||||
@ -54,12 +57,12 @@ export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotio
|
||||
*/
|
||||
private createDialog(): void {
|
||||
this.dialogVoteForm = this.fb.group({
|
||||
Y: ['', [Validators.min(-2)]],
|
||||
N: ['', [Validators.min(-2)]],
|
||||
A: ['', [Validators.min(-2)]],
|
||||
votesvalid: ['', [Validators.min(-2)]],
|
||||
votesinvalid: ['', [Validators.min(-2)]],
|
||||
votescast: ['', [Validators.min(-2)]]
|
||||
Y: [0, [Validators.min(-2)]],
|
||||
N: [0, [Validators.min(-2)]],
|
||||
A: [0, [Validators.min(-2)]],
|
||||
votesvalid: [0, [Validators.min(-2)]],
|
||||
votesinvalid: [0, [Validators.min(-2)]],
|
||||
votescast: [0, [Validators.min(-2)]]
|
||||
});
|
||||
|
||||
if (this.pollData.poll) {
|
||||
|
@ -7,8 +7,10 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||
import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||
@ -66,7 +68,8 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
||||
vmanager: VotingService,
|
||||
operator: OperatorService,
|
||||
private voteRepo: MotionVoteRepositoryService,
|
||||
private pollRepo: MotionPollRepositoryService
|
||||
private pollRepo: MotionPollRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(title, translate, matSnackbar, vmanager, operator);
|
||||
}
|
||||
@ -100,6 +103,16 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
||||
* TODO: 'Y' | 'N' | 'A' should refer to some ENUM
|
||||
*/
|
||||
public saveVote(vote: 'Y' | 'N' | 'A'): void {
|
||||
if (this.poll.type === PollType.Pseudoanonymous) {
|
||||
const title = this.translate.instant('Are you sure?');
|
||||
const content = this.translate.instant('Your decision cannot be changed afterwards');
|
||||
this.promptService.open(title, content).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,19 @@
|
||||
<!-- Title -->
|
||||
<span class="poll-title">
|
||||
<a [routerLink]="pollLink">
|
||||
{{ poll.title }}
|
||||
{{ poll.title | translate }}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<span *osPerms="'motions.can_manage_polls'; and: poll.type === 'pseudoanonymous'">
|
||||
<button mat-icon-button color="warn" (click)="openVotingWarning()">
|
||||
<mat-icon>
|
||||
warning
|
||||
</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<span *ngIf="pollService.isElectronicVotingEnabled && poll.type !== 'analog'">
|
||||
{{ poll.typeVerbose | translate }} ·
|
||||
@ -18,7 +26,7 @@
|
||||
|
||||
<!-- State chip -->
|
||||
<span>
|
||||
{{ poll.stateVerbose }}
|
||||
{{ poll.stateVerbose | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,13 +40,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Change state button -->
|
||||
<div *osPerms="'motions.can_manage_polls'">
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngIf="!poll.isPublished"
|
||||
[ngClass]="pollStateActions[poll.state].css"
|
||||
(click)="changeState(poll.nextState)"
|
||||
>
|
||||
<div *osPerms="'motions.can_manage_polls'; and: !hideChangeState">
|
||||
<button mat-stroked-button [ngClass]="pollStateActions[poll.state].css" (click)="changeState(poll.nextState)">
|
||||
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
|
||||
<span class="next-state-label">
|
||||
{{ poll.nextStateActionVerbose | translate }}
|
||||
@ -73,22 +76,12 @@
|
||||
<!-- Result Chart and legend -->
|
||||
<div class="poll-chart-wrapper" *osPerms="'motions.can_manage_polls'; or: poll.isPublished">
|
||||
<div class="vote-legend" [routerLink]="pollLink">
|
||||
<div class="votes-yes">
|
||||
<os-icon-container icon="thumb_up" size="large">
|
||||
{{ voteYes | parsePollNumber }}
|
||||
{{ voteYes | pollPercentBase: poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div class="votes-no">
|
||||
<os-icon-container icon="thumb_down" size="large">
|
||||
{{ voteNo | parsePollNumber }}
|
||||
{{ voteNo | pollPercentBase: poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div class="votes-abstain">
|
||||
<os-icon-container icon="trip_origin" size="large">
|
||||
{{ voteAbstain | parsePollNumber }}
|
||||
{{ voteAbstain | pollPercentBase: poll }}
|
||||
<div *ngFor="let row of reducedPollTableData" [class]="row.votingOption">
|
||||
<os-icon-container [icon]="row.value[0].icon" size="large">
|
||||
{{ row.value[0].amount | parsePollNumber }}
|
||||
<span *ngIf="row.value[0].showPercent">
|
||||
{{ row.value[0].amount | pollPercentBase: poll }}
|
||||
</span>
|
||||
</os-icon-container>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,7 +106,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #emptyTemplate>
|
||||
<div *osPerms="'motions.can_manage_polls'">
|
||||
<div *osPerms="'motions.can_manage_polls'; and: poll.type === 'analog'">
|
||||
{{ 'Edit to enter votes.' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -47,15 +47,15 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.votes-yes {
|
||||
.yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.votes-no {
|
||||
.no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.votes-abstain {
|
||||
.abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,14 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
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 { PollTableData } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
|
||||
/**
|
||||
@ -28,19 +32,7 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
@Input()
|
||||
public set poll(value: ViewMotionPoll) {
|
||||
this.initPoll(value);
|
||||
|
||||
const chartData = this.pollService.generateChartData(value);
|
||||
for (const data of chartData) {
|
||||
if (data.label === 'YES') {
|
||||
this.voteYes = data.data[0];
|
||||
}
|
||||
if (data.label === 'NO') {
|
||||
this.voteNo = data.data[0];
|
||||
}
|
||||
if (data.label === 'ABSTAIN') {
|
||||
this.voteAbstain = data.data[0];
|
||||
}
|
||||
}
|
||||
this.chartDataSubject.next(chartData);
|
||||
}
|
||||
|
||||
@ -52,48 +44,17 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
return `/motions/polls/${this.poll.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of votes for `Yes`.
|
||||
*/
|
||||
public set voteYes(n: number) {
|
||||
this._voteYes = n;
|
||||
}
|
||||
|
||||
public get voteYes(): number {
|
||||
return this._voteYes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of votes for `No`.
|
||||
*/
|
||||
public set voteNo(n: number) {
|
||||
this._voteNo = n;
|
||||
}
|
||||
|
||||
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;
|
||||
return this.poll.hasPresentableValues;
|
||||
}
|
||||
|
||||
private _voteNo: number;
|
||||
public get hideChangeState(): boolean {
|
||||
return this.poll.isPublished || (this.poll.isCreated && this.poll.type === PollType.Analog);
|
||||
}
|
||||
|
||||
private _voteYes: number;
|
||||
|
||||
private _voteAbstain: number;
|
||||
public get reducedPollTableData(): PollTableData[] {
|
||||
return this.poll.tableData.filter(data => ['yes', 'no', 'abstain', 'votesinvalid'].includes(data.votingOption));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@ -118,6 +79,10 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
|
||||
}
|
||||
|
||||
public openVotingWarning(): void {
|
||||
this.dialog.open(VotingPrivacyWarningComponent, infoDialogSettings);
|
||||
}
|
||||
|
||||
public downloadPdf(): void {
|
||||
this.pdfService.printBallots(this.poll);
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
|
||||
* @param title Set the page title
|
||||
* @param translate Handle translations
|
||||
* @param matSnackBar Showing error
|
||||
* @param promtService Promts
|
||||
* @param promptService Promts
|
||||
* @param dialog Opening dialogs
|
||||
* @param workflowRepo The repository for workflows
|
||||
* @param route Read out URL paramters
|
||||
@ -155,7 +155,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
|
||||
title: Title,
|
||||
protected translate: TranslateService, // protected required for ng-translate-extract
|
||||
matSnackBar: MatSnackBar,
|
||||
private promtService: PromptService,
|
||||
private promptService: PromptService,
|
||||
private dialog: MatDialog,
|
||||
private workflowRepo: WorkflowRepositoryService,
|
||||
private stateRepo: StateRepositoryService,
|
||||
@ -208,7 +208,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
|
||||
} else if (result.action === 'delete') {
|
||||
const content = this.translate.instant('Delete') + ` ${state.name}?`;
|
||||
|
||||
this.promtService.open('Are you sure', content).then(promptResult => {
|
||||
this.promptService.open('Are you sure', content).then(promptResult => {
|
||||
if (promptResult) {
|
||||
this.stateRepo.delete(state).then(() => {}, this.raiseError);
|
||||
}
|
||||
|
@ -55,13 +55,19 @@ export class MotionPollService extends PollService {
|
||||
config.get<number[]>(MotionPoll.defaultGroupsConfig).subscribe(ids => (this.defaultGroupIds = ids));
|
||||
}
|
||||
|
||||
public getDefaultPollData(): MotionPoll {
|
||||
public getDefaultPollData(contextId?: number): MotionPoll {
|
||||
const poll = new MotionPoll(super.getDefaultPollData());
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === poll.motion_id).length;
|
||||
|
||||
poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`;
|
||||
poll.title = this.translate.instant('Vote');
|
||||
poll.pollmethod = MotionPollMethod.YNA;
|
||||
|
||||
if (contextId) {
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === contextId).length;
|
||||
if (length) {
|
||||
poll.title += ` (${length + 1})`;
|
||||
}
|
||||
}
|
||||
|
||||
return poll;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
@ -13,7 +14,7 @@ import { ViewBasePoll } from '../models/view-base-poll';
|
||||
/**
|
||||
* A dialog for updating the values of a poll.
|
||||
*/
|
||||
export abstract class BasePollDialogComponent<T extends ViewBasePoll> extends BaseViewComponent {
|
||||
export abstract class BasePollDialogComponent<T extends ViewBasePoll> extends BaseViewComponent implements OnInit {
|
||||
public publishImmediately: boolean;
|
||||
|
||||
protected pollForm: PollFormComponent<T>;
|
||||
@ -29,6 +30,21 @@ export abstract class BasePollDialogComponent<T extends ViewBasePoll> extends Ba
|
||||
super(title, translate, matSnackbar);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
if (this.dialogRef) {
|
||||
// Jasmin/Karma fails here. TODO:
|
||||
this.dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && event.shiftKey) {
|
||||
this.submitPoll();
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the values from dialog.
|
||||
*/
|
||||
|
@ -9,7 +9,7 @@
|
||||
</form>
|
||||
|
||||
<!-- TODO: rather disable forms than duplicate them -->
|
||||
<div *ngIf="data && data.state > 1" class="poll-preview-meta-info">
|
||||
<div *ngIf="data && data.state" class="poll-preview-meta-info">
|
||||
<span class="short-description" *ngFor="let value of pollValues">
|
||||
<span class="short-description-label subtitle" translate>
|
||||
{{ value[0] }}
|
||||
@ -22,13 +22,14 @@
|
||||
<form [formGroup]="contentForm" class="poll-preview-meta-info-form">
|
||||
<ng-container *ngIf="!data || !data.state || data.isCreated">
|
||||
<!-- Poll Type -->
|
||||
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
|
||||
<mat-form-field class="pollType" *ngIf="pollService.isElectronicVotingEnabled">
|
||||
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
|
||||
<mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">
|
||||
{{ option.value | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error translate>This field is required</mat-error>
|
||||
<mat-hint (click)="openVotingWarning()" *ngIf="showNonNominalWarning"> {{ 'Not suitable for formal secret voting!' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Groups entitled to Vote -->
|
||||
|
@ -2,6 +2,13 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pollType {
|
||||
.mat-hint {
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-preview-meta-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -8,12 +8,13 @@ import { Observable } from 'rxjs';
|
||||
|
||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
|
||||
import { AssignmentPollMethod, AssignmentPollPercentBase } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PercentBase } from 'app/shared/models/poll/base-poll';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import {
|
||||
MajorityMethodVerbose,
|
||||
PollClassType,
|
||||
@ -86,6 +87,8 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
|
||||
public showSingleAmountHint = false;
|
||||
|
||||
public showNonNominalWarning = false;
|
||||
|
||||
/**
|
||||
* Constructor. Retrieves necessary metadata from the pollService,
|
||||
* injects the poll itself
|
||||
@ -97,7 +100,8 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
private fb: FormBuilder,
|
||||
private groupRepo: GroupRepositoryService,
|
||||
public pollService: PollService,
|
||||
private configService: ConfigService
|
||||
private configService: ConfigService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
super(title, translate, snackbar);
|
||||
this.initContentForm();
|
||||
@ -119,7 +123,6 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
if (!this.data.pollmethod) {
|
||||
this.data.pollmethod = this.configService.instant('assignment_poll_method');
|
||||
}
|
||||
} else if (this.data instanceof ViewMotionPoll) {
|
||||
}
|
||||
|
||||
Object.keys(this.contentForm.controls).forEach(key => {
|
||||
@ -209,6 +212,16 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
private setVotesAmountCtrl(): void {
|
||||
// Disable "Amounts of votes" if anonymous and yes-method
|
||||
const votesAmountCtrl = this.contentForm.get('votes_amount');
|
||||
|
||||
if (this.contentForm.get('type').value === PollType.Pseudoanonymous) {
|
||||
this.showNonNominalWarning = true;
|
||||
} else {
|
||||
this.showNonNominalWarning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Not required when batch sending works again
|
||||
*/
|
||||
if (
|
||||
this.contentForm.get('type').value === PollType.Pseudoanonymous &&
|
||||
this.contentForm.get('pollmethod').value === 'votes'
|
||||
@ -276,6 +289,10 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
|
||||
});
|
||||
}
|
||||
|
||||
public openVotingWarning(): void {
|
||||
this.dialog.open(VotingPrivacyWarningComponent, infoDialogSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* compare function used with the KeyValuePipe to display the percent bases in original order
|
||||
*/
|
||||
|
@ -1,12 +1,8 @@
|
||||
<div *ngIf="poll" class="poll-progress-wrapper">
|
||||
<div class="motion-vote-number" *ngIf="poll.pollClassType === 'motion'">
|
||||
<div class="vote-number">
|
||||
<span>{{ poll.votescast }} / {{ max }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="poll.pollClassType === 'assignment'">
|
||||
<div>{{ 'Total' | translate }}: {{ poll.votescast }} / {{ max }},</div>
|
||||
<div>{{ 'Valid' | translate }}: {{ poll.votesvalid }} / {{ max }},</div>
|
||||
<div>{{ 'Invalid votes' | translate }}: {{ poll.votesinvalid }} / {{ max }}</div>
|
||||
</div>
|
||||
<span translate>Received votes</span>
|
||||
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
||||
</div>
|
||||
|
@ -1,10 +1,8 @@
|
||||
.poll-progress-wrapper {
|
||||
margin: 1em 0 2em 0;
|
||||
.voting-progress-bar {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.motion-vote-number {
|
||||
.vote-number {
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ import { ViewBasePoll } from './view-base-poll';
|
||||
import { ViewBaseVote } from './view-base-vote';
|
||||
|
||||
export class ViewBaseOption<M extends BaseOption<M> = any> extends BaseViewModel<M> {
|
||||
public get hasPresentableValues(): boolean {
|
||||
return this.yes >= 0 && this.no >= 0;
|
||||
}
|
||||
|
||||
public get option(): M {
|
||||
return this._model;
|
||||
}
|
||||
|
@ -109,17 +109,19 @@ export abstract class ViewBasePoll<
|
||||
protected sumTableKeys: VotingResult[] = [
|
||||
{
|
||||
vote: 'votesvalid',
|
||||
hide: this.poll.votesvalid === -2,
|
||||
showPercent: this.poll.isPercentBaseValidOrCast
|
||||
},
|
||||
{
|
||||
vote: 'votesinvalid',
|
||||
hide: this.poll.type !== PollType.Analog,
|
||||
showPercent: this.poll.isPercentBaseValidOrCast
|
||||
icon: 'not_interested',
|
||||
hide: this.poll.type !== PollType.Analog || this.poll.votesinvalid === -2,
|
||||
showPercent: this.poll.isPercentBaseCast
|
||||
},
|
||||
{
|
||||
vote: 'votescast',
|
||||
hide: this.poll.type !== PollType.Analog,
|
||||
showPercent: this.poll.isPercentBaseValidOrCast
|
||||
hide: this.poll.type !== PollType.Analog || this.poll.votescast === -2,
|
||||
showPercent: this.poll.isPercentBaseCast
|
||||
}
|
||||
];
|
||||
|
||||
@ -163,7 +165,11 @@ export abstract class ViewBasePoll<
|
||||
public abstract get percentBaseVerbose(): string;
|
||||
|
||||
public get showAbstainPercent(): boolean {
|
||||
return this.poll.onehundred_percent_base === PercentBase.YNA;
|
||||
return (
|
||||
this.poll.onehundred_percent_base === PercentBase.YNA ||
|
||||
this.poll.onehundred_percent_base === PercentBase.Valid ||
|
||||
this.poll.onehundred_percent_base === PercentBase.Cast
|
||||
);
|
||||
}
|
||||
|
||||
public abstract readonly pollClassType: 'motion' | 'assignment';
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { _ } from 'app/core/translate/translation-marker';
|
||||
import { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
|
||||
import { ChartData, ChartDate } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
|
||||
import { BasePoll, MajorityMethod, PollColor, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { BasePoll, MajorityMethod, PercentBase, PollColor, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { AssignmentPollMethodVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import {
|
||||
MajorityMethodVerbose,
|
||||
@ -94,7 +93,7 @@ export interface PollData {
|
||||
onehundred_percent_base: string;
|
||||
options: {
|
||||
user?: {
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
};
|
||||
yes?: number;
|
||||
no?: number;
|
||||
@ -181,47 +180,50 @@ export abstract class PollService {
|
||||
}
|
||||
|
||||
public generateChartData(poll: PollData): ChartData {
|
||||
if (poll.pollmethod === AssignmentPollMethod.Votes) {
|
||||
return this.generateCircleChartData(poll);
|
||||
let fields: CalculablePollKey[];
|
||||
|
||||
// TODO: PollData should either be `ViewBasePoll` or `BasePoll` to get SOLID
|
||||
const isAssignment = Object.keys(poll.options[0]).includes('user');
|
||||
|
||||
if (isAssignment) {
|
||||
if (poll.pollmethod === AssignmentPollMethod.YNA) {
|
||||
fields = ['yes', 'no', 'abstain'];
|
||||
} else if (poll.pollmethod === AssignmentPollMethod.YN) {
|
||||
fields = ['yes', 'no'];
|
||||
} else {
|
||||
return this.generateBarChartData(poll);
|
||||
fields = ['yes'];
|
||||
}
|
||||
} else {
|
||||
if (poll.onehundred_percent_base === PercentBase.YN) {
|
||||
fields = ['yes', 'no'];
|
||||
} else if (poll.onehundred_percent_base === PercentBase.Cast) {
|
||||
fields = ['yes', 'no', 'abstain', 'votesinvalid'];
|
||||
} else {
|
||||
fields = ['yes', 'no', 'abstain'];
|
||||
}
|
||||
}
|
||||
|
||||
public generateBarChartData(poll: PollData): ChartData {
|
||||
const fields = ['yes', 'no'];
|
||||
// cast is needed because ViewBasePoll doesn't have the field `pollmethod`, no easy fix :(
|
||||
if ((<any>poll).pollmethod === MotionPollMethod.YNA) {
|
||||
fields.push('abstain');
|
||||
}
|
||||
const data: ChartData = fields.map(key => ({
|
||||
const data: ChartData = fields.map(key => {
|
||||
return {
|
||||
data: this.getResultFromPoll(poll, key),
|
||||
label: key.toUpperCase(),
|
||||
data: poll.options.map(option => option[key]),
|
||||
backgroundColor: PollColor[key],
|
||||
hoverBackgroundColor: PollColor[key]
|
||||
}));
|
||||
} as ChartDate;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public generateCircleChartData(poll: PollData): ChartData {
|
||||
const data: ChartData = poll.options.map(candidate => ({
|
||||
label: candidate.user.full_name,
|
||||
data: [candidate.yes]
|
||||
}));
|
||||
return data;
|
||||
}
|
||||
|
||||
public getChartType(poll: PollData): ChartType {
|
||||
if ((<any>poll).pollmethod === AssignmentPollMethod.Votes) {
|
||||
return 'doughnut';
|
||||
} else {
|
||||
return 'horizontalBar';
|
||||
}
|
||||
/**
|
||||
* Extracts yes-no-abstain such as valid, invalids and totals from Poll and PollData-Objects
|
||||
*/
|
||||
private getResultFromPoll(poll: PollData, key: CalculablePollKey): number[] {
|
||||
return poll[key] ? [poll[key]] : poll.options.map(option => option[key]);
|
||||
}
|
||||
|
||||
public getChartLabels(poll: PollData): string[] {
|
||||
return poll.options.map(candidate => candidate.user.full_name);
|
||||
return poll.options.map(candidate => candidate.user.short_name);
|
||||
}
|
||||
|
||||
public isVoteDocumented(vote: number): boolean {
|
||||
|
@ -17,7 +17,7 @@ export interface AssignmentPollSlideData extends BasePollSlideData {
|
||||
|
||||
options: {
|
||||
user: {
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
};
|
||||
yes?: number;
|
||||
no?: number;
|
||||
|
@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="charts-wrapper" *ngIf="data.data.poll.state === PollState.Published">
|
||||
<os-charts
|
||||
[type]="pollService.getChartType(data.data.poll)"
|
||||
[type]="stackedBar"
|
||||
[labels]="pollService.getChartLabels(data.data.poll)"
|
||||
[data]="chartDataSubject"
|
||||
[hasPadding]="false"
|
||||
|
@ -27,7 +27,7 @@ def get_config_variables():
|
||||
|
||||
yield ConfigVariable(
|
||||
name="assignment_poll_default_100_percent_base",
|
||||
default_value="YNA",
|
||||
default_value="valid",
|
||||
input_type="choice",
|
||||
label="The 100-%-base of an election result consists of",
|
||||
choices=tuple(
|
||||
|
Loading…
Reference in New Issue
Block a user