Cleanup Voting, enhance UI and UX
removed certain unnecesary fields cleaned up a lot of code redone some of the UI some database and server adjustments
This commit is contained in:
parent
7598fc5367
commit
97a5bb4aa6
@ -80,7 +80,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
|||||||
private readonly restPath = '/rest/assignments/assignment/';
|
private readonly restPath = '/rest/assignments/assignment/';
|
||||||
private readonly candidatureOtherPath = '/candidature_other/';
|
private readonly candidatureOtherPath = '/candidature_other/';
|
||||||
private readonly candidatureSelfPath = '/candidature_self/';
|
private readonly candidatureSelfPath = '/candidature_self/';
|
||||||
private readonly markElectedPath = '/mark_elected/';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the Assignment Repository.
|
* Constructor for the Assignment Repository.
|
||||||
@ -158,26 +157,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
|||||||
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
|
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* change the 'elected' state of an election candidate
|
|
||||||
*
|
|
||||||
* @param assignmentRelatedUser
|
|
||||||
* @param assignment
|
|
||||||
* @param elected true if the candidate is to be elected, false if unelected
|
|
||||||
*/
|
|
||||||
public async markElected(
|
|
||||||
assignmentRelatedUser: ViewAssignmentRelatedUser,
|
|
||||||
assignment: ViewAssignment,
|
|
||||||
elected: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
const data = { user: assignmentRelatedUser.user_id };
|
|
||||||
if (elected) {
|
|
||||||
await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data);
|
|
||||||
} else {
|
|
||||||
await this.httpService.delete(this.restPath + assignment.id + this.markElectedPath, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a request to sort an assignment's candidates
|
* Sends a request to sort an assignment's candidates
|
||||||
*
|
*
|
||||||
|
@ -125,6 +125,18 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
return name.trim();
|
return name.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLevelAndNumber(titleInformation: UserTitleInformation): string {
|
||||||
|
if (titleInformation.structure_level && titleInformation.number) {
|
||||||
|
return `${titleInformation.structure_level} · ${this.translate.instant('No.')} ${titleInformation.number}`;
|
||||||
|
} else if (titleInformation.structure_level) {
|
||||||
|
return titleInformation.structure_level;
|
||||||
|
} else if (titleInformation.number) {
|
||||||
|
return `${this.translate.instant('No.')} ${titleInformation.number}`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Participants' : 'Participant');
|
return this.translate.instant(plural ? 'Participants' : 'Participant');
|
||||||
};
|
};
|
||||||
@ -145,12 +157,13 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds teh short and full name to the view user.
|
* Adds the short and full name to the view user.
|
||||||
*/
|
*/
|
||||||
protected createViewModelWithTitles(model: User): ViewUser {
|
protected createViewModelWithTitles(model: User): ViewUser {
|
||||||
const viewModel = super.createViewModelWithTitles(model);
|
const viewModel = super.createViewModelWithTitles(model);
|
||||||
viewModel.getFullName = () => this.getFullName(viewModel);
|
viewModel.getFullName = () => this.getFullName(viewModel);
|
||||||
viewModel.getShortName = () => this.getShortName(viewModel);
|
viewModel.getShortName = () => this.getShortName(viewModel);
|
||||||
|
viewModel.getLevelAndNumber = () => this.getLevelAndNumber(viewModel);
|
||||||
return viewModel;
|
return viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
|||||||
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
|
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
import { PollService } from '../../site/polls/services/poll.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for showing a poll dialog. Has to be subclassed to provide the right `PollService`
|
* Abstract class for showing a poll dialog. Has to be subclassed to provide the right `PollService`
|
||||||
@ -17,42 +16,35 @@ import { PollService } from '../../site/polls/services/poll.service';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export abstract class BasePollDialogService<V extends ViewBasePoll> {
|
export abstract class BasePollDialogService<V extends ViewBasePoll> {
|
||||||
protected dialogComponent: ComponentType<BasePollDialogComponent>;
|
protected dialogComponent: ComponentType<BasePollDialogComponent<V>>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(private dialog: MatDialog, private mapper: CollectionStringMapperService) {}
|
||||||
private dialog: MatDialog,
|
|
||||||
private mapper: CollectionStringMapperService,
|
|
||||||
private service: PollService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the dialog to enter votes and edit the meta-info for a poll.
|
* Opens the dialog to enter votes and edit the meta-info for a poll.
|
||||||
*
|
*
|
||||||
* @param data Passing the (existing or new) data for the poll
|
* @param data Passing the (existing or new) data for the poll
|
||||||
*/
|
*/
|
||||||
public async openDialog(poll: Partial<V> & Collection): Promise<void> {
|
public async openDialog(viewPoll: Partial<V> & Collection): Promise<void> {
|
||||||
if (!poll.poll) {
|
|
||||||
this.service.fillDefaultPollData(poll);
|
|
||||||
}
|
|
||||||
const dialogRef = this.dialog.open(this.dialogComponent, {
|
const dialogRef = this.dialog.open(this.dialogComponent, {
|
||||||
data: poll,
|
data: viewPoll,
|
||||||
...mediumDialogSettings
|
...mediumDialogSettings
|
||||||
});
|
});
|
||||||
const result = await dialogRef.afterClosed().toPromise();
|
const result = await dialogRef.afterClosed().toPromise();
|
||||||
if (result) {
|
if (result) {
|
||||||
const repo = this.mapper.getRepository(poll.collectionString);
|
const repo = this.mapper.getRepository(viewPoll.collectionString);
|
||||||
if (!poll.poll) {
|
if (!viewPoll.poll) {
|
||||||
await repo.create(result);
|
await repo.create(result);
|
||||||
} else {
|
} else {
|
||||||
let update = result;
|
let update = result;
|
||||||
if (poll.state !== PollState.Created) {
|
if (viewPoll.state !== PollState.Created) {
|
||||||
update = {
|
update = {
|
||||||
title: result.title,
|
title: result.title,
|
||||||
onehundred_percent_base: result.onehundred_percent_base,
|
onehundred_percent_base: result.onehundred_percent_base,
|
||||||
majority_method: result.majority_method,
|
majority_method: result.majority_method,
|
||||||
description: result.description
|
description: result.description
|
||||||
};
|
};
|
||||||
if (poll.type === PollType.Analog) {
|
if (viewPoll.type === PollType.Analog) {
|
||||||
update = {
|
update = {
|
||||||
...update,
|
...update,
|
||||||
votes: result.votes,
|
votes: result.votes,
|
||||||
@ -60,7 +52,7 @@ export abstract class BasePollDialogService<V extends ViewBasePoll> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await repo.patch(update, <V>poll);
|
await repo.patch(update, <V>viewPoll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
<div>
|
|
||||||
<mat-button-toggle-group>
|
|
||||||
<mat-button-toggle
|
|
||||||
*ngFor="let breadcrumb of breadcrumbList"
|
|
||||||
[disabled]="breadcrumb.action === null"
|
|
||||||
(click)="breadcrumb.action ? breadcrumb.action() : null"
|
|
||||||
[ngClass]="{ 'active-breadcrumb': breadcrumb.active }"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{ breadcrumb.label }}
|
|
||||||
</span>
|
|
||||||
</mat-button-toggle>
|
|
||||||
</mat-button-toggle-group>
|
|
||||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||||||
.active-breadcrumb {
|
|
||||||
// Theme
|
|
||||||
color: rgba($color: #317796, $alpha: 1);
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
|
||||||
|
|
||||||
import { BreadcrumbComponent } from './breadcrumb.component';
|
|
||||||
|
|
||||||
describe('BreadcrumbComponent', () => {
|
|
||||||
let component: BreadcrumbComponent;
|
|
||||||
let fixture: ComponentFixture<BreadcrumbComponent>;
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [E2EImportsModule]
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(BreadcrumbComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,69 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes, how one breadcrumb can look like.
|
|
||||||
*/
|
|
||||||
export interface Breadcrumb {
|
|
||||||
label: string;
|
|
||||||
action: () => any;
|
|
||||||
active?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'os-breadcrumb',
|
|
||||||
templateUrl: './breadcrumb.component.html',
|
|
||||||
styleUrls: ['./breadcrumb.component.scss']
|
|
||||||
})
|
|
||||||
export class BreadcrumbComponent implements OnInit {
|
|
||||||
/**
|
|
||||||
* A list of all breadcrumbs, that should be rendered.
|
|
||||||
*
|
|
||||||
* @param labels A list of strings or the interface `Breadcrumb`.
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public set breadcrumbs(labels: string[] | Breadcrumb[]) {
|
|
||||||
this.breadcrumbList = [];
|
|
||||||
|
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats
|
|
||||||
for (const breadcrumb of labels) {
|
|
||||||
if (typeof breadcrumb === 'string') {
|
|
||||||
this.breadcrumbList.push({ label: breadcrumb, action: null });
|
|
||||||
} else {
|
|
||||||
this.breadcrumbList.push(breadcrumb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current active index, if not the last one.
|
|
||||||
*
|
|
||||||
* @param index The index as number.
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public set activeIndex(index: number) {
|
|
||||||
for (const breadcrumb of this.breadcrumbList) {
|
|
||||||
breadcrumb.active = false;
|
|
||||||
}
|
|
||||||
this.breadcrumbList[index].active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of the breadcrumbs built by the input.
|
|
||||||
*/
|
|
||||||
public breadcrumbList: Breadcrumb[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor.
|
|
||||||
*/
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OnInit.
|
|
||||||
* Sets the last breadcrumb as the active breadcrumb if not defined before.
|
|
||||||
*/
|
|
||||||
public ngOnInit(): void {
|
|
||||||
if (this.breadcrumbList.length && !this.breadcrumbList.some(breadcrumb => breadcrumb.active)) {
|
|
||||||
this.breadcrumbList[this.breadcrumbList.length - 1].active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,16 +5,22 @@
|
|||||||
'message action'
|
'message action'
|
||||||
'bar action';
|
'bar action';
|
||||||
grid-template-columns: auto min-content;
|
grid-template-columns: auto min-content;
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
.mat-progress-bar-buffer {
|
||||||
grid-area: message;
|
// TODO theme
|
||||||
}
|
// background-color: mat-color($background, card) !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
.bar {
|
.message {
|
||||||
grid-area: bar;
|
grid-area: message;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.bar {
|
||||||
grid-area: action;
|
grid-area: bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
grid-area: action;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ export enum AssignmentPollMethods {
|
|||||||
*/
|
*/
|
||||||
export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
|
export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
|
||||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||||
|
public static defaultGroupsConfig = 'assignment_poll_default_groups';
|
||||||
|
public static defaultPollMethodConfig = 'assignment_poll_method';
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
public assignment_id: number;
|
public assignment_id: number;
|
||||||
|
@ -8,7 +8,6 @@ export class AssignmentRelatedUser extends BaseModel<AssignmentRelatedUser> {
|
|||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
public user_id: number;
|
public user_id: number;
|
||||||
public elected: boolean;
|
|
||||||
public assignment_id: number;
|
public assignment_id: number;
|
||||||
public weight: number;
|
public weight: number;
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export enum MotionPollMethods {
|
|||||||
*/
|
*/
|
||||||
export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
|
export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
|
||||||
public static COLLECTIONSTRING = 'motions/motion-poll';
|
public static COLLECTIONSTRING = 'motions/motion-poll';
|
||||||
|
public static defaultGroupsConfig = 'motion_poll_default_groups';
|
||||||
|
|
||||||
public id: number;
|
public id: number;
|
||||||
public motion_id: number;
|
public motion_id: number;
|
||||||
|
@ -2,7 +2,7 @@ import { BaseDecimalModel } from '../base/base-decimal-model';
|
|||||||
import { BaseOption } from './base-option';
|
import { BaseOption } from './base-option';
|
||||||
|
|
||||||
export enum PollColor {
|
export enum PollColor {
|
||||||
yes = '#9fd773',
|
yes = '#4caf50',
|
||||||
no = '#cc6c5b',
|
no = '#cc6c5b',
|
||||||
abstain = '#a6a6a6',
|
abstain = '#a6a6a6',
|
||||||
votesvalid = '#e2e2e2',
|
votesvalid = '#e2e2e2',
|
||||||
@ -76,6 +76,10 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
|||||||
return this.isFinished || this.isPublished;
|
return this.isFinished || this.isPublished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get nextState(): PollState {
|
||||||
|
return this.state + 1;
|
||||||
|
}
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
||||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,6 @@ const PollValues = {
|
|||||||
})
|
})
|
||||||
export class PollKeyVerbosePipe implements PipeTransform {
|
export class PollKeyVerbosePipe implements PipeTransform {
|
||||||
public transform(value: string): string {
|
public transform(value: string): string {
|
||||||
return PollValues[value];
|
return PollValues[value] || value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { AssignmentPollService } from 'app/site/assignments/services/assignment-
|
|||||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||||
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
||||||
|
|
||||||
fdescribe('PollPercentBasePipe', () => {
|
describe('PollPercentBasePipe', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule]
|
imports: [E2EImportsModule]
|
||||||
|
@ -40,7 +40,7 @@ export class PollPercentBasePipe implements PipeTransform {
|
|||||||
const percentNumber = (value / totalByBase) * 100;
|
const percentNumber = (value / totalByBase) * 100;
|
||||||
if (percentNumber > 0) {
|
if (percentNumber > 0) {
|
||||||
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
|
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
|
||||||
return `(${result}%)`;
|
return `(${result} %)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -111,7 +111,6 @@ import { GlobalSpinnerComponent } from 'app/site/common/components/global-spinne
|
|||||||
import { HeightResizingDirective } from './directives/height-resizing.directive';
|
import { HeightResizingDirective } from './directives/height-resizing.directive';
|
||||||
import { TrustPipe } from './pipes/trust.pipe';
|
import { TrustPipe } from './pipes/trust.pipe';
|
||||||
import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||||
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
|
|
||||||
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';
|
||||||
@ -277,7 +276,6 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
|||||||
ChartsModule,
|
ChartsModule,
|
||||||
TrustPipe,
|
TrustPipe,
|
||||||
LocalizedDatePipe,
|
LocalizedDatePipe,
|
||||||
BreadcrumbComponent,
|
|
||||||
ChartsComponent,
|
ChartsComponent,
|
||||||
CheckInputComponent,
|
CheckInputComponent,
|
||||||
BannerComponent,
|
BannerComponent,
|
||||||
@ -335,7 +333,6 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
|||||||
HeightResizingDirective,
|
HeightResizingDirective,
|
||||||
TrustPipe,
|
TrustPipe,
|
||||||
LocalizedDatePipe,
|
LocalizedDatePipe,
|
||||||
BreadcrumbComponent,
|
|
||||||
ChartsComponent,
|
ChartsComponent,
|
||||||
CheckInputComponent,
|
CheckInputComponent,
|
||||||
BannerComponent,
|
BannerComponent,
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
<!-- candidates list -->
|
<!-- candidates list -->
|
||||||
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
||||||
<!-- closed polls -->
|
<!-- closed polls -->
|
||||||
<ng-container *ngIf="assignment">
|
<ng-container *ngIf="assignment && assignment.polls.length">
|
||||||
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
||||||
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -162,7 +162,12 @@
|
|||||||
<!-- Search for candidates -->
|
<!-- Search for candidates -->
|
||||||
<div class="search-field" *ngIf="hasPerms('addOthers')">
|
<div class="search-field" *ngIf="hasPerms('addOthers')">
|
||||||
<form
|
<form
|
||||||
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
|
*ngIf="
|
||||||
|
hasPerms('addOthers') &&
|
||||||
|
filteredCandidates &&
|
||||||
|
filteredCandidates.value &&
|
||||||
|
filteredCandidates.value.length
|
||||||
|
"
|
||||||
[formGroup]="candidatesForm"
|
[formGroup]="candidatesForm"
|
||||||
>
|
>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
@ -285,7 +290,6 @@
|
|||||||
<span translate>Number poll candidates</span>
|
<span translate>Number poll candidates</span>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO searchValueSelector: Parent -->
|
|
||||||
</form>
|
</form>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||||
import { AssignmentDetailComponent } from './assignment-detail.component';
|
import { AssignmentDetailComponent } from './assignment-detail.component';
|
||||||
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||||
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
||||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
|
||||||
|
|
||||||
describe('AssignmentDetailComponent', () => {
|
describe('AssignmentDetailComponent', () => {
|
||||||
let component: AssignmentDetailComponent;
|
let component: AssignmentDetailComponent;
|
||||||
@ -12,7 +14,12 @@ describe('AssignmentDetailComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule],
|
imports: [E2EImportsModule],
|
||||||
declarations: [AssignmentDetailComponent, AssignmentPollComponent, AssignmentPollVoteComponent]
|
declarations: [
|
||||||
|
AssignmentDetailComponent,
|
||||||
|
AssignmentPollComponent,
|
||||||
|
AssignmentPollVoteComponent,
|
||||||
|
PollProgressComponent
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
|
|||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
|
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||||
@ -176,7 +177,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
private pdfService: AssignmentPdfExportService,
|
private pdfService: AssignmentPdfExportService,
|
||||||
private mediafileRepo: MediafileRepositoryService,
|
private mediafileRepo: MediafileRepositoryService,
|
||||||
private pollDialog: AssignmentPollDialogService
|
private pollDialog: AssignmentPollDialogService,
|
||||||
|
private assignmentPollService: AssignmentPollService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
@ -306,11 +308,15 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
* Creates a new Poll
|
* Creates a new Poll
|
||||||
*/
|
*/
|
||||||
public openDialog(): void {
|
public openDialog(): void {
|
||||||
this.pollDialog.openDialog({
|
// TODO: That is not really a ViewObject
|
||||||
|
const dialogData = {
|
||||||
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.pollDialog.openDialog(dialogData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
|
@mixin os-assignment-poll-detail-style($theme) {
|
||||||
|
$background: map-get($theme, background);
|
||||||
|
|
||||||
|
.assignment-result-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid mat-color($background, focused-button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,46 +21,51 @@
|
|||||||
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
||||||
|
|
||||||
<div *ngIf="poll.stateHasVotes">
|
<div *ngIf="poll.stateHasVotes">
|
||||||
<h2 translate>Result</h2>
|
<div [class]="chartType === 'horizontalBar' ? 'result-wrapper-bar-chart' : 'result-wrapper-pie-chart'">
|
||||||
|
|
||||||
<div class="result-wrapper">
|
|
||||||
<!-- Result Table -->
|
<!-- Result Table -->
|
||||||
<mat-table class="result-table" [dataSource]="poll.tableData">
|
<table class="assignment-result-table">
|
||||||
<ng-container matColumnDef="user" sticky>
|
<tbody>
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
|
<tr>
|
||||||
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
|
<th translate>Candidates</th>
|
||||||
</ng-container>
|
<th translate>Votes</th>
|
||||||
<div *ngIf="!isVotedPoll">
|
</tr>
|
||||||
<ng-container matColumnDef="yes">
|
<tr *ngFor="let row of poll.tableData">
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Yes' | translate }}</mat-header-cell>
|
<td>
|
||||||
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
|
<span>
|
||||||
</ng-container>
|
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||||
|
</span>
|
||||||
|
<span class="user-subtitle" *ngIf="row.votingOptionSubtitle">
|
||||||
|
<br />
|
||||||
|
{{ row.votingOptionSubtitle }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div *ngFor="let vote of row.value">
|
||||||
|
<div class="single-result" *ngIf="voteFitsMethod(vote)">
|
||||||
|
<os-icon-container *ngIf="vote.icon" [icon]="vote.icon">
|
||||||
|
{{ vote.vote | pollKeyVerbose | translate }}
|
||||||
|
</os-icon-container>
|
||||||
|
<span *ngIf="!vote.icon">
|
||||||
|
{{ vote.vote | pollKeyVerbose | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<ng-container matColumnDef="no">
|
<span>
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'No' | translate }}</mat-header-cell>
|
{{ vote.amount | parsePollNumber }}
|
||||||
<mat-cell *matCellDef="let row">{{ row.no }}</mat-cell>
|
<span *ngIf="vote.showPercent">
|
||||||
</ng-container>
|
{{ vote.amount | pollPercentBase: poll }}
|
||||||
|
</span>
|
||||||
<ng-container matColumnDef="abstain">
|
</span>
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Abstain' | translate }}</mat-header-cell>
|
</div>
|
||||||
<mat-cell *matCellDef="let row">{{ row.abstain }}</mat-cell>
|
</div>
|
||||||
</ng-container>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
|
</tbody>
|
||||||
<div *ngIf="isVotedPoll">
|
</table>
|
||||||
<ng-container matColumnDef="votes">
|
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Votes' | translate }}</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="columnDefinitionOverview"></mat-header-row>
|
|
||||||
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
|
|
||||||
</mat-table>
|
|
||||||
|
|
||||||
<!-- Result Chart -->
|
<!-- Result Chart -->
|
||||||
<os-charts
|
<os-charts
|
||||||
class="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"
|
||||||
@ -105,7 +110,8 @@
|
|||||||
[ngClass]="voteOptionStyle[vote.votes[option.user_id].value].css"
|
[ngClass]="voteOptionStyle[vote.votes[option.user_id].value].css"
|
||||||
class="vote-field"
|
class="vote-field"
|
||||||
>
|
>
|
||||||
<mat-icon> {{ voteOptionStyle[vote.votes[option.user_id].value].icon }}</mat-icon> {{ vote.votes[option.user_id].valueVerbose | translate }}
|
<mat-icon> {{ voteOptionStyle[vote.votes[option.user_id].value].icon }}</mat-icon>
|
||||||
|
{{ vote.votes[option.user_id].valueVerbose | translate }}
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -123,17 +129,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Meta Infos -->
|
<!-- Meta Infos -->
|
||||||
<div class="poll-content small">
|
<div class="assignment-poll-meta">
|
||||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
{{ 'Groups' | translate }}:
|
{{ 'Groups' | translate }}:
|
||||||
|
|
||||||
<span *ngFor="let group of poll.groups; let i = index">
|
<span *ngFor="let group of poll.groups; let i = index">
|
||||||
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</small>
|
||||||
|
|
||||||
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
<small *ngIf="poll.onehundred_percent_base">
|
||||||
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -141,7 +148,7 @@
|
|||||||
<!-- More Menu -->
|
<!-- More Menu -->
|
||||||
<mat-menu #pollDetailMenu="matMenu">
|
<mat-menu #pollDetailMenu="matMenu">
|
||||||
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||||
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="openDialog()">
|
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="openDialog(poll)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Edit</span>
|
<span translate>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
@import '~assets/styles/variables.scss';
|
@import '~assets/styles/variables.scss';
|
||||||
@import '~assets/styles/poll-colors.scss';
|
@import '~assets/styles/poll-colors.scss';
|
||||||
|
|
||||||
.result-wrapper {
|
%assignment-result-wrapper {
|
||||||
|
margin-top: 2em;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-wrapper-bar-chart {
|
||||||
|
@extend %assignment-result-wrapper;
|
||||||
|
grid-template-areas:
|
||||||
|
'results'
|
||||||
|
'chart'
|
||||||
|
'names';
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-wrapper-pie-chart {
|
||||||
|
@extend %assignment-result-wrapper;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'chart'
|
'chart'
|
||||||
'results'
|
'results'
|
||||||
@ -11,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include desktop {
|
@include desktop {
|
||||||
.result-wrapper {
|
.result-wrapper-pie-chart {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'results chart'
|
'results chart'
|
||||||
'names names';
|
'names names';
|
||||||
@ -19,13 +32,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-table {
|
.assignment-result-table {
|
||||||
grid-area: results;
|
grid-area: results;
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-result {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-chart {
|
.assignment-result-chart {
|
||||||
grid-area: chart;
|
grid-area: chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.named-result-table {
|
.named-result-table {
|
||||||
@ -36,27 +70,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-content {
|
.assignment-poll-meta {
|
||||||
|
display: grid;
|
||||||
|
text-align: right;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
|
||||||
&.flex {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.mat-table {
|
|
||||||
flex: 2;
|
|
||||||
.mat-column-votes {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.chart-inner-wrapper {
|
|
||||||
flex: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-votes-table {
|
.single-votes-table {
|
||||||
|
display: block;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
|
|||||||
import { ChartType } from 'app/shared/components/charts/charts.component';
|
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||||
|
import { VotingResult } 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';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
@ -43,13 +44,6 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
return this.poll.pollmethod === AssignmentPollMethods.Votes;
|
return this.poll.pollmethod === AssignmentPollMethods.Votes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get columnDefinitionOverview(): string[] {
|
|
||||||
const columns = this.isVotedPoll ? ['user', 'votes'] : ['user', 'yes', 'no'];
|
|
||||||
if (this.poll.pollmethod === AssignmentPollMethods.YNA) {
|
|
||||||
columns.splice(3, 0, 'abstain');
|
|
||||||
}
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
private _chartType: ChartType = 'horizontalBar';
|
private _chartType: ChartType = 'horizontalBar';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -130,9 +124,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setVotesData(Object.values(votes));
|
this.setVotesData(Object.values(votes));
|
||||||
|
|
||||||
this.candidatesLabels = this.pollService.getChartLabels(this.poll);
|
this.candidatesLabels = this.pollService.getChartLabels(this.poll);
|
||||||
|
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,4 +149,17 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
protected hasPerms(): boolean {
|
protected hasPerms(): boolean {
|
||||||
return this.operator.hasPerms('assignments.can_manage');
|
return this.operator.hasPerms('assignments.can_manage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public voteFitsMethod(result: VotingResult): boolean {
|
||||||
|
if (this.poll.pollmethod === AssignmentPollMethods.Votes) {
|
||||||
|
if (result.vote === 'abstain' || result.vote === 'no') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (this.poll.pollmethod === AssignmentPollMethods.YN) {
|
||||||
|
if (result.vote === 'abstain') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<os-poll-form [data]="pollData" [pollMethods]="assignmentPollMethods" #pollForm></os-poll-form>
|
<os-poll-form [data]="pollData" [pollMethods]="assignmentPollMethods" #pollForm></os-poll-form>
|
||||||
|
|
||||||
|
<!-- Analog voting -->
|
||||||
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
||||||
<!-- Candidate values -->
|
|
||||||
<form [formGroup]="dialogVoteForm">
|
<form [formGroup]="dialogVoteForm">
|
||||||
|
<!-- Candidates -->
|
||||||
<div formGroupName="options">
|
<div formGroupName="options">
|
||||||
<div *ngFor="let option of options" class="votes-grid">
|
<div *ngFor="let option of options" class="votes-grid">
|
||||||
<div>
|
<div>
|
||||||
@ -33,36 +35,30 @@
|
|||||||
></os-check-input>
|
></os-check-input>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
|
<!-- Publish Check -->
|
||||||
<div class="spacer-top-20">
|
<div class="spacer-top-20">
|
||||||
<mat-checkbox
|
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
|
||||||
[(ngModel)]="publishImmediately"
|
|
||||||
(change)="publishStateChanged($event.checked)"
|
|
||||||
>
|
|
||||||
<span translate>Publish immediately</span>
|
<span translate>Publish immediately</span>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<mat-error *ngIf="!dialogVoteForm.valid" translate>
|
<mat-error *ngIf="!dialogVoteForm.valid" translate>
|
||||||
If you want to publish after creating, you have to fill at least one of the fields.
|
If you want to publish after creating, you have to fill at least one of the fields.
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</div>
|
</div>
|
||||||
<!-- Summary values -->
|
|
||||||
<!-- <div *ngFor="let sumValue of sumValues" class="sum-value">
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
matInput
|
|
||||||
[value]="getSumValue(sumValue)"
|
|
||||||
(change)="setSumValue(sumValue, $event.target.value)"
|
|
||||||
/>
|
|
||||||
<mat-label>{{ pollService.getLabel(sumValue) | translate }}</mat-label>
|
|
||||||
</mat-form-field>
|
|
||||||
</div> -->
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
|
<!-- Actions -->
|
||||||
<div mat-dialog-actions>
|
<div mat-dialog-actions>
|
||||||
<button mat-button (click)="submitPoll()" [disabled]="!pollForm.contentForm || pollForm.contentForm.invalid || dialogVoteForm.invalid">
|
<!-- Save Button -->
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
(click)="submitPoll()"
|
||||||
|
[disabled]="!pollForm.contentForm || pollForm.contentForm.invalid || dialogVoteForm.invalid"
|
||||||
|
>
|
||||||
<span translate>Save</span>
|
<span translate>Save</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Cancel Button -->
|
||||||
<button mat-button [mat-dialog-close]="false">
|
<button mat-button [mat-dialog-close]="false">
|
||||||
<span translate>Cancel</span>
|
<span translate>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,9 +11,7 @@ import { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/mod
|
|||||||
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.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 { CalculablePollKey, PollVoteValue } from 'app/site/polls/services/poll.service';
|
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
|
||||||
type OptionsObject = { user_id: number; user: ViewUser }[];
|
type OptionsObject = { user_id: number; user: ViewUser }[];
|
||||||
@ -26,7 +24,7 @@ type OptionsObject = { user_id: number; user: ViewUser }[];
|
|||||||
templateUrl: './assignment-poll-dialog.component.html',
|
templateUrl: './assignment-poll-dialog.component.html',
|
||||||
styleUrls: ['./assignment-poll-dialog.component.scss']
|
styleUrls: ['./assignment-poll-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class AssignmentPollDialogComponent extends BasePollDialogComponent implements OnInit {
|
export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewAssignmentPoll> implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The summary values that will have fields in the dialog
|
* The summary values that will have fields in the dialog
|
||||||
*/
|
*/
|
||||||
@ -41,7 +39,7 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent imple
|
|||||||
public specialValues: [number, string][];
|
public specialValues: [number, string][];
|
||||||
|
|
||||||
@ViewChild('pollForm', { static: true })
|
@ViewChild('pollForm', { static: true })
|
||||||
protected pollForm: PollFormComponent;
|
protected pollForm: PollFormComponent<ViewAssignmentPoll>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vote entries for each option in this component. Is empty if method
|
* vote entries for each option in this component. Is empty if method
|
||||||
@ -65,13 +63,14 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent imple
|
|||||||
title: Title,
|
title: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
matSnackbar: MatSnackBar,
|
matSnackbar: MatSnackBar,
|
||||||
public dialogRef: MatDialogRef<BasePollDialogComponent>,
|
public dialogRef: MatDialogRef<BasePollDialogComponent<ViewAssignmentPoll>>,
|
||||||
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewAssignmentPoll>
|
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewAssignmentPoll>
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar, dialogRef);
|
super(title, translate, matSnackbar, dialogRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
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
|
// 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.options = this.pollData.options
|
||||||
? this.pollData.options
|
? this.pollData.options
|
||||||
@ -154,85 +153,6 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent imple
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates candidates input (every candidate has their options filled in),
|
|
||||||
* submits and closes the dialog if successful, else displays an error popup.
|
|
||||||
* TODO better validation
|
|
||||||
*/
|
|
||||||
public submit(): void {
|
|
||||||
/*const error = this.data.options.find(dataoption => {
|
|
||||||
this.optionPollKeys.some(key => {
|
|
||||||
const keyValue = dataoption.votes.find(o => o.value === key);
|
|
||||||
return !keyValue || keyValue.weight === undefined;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
this.matSnackBar.open(
|
|
||||||
this.translate.instant('Please fill in the values for each candidate'),
|
|
||||||
this.translate.instant('OK'),
|
|
||||||
{
|
|
||||||
duration: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.dialogRef.close(this.data);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: currently unused
|
|
||||||
*
|
|
||||||
* @param key poll option to be labeled
|
|
||||||
* @returns a label for a poll option
|
|
||||||
*/
|
|
||||||
public getLabel(key: CalculablePollKey): string {
|
|
||||||
// return this.pollService.getLabel(key);
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a vote value
|
|
||||||
*
|
|
||||||
* @param value the value to update
|
|
||||||
* @param candidate the candidate for whom to update the value
|
|
||||||
* @param newData the new value
|
|
||||||
*/
|
|
||||||
public setValue(value: PollVoteValue, candidate: ViewAssignmentOption, newData: string): void {
|
|
||||||
/*const vote = candidate.votes.find(v => v.value === value);
|
|
||||||
if (vote) {
|
|
||||||
vote.weight = parseFloat(newData);
|
|
||||||
} else {
|
|
||||||
candidate.votes.push({
|
|
||||||
value: value,
|
|
||||||
weight: parseFloat(newData)
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current value for a voting option
|
|
||||||
*
|
|
||||||
* @param value the vote value (e.g. 'Abstain')
|
|
||||||
* @param candidate the pollOption
|
|
||||||
* @returns the currently entered number or undefined if no number has been set
|
|
||||||
*/
|
|
||||||
public getValue(value: PollVoteValue, candidate: ViewAssignmentOption): number | undefined {
|
|
||||||
/*const val = candidate.votes.find(v => v.value === value);
|
|
||||||
return val ? val.weight : undefined;*/
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a per-poll value
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
* @returns integer or undefined
|
|
||||||
*/
|
|
||||||
public getSumValue(value: any /*SummaryPollKey*/): number | undefined {
|
|
||||||
// return this.data[value] || undefined;
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a per-poll value
|
* Sets a per-poll value
|
||||||
*
|
*
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
<!-- Poll progress bar -->
|
|
||||||
<div *osPerms="'assignments.can_manage_polls'; and: poll.isStarted">
|
|
||||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||||
<!-- TODO: Someone should make this pretty -->
|
<!-- TODO: Someone should make this pretty -->
|
||||||
|
@ -50,7 +50,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.defineVoteOptions();
|
if (this.poll) {
|
||||||
|
this.defineVoteOptions();
|
||||||
|
}
|
||||||
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
||||||
this.votes = votes;
|
this.votes = votes;
|
||||||
@ -91,8 +94,6 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updateVotes(): void {
|
protected updateVotes(): void {
|
||||||
console.log('currentVotes: ', this.currentVotes);
|
|
||||||
|
|
||||||
if (this.user && this.votes && this.poll) {
|
if (this.user && this.votes && this.poll) {
|
||||||
const filtered = this.votes.filter(
|
const filtered = this.votes.filter(
|
||||||
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
|
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
|
||||||
|
@ -1,64 +1,75 @@
|
|||||||
<mat-card class="os-card" *ngIf="poll && showPoll()">
|
<mat-card class="os-card" *ngIf="poll && showPoll()">
|
||||||
<div class="assignment-poll-wrapper">
|
<div class="assignment-poll-wrapper">
|
||||||
<div class="assignment-poll-title-header">
|
<div>
|
||||||
|
<!-- Title -->
|
||||||
<mat-card-title>
|
<mat-card-title>
|
||||||
<a routerLink="/assignments/polls/{{ poll.id }}">
|
<a routerLink="/assignments/polls/{{ poll.id }}">
|
||||||
{{ poll.title }}
|
{{ poll.title }}
|
||||||
</a>
|
</a>
|
||||||
</mat-card-title>
|
</mat-card-title>
|
||||||
<div class="poll-properties">
|
|
||||||
<mat-chip
|
<!-- Type and State -->
|
||||||
*osPerms="'assignments.can_manage_polls'"
|
<div>
|
||||||
class="poll-state active"
|
<span *ngIf="poll.type !== 'analog'"> {{ poll.typeVerbose | translate }} · </span>
|
||||||
[disableRipple]="true"
|
<span>
|
||||||
[matMenuTriggerFor]="triggerMenu"
|
|
||||||
[class]="poll.stateVerbose.toLowerCase()"
|
|
||||||
[ngClass]="{ disabled: !poll.getNextStates() }"
|
|
||||||
>
|
|
||||||
{{ poll.stateVerbose | translate }}
|
{{ poll.stateVerbose | translate }}
|
||||||
</mat-chip>
|
|
||||||
<span *ngIf="poll.type !== 'analog'">
|
|
||||||
{{ poll.typeVerbose | translate }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
<div class="poll-menu">
|
<div class="poll-menu">
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
*osPerms="'assignments.motions.can_manage_polls';or: 'core.can_manage_projector'"
|
*osPerms="'assignments.motions.can_manage_polls'; or: 'core.can_manage_projector'"
|
||||||
[matMenuTriggerFor]="pollItemMenu"
|
[matMenuTriggerFor]="pollItemMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<mat-icon>more_horiz</mat-icon>
|
<mat-icon>more_horiz</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Change state button -->
|
||||||
|
<div *osPerms="'assignments.can_manage_polls'">
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
*ngIf="!poll.isPublished"
|
||||||
|
[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 }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasVotes">
|
<div *ngIf="hasVotes">
|
||||||
<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"
|
||||||
></os-charts>
|
></os-charts>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Poll progress bar -->
|
||||||
|
<div *osPerms="'assignments.can_manage_polls'; and: poll && poll.isStarted">
|
||||||
|
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||||
|
</div>
|
||||||
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
||||||
<div class="poll-detail-button-wrapper">
|
<div class="poll-detail-button-wrapper">
|
||||||
<a mat-button routerLink="/assignments/polls/{{ poll.id }}">
|
<a mat-icon-button routerLink="/assignments/polls/{{ poll.id }}" matTooltip="{{ 'More' | translate }}">
|
||||||
{{ 'More' | translate }}
|
<mat-icon class="small-icon">
|
||||||
|
visibility
|
||||||
|
</mat-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-menu #triggerMenu="matMenu">
|
|
||||||
<ng-container *ngIf="poll">
|
|
||||||
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.getNextStates() | keyvalue">
|
|
||||||
<span translate>{{ state.key }}</span>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</mat-menu>
|
|
||||||
|
|
||||||
<mat-menu #pollItemMenu="matMenu">
|
<mat-menu #pollItemMenu="matMenu">
|
||||||
<div *osPerms="'assignments.can_manage'">
|
<div *osPerms="'assignments.can_manage'">
|
||||||
<button mat-menu-item (click)="openDialog()">
|
<button mat-menu-item (click)="openDialog()">
|
||||||
@ -75,6 +86,11 @@
|
|||||||
<span translate>Ballot paper</span>
|
<span translate>Ballot paper</span>
|
||||||
</button>
|
</button>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
<!-- Reset Button -->
|
||||||
|
<button mat-menu-item (click)="resetState()">
|
||||||
|
<mat-icon color="warn">replay</mat-icon>
|
||||||
|
<span translate>Reset state</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
|
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
@import '~assets/styles/poll-colors.scss';
|
||||||
|
|
||||||
.assignment-poll-wrapper {
|
.assignment-poll-wrapper {
|
||||||
@import '~assets/styles/poll-common-styles.scss';
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 15px;
|
margin: 0 15px;
|
||||||
|
|
||||||
@ -9,37 +10,6 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-properties {
|
|
||||||
margin: 4px 0;
|
|
||||||
|
|
||||||
.mat-chip {
|
|
||||||
margin: 0 4px;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-state {
|
|
||||||
&.created {
|
|
||||||
background-color: #2196f3;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&.started {
|
|
||||||
background-color: #4caf50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&.finished {
|
|
||||||
background-color: #ff5252;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&.published {
|
|
||||||
background-color: #ffd800;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-detail-button-wrapper {
|
.poll-detail-button-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
@ -47,4 +17,23 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.start-poll-button {
|
||||||
|
color: green !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-poll-button {
|
||||||
|
color: $poll-stop-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-poll-button {
|
||||||
|
color: $poll-publish-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doughnut-chart {
|
||||||
|
display: block;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||||
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||||
import { AssignmentPollComponent } from './assignment-poll.component';
|
import { AssignmentPollComponent } from './assignment-poll.component';
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ describe('AssignmentPollComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [AssignmentPollComponent, AssignmentPollVoteComponent],
|
imports: [E2EImportsModule],
|
||||||
imports: [E2EImportsModule]
|
declarations: [AssignmentPollComponent, AssignmentPollVoteComponent, PollProgressComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ import { BasePollComponent } from 'app/site/polls/components/base-poll.component
|
|||||||
import { PollService } from 'app/site/polls/services/poll.service';
|
import { PollService } from 'app/site/polls/services/poll.service';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,9 +51,6 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
|||||||
public descriptionForm: FormGroup;
|
public descriptionForm: FormGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* permission checks.
|
|
||||||
* TODO stub
|
|
||||||
*
|
|
||||||
* @returns true if the user is permitted to do operations
|
* @returns true if the user is permitted to do operations
|
||||||
*/
|
*/
|
||||||
public get canManage(): boolean {
|
public get canManage(): boolean {
|
||||||
@ -93,9 +89,6 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
/*this.majorityChoice =
|
|
||||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
|
||||||
null;*/
|
|
||||||
this.descriptionForm = this.formBuilder.group({
|
this.descriptionForm = this.formBuilder.group({
|
||||||
description: this.poll ? this.poll.description : ''
|
description: this.poll ? this.poll.description : ''
|
||||||
});
|
});
|
||||||
@ -115,70 +108,4 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
|||||||
(this.poll.type !== 'analog' && this.poll.isStarted)
|
(this.poll.type !== 'analog' && this.poll.isStarted)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the candidate has reached the majority needed to pass
|
|
||||||
* the quorum
|
|
||||||
*
|
|
||||||
* @param option
|
|
||||||
* @returns true if the quorum is successfully met
|
|
||||||
*/
|
|
||||||
public quorumReached(option: ViewAssignmentOption): boolean {
|
|
||||||
/*const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes';
|
|
||||||
const amount = option.votes.find(v => v.value === yesValue).weight;
|
|
||||||
const yesQuorum = this.pollService.yesQuorum(
|
|
||||||
this.majorityChoice,
|
|
||||||
this.pollService.calculationDataFromPoll(this.poll),
|
|
||||||
option
|
|
||||||
);
|
|
||||||
return yesQuorum && amount >= yesQuorum;*/
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark/unmark an option as elected
|
|
||||||
*
|
|
||||||
* @param option
|
|
||||||
*/
|
|
||||||
public toggleElected(option: ViewAssignmentOption): void {
|
|
||||||
/*if (!this.operator.hasPerms('assignments.can_manage')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO additional conditions: assignment not finished?
|
|
||||||
const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find(
|
|
||||||
user => user.user_id === option.candidate_id
|
|
||||||
);
|
|
||||||
if (viewAssignmentRelatedUser) {
|
|
||||||
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the edited poll description to the server
|
|
||||||
* TODO: Better feedback
|
|
||||||
*/
|
|
||||||
public async onEditDescriptionButton(): Promise<void> {
|
|
||||||
/*const desc: string = this.descriptionForm.get('description').value;
|
|
||||||
await this.assignmentRepo.updatePoll({ description: desc }, this.poll).catch(this.raiseError);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a tooltip string about the quorum
|
|
||||||
* @param option
|
|
||||||
* @returns a translated
|
|
||||||
*/
|
|
||||||
public getQuorumReachedString(option: ViewAssignmentOption): string {
|
|
||||||
/*const name = this.translate.instant(this.majorityChoice.display_name);
|
|
||||||
const quorum = this.pollService.yesQuorum(
|
|
||||||
this.majorityChoice,
|
|
||||||
this.pollService.calculationDataFromPoll(this.poll),
|
|
||||||
option
|
|
||||||
);
|
|
||||||
const isReached = this.quorumReached(option)
|
|
||||||
? this.translate.instant('reached')
|
|
||||||
: this.translate.instant('not reached');
|
|
||||||
return `${name} (${quorum}) ${isReached}`;*/
|
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,9 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
|
|
||||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { PollTableData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
import { PollTableData, ViewBasePoll, VotingResult } from 'app/site/polls/models/view-base-poll';
|
||||||
import { ViewAssignment } from './view-assignment';
|
import { ViewAssignment } from './view-assignment';
|
||||||
import { ViewAssignmentOption } from './view-assignment-option';
|
import { ViewAssignmentOption } from './view-assignment-option';
|
||||||
|
|
||||||
@ -35,7 +34,6 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
// TODO: update to new voting system?
|
|
||||||
return {
|
return {
|
||||||
getBasicProjectorElement: options => ({
|
getBasicProjectorElement: options => ({
|
||||||
name: AssignmentPoll.COLLECTIONSTRING,
|
name: AssignmentPoll.COLLECTIONSTRING,
|
||||||
@ -49,27 +47,36 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateTableData(): PollTableData[] {
|
public generateTableData(): PollTableData[] {
|
||||||
const data = this.options
|
const tableData: PollTableData[] = this.options.map(candidate => ({
|
||||||
.map(candidate => ({
|
votingOption: candidate.user.short_name,
|
||||||
yes: candidate.yes,
|
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
|
||||||
no: candidate.no,
|
|
||||||
abstain: candidate.abstain,
|
value: this.voteTableKeys.map(
|
||||||
user: candidate.user.full_name,
|
key =>
|
||||||
showPercent: true
|
({
|
||||||
|
vote: key.vote,
|
||||||
|
amount: candidate[key.vote],
|
||||||
|
icon: key.icon,
|
||||||
|
hide: key.hide,
|
||||||
|
showPercent: key.showPercent
|
||||||
|
} as VotingResult)
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
tableData.push(
|
||||||
|
...this.sumTableKeys.map(key => ({
|
||||||
|
votingOption: key.vote,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
amount: this[key.vote],
|
||||||
|
hide: key.hide,
|
||||||
|
showPercent: key.showPercent
|
||||||
|
} as VotingResult
|
||||||
|
]
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.yes - a.yes);
|
);
|
||||||
|
|
||||||
return data;
|
return tableData;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override from base poll to skip started state in analog poll type
|
|
||||||
*/
|
|
||||||
public getNextStates(): { [key: number]: string } {
|
|
||||||
if (this.poll.type === 'analog' && this.state === PollState.Created) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return super.getNextStates();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-wi
|
|||||||
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
|
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
|
import { HasViewPolls } from 'app/site/polls/models/has-view-polls';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { ViewAssignmentPoll } from './view-assignment-poll';
|
import { ViewAssignmentPoll } from './view-assignment-poll';
|
||||||
@ -102,9 +103,8 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface IAssignmentRelations {
|
interface IAssignmentRelations extends HasViewPolls<ViewAssignmentPoll> {
|
||||||
assignment_related_users: ViewAssignmentRelatedUser[];
|
assignment_related_users: ViewAssignmentRelatedUser[];
|
||||||
polls: ViewAssignmentPoll[];
|
|
||||||
tags?: ViewTag[];
|
tags?: ViewTag[];
|
||||||
attachments?: ViewMediafile[];
|
attachments?: ViewMediafile[];
|
||||||
}
|
}
|
||||||
|
@ -185,11 +185,11 @@ export class AssignmentPdfService {
|
|||||||
const tableData = poll.generateTableData();
|
const tableData = poll.generateTableData();
|
||||||
|
|
||||||
for (const pollResult of tableData) {
|
for (const pollResult of tableData) {
|
||||||
|
const voteOption = this.translate.instant(this.pollKeyVerbose.transform(pollResult.votingOption));
|
||||||
const resultLine = this.getPollResult(pollResult, poll);
|
const resultLine = this.getPollResult(pollResult, poll);
|
||||||
|
|
||||||
const tableLine = [
|
const tableLine = [
|
||||||
{
|
{
|
||||||
text: pollResult.user
|
text: voteOption
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: resultLine
|
text: resultLine
|
||||||
@ -217,11 +217,13 @@ export class AssignmentPdfService {
|
|||||||
* Converts pollData to a printable string representation
|
* Converts pollData to a printable string representation
|
||||||
*/
|
*/
|
||||||
private getPollResult(votingResult: PollTableData, poll: ViewAssignmentPoll): string {
|
private getPollResult(votingResult: PollTableData, poll: ViewAssignmentPoll): string {
|
||||||
const resultList = poll.pollmethodFields.map(field => {
|
const resultList = votingResult.value.map(singleResult => {
|
||||||
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(field));
|
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(singleResult.vote));
|
||||||
const resultValue = this.parsePollNumber.transform(votingResult[field]);
|
const resultValue = this.parsePollNumber.transform(singleResult.amount);
|
||||||
const resultInPercent = this.pollPercentBase.transform(votingResult[field], poll);
|
const resultInPercent = this.pollPercentBase.transform(singleResult.amount, poll);
|
||||||
return `${votingKey}: ${resultValue} ${resultInPercent ? resultInPercent : ''}`;
|
return `${votingKey}${!!votingKey ? ': ' : ''}${resultValue} ${
|
||||||
|
singleResult.showPercent && resultInPercent ? resultInPercent : ''
|
||||||
|
}`;
|
||||||
});
|
});
|
||||||
return resultList.join('\n');
|
return resultList.join('\n');
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { MatDialog } from '@angular/material';
|
|||||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||||
import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
|
import { AssignmentPollDialogComponent } from 'app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component';
|
||||||
import { AssignmentPollService } from './assignment-poll.service';
|
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,7 +15,7 @@ import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
|||||||
export class AssignmentPollDialogService extends BasePollDialogService<ViewAssignmentPoll> {
|
export class AssignmentPollDialogService extends BasePollDialogService<ViewAssignmentPoll> {
|
||||||
protected dialogComponent = AssignmentPollDialogComponent;
|
protected dialogComponent = AssignmentPollDialogComponent;
|
||||||
|
|
||||||
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService, service: AssignmentPollService) {
|
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService) {
|
||||||
super(dialog, mapper, service);
|
super(dialog, mapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,6 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
* @param title The identifier of the motion
|
* @param title The identifier of the motion
|
||||||
* @param subtitle The actual motion title
|
* @param subtitle The actual motion title
|
||||||
*/
|
*/
|
||||||
// TODO: typing of result
|
|
||||||
protected createBallot(data: AbstractPollData): object {
|
protected createBallot(data: AbstractPollData): object {
|
||||||
return {
|
return {
|
||||||
columns: [
|
columns: [
|
||||||
@ -137,7 +136,6 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: typing of result
|
|
||||||
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
||||||
const candidates = poll.options.sort((a, b) => {
|
const candidates = poll.options.sort((a, b) => {
|
||||||
return a.weight - b.weight;
|
return a.weight - b.weight;
|
||||||
@ -147,15 +145,23 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
? this.createBallotOption(cand.user.full_name)
|
? this.createBallotOption(cand.user.full_name)
|
||||||
: this.createYNBallotEntry(cand.user.full_name, poll.pollmethod);
|
: this.createYNBallotEntry(cand.user.full_name, poll.pollmethod);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (poll.pollmethod === 'votes') {
|
if (poll.pollmethod === 'votes') {
|
||||||
const noEntry = this.createBallotOption(this.translate.instant('No'));
|
if (poll.global_no) {
|
||||||
noEntry.margin[1] = 25;
|
const noEntry = this.createBallotOption(this.translate.instant('No'));
|
||||||
resultObject.push(noEntry);
|
noEntry.margin[1] = 25;
|
||||||
|
resultObject.push(noEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll.global_abstain) {
|
||||||
|
const abstainEntry = this.createBallotOption(this.translate.instant('Abstain'));
|
||||||
|
abstainEntry.margin[1] = 25;
|
||||||
|
resultObject.push(abstainEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return resultObject;
|
return resultObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: typing of result
|
|
||||||
private createYNBallotEntry(option: string, method: AssignmentPollMethods): object {
|
private createYNBallotEntry(option: string, method: AssignmentPollMethods): object {
|
||||||
const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
||||||
const columnstack = choices.map(choice => {
|
const columnstack = choices.map(choice => {
|
||||||
@ -182,7 +188,6 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
* @param poll
|
* @param poll
|
||||||
* @returns pdfMake definitions
|
* @returns pdfMake definitions
|
||||||
*/
|
*/
|
||||||
// TODO: typing of result
|
|
||||||
private createPollHint(poll: ViewAssignmentPoll): object {
|
private createPollHint(poll: ViewAssignmentPoll): object {
|
||||||
return {
|
return {
|
||||||
text: poll.description || '',
|
text: poll.description || '',
|
||||||
|
@ -5,11 +5,9 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { Collection } from 'app/shared/models/base/collection';
|
|
||||||
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
||||||
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -25,6 +23,10 @@ export class AssignmentPollService extends PollService {
|
|||||||
*/
|
*/
|
||||||
public defaultMajorityMethod: MajorityMethod;
|
public defaultMajorityMethod: MajorityMethod;
|
||||||
|
|
||||||
|
public defaultGroupIds: number[];
|
||||||
|
|
||||||
|
public defaultPollMethod: AssignmentPollMethods;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Subscribes to the configuration values needed
|
* Constructor. Subscribes to the configuration values needed
|
||||||
* @param config ConfigService
|
* @param config ConfigService
|
||||||
@ -42,16 +44,21 @@ export class AssignmentPollService extends PollService {
|
|||||||
config
|
config
|
||||||
.get<MajorityMethod>('motion_poll_default_majority_method')
|
.get<MajorityMethod>('motion_poll_default_majority_method')
|
||||||
.subscribe(method => (this.defaultMajorityMethod = method));
|
.subscribe(method => (this.defaultMajorityMethod = method));
|
||||||
|
config.get<number[]>(AssignmentPoll.defaultGroupsConfig).subscribe(ids => (this.defaultGroupIds = ids));
|
||||||
|
config
|
||||||
|
.get<AssignmentPollMethods>(AssignmentPoll.defaultPollMethodConfig)
|
||||||
|
.subscribe(method => (this.defaultPollMethod = method));
|
||||||
}
|
}
|
||||||
|
|
||||||
public fillDefaultPollData(poll: Partial<ViewAssignmentPoll> & Collection): void {
|
public getDefaultPollData(): AssignmentPoll {
|
||||||
super.fillDefaultPollData(poll);
|
const poll = new AssignmentPoll(super.getDefaultPollData());
|
||||||
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
|
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
|
||||||
.length;
|
.length;
|
||||||
|
|
||||||
poll.title = !length ? this.translate.instant('Ballot') : `${this.translate.instant('Ballot')} (${length + 1})`;
|
poll.title = !length ? this.translate.instant('Ballot') : `${this.translate.instant('Ballot')} (${length + 1})`;
|
||||||
poll.pollmethod = AssignmentPollMethods.YN;
|
poll.pollmethod = this.defaultPollMethod;
|
||||||
poll.assignment_id = poll.assignment_id;
|
|
||||||
|
return poll;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sumOptionsYN(poll: PollData): number {
|
private sumOptionsYN(poll: PollData): number {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
import { PollTableData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
import { PollTableData, ViewBasePoll, VotingResult } from 'app/site/polls/models/view-base-poll';
|
||||||
import { ViewMotion } from './view-motion';
|
import { ViewMotion } from './view-motion';
|
||||||
|
|
||||||
export interface MotionPollTitleInformation {
|
export interface MotionPollTitleInformation {
|
||||||
@ -15,58 +14,12 @@ export const MotionPollMethodsVerbose = {
|
|||||||
YNA: 'Yes/No/Abstain'
|
YNA: 'Yes/No/Abstain'
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TableKey {
|
|
||||||
vote: string;
|
|
||||||
icon?: string;
|
|
||||||
canHide: boolean;
|
|
||||||
showPercent: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
|
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
|
||||||
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
||||||
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
||||||
|
|
||||||
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
|
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
|
||||||
|
|
||||||
private tableKeys: TableKey[] = [
|
|
||||||
{
|
|
||||||
vote: 'yes',
|
|
||||||
icon: 'thumb_up',
|
|
||||||
canHide: false,
|
|
||||||
showPercent: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
vote: 'no',
|
|
||||||
icon: 'thumb_down',
|
|
||||||
canHide: false,
|
|
||||||
showPercent: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
vote: 'abstain',
|
|
||||||
icon: 'trip_origin',
|
|
||||||
canHide: false,
|
|
||||||
showPercent: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
private voteKeys: TableKey[] = [
|
|
||||||
{
|
|
||||||
vote: 'votesvalid',
|
|
||||||
canHide: true,
|
|
||||||
showPercent: this.poll.isPercentBaseValidOrCast
|
|
||||||
},
|
|
||||||
{
|
|
||||||
vote: 'votesinvalid',
|
|
||||||
canHide: true,
|
|
||||||
showPercent: this.poll.isPercentBaseValidOrCast
|
|
||||||
},
|
|
||||||
{
|
|
||||||
vote: 'votescast',
|
|
||||||
canHide: true,
|
|
||||||
showPercent: this.poll.isPercentBaseValidOrCast
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
public get result(): ViewMotionOption {
|
public get result(): ViewMotionOption {
|
||||||
return this.options[0];
|
return this.options[0];
|
||||||
}
|
}
|
||||||
@ -80,22 +33,30 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateTableData(): PollTableData[] {
|
public generateTableData(): PollTableData[] {
|
||||||
let tableData = this.options.flatMap(vote =>
|
let tableData: PollTableData[] = this.options.flatMap(vote =>
|
||||||
this.tableKeys.map(key => ({
|
this.voteTableKeys.map(key => this.createTableDataEntry(key, vote))
|
||||||
key: key.vote,
|
|
||||||
value: vote[key.vote],
|
|
||||||
canHide: key.canHide,
|
|
||||||
icon: key.icon,
|
|
||||||
showPercent: key.showPercent
|
|
||||||
}))
|
|
||||||
);
|
);
|
||||||
tableData.push(
|
tableData.push(...this.sumTableKeys.map(key => this.createTableDataEntry(key)));
|
||||||
...this.voteKeys.map(key => ({ key: key.vote, value: this[key.vote], showPercent: key.showPercent }))
|
|
||||||
);
|
tableData = tableData.filter(localeTableData => !localeTableData.value.some(result => result.hide));
|
||||||
tableData = tableData.filter(entry => entry.canHide === false || entry.value || entry.value !== -2);
|
|
||||||
return tableData;
|
return tableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createTableDataEntry(result: VotingResult, vote?: ViewMotionOption): PollTableData {
|
||||||
|
return {
|
||||||
|
votingOption: result.vote,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
amount: vote ? vote[result.vote] : this[result.vote],
|
||||||
|
hide: result.hide,
|
||||||
|
icon: result.icon,
|
||||||
|
showPercent: result.showPercent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
return {
|
return {
|
||||||
getBasicProjectorElement: options => ({
|
getBasicProjectorElement: options => ({
|
||||||
@ -116,16 +77,6 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
|||||||
public anySpecialVotes(): boolean {
|
public anySpecialVotes(): boolean {
|
||||||
return this.result.yes < 0 || this.result.no < 0 || this.result.abstain < 0;
|
return this.result.yes < 0 || this.result.no < 0 || this.result.abstain < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Override from base poll to skip started state in analog poll type
|
|
||||||
*/
|
|
||||||
public getNextStates(): { [key: number]: string } {
|
|
||||||
if (this.poll.type === 'analog' && this.state === PollState.Created) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return super.getNextStates();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewMotionPoll extends MotionPoll {
|
export interface ViewMotionPoll extends MotionPoll {
|
||||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||||
import { ManageSubmittersComponent } from '../manage-submitters/manage-submitters.component';
|
import { ManageSubmittersComponent } from '../manage-submitters/manage-submitters.component';
|
||||||
import { MotionCommentsComponent } from '../motion-comments/motion-comments.component';
|
import { MotionCommentsComponent } from '../motion-comments/motion-comments.component';
|
||||||
import { MotionDetailDiffComponent } from '../motion-detail-diff/motion-detail-diff.component';
|
import { MotionDetailDiffComponent } from '../motion-detail-diff/motion-detail-diff.component';
|
||||||
@ -26,7 +27,8 @@ describe('MotionDetailComponent', () => {
|
|||||||
MotionPollComponent,
|
MotionPollComponent,
|
||||||
MotionDetailOriginalChangeRecommendationsComponent,
|
MotionDetailOriginalChangeRecommendationsComponent,
|
||||||
MotionDetailDiffComponent,
|
MotionDetailDiffComponent,
|
||||||
MotionPollVoteComponent
|
MotionPollVoteComponent,
|
||||||
|
PollProgressComponent
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@ -64,6 +64,7 @@ import { LocalPermissionsService } from 'app/site/motions/services/local-permiss
|
|||||||
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.service';
|
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.service';
|
||||||
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
|
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
|
||||||
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
||||||
|
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||||
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
|
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
@ -467,7 +468,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
private motionFilterService: MotionFilterListService,
|
private motionFilterService: MotionFilterListService,
|
||||||
private amendmentFilterService: AmendmentFilterListService,
|
private amendmentFilterService: AmendmentFilterListService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private pollDialog: MotionPollDialogService
|
private pollDialog: MotionPollDialogService,
|
||||||
|
private motionPollService: MotionPollService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
@ -1625,9 +1627,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public openDialog(poll?: ViewMotionPoll): void {
|
public openDialog(): void {
|
||||||
this.pollDialog.openDialog(
|
// TODO: Could be simpler, requires a lot of refactoring
|
||||||
poll ? poll : { collectionString: ViewMotionPoll.COLLECTIONSTRING, motion_id: this.motion.id }
|
const dialogData = {
|
||||||
);
|
collectionString: ViewMotionPoll.COLLECTIONSTRING,
|
||||||
|
motion_id: this.motion.id,
|
||||||
|
motion: this.motion,
|
||||||
|
...this.motionPollService.getDefaultPollData()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pollDialog.openDialog(dialogData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,97 +18,113 @@
|
|||||||
<ng-template #viewTemplate>
|
<ng-template #viewTemplate>
|
||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
<h1>{{ poll.title }}</h1>
|
<h1>{{ poll.title }}</h1>
|
||||||
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
|
||||||
|
<div>
|
||||||
|
<!-- Subtitle -->
|
||||||
|
<span *ngIf="poll.type !== 'analog'"> {{ poll.typeVerbose | translate }} · </span>
|
||||||
|
|
||||||
|
<!-- State chip -->
|
||||||
|
<span>
|
||||||
|
{{ poll.stateVerbose }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results to show' | translate }}</div>
|
<div *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results to show' | translate }}</div>
|
||||||
|
|
||||||
<div *ngIf="poll.stateHasVotes">
|
<div *ngIf="poll.stateHasVotes">
|
||||||
<h2 translate>Result</h2>
|
|
||||||
|
|
||||||
<div class="result-wrapper" *ngIf="poll.hasVotes">
|
<div class="result-wrapper" *ngIf="poll.hasVotes">
|
||||||
<!-- Chart -->
|
|
||||||
<os-charts
|
|
||||||
class="result-chart"
|
|
||||||
*ngIf="chartDataSubject.value"
|
|
||||||
[type]="chartType"
|
|
||||||
[data]="chartDataSubject"
|
|
||||||
></os-charts>
|
|
||||||
|
|
||||||
<!-- result table -->
|
<!-- result table -->
|
||||||
<table class="result-table">
|
<table class="result-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th translate>Votes</th>
|
<th colspan="2" translate>Votes</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngFor="let row of poll.tableData">
|
<tr *ngFor="let row of poll.tableData" [class]="row.votingOption">
|
||||||
|
<!-- YNA/Valid etc -->
|
||||||
<td>
|
<td>
|
||||||
<os-icon-container *ngIf="row.icon" [icon]="row.icon">
|
<os-icon-container *ngIf="row.value[0].icon" [icon]="row.value[0].icon">
|
||||||
{{ row.key | pollKeyVerbose | translate }}
|
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||||
</os-icon-container>
|
</os-icon-container>
|
||||||
<span *ngIf="!row.icon">
|
<span *ngIf="!row.value[0].icon">
|
||||||
{{ row.key | pollKeyVerbose | translate }}
|
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Percent numbers -->
|
||||||
<td class="result-cell-definition">
|
<td class="result-cell-definition">
|
||||||
{{ row.value | parsePollNumber }}
|
<span *ngIf="row.value[0].showPercent">
|
||||||
<span *ngIf="row.showPercent">
|
{{ row.value[0].amount | pollPercentBase: poll }}
|
||||||
{{ row.value | pollPercentBase: poll }}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Voices -->
|
||||||
|
<td class="result-cell-definition">
|
||||||
|
{{ row.value[0].amount | parsePollNumber }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Named table: only show if votes are present -->
|
<!-- Chart -->
|
||||||
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
<div class="doughnut-chart">
|
||||||
<h3>{{ 'Single votes' | translate }}</h3>
|
<os-charts
|
||||||
<os-list-view-table
|
*ngIf="chartDataSubject.value"
|
||||||
[listObservable]="votesDataObservable"
|
[type]="chartType"
|
||||||
[columns]="columnDefinition"
|
[data]="chartDataSubject"
|
||||||
[filterProps]="filterProps"
|
[showLegend]="false"
|
||||||
[allowProjector]="false"
|
[hasPadding]="false"
|
||||||
[fullScreen]="false"
|
></os-charts>
|
||||||
[vScrollFixed]="60"
|
|
||||||
listStorageKey="motion-poll-vote"
|
|
||||||
[cssClasses]="{ 'single-votes-table': true }"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<div *pblNgridHeaderCellDef="'*'; col as col">
|
|
||||||
{{ col.label | translate }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div *pblNgridCellDef="'user'; row as vote">
|
|
||||||
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
|
||||||
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
|
||||||
</div>
|
|
||||||
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
|
||||||
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
|
||||||
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
|
|
||||||
</div>
|
|
||||||
<div>{{ vote.valueVerbose | translate }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No Results -->
|
|
||||||
<div *pblNgridNoDataRef class="pbl-ngrid-no-data">
|
|
||||||
{{ 'The individual votes were made anonymous.' | translate }}
|
|
||||||
</div>
|
|
||||||
</os-list-view-table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Named table: only show if votes are present -->
|
||||||
|
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
||||||
|
<h2>{{ 'Single votes' | translate }}</h2>
|
||||||
|
<os-list-view-table
|
||||||
|
[listObservable]="votesDataObservable"
|
||||||
|
[columns]="columnDefinition"
|
||||||
|
[filterProps]="filterProps"
|
||||||
|
[allowProjector]="false"
|
||||||
|
[fullScreen]="true"
|
||||||
|
[vScrollFixed]="60"
|
||||||
|
listStorageKey="motion-poll-vote"
|
||||||
|
[cssClasses]="{ 'single-votes-table': true }"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||||
|
{{ col.label | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div *pblNgridCellDef="'user'; row as vote">
|
||||||
|
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
||||||
|
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
||||||
|
</div>
|
||||||
|
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
||||||
|
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
||||||
|
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div>{{ vote.valueVerbose | translate }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Results -->
|
||||||
|
<div *pblNgridNoDataRef class="pbl-ngrid-no-data">
|
||||||
|
{{ 'The individual votes were made anonymous.' | translate }}
|
||||||
|
</div>
|
||||||
|
</os-list-view-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="poll-content small">
|
<div class="poll-content">
|
||||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
{{ 'Groups' | translate }}:
|
{{ 'Groups' | translate }}:
|
||||||
|
|
||||||
<span *ngFor="let group of poll.groups; let i = index">
|
<span *ngFor="let group of poll.groups; let i = index">
|
||||||
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</small>
|
||||||
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
<small>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</small>
|
||||||
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -116,7 +132,7 @@
|
|||||||
<!-- More Menu -->
|
<!-- More Menu -->
|
||||||
<mat-menu #pollDetailMenu="matMenu">
|
<mat-menu #pollDetailMenu="matMenu">
|
||||||
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||||
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog()">
|
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog(poll)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Edit</span>
|
<span translate>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,53 +2,51 @@
|
|||||||
@import '~assets/styles/poll-colors.scss';
|
@import '~assets/styles/poll-colors.scss';
|
||||||
|
|
||||||
.poll-content {
|
.poll-content {
|
||||||
|
text-align: right;
|
||||||
|
display: grid;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-wrapper {
|
.result-wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 10px;
|
grid-gap: 2em;
|
||||||
grid-template-areas:
|
margin: 2em;
|
||||||
'chart'
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
'results'
|
|
||||||
'names';
|
|
||||||
}
|
|
||||||
|
|
||||||
@include desktop {
|
.result-table {
|
||||||
.result-wrapper {
|
// display: block;
|
||||||
grid-template-areas:
|
th {
|
||||||
'results chart'
|
text-align: right;
|
||||||
'names names';
|
font-weight: initial;
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-table {
|
|
||||||
grid-area: results;
|
|
||||||
|
|
||||||
tr {
|
|
||||||
height: 48px;
|
|
||||||
min-height: 48px;
|
|
||||||
th:first-child,
|
|
||||||
td:first-child {
|
|
||||||
padding-left: 24px;
|
|
||||||
}
|
|
||||||
th:last-child,
|
|
||||||
td:last-child {
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-cell-definition {
|
tr {
|
||||||
text-align: center;
|
height: 48px;
|
||||||
|
border-bottom: none !important;
|
||||||
|
|
||||||
|
.result-cell-definition {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.yes {
|
||||||
|
color: $votes-yes-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no {
|
||||||
|
color: $votes-no-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abstain {
|
||||||
|
color: $votes-abstain-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.result-chart {
|
.doughnut-chart {
|
||||||
grid-area: chart;
|
display: block;
|
||||||
max-width: 300px;
|
margin-top: auto;
|
||||||
margin-left: auto;
|
margin-bottom: auto;
|
||||||
margin-right: auto;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.named-result-table {
|
.named-result-table {
|
||||||
|
@ -1,12 +1,4 @@
|
|||||||
@import '~@angular/material/theming';
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
@mixin os-motion-poll-detail-style($theme) {
|
@mixin os-motion-poll-detail-style($theme) {
|
||||||
$background: map-get($theme, background);
|
|
||||||
|
|
||||||
.result-table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
tr {
|
|
||||||
border-bottom: 1px solid mat-color($background, focused-button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { E2EImportsModule } from 'e2e-imports.module';
|
|||||||
|
|
||||||
import { MotionPollDialogComponent } from './motion-poll-dialog.component';
|
import { MotionPollDialogComponent } from './motion-poll-dialog.component';
|
||||||
|
|
||||||
fdescribe('MotionPollDialogComponent', () => {
|
describe('MotionPollDialogComponent', () => {
|
||||||
let component: MotionPollDialogComponent;
|
let component: MotionPollDialogComponent;
|
||||||
let fixture: ComponentFixture<MotionPollDialogComponent>;
|
let fixture: ComponentFixture<MotionPollDialogComponent>;
|
||||||
|
|
||||||
|
@ -15,18 +15,18 @@ import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form
|
|||||||
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 {
|
export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotionPoll> {
|
||||||
public motionPollMethods = { YNA: MotionPollMethodsVerbose.YNA };
|
public motionPollMethods = { YNA: MotionPollMethodsVerbose.YNA };
|
||||||
|
|
||||||
@ViewChild('pollForm', { static: false })
|
@ViewChild('pollForm', { static: false })
|
||||||
protected pollForm: PollFormComponent;
|
protected pollForm: PollFormComponent<ViewMotionPoll>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
title: Title,
|
title: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
matSnackbar: MatSnackBar,
|
matSnackbar: MatSnackBar,
|
||||||
public dialogRef: MatDialogRef<BasePollDialogComponent>,
|
public dialogRef: MatDialogRef<BasePollDialogComponent<ViewMotionPoll>>,
|
||||||
@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);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
<div *osPerms="'motions.can_manage_polls';and:poll.isStarted">
|
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
<div *ngIf="vmanager.canVote(poll)" class="vote-button-grid">
|
||||||
<!-- Voting -->
|
<!-- Voting -->
|
||||||
<p *ngFor="let option of voteOptions">
|
<div class="vote-button" *ngFor="let option of voteOptions">
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
(click)="saveVote(option.vote)"
|
(click)="saveVote(option.vote)"
|
||||||
@ -13,11 +13,6 @@
|
|||||||
<mat-icon> {{ option.icon }}</mat-icon>
|
<mat-icon> {{ option.icon }}</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<span class="vote-label"> {{ option.label | translate }} </span>
|
<span class="vote-label"> {{ option.label | translate }} </span>
|
||||||
</p>
|
</div>
|
||||||
</ng-container>
|
</div>
|
||||||
|
|
||||||
<!-- TODO most of the messages are not making sense -->
|
|
||||||
<!-- <ng-container *ngIf="!vmanager.canVote(poll)">
|
|
||||||
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
|
||||||
</ng-container> -->
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,17 +1,33 @@
|
|||||||
@import '~assets/styles/poll-colors.scss';
|
@import '~assets/styles/poll-colors.scss';
|
||||||
|
|
||||||
|
.vote-button-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 20px;
|
||||||
|
margin-top: 2em;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-button {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-gap: 1em;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
.vote-label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.voted-yes {
|
.voted-yes {
|
||||||
background-color: $votes-yes-color;
|
background-color: $votes-yes-color;
|
||||||
|
color: $vote-active-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voted-no {
|
.voted-no {
|
||||||
background-color: $votes-no-color;
|
background-color: $votes-no-color;
|
||||||
|
color: $vote-active-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voted-abstain {
|
.voted-abstain {
|
||||||
background-color: $votes-abstain-color;
|
background-color: $votes-abstain-color;
|
||||||
}
|
color: $vote-active-color;
|
||||||
|
|
||||||
.vote-label {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||||
import { MotionPollVoteComponent } from './motion-poll-vote.component';
|
import { MotionPollVoteComponent } from './motion-poll-vote.component';
|
||||||
|
|
||||||
describe('MotionPollVoteComponent', () => {
|
describe('MotionPollVoteComponent', () => {
|
||||||
@ -11,7 +12,7 @@ describe('MotionPollVoteComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule],
|
imports: [E2EImportsModule],
|
||||||
declarations: [MotionPollVoteComponent]
|
declarations: [MotionPollVoteComponent, PollProgressComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1,44 +1,68 @@
|
|||||||
<div class="poll-preview-wrapper" *ngIf="poll && showPoll()">
|
<mat-card class="motion-poll-wrapper" *ngIf="poll">
|
||||||
<!-- Poll Infos -->
|
<!-- Poll Infos -->
|
||||||
<div class="poll-title-wrapper">
|
<div class="poll-title-wrapper">
|
||||||
<!-- Title -->
|
<!-- Title Area -->
|
||||||
<a class="poll-title" [routerLink]="pollLink">
|
<div class="poll-title-area">
|
||||||
{{ poll.title }}
|
<!-- Title -->
|
||||||
</a>
|
<span class="poll-title">
|
||||||
|
<a [routerLink]="pollLink">
|
||||||
|
{{ poll.title }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Subtitle -->
|
||||||
|
<span *ngIf="pollService.isElectronicVotingEnabled && poll.type !== 'analog'">
|
||||||
|
{{ poll.typeVerbose | translate }} ·
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- State chip -->
|
||||||
|
<span>
|
||||||
|
{{ poll.stateVerbose }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Dot Menu -->
|
<!-- Dot Menu -->
|
||||||
<span class="poll-title-actions" *osPerms="'motions.can_manage_polls'">
|
<span class="poll-actions" *osPerms="'motions.can_manage_polls'">
|
||||||
<button mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
<button mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
||||||
<mat-icon class="small-icon">more_horiz</mat-icon>
|
<mat-icon class="small-icon">more_horiz</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- State chip -->
|
<!-- Change state button -->
|
||||||
<div class="poll-properties" *osPerms="'motions.can_manage_polls'">
|
<div *osPerms="'motions.can_manage_polls'">
|
||||||
<div *ngIf="pollService.isElectronicVotingEnabled && poll.type !== 'analog'">
|
<button
|
||||||
{{ poll.typeVerbose | translate }}
|
mat-stroked-button
|
||||||
</div>
|
*ngIf="!poll.isPublished"
|
||||||
|
[ngClass]="pollStateActions[poll.state].css"
|
||||||
<mat-chip
|
(click)="changeState(poll.nextState)"
|
||||||
disableRipple
|
>
|
||||||
[matMenuTriggerFor]="triggerMenu"
|
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
|
||||||
class="poll-state active"
|
<span class="next-state-label">
|
||||||
[class]="poll.stateVerbose.toLowerCase()"
|
{{ poll.nextStateActionVerbose | translate }}
|
||||||
[ngClass]="{ 'disabled': !poll.getNextStates() }"
|
</span>
|
||||||
>
|
</button>
|
||||||
{{ poll.stateVerbose }}
|
|
||||||
</mat-chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
||||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
|
||||||
|
<!-- Detail link -->
|
||||||
|
<div class="poll-detail-button-wrapper">
|
||||||
|
<a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}">
|
||||||
|
<mat-icon class="small-icon">
|
||||||
|
visibility
|
||||||
|
</mat-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
<ng-template #votingResult>
|
<ng-template #votingResult>
|
||||||
<div [routerLink]="pollLink" class="poll-link-wrapper">
|
<div class="poll-link-wrapper">
|
||||||
<ng-container
|
<ng-container
|
||||||
[ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"
|
[ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"
|
||||||
></ng-container>
|
></ng-container>
|
||||||
@ -46,7 +70,28 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #viewTemplate>
|
<ng-template #viewTemplate>
|
||||||
<div class="poll-chart-wrapper">
|
<!-- 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 }}
|
||||||
|
</os-icon-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="doughnut-chart">
|
<div class="doughnut-chart">
|
||||||
<os-charts
|
<os-charts
|
||||||
*ngIf="showChart"
|
*ngIf="showChart"
|
||||||
@ -57,31 +102,13 @@
|
|||||||
>
|
>
|
||||||
</os-charts>
|
</os-charts>
|
||||||
</div>
|
</div>
|
||||||
<div class="vote-legend">
|
|
||||||
<div class="votes-yes" *ngIf="pollService.isVoteDocumented(voteYes)">
|
|
||||||
<os-icon-container icon="thumb_up" size="large">
|
|
||||||
{{ voteYes | parsePollNumber }}
|
|
||||||
{{ voteYes | pollPercentBase: poll }}
|
|
||||||
</os-icon-container>
|
|
||||||
</div>
|
|
||||||
<div class="votes-no" *ngIf="pollService.isVoteDocumented(voteNo)">
|
|
||||||
<os-icon-container icon="thumb_down" size="large">
|
|
||||||
{{ voteNo | parsePollNumber }}
|
|
||||||
{{ voteNo | pollPercentBase: poll }}
|
|
||||||
</os-icon-container>
|
|
||||||
</div>
|
|
||||||
<div class="votes-abstain" *ngIf="pollService.isVoteDocumented(voteAbstain)">
|
|
||||||
<os-icon-container icon="trip_origin" size="large">
|
|
||||||
{{ voteAbstain | parsePollNumber }}
|
|
||||||
{{ voteAbstain | pollPercentBase: poll }}
|
|
||||||
</os-icon-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-detail-button-wrapper">
|
|
||||||
<a mat-button [routerLink]="pollLink">
|
<!-- In Progress hint -->
|
||||||
{{ 'More' | translate }}
|
<div class="motion-couting-in-progress-hint" *osPerms="'motions.can_manage_polls'; complement: true">
|
||||||
</a>
|
<span *ngIf="poll.isFinished">
|
||||||
|
{{ 'Counting is in progress' | translate }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
@ -104,18 +131,16 @@
|
|||||||
</button>
|
</button>
|
||||||
<div *osPerms="'motions.can_manage_polls'">
|
<div *osPerms="'motions.can_manage_polls'">
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
<!-- Reset Button -->
|
||||||
|
<button mat-menu-item (click)="resetState()">
|
||||||
|
<mat-icon color="warn">replay</mat-icon>
|
||||||
|
<span translate>Reset state</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Delete button -->
|
||||||
<button mat-menu-item (click)="deletePoll()">
|
<button mat-menu-item (click)="deletePoll()">
|
||||||
<mat-icon color="warn">delete</mat-icon>
|
<mat-icon color="warn">delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
<!-- Select state menu -->
|
|
||||||
<mat-menu #triggerMenu="matMenu">
|
|
||||||
<ng-container *ngIf="poll">
|
|
||||||
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.getNextStates() | keyvalue">
|
|
||||||
<span translate>{{ state.key }}</span>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</mat-menu>
|
|
||||||
|
@ -4,70 +4,47 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-preview-wrapper {
|
.motion-poll-wrapper {
|
||||||
padding: 8px;
|
margin-bottom: 30px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
||||||
|
|
||||||
.poll-title {
|
.poll-title-wrapper {
|
||||||
text-decoration: none;
|
display: grid;
|
||||||
font-weight: 500;
|
grid-gap: 10px;
|
||||||
}
|
grid-template-areas: 'title actions';
|
||||||
|
grid-template-columns: auto min-content;
|
||||||
|
|
||||||
.poll-title-actions {
|
.poll-title-area {
|
||||||
float: right;
|
grid-area: title;
|
||||||
}
|
margin-top: 1em;
|
||||||
|
|
||||||
.poll-properties {
|
.poll-title {
|
||||||
margin: 4px 0;
|
font-size: 125%;
|
||||||
|
|
||||||
.mat-chip {
|
|
||||||
margin: 0 4px;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-state {
|
.poll-actions {
|
||||||
&.created {
|
grid-area: actions;
|
||||||
background-color: #2196f3;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&.started {
|
|
||||||
background-color: #4caf50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&.finished {
|
|
||||||
background-color: #ff5252;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&.published {
|
|
||||||
background-color: #ffd800;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-chart-wrapper {
|
.poll-chart-wrapper {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 10px;
|
grid-gap: 20px;
|
||||||
grid-template-areas: 'placeholder chart legend';
|
margin: 2em;
|
||||||
grid-template-columns: auto minmax(50px, 20%) auto;
|
// try to find max scale
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
|
||||||
.doughnut-chart {
|
.doughnut-chart {
|
||||||
grid-area: chart;
|
display: block;
|
||||||
|
max-width: 200px;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vote-legend {
|
.vote-legend {
|
||||||
grid-area: legend;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
|
|
||||||
div + div {
|
div + div {
|
||||||
margin-top: 10px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.votes-yes {
|
.votes-yes {
|
||||||
@ -93,16 +70,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-preview-meta-info {
|
.next-state-label {
|
||||||
span {
|
margin-top: auto;
|
||||||
padding: 0 5px;
|
margin-bottom: auto;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-content {
|
.start-poll-button {
|
||||||
padding-bottom: 8px;
|
color: green !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-footer {
|
.stop-poll-button {
|
||||||
text-align: end;
|
color: $poll-stop-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-poll-button {
|
||||||
|
color: $poll-publish-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-couting-in-progress-hint {
|
||||||
|
margin-top: 1em;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
@import '~@angular/material/theming';
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
@mixin os-motion-poll-style($theme) {
|
@mixin os-motion-poll-style($theme) {
|
||||||
$background: map-get($theme, background);
|
|
||||||
|
|
||||||
.poll-preview-wrapper {
|
|
||||||
background-color: mat-color($background, card);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||||
import { MotionPollVoteComponent } from '../motion-poll-vote/motion-poll-vote.component';
|
import { MotionPollVoteComponent } from '../motion-poll-vote/motion-poll-vote.component';
|
||||||
import { MotionPollComponent } from './motion-poll.component';
|
import { MotionPollComponent } from './motion-poll.component';
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ describe('MotionPollComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule],
|
imports: [E2EImportsModule],
|
||||||
declarations: [MotionPollComponent, MotionPollVoteComponent]
|
declarations: [MotionPollComponent, MotionPollVoteComponent, PollProgressComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -4,7 +4,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 { 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 { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
@ -114,22 +113,12 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
|||||||
public pollRepo: MotionPollRepositoryService,
|
public pollRepo: MotionPollRepositoryService,
|
||||||
pollDialog: MotionPollDialogService,
|
pollDialog: MotionPollDialogService,
|
||||||
public pollService: PollService,
|
public pollService: PollService,
|
||||||
private operator: OperatorService,
|
|
||||||
private pdfService: MotionPollPdfService
|
private pdfService: MotionPollPdfService
|
||||||
) {
|
) {
|
||||||
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
|
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showPoll(): boolean {
|
|
||||||
return (
|
|
||||||
this.operator.hasPerms('motions.can_manage_polls') ||
|
|
||||||
this.poll.isPublished ||
|
|
||||||
(this.poll.type !== 'analog' && this.poll.isStarted)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public downloadPdf(): void {
|
public downloadPdf(): void {
|
||||||
console.log('picture_as_pdf');
|
|
||||||
this.pdfService.printBallots(this.poll);
|
this.pdfService.printBallots(this.poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,13 +371,17 @@ export class MotionPdfService {
|
|||||||
motion.polls.forEach(poll => {
|
motion.polls.forEach(poll => {
|
||||||
if (poll.hasVotes) {
|
if (poll.hasVotes) {
|
||||||
const tableData = poll.generateTableData();
|
const tableData = poll.generateTableData();
|
||||||
|
|
||||||
tableData.forEach(votingResult => {
|
tableData.forEach(votingResult => {
|
||||||
const resultKey = this.translate.instant(this.pollKeyVerbose.transform(votingResult.key));
|
const votingOption = this.translate.instant(
|
||||||
const resultValue = this.parsePollNumber.transform(votingResult.value);
|
this.pollKeyVerbose.transform(votingResult.votingOption)
|
||||||
column1.push(`${resultKey}:`);
|
);
|
||||||
|
const value = votingResult.value[0];
|
||||||
|
const resultValue = this.parsePollNumber.transform(value.amount);
|
||||||
|
column1.push(`${votingOption}:`);
|
||||||
column2.push(resultValue);
|
column2.push(resultValue);
|
||||||
if (votingResult.showPercent) {
|
if (value.showPercent) {
|
||||||
const resultInPercent = this.pollPercentBase.transform(votingResult.value, poll);
|
const resultInPercent = this.pollPercentBase.transform(value.amount, poll);
|
||||||
column3.push(resultInPercent);
|
column3.push(resultInPercent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,6 @@ import { MatDialog } from '@angular/material';
|
|||||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||||
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 { MotionPollService } from './motion-poll.service';
|
|
||||||
import { ViewMotionPoll } from '../models/view-motion-poll';
|
import { ViewMotionPoll } from '../models/view-motion-poll';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,7 +15,7 @@ import { ViewMotionPoll } from '../models/view-motion-poll';
|
|||||||
export class MotionPollDialogService extends BasePollDialogService<ViewMotionPoll> {
|
export class MotionPollDialogService extends BasePollDialogService<ViewMotionPoll> {
|
||||||
protected dialogComponent = MotionPollDialogComponent;
|
protected dialogComponent = MotionPollDialogComponent;
|
||||||
|
|
||||||
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService, service: MotionPollService) {
|
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService) {
|
||||||
super(dialog, mapper, service);
|
super(dialog, mapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,8 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { Collection } from 'app/shared/models/base/collection';
|
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||||
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
|
||||||
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
||||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
|
||||||
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
||||||
|
|
||||||
interface PollResultData {
|
interface PollResultData {
|
||||||
@ -34,6 +32,8 @@ export class MotionPollService extends PollService {
|
|||||||
*/
|
*/
|
||||||
public defaultMajorityMethod: MajorityMethod;
|
public defaultMajorityMethod: MajorityMethod;
|
||||||
|
|
||||||
|
public defaultGroupIds: number[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Subscribes to the configuration values needed
|
* Constructor. Subscribes to the configuration values needed
|
||||||
* @param config ConfigService
|
* @param config ConfigService
|
||||||
@ -51,15 +51,18 @@ export class MotionPollService extends PollService {
|
|||||||
config
|
config
|
||||||
.get<MajorityMethod>('motion_poll_default_majority_method')
|
.get<MajorityMethod>('motion_poll_default_majority_method')
|
||||||
.subscribe(method => (this.defaultMajorityMethod = method));
|
.subscribe(method => (this.defaultMajorityMethod = method));
|
||||||
|
|
||||||
|
config.get<number[]>(MotionPoll.defaultGroupsConfig).subscribe(ids => (this.defaultGroupIds = ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
public fillDefaultPollData(poll: Partial<ViewMotionPoll> & Collection): void {
|
public getDefaultPollData(): MotionPoll {
|
||||||
super.fillDefaultPollData(poll);
|
const poll = new MotionPoll(super.getDefaultPollData());
|
||||||
const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === poll.motion_id).length;
|
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 = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`;
|
||||||
poll.pollmethod = MotionPollMethods.YNA;
|
poll.pollmethod = MotionPollMethods.YNA;
|
||||||
poll.motion_id = poll.motion_id;
|
|
||||||
|
return poll;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPercentBase(poll: PollData): number {
|
public getPercentBase(poll: PollData): number {
|
||||||
|
@ -10,9 +10,7 @@ import { BehaviorSubject, from, Observable } from 'rxjs';
|
|||||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { Breadcrumb } from 'app/shared/components/breadcrumb/breadcrumb.component';
|
|
||||||
import { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
|
import { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
|
||||||
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
@ -58,11 +56,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
*/
|
*/
|
||||||
public poll: V = null;
|
public poll: V = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* The breadcrumbs for the poll-states.
|
|
||||||
*/
|
|
||||||
public breadcrumbs: Breadcrumb[] = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the type of the shown chart, if votes are entered.
|
* Sets the type of the shown chart, if votes are entered.
|
||||||
*/
|
*/
|
||||||
@ -143,8 +136,8 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
/**
|
/**
|
||||||
* Opens dialog for editing the poll
|
* Opens dialog for editing the poll
|
||||||
*/
|
*/
|
||||||
public openDialog(): void {
|
public openDialog(viewPoll: V): void {
|
||||||
this.pollDialog.openDialog(this.poll);
|
this.pollDialog.openDialog(viewPoll);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDeleted(): void {}
|
protected onDeleted(): void {}
|
||||||
@ -198,7 +191,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
this.repo.getViewModelObservable(params.id).subscribe(poll => {
|
this.repo.getViewModelObservable(params.id).subscribe(poll => {
|
||||||
if (poll) {
|
if (poll) {
|
||||||
this.poll = poll;
|
this.poll = poll;
|
||||||
this.updateBreadcrumbs();
|
|
||||||
this.onPollLoaded();
|
this.onPollLoaded();
|
||||||
this.waitForOptions();
|
this.waitForOptions();
|
||||||
this.checkData();
|
this.checkData();
|
||||||
@ -218,81 +210,4 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
|||||||
this.onPollWithOptionsLoaded();
|
this.onPollWithOptionsLoaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Action for the different breadcrumbs.
|
|
||||||
*/
|
|
||||||
private async changeState(): Promise<void> {
|
|
||||||
this.actionWrapper(this.repo.changePollState(this.poll), this.onStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the state of a motion-poll.
|
|
||||||
*/
|
|
||||||
private async resetState(): Promise<void> {
|
|
||||||
this.actionWrapper(this.repo.resetPoll(this.poll), this.onStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to execute same logic after fullfilling a promise.
|
|
||||||
*
|
|
||||||
* @param action Any promise to execute.
|
|
||||||
*
|
|
||||||
* @returns Any promise-like.
|
|
||||||
*/
|
|
||||||
private actionWrapper(action: Promise<any>, callback?: () => any): any {
|
|
||||||
action
|
|
||||||
.then(() => {
|
|
||||||
this.checkData();
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(this.raiseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to change the breadcrumbs depending on the state of the given motion-poll.
|
|
||||||
*/
|
|
||||||
private updateBreadcrumbs(): void {
|
|
||||||
this.breadcrumbs = Object.values(PollState)
|
|
||||||
.filter(state => typeof state === 'string')
|
|
||||||
.map((state: string) => ({
|
|
||||||
label: state,
|
|
||||||
action: this.getBreadcrumbAction(PollState[state]),
|
|
||||||
active: this.poll ? this.poll.state === PollState[state] : false
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Depending on the state of the motion-poll, the breadcrumb has another action and state.
|
|
||||||
*
|
|
||||||
* @param state The state of the motion-poll as number.
|
|
||||||
*
|
|
||||||
* @returns An action, that is executed, if the breadcrumb is clicked, or null.
|
|
||||||
*/
|
|
||||||
private getBreadcrumbAction(state: number): () => any | null {
|
|
||||||
if (!this.poll) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!this.hasPerms()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
switch (this.poll.state) {
|
|
||||||
case PollState.Created:
|
|
||||||
return state === 2 ? () => this.changeState() : null;
|
|
||||||
case PollState.Started:
|
|
||||||
return this.poll.type !== PollType.Analog && state === 3 ? () => this.changeState() : null;
|
|
||||||
case PollState.Finished:
|
|
||||||
if (state === 1) {
|
|
||||||
return () => this.resetState();
|
|
||||||
} else if (state === 4) {
|
|
||||||
return () => this.changeState();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case PollState.Published:
|
|
||||||
return state === 1 ? () => this.resetState() : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,15 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { OneOfValidator } from 'app/shared/validators/one-of-validator';
|
import { OneOfValidator } from 'app/shared/validators/one-of-validator';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { PollFormComponent } from './poll-form/poll-form.component';
|
import { PollFormComponent } from './poll-form/poll-form.component';
|
||||||
|
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 extends BaseViewComponent {
|
export abstract class BasePollDialogComponent<T extends ViewBasePoll> extends BaseViewComponent {
|
||||||
public publishImmediately: boolean;
|
public publishImmediately: boolean;
|
||||||
|
|
||||||
protected pollForm: PollFormComponent;
|
protected pollForm: PollFormComponent<T>;
|
||||||
|
|
||||||
public dialogVoteForm: FormGroup;
|
public dialogVoteForm: FormGroup;
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ export abstract class BasePollDialogComponent extends BaseViewComponent {
|
|||||||
title: Title,
|
title: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
matSnackbar: MatSnackBar,
|
matSnackbar: MatSnackBar,
|
||||||
public dialogRef: MatDialogRef<BasePollDialogComponent>
|
public dialogRef: MatDialogRef<BasePollDialogComponent<T>>
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar);
|
super(title, translate, matSnackbar);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,21 @@ export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseView
|
|||||||
|
|
||||||
protected _poll: V;
|
protected _poll: V;
|
||||||
|
|
||||||
|
public pollStateActions = {
|
||||||
|
[PollState.Created]: {
|
||||||
|
icon: 'play_arrow',
|
||||||
|
css: 'start-poll-button'
|
||||||
|
},
|
||||||
|
[PollState.Started]: {
|
||||||
|
icon: 'stop',
|
||||||
|
css: 'stop-poll-button'
|
||||||
|
},
|
||||||
|
[PollState.Finished]: {
|
||||||
|
icon: 'public',
|
||||||
|
css: 'publish-poll-button'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
@ -41,6 +56,10 @@ export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public resetState(): void {
|
||||||
|
this.changeState(PollState.Created);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for the 'delete poll' button
|
* Handler for the 'delete poll' button
|
||||||
*/
|
*/
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- TODO: rather disable forms than duplicate them -->
|
||||||
<div *ngIf="data && data.state > 1" class="poll-preview-meta-info">
|
<div *ngIf="data && data.state > 1" 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>
|
||||||
|
@ -5,8 +5,8 @@ import { E2EImportsModule } from 'e2e-imports.module';
|
|||||||
import { PollFormComponent } from './poll-form.component';
|
import { PollFormComponent } from './poll-form.component';
|
||||||
|
|
||||||
describe('PollFormComponent', () => {
|
describe('PollFormComponent', () => {
|
||||||
let component: PollFormComponent;
|
let component: PollFormComponent<any>;
|
||||||
let fixture: ComponentFixture<PollFormComponent>;
|
let fixture: ComponentFixture<PollFormComponent<any>>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
@ -12,6 +12,7 @@ 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 { 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,
|
||||||
PercentBaseVerbose,
|
PercentBaseVerbose,
|
||||||
@ -28,7 +29,7 @@ import { PollService } from '../../services/poll.service';
|
|||||||
templateUrl: './poll-form.component.html',
|
templateUrl: './poll-form.component.html',
|
||||||
styleUrls: ['./poll-form.component.scss']
|
styleUrls: ['./poll-form.component.scss']
|
||||||
})
|
})
|
||||||
export class PollFormComponent extends BaseViewComponent implements OnInit {
|
export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The form-group for the meta-info.
|
* The form-group for the meta-info.
|
||||||
*/
|
*/
|
||||||
@ -44,7 +45,7 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
|||||||
public pollMethods: { [key: string]: string };
|
public pollMethods: { [key: string]: string };
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public data: Partial<ViewBasePoll>;
|
public data: Partial<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The different types the poll can accept.
|
* The different types the poll can accept.
|
||||||
@ -103,18 +104,15 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
|||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.groupObservable = this.groupRepo.getViewModelListObservable();
|
this.groupObservable = this.groupRepo.getViewModelListObservable();
|
||||||
|
|
||||||
const cast = <ViewAssignmentPoll>this.data;
|
|
||||||
if (cast.assignment && !cast.votes_amount) {
|
|
||||||
cast.votes_amount = cast.assignment.open_posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
if (!this.data.groups_id) {
|
if (this.data instanceof ViewAssignmentPoll) {
|
||||||
if (this.data.collectionString === ViewAssignmentPoll.COLLECTIONSTRING) {
|
if (this.data.assignment && !this.data.votes_amount) {
|
||||||
this.data.groups_id = this.configService.instant('assignment_poll_default_groups');
|
this.data.votes_amount = this.data.assignment.open_posts;
|
||||||
} else {
|
|
||||||
this.data.groups_id = this.configService.instant('motion_poll_default_groups');
|
|
||||||
}
|
}
|
||||||
|
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 => {
|
Object.keys(this.contentForm.controls).forEach(key => {
|
||||||
@ -193,26 +191,31 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
|||||||
* @param data Passing the properties of the poll.
|
* @param data Passing the properties of the poll.
|
||||||
*/
|
*/
|
||||||
private updatePollValues(data: { [key: string]: any }): void {
|
private updatePollValues(data: { [key: string]: any }): void {
|
||||||
this.pollValues = [
|
if (this.data) {
|
||||||
[this.pollService.getVerboseNameForKey('type'), this.pollService.getVerboseNameForValue('type', data.type)]
|
this.pollValues = [
|
||||||
];
|
[
|
||||||
// show pollmethod only for assignment polls
|
this.pollService.getVerboseNameForKey('type'),
|
||||||
if (this.data.pollClassType === PollClassType.Assignment) {
|
this.pollService.getVerboseNameForValue('type', data.type)
|
||||||
this.pollValues.push([
|
]
|
||||||
this.pollService.getVerboseNameForKey('pollmethod'),
|
];
|
||||||
this.pollService.getVerboseNameForValue('pollmethod', data.pollmethod)
|
// show pollmethod only for assignment polls
|
||||||
]);
|
if (this.data.pollClassType === PollClassType.Assignment) {
|
||||||
}
|
this.pollValues.push([
|
||||||
if (data.type !== 'analog') {
|
this.pollService.getVerboseNameForKey('pollmethod'),
|
||||||
this.pollValues.push([
|
this.pollService.getVerboseNameForValue('pollmethod', data.pollmethod)
|
||||||
this.pollService.getVerboseNameForKey('groups'),
|
]);
|
||||||
this.groupRepo.getNameForIds(...([] || (data && data.groups_id)))
|
}
|
||||||
]);
|
if (data.type !== 'analog') {
|
||||||
}
|
this.pollValues.push([
|
||||||
if (data.pollmethod === 'votes') {
|
this.pollService.getVerboseNameForKey('groups'),
|
||||||
this.pollValues.push([this.pollService.getVerboseNameForKey('votes_amount'), data.votes_amount]);
|
this.groupRepo.getNameForIds(...([] || (data && data.groups_id)))
|
||||||
this.pollValues.push([this.pollService.getVerboseNameForKey('global_no'), data.global_no]);
|
]);
|
||||||
this.pollValues.push([this.pollService.getVerboseNameForKey('global_abstain'), data.global_abstain]);
|
}
|
||||||
|
if (data.pollmethod === 'votes') {
|
||||||
|
this.pollValues.push([this.pollService.getVerboseNameForKey('votes_amount'), data.votes_amount]);
|
||||||
|
this.pollValues.push([this.pollService.getVerboseNameForKey('global_no'), data.global_no]);
|
||||||
|
this.pollValues.push([this.pollService.getVerboseNameForKey('global_abstain'), data.global_abstain]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
<span>
|
<div *ngIf="poll" class="poll-progress-wrapper">
|
||||||
{{ 'Casted votes' | translate }}: {{ poll.votescast }} / {{ max }},
|
<div class="motion-vote-number" *ngIf="poll.pollClassType === 'motion'">
|
||||||
{{ 'valid votes' | translate}}: {{ poll.votesvalid }} / {{ max }},
|
<span>{{ poll.votescast }} / {{ max }}</span>
|
||||||
{{ 'invalid votes' | translate}}: {{ poll.votesinvalid }} / {{ max }}
|
</div>
|
||||||
</span>
|
|
||||||
|
|
||||||
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
<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>
|
||||||
|
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
||||||
|
</div>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
.poll-progress-wrapper {
|
||||||
|
margin: 1em 0 2em 0;
|
||||||
|
.voting-progress-bar {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-vote-number {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,8 @@ describe('PollProgressComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule]
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [PollProgressComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -30,7 +30,11 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get valueInPercent(): number {
|
public get valueInPercent(): number {
|
||||||
return (this.poll.votesvalid / this.max) * 100;
|
if (this.poll) {
|
||||||
|
return (this.poll.votesvalid / this.max) * 100;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,15 +42,17 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
|
|||||||
* Sets the observable for groups.
|
* Sets the observable for groups.
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.userRepo
|
if (this.poll) {
|
||||||
.getViewModelListObservable()
|
this.userRepo
|
||||||
.pipe(
|
.getViewModelListObservable()
|
||||||
map(users =>
|
.pipe(
|
||||||
users.filter(user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length)
|
map(users =>
|
||||||
|
users.filter(user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
.subscribe(users => {
|
||||||
.subscribe(users => {
|
this.max = users.length;
|
||||||
this.max = users.length;
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
client/src/app/site/polls/models/has-view-polls.ts
Normal file
5
client/src/app/site/polls/models/has-view-polls.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ViewBasePoll } from './view-base-poll';
|
||||||
|
|
||||||
|
export interface HasViewPolls<T extends ViewBasePoll> {
|
||||||
|
polls: T[];
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { BasePoll, PollState } from 'app/shared/models/poll/base-poll';
|
import { BasePoll, PercentBase, PollType } from 'app/shared/models/poll/base-poll';
|
||||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
@ -16,14 +16,16 @@ export enum PollClassType {
|
|||||||
* Interface describes the possible data for the result-table.
|
* Interface describes the possible data for the result-table.
|
||||||
*/
|
*/
|
||||||
export interface PollTableData {
|
export interface PollTableData {
|
||||||
key?: string;
|
votingOption: string;
|
||||||
value?: number;
|
votingOptionSubtitle?: string;
|
||||||
yes?: number;
|
value: VotingResult[];
|
||||||
no?: number;
|
}
|
||||||
abstain?: number;
|
|
||||||
user?: string;
|
export interface VotingResult {
|
||||||
canHide?: boolean;
|
vote?: 'yes' | 'no' | 'abstain' | 'votesvalid' | 'votesinvalid' | 'votescast';
|
||||||
|
amount?: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
hide?: boolean;
|
||||||
showPercent?: boolean;
|
showPercent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ export const PollClassTypeVerbose = {
|
|||||||
export const PollStateVerbose = {
|
export const PollStateVerbose = {
|
||||||
1: 'Created',
|
1: 'Created',
|
||||||
2: 'Started',
|
2: 'Started',
|
||||||
3: 'Finished',
|
3: 'Finished (unpublished)',
|
||||||
4: 'Published'
|
4: 'Published'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,6 +87,41 @@ export const PercentBaseVerbose = {
|
|||||||
export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends BaseProjectableViewModel<M> {
|
export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends BaseProjectableViewModel<M> {
|
||||||
private _tableData: PollTableData[] = [];
|
private _tableData: PollTableData[] = [];
|
||||||
|
|
||||||
|
protected voteTableKeys: VotingResult[] = [
|
||||||
|
{
|
||||||
|
vote: 'yes',
|
||||||
|
icon: 'thumb_up',
|
||||||
|
showPercent: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'no',
|
||||||
|
icon: 'thumb_down',
|
||||||
|
showPercent: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'abstain',
|
||||||
|
icon: 'trip_origin',
|
||||||
|
showPercent: this.showAbstainPercent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
protected sumTableKeys: VotingResult[] = [
|
||||||
|
{
|
||||||
|
vote: 'votesvalid',
|
||||||
|
showPercent: this.poll.isPercentBaseValidOrCast
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'votesinvalid',
|
||||||
|
hide: this.poll.type !== PollType.Analog,
|
||||||
|
showPercent: this.poll.isPercentBaseValidOrCast
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'votescast',
|
||||||
|
hide: this.poll.type !== PollType.Analog,
|
||||||
|
showPercent: this.poll.isPercentBaseValidOrCast
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
public get tableData(): PollTableData[] {
|
public get tableData(): PollTableData[] {
|
||||||
if (!this._tableData.length) {
|
if (!this._tableData.length) {
|
||||||
this._tableData = this.generateTableData();
|
this._tableData = this.generateTableData();
|
||||||
@ -108,6 +145,10 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
|||||||
return PollStateVerbose[this.state];
|
return PollStateVerbose[this.state];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get nextStateActionVerbose(): string {
|
||||||
|
return PollStateChangeActionVerbose[this.nextState];
|
||||||
|
}
|
||||||
|
|
||||||
public get typeVerbose(): string {
|
public get typeVerbose(): string {
|
||||||
return PollTypeVerbose[this.type];
|
return PollTypeVerbose[this.type];
|
||||||
}
|
}
|
||||||
@ -120,23 +161,14 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
|||||||
return PercentBaseVerbose[this.onehundred_percent_base];
|
return PercentBaseVerbose[this.onehundred_percent_base];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get showAbstainPercent(): boolean {
|
||||||
|
return this.onehundred_percent_base === PercentBase.YNA;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract readonly pollClassType: 'motion' | 'assignment';
|
public abstract readonly pollClassType: 'motion' | 'assignment';
|
||||||
|
|
||||||
public canBeVotedFor: () => boolean;
|
public canBeVotedFor: () => boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a mapping "verbose_state" -> "state_id" for all valid next states
|
|
||||||
*/
|
|
||||||
public getNextStates(): { [key: number]: string } {
|
|
||||||
const next_state = (this.state % Object.keys(PollStateVerbose).length) + 1;
|
|
||||||
const states = {};
|
|
||||||
states[PollStateChangeActionVerbose[next_state]] = next_state;
|
|
||||||
if (this.state === PollState.Finished) {
|
|
||||||
states[PollStateChangeActionVerbose[PollState.Created]] = PollState.Created;
|
|
||||||
}
|
|
||||||
return states;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get user_has_voted_invalid(): boolean {
|
public get user_has_voted_invalid(): boolean {
|
||||||
return this.options.some(option => option.user_has_voted) && !this.user_has_voted_valid;
|
return this.options.some(option => option.user_has_voted) && !this.user_has_voted_valid;
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,14 @@ 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, ChartType } from 'app/shared/components/charts/charts.component';
|
||||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { Collection } from 'app/shared/models/base/collection';
|
|
||||||
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||||
import { MajorityMethod, PercentBase, PollColor, PollType } from 'app/shared/models/poll/base-poll';
|
import { BasePoll, MajorityMethod, PercentBase, PollColor, PollType } from 'app/shared/models/poll/base-poll';
|
||||||
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import {
|
import {
|
||||||
MajorityMethodVerbose,
|
MajorityMethodVerbose,
|
||||||
PercentBaseVerbose,
|
PercentBaseVerbose,
|
||||||
PollPropertyVerbose,
|
PollPropertyVerbose,
|
||||||
PollTypeVerbose,
|
PollTypeVerbose
|
||||||
ViewBasePoll
|
|
||||||
} from 'app/site/polls/models/view-base-poll';
|
} from 'app/site/polls/models/view-base-poll';
|
||||||
import { ConstantsService } from '../../../core/core-services/constants.service';
|
import { ConstantsService } from '../../../core/core-services/constants.service';
|
||||||
|
|
||||||
@ -129,6 +127,11 @@ export abstract class PollService {
|
|||||||
*/
|
*/
|
||||||
public abstract defaultMajorityMethod: MajorityMethod;
|
public abstract defaultMajorityMethod: MajorityMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per default entitled to vote
|
||||||
|
*/
|
||||||
|
public abstract defaultGroupIds: number[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The majority method currently in use
|
* The majority method currently in use
|
||||||
*/
|
*/
|
||||||
@ -151,10 +154,13 @@ export abstract class PollService {
|
|||||||
* Assigns the default poll data to the object. To be extended in subclasses
|
* Assigns the default poll data to the object. To be extended in subclasses
|
||||||
* @param poll the poll/object to fill
|
* @param poll the poll/object to fill
|
||||||
*/
|
*/
|
||||||
public fillDefaultPollData(poll: Partial<ViewBasePoll> & Collection): void {
|
public getDefaultPollData(): Partial<BasePoll> {
|
||||||
poll.onehundred_percent_base = this.defaultPercentBase;
|
return {
|
||||||
poll.majority_method = this.defaultMajorityMethod;
|
onehundred_percent_base: this.defaultPercentBase,
|
||||||
poll.type = PollType.Analog;
|
majority_method: this.defaultMajorityMethod,
|
||||||
|
groups_id: this.defaultGroupIds,
|
||||||
|
type: PollType.Analog
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVerboseNameForValue(key: string, value: string): string {
|
public getVerboseNameForValue(key: string, value: string): string {
|
||||||
|
@ -48,6 +48,7 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
|
|||||||
// Will be set by the repository
|
// Will be set by the repository
|
||||||
public getFullName: () => string;
|
public getFullName: () => string;
|
||||||
public getShortName: () => string;
|
public getShortName: () => string;
|
||||||
|
public getLevelAndNumber: () => string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats the category for search
|
* Formats the category for search
|
||||||
|
@ -5,7 +5,6 @@ export interface AssignmentSlideData {
|
|||||||
open_posts: number;
|
open_posts: number;
|
||||||
assignment_related_users: {
|
assignment_related_users: {
|
||||||
user: string;
|
user: string;
|
||||||
elected: boolean;
|
|
||||||
}[];
|
}[];
|
||||||
number_poll_candidates: boolean;
|
number_poll_candidates: boolean;
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,11 @@
|
|||||||
<ol *ngIf="data.data.number_poll_candidates">
|
<ol *ngIf="data.data.number_poll_candidates">
|
||||||
<li *ngFor="let candidate of data.data.assignment_related_users">
|
<li *ngFor="let candidate of data.data.assignment_related_users">
|
||||||
{{ candidate.user }}
|
{{ candidate.user }}
|
||||||
<mat-icon *ngIf="candidate.elected">star</mat-icon>
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ul *ngIf="!data.data.number_poll_candidates">
|
<ul *ngIf="!data.data.number_poll_candidates">
|
||||||
<li *ngFor="let candidate of data.data.assignment_related_users">
|
<li *ngFor="let candidate of data.data.assignment_related_users">
|
||||||
{{ candidate.user }}
|
{{ candidate.user }}
|
||||||
<mat-icon *ngIf="candidate.elected">star</mat-icon>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-subtitle {
|
||||||
|
color: mat-color($foreground, secondary-text);
|
||||||
|
}
|
||||||
|
|
||||||
mat-card-header {
|
mat-card-header {
|
||||||
background-color: mat-color($background, app-bar);
|
background-color: mat-color($background, app-bar);
|
||||||
}
|
}
|
||||||
@ -162,10 +166,6 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-progress-bar-buffer {
|
|
||||||
background-color: mat-color($background, card) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-foreground {
|
.primary-foreground {
|
||||||
color: mat-color($primary);
|
color: mat-color($primary);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Define the colors used for yes, no and abstain
|
* Define the colors used for yes, no and abstain
|
||||||
*/
|
*/
|
||||||
$votes-yes-color: #9fd773;
|
$votes-yes-color: #4caf50;
|
||||||
$votes-no-color: #cc6c5b;
|
$votes-no-color: #cc6c5b;
|
||||||
$votes-abstain-color: #a6a6a6;
|
$votes-abstain-color: #a6a6a6;
|
||||||
|
$vote-active-color: white;
|
||||||
|
$poll-create-color: #4caf50;
|
||||||
|
$poll-stop-color: #ff5252;
|
||||||
|
$poll-publish-color: #e6b100;
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
.poll-result {
|
|
||||||
.poll-progress-bar {
|
|
||||||
height: 5px;
|
|
||||||
width: 100%;
|
|
||||||
.mat-progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.poll-progress {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-top: 15px;
|
|
||||||
mat-icon {
|
|
||||||
min-width: 40px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
.progress-container {
|
|
||||||
width: 85%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-progress-bar {
|
|
||||||
mat-progress-bar {
|
|
||||||
&.progress-green {
|
|
||||||
.mat-progress-bar-fill::after {
|
|
||||||
background-color: #4caf50;
|
|
||||||
}
|
|
||||||
.mat-progress-bar-buffer {
|
|
||||||
background-color: #d5ecd5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.progress-red {
|
|
||||||
.mat-progress-bar-fill::after {
|
|
||||||
background-color: #f44336;
|
|
||||||
}
|
|
||||||
.mat-progress-bar-buffer {
|
|
||||||
background-color: #fcd2cf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.progress-yellow {
|
|
||||||
.mat-progress-bar-fill::after {
|
|
||||||
background-color: #ffc107;
|
|
||||||
}
|
|
||||||
.mat-progress-bar-buffer {
|
|
||||||
background-color: #fff0c4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-quorum-line {
|
|
||||||
display: flex;
|
|
||||||
vertical-align: bottom;
|
|
||||||
.mat-button {
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-nav-color {
|
|
||||||
color: rgba(0, 0, 0, 0.54);
|
|
||||||
}
|
|
@ -30,6 +30,7 @@
|
|||||||
@import './app/shared/components/banner/banner.component.scss-theme.scss';
|
@import './app/shared/components/banner/banner.component.scss-theme.scss';
|
||||||
@import './app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.scss-theme.scss';
|
@import './app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.scss-theme.scss';
|
||||||
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
||||||
|
@import './app/site/assignments/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
||||||
|
|
||||||
/** fonts */
|
/** fonts */
|
||||||
@import './assets/styles/fonts.scss';
|
@import './assets/styles/fonts.scss';
|
||||||
@ -60,6 +61,7 @@ $narrow-spacing: (
|
|||||||
@include os-banner-style($theme);
|
@include os-banner-style($theme);
|
||||||
@include os-motion-poll-style($theme);
|
@include os-motion-poll-style($theme);
|
||||||
@include os-motion-poll-detail-style($theme);
|
@include os-motion-poll-detail-style($theme);
|
||||||
|
@include os-assignment-poll-detail-style($theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load projector specific SCSS values */
|
/** Load projector specific SCSS values */
|
||||||
|
@ -51,12 +51,26 @@ def get_config_variables():
|
|||||||
subgroup="Voting",
|
subgroup="Voting",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="assignment_poll_method",
|
||||||
|
default_value="votes",
|
||||||
|
input_type="choice",
|
||||||
|
label="Preselected poll method",
|
||||||
|
choices=tuple(
|
||||||
|
{"value": method[0], "display_name": method[1]}
|
||||||
|
for method in AssignmentPoll.POLLMETHODS
|
||||||
|
),
|
||||||
|
weight=415,
|
||||||
|
group="Elections",
|
||||||
|
subgroup="Voting",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="assignment_poll_add_candidates_to_list_of_speakers",
|
name="assignment_poll_add_candidates_to_list_of_speakers",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type="boolean",
|
input_type="boolean",
|
||||||
label="Put all candidates on the list of speakers",
|
label="Put all candidates on the list of speakers",
|
||||||
weight=415,
|
weight=420,
|
||||||
group="Elections",
|
group="Elections",
|
||||||
subgroup="Voting",
|
subgroup="Voting",
|
||||||
)
|
)
|
||||||
|
@ -175,7 +175,12 @@ class Migration(migrations.Migration):
|
|||||||
model_name="assignmentpoll",
|
model_name="assignmentpoll",
|
||||||
name="pollmethod",
|
name="pollmethod",
|
||||||
field=models.CharField(
|
field=models.CharField(
|
||||||
choices=[("YN", "YN"), ("YNA", "YNA"), ("votes", "votes")], max_length=5
|
choices=[
|
||||||
|
("votes", "Yes per candidate"),
|
||||||
|
("YN", "Yes/No per candidate"),
|
||||||
|
("YNA", "Yes/No/Abstain per candidate"),
|
||||||
|
],
|
||||||
|
max_length=5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
|
@ -20,4 +20,5 @@ class Migration(migrations.Migration):
|
|||||||
migrations.RemoveField(model_name="assignmentpoll", name="votesabstain"),
|
migrations.RemoveField(model_name="assignmentpoll", name="votesabstain"),
|
||||||
migrations.RemoveField(model_name="assignmentpoll", name="votesno"),
|
migrations.RemoveField(model_name="assignmentpoll", name="votesno"),
|
||||||
migrations.RemoveField(model_name="assignmentpoll", name="published"),
|
migrations.RemoveField(model_name="assignmentpoll", name="published"),
|
||||||
|
migrations.RemoveField(model_name="assignmentrelateduser", name="elected",),
|
||||||
]
|
]
|
||||||
|
@ -41,11 +41,6 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
|||||||
ForeinKey to the user who is related to the assignment.
|
ForeinKey to the user who is related to the assignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
elected = models.BooleanField(default=False)
|
|
||||||
"""
|
|
||||||
Saves the election state of each user
|
|
||||||
"""
|
|
||||||
|
|
||||||
weight = models.IntegerField(default=0)
|
weight = models.IntegerField(default=0)
|
||||||
"""
|
"""
|
||||||
The sort order of the candidates.
|
The sort order of the candidates.
|
||||||
@ -141,7 +136,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
settings.AUTH_USER_MODEL, through="AssignmentRelatedUser"
|
settings.AUTH_USER_MODEL, through="AssignmentRelatedUser"
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
Users that are candidates or elected.
|
Users that are candidates.
|
||||||
|
|
||||||
See AssignmentRelatedUser for more information.
|
See AssignmentRelatedUser for more information.
|
||||||
"""
|
"""
|
||||||
@ -180,14 +175,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
"""
|
"""
|
||||||
Queryset that represents the candidates for the assignment.
|
Queryset that represents the candidates for the assignment.
|
||||||
"""
|
"""
|
||||||
return self.related_users.filter(assignmentrelateduser__elected=False)
|
return self.related_users.all()
|
||||||
|
|
||||||
@property
|
|
||||||
def elected(self):
|
|
||||||
"""
|
|
||||||
Queryset that represents all elected users for the assignment.
|
|
||||||
"""
|
|
||||||
return self.related_users.filter(assignmentrelateduser__elected=True)
|
|
||||||
|
|
||||||
def is_candidate(self, user):
|
def is_candidate(self, user):
|
||||||
"""
|
"""
|
||||||
@ -197,14 +185,6 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
"""
|
"""
|
||||||
return self.candidates.filter(pk=user.pk).exists()
|
return self.candidates.filter(pk=user.pk).exists()
|
||||||
|
|
||||||
def is_elected(self, user):
|
|
||||||
"""
|
|
||||||
Returns True if the user is elected for this assignment.
|
|
||||||
|
|
||||||
Costs one database query.
|
|
||||||
"""
|
|
||||||
return self.elected.filter(pk=user.pk).exists()
|
|
||||||
|
|
||||||
def add_candidate(self, user):
|
def add_candidate(self, user):
|
||||||
"""
|
"""
|
||||||
Adds the user as candidate.
|
Adds the user as candidate.
|
||||||
@ -213,17 +193,9 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
self.assignment_related_users.aggregate(models.Max("weight"))["weight__max"]
|
self.assignment_related_users.aggregate(models.Max("weight"))["weight__max"]
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
defaults = {"elected": False, "weight": weight + 1}
|
defaults = {"weight": weight + 1}
|
||||||
self.assignment_related_users.update_or_create(user=user, defaults=defaults)
|
self.assignment_related_users.update_or_create(user=user, defaults=defaults)
|
||||||
|
|
||||||
def set_elected(self, user):
|
|
||||||
"""
|
|
||||||
Makes user an elected user for this assignment.
|
|
||||||
"""
|
|
||||||
self.assignment_related_users.update_or_create(
|
|
||||||
user=user, defaults={"elected": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
def remove_candidate(self, user):
|
def remove_candidate(self, user):
|
||||||
"""
|
"""
|
||||||
Delete the connection from the assignment to the user.
|
Delete the connection from the assignment to the user.
|
||||||
@ -348,7 +320,11 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
POLLMETHOD_YN = "YN"
|
POLLMETHOD_YN = "YN"
|
||||||
POLLMETHOD_YNA = "YNA"
|
POLLMETHOD_YNA = "YNA"
|
||||||
POLLMETHOD_VOTES = "votes"
|
POLLMETHOD_VOTES = "votes"
|
||||||
POLLMETHODS = (("YN", "YN"), ("YNA", "YNA"), ("votes", "votes"))
|
POLLMETHODS = (
|
||||||
|
(POLLMETHOD_VOTES, "Yes per candidate"),
|
||||||
|
(POLLMETHOD_YN, "Yes/No per candidate"),
|
||||||
|
(POLLMETHOD_YNA, "Yes/No/Abstain per candidate"),
|
||||||
|
)
|
||||||
pollmethod = models.CharField(max_length=5, choices=POLLMETHODS)
|
pollmethod = models.CharField(max_length=5, choices=POLLMETHODS)
|
||||||
|
|
||||||
PERCENT_BASE_YN = "YN"
|
PERCENT_BASE_YN = "YN"
|
||||||
@ -404,7 +380,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
def create_options(self, skip_autoupdate=False):
|
def create_options(self, skip_autoupdate=False):
|
||||||
related_users = AssignmentRelatedUser.objects.filter(
|
related_users = AssignmentRelatedUser.objects.filter(
|
||||||
assignment__id=self.assignment.id
|
assignment__id=self.assignment.id
|
||||||
).exclude(elected=True)
|
)
|
||||||
|
|
||||||
for related_user in related_users:
|
for related_user in related_users:
|
||||||
option = AssignmentOption(
|
option = AssignmentOption(
|
||||||
|
@ -19,10 +19,7 @@ async def assignment_slide(
|
|||||||
assignment = get_model(all_data, "assignments/assignment", element.get("id"))
|
assignment = get_model(all_data, "assignments/assignment", element.get("id"))
|
||||||
|
|
||||||
assignment_related_users: List[Dict[str, Any]] = [
|
assignment_related_users: List[Dict[str, Any]] = [
|
||||||
{
|
{"user": await get_user_name(all_data, aru["user_id"])}
|
||||||
"user": await get_user_name(all_data, aru["user_id"]),
|
|
||||||
"elected": aru["elected"],
|
|
||||||
}
|
|
||||||
for aru in sorted(
|
for aru in sorted(
|
||||||
assignment["assignment_related_users"], key=lambda aru: aru["weight"]
|
assignment["assignment_related_users"], key=lambda aru: aru["weight"]
|
||||||
)
|
)
|
||||||
|
@ -45,7 +45,7 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentRelatedUser
|
model = AssignmentRelatedUser
|
||||||
fields = ("id", "user", "elected", "weight")
|
fields = ("id", "user", "weight")
|
||||||
|
|
||||||
|
|
||||||
class AssignmentVoteSerializer(BaseVoteSerializer):
|
class AssignmentVoteSerializer(BaseVoteSerializer):
|
||||||
|
@ -32,8 +32,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
API endpoint for assignments.
|
API endpoint for assignments.
|
||||||
|
|
||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update, destroy, candidature_self, candidature_other,
|
partial_update, update, destroy, candidature_self, candidature_other and create_poll.
|
||||||
mark_elected and create_poll.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = AssignmentAccessPermissions()
|
access_permissions = AssignmentAccessPermissions()
|
||||||
@ -53,7 +52,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
"destroy",
|
"destroy",
|
||||||
"mark_elected",
|
|
||||||
"sort_related_users",
|
"sort_related_users",
|
||||||
):
|
):
|
||||||
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
@ -81,8 +79,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
candidature (DELETE).
|
candidature (DELETE).
|
||||||
"""
|
"""
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if assignment.is_elected(request.user):
|
|
||||||
raise ValidationError({"detail": "You are already elected."})
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
message = self.nominate_self(request, assignment)
|
message = self.nominate_self(request, assignment)
|
||||||
else:
|
else:
|
||||||
@ -132,8 +128,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
def get_user_from_request_data(self, request):
|
def get_user_from_request_data(self, request):
|
||||||
"""
|
"""
|
||||||
Helper method to get a specific user from request data (not the
|
Helper method to get a specific user from request data (not the
|
||||||
request.user) so that the views self.candidature_other or
|
request.user) so that the view self.candidature_other can play with it.
|
||||||
self.mark_elected can play with it.
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@ -172,10 +167,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
return self.delete_other(request, user, assignment)
|
return self.delete_other(request, user, assignment)
|
||||||
|
|
||||||
def nominate_other(self, request, user, assignment):
|
def nominate_other(self, request, user, assignment):
|
||||||
if assignment.is_elected(user):
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "User {0} is already elected.", "args": [str(user)]}
|
|
||||||
)
|
|
||||||
if assignment.phase == assignment.PHASE_FINISHED:
|
if assignment.phase == assignment.PHASE_FINISHED:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
@ -209,7 +200,7 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
"detail": "You can not delete someone's candidature to this election because it is finished."
|
"detail": "You can not delete someone's candidature to this election because it is finished."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if not assignment.is_candidate(user) and not assignment.is_elected(user):
|
if not assignment.is_candidate(user):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
"detail": "User {0} has no status in this election.",
|
"detail": "User {0} has no status in this election.",
|
||||||
@ -221,37 +212,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
{"detail": "Candidate {0} was withdrawn successfully.", "args": [str(user)]}
|
{"detail": "Candidate {0} was withdrawn successfully.", "args": [str(user)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
@detail_route(methods=["post", "delete"])
|
|
||||||
def mark_elected(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
View to mark other users as elected (POST) or undo this (DELETE).
|
|
||||||
The client has to send {'user': <id>}.
|
|
||||||
"""
|
|
||||||
user = self.get_user_from_request_data(request)
|
|
||||||
assignment = self.get_object()
|
|
||||||
if request.method == "POST":
|
|
||||||
if not assignment.is_candidate(user):
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"detail": "User {0} is not a candidate of this election.",
|
|
||||||
"args": [str(user)],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assignment.set_elected(user)
|
|
||||||
message = "User {0} was successfully elected."
|
|
||||||
else:
|
|
||||||
# request.method == 'DELETE'
|
|
||||||
if not assignment.is_elected(user):
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"detail": "User {0} is not an elected candidate of this election.",
|
|
||||||
"args": [str(user)],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assignment.add_candidate(user)
|
|
||||||
message = "User {0} was successfully unelected."
|
|
||||||
return Response({"detail": message, "args": [str(user)]})
|
|
||||||
|
|
||||||
@detail_route(methods=["post"])
|
@detail_route(methods=["post"])
|
||||||
def sort_related_users(self, request, pk=None):
|
def sort_related_users(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
|
@ -401,52 +401,3 @@ class CandidatureOther(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
class MarkElectedOtherUser(TestCase):
|
|
||||||
"""
|
|
||||||
Tests marking an elected user. We use an extra user here to show that
|
|
||||||
admin can not only mark himself but also other users.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.login(username="admin", password="admin")
|
|
||||||
self.assignment = Assignment.objects.create(
|
|
||||||
title="test_assignment_Ierohsh8rahshofiejai", open_posts=1
|
|
||||||
)
|
|
||||||
self.user = get_user_model().objects.create_user(
|
|
||||||
username="test_user_Oonei3rahji5jugh1eev",
|
|
||||||
password="test_password_aiphahb5Nah0cie4iP7o",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mark_elected(self):
|
|
||||||
self.assignment.add_candidate(
|
|
||||||
get_user_model().objects.get(username="test_user_Oonei3rahji5jugh1eev")
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("assignment-mark-elected", args=[self.assignment.pk]),
|
|
||||||
{"user": self.user.pk},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertTrue(
|
|
||||||
Assignment.objects.get(pk=self.assignment.pk)
|
|
||||||
.elected.filter(username="test_user_Oonei3rahji5jugh1eev")
|
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mark_unelected(self):
|
|
||||||
user = get_user_model().objects.get(username="test_user_Oonei3rahji5jugh1eev")
|
|
||||||
self.assignment.set_elected(user)
|
|
||||||
response = self.client.delete(
|
|
||||||
reverse("assignment-mark-elected", args=[self.assignment.pk]),
|
|
||||||
{"user": self.user.pk},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertFalse(
|
|
||||||
Assignment.objects.get(pk=self.assignment.pk)
|
|
||||||
.elected.filter(username="test_user_Oonei3rahji5jugh1eev")
|
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user