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