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 candidatureOtherPath = '/candidature_other/';
|
||||
private readonly candidatureSelfPath = '/candidature_self/';
|
||||
private readonly markElectedPath = '/mark_elected/';
|
||||
|
||||
/**
|
||||
* Constructor for the Assignment Repository.
|
||||
@ -158,26 +157,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
||||
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
|
||||
*
|
||||
|
@ -125,6 +125,18 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
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) => {
|
||||
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 {
|
||||
const viewModel = super.createViewModelWithTitles(model);
|
||||
viewModel.getFullName = () => this.getFullName(viewModel);
|
||||
viewModel.getShortName = () => this.getShortName(viewModel);
|
||||
viewModel.getLevelAndNumber = () => this.getLevelAndNumber(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 { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||
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`
|
||||
@ -17,42 +16,35 @@ import { PollService } from '../../site/polls/services/poll.service';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export abstract class BasePollDialogService<V extends ViewBasePoll> {
|
||||
protected dialogComponent: ComponentType<BasePollDialogComponent>;
|
||||
protected dialogComponent: ComponentType<BasePollDialogComponent<V>>;
|
||||
|
||||
public constructor(
|
||||
private dialog: MatDialog,
|
||||
private mapper: CollectionStringMapperService,
|
||||
private service: PollService
|
||||
) {}
|
||||
public constructor(private dialog: MatDialog, private mapper: CollectionStringMapperService) {}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public async openDialog(poll: Partial<V> & Collection): Promise<void> {
|
||||
if (!poll.poll) {
|
||||
this.service.fillDefaultPollData(poll);
|
||||
}
|
||||
public async openDialog(viewPoll: Partial<V> & Collection): Promise<void> {
|
||||
const dialogRef = this.dialog.open(this.dialogComponent, {
|
||||
data: poll,
|
||||
data: viewPoll,
|
||||
...mediumDialogSettings
|
||||
});
|
||||
const result = await dialogRef.afterClosed().toPromise();
|
||||
if (result) {
|
||||
const repo = this.mapper.getRepository(poll.collectionString);
|
||||
if (!poll.poll) {
|
||||
const repo = this.mapper.getRepository(viewPoll.collectionString);
|
||||
if (!viewPoll.poll) {
|
||||
await repo.create(result);
|
||||
} else {
|
||||
let update = result;
|
||||
if (poll.state !== PollState.Created) {
|
||||
if (viewPoll.state !== PollState.Created) {
|
||||
update = {
|
||||
title: result.title,
|
||||
onehundred_percent_base: result.onehundred_percent_base,
|
||||
majority_method: result.majority_method,
|
||||
description: result.description
|
||||
};
|
||||
if (poll.type === PollType.Analog) {
|
||||
if (viewPoll.type === PollType.Analog) {
|
||||
update = {
|
||||
...update,
|
||||
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'
|
||||
'bar action';
|
||||
grid-template-columns: auto min-content;
|
||||
}
|
||||
|
||||
.message {
|
||||
grid-area: message;
|
||||
}
|
||||
.mat-progress-bar-buffer {
|
||||
// TODO theme
|
||||
// background-color: mat-color($background, card) !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.bar {
|
||||
grid-area: bar;
|
||||
}
|
||||
.message {
|
||||
grid-area: message;
|
||||
}
|
||||
|
||||
.action {
|
||||
grid-area: action;
|
||||
.bar {
|
||||
grid-area: bar;
|
||||
}
|
||||
|
||||
.action {
|
||||
grid-area: action;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ export enum AssignmentPollMethods {
|
||||
*/
|
||||
export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
|
||||
public static COLLECTIONSTRING = 'assignments/assignment-poll';
|
||||
public static defaultGroupsConfig = 'assignment_poll_default_groups';
|
||||
public static defaultPollMethodConfig = 'assignment_poll_method';
|
||||
|
||||
public id: number;
|
||||
public assignment_id: number;
|
||||
|
@ -8,7 +8,6 @@ export class AssignmentRelatedUser extends BaseModel<AssignmentRelatedUser> {
|
||||
|
||||
public id: number;
|
||||
public user_id: number;
|
||||
public elected: boolean;
|
||||
public assignment_id: number;
|
||||
public weight: number;
|
||||
|
||||
|
@ -12,6 +12,7 @@ export enum MotionPollMethods {
|
||||
*/
|
||||
export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
|
||||
public static COLLECTIONSTRING = 'motions/motion-poll';
|
||||
public static defaultGroupsConfig = 'motion_poll_default_groups';
|
||||
|
||||
public id: number;
|
||||
public motion_id: number;
|
||||
|
@ -2,7 +2,7 @@ import { BaseDecimalModel } from '../base/base-decimal-model';
|
||||
import { BaseOption } from './base-option';
|
||||
|
||||
export enum PollColor {
|
||||
yes = '#9fd773',
|
||||
yes = '#4caf50',
|
||||
no = '#cc6c5b',
|
||||
abstain = '#a6a6a6',
|
||||
votesvalid = '#e2e2e2',
|
||||
@ -76,6 +76,10 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
||||
return this.isFinished || this.isPublished;
|
||||
}
|
||||
|
||||
public get nextState(): PollState {
|
||||
return this.state + 1;
|
||||
}
|
||||
|
||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
}
|
||||
|
@ -19,6 +19,6 @@ const PollValues = {
|
||||
})
|
||||
export class PollKeyVerbosePipe implements PipeTransform {
|
||||
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 { PollPercentBasePipe } from './poll-percent-base.pipe';
|
||||
|
||||
fdescribe('PollPercentBasePipe', () => {
|
||||
describe('PollPercentBasePipe', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
|
@ -40,7 +40,7 @@ export class PollPercentBasePipe implements PipeTransform {
|
||||
const percentNumber = (value / totalByBase) * 100;
|
||||
if (percentNumber > 0) {
|
||||
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
|
||||
return `(${result}%)`;
|
||||
return `(${result} %)`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -111,7 +111,6 @@ import { GlobalSpinnerComponent } from 'app/site/common/components/global-spinne
|
||||
import { HeightResizingDirective } from './directives/height-resizing.directive';
|
||||
import { TrustPipe } from './pipes/trust.pipe';
|
||||
import { LocalizedDatePipe } from './pipes/localized-date.pipe';
|
||||
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
|
||||
import { ChartsComponent } from './components/charts/charts.component';
|
||||
import { CheckInputComponent } from './components/check-input/check-input.component';
|
||||
import { BannerComponent } from './components/banner/banner.component';
|
||||
@ -277,7 +276,6 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
||||
ChartsModule,
|
||||
TrustPipe,
|
||||
LocalizedDatePipe,
|
||||
BreadcrumbComponent,
|
||||
ChartsComponent,
|
||||
CheckInputComponent,
|
||||
BannerComponent,
|
||||
@ -335,7 +333,6 @@ import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
|
||||
HeightResizingDirective,
|
||||
TrustPipe,
|
||||
LocalizedDatePipe,
|
||||
BreadcrumbComponent,
|
||||
ChartsComponent,
|
||||
CheckInputComponent,
|
||||
BannerComponent,
|
||||
|
@ -67,7 +67,7 @@
|
||||
<!-- candidates list -->
|
||||
<ng-container [ngTemplateOutlet]="candidatesTemplate"></ng-container>
|
||||
<!-- closed polls -->
|
||||
<ng-container *ngIf="assignment">
|
||||
<ng-container *ngIf="assignment && assignment.polls.length">
|
||||
<ng-container *ngFor="let poll of assignment.polls | reverse; trackBy: trackByIndex">
|
||||
<os-assignment-poll [poll]="poll"> </os-assignment-poll>
|
||||
</ng-container>
|
||||
@ -162,7 +162,12 @@
|
||||
<!-- Search for candidates -->
|
||||
<div class="search-field" *ngIf="hasPerms('addOthers')">
|
||||
<form
|
||||
*ngIf="hasPerms('addOthers') && filteredCandidates && filteredCandidates.value.length > 0"
|
||||
*ngIf="
|
||||
hasPerms('addOthers') &&
|
||||
filteredCandidates &&
|
||||
filteredCandidates.value &&
|
||||
filteredCandidates.value.length
|
||||
"
|
||||
[formGroup]="candidatesForm"
|
||||
>
|
||||
<mat-form-field>
|
||||
@ -285,7 +290,6 @@
|
||||
<span translate>Number poll candidates</span>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<!-- TODO searchValueSelector: Parent -->
|
||||
</form>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
|
@ -1,9 +1,11 @@
|
||||
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 { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from '../assignment-poll/assignment-poll.component';
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
|
||||
describe('AssignmentDetailComponent', () => {
|
||||
let component: AssignmentDetailComponent;
|
||||
@ -12,7 +14,12 @@ describe('AssignmentDetailComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [AssignmentDetailComponent, AssignmentPollComponent, AssignmentPollVoteComponent]
|
||||
declarations: [
|
||||
AssignmentDetailComponent,
|
||||
AssignmentPollComponent,
|
||||
AssignmentPollVoteComponent,
|
||||
PollProgressComponent
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
@ -23,6 +23,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { AssignmentPhases, ViewAssignment } from '../../models/view-assignment';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||
@ -176,7 +177,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
private promptService: PromptService,
|
||||
private pdfService: AssignmentPdfExportService,
|
||||
private mediafileRepo: MediafileRepositoryService,
|
||||
private pollDialog: AssignmentPollDialogService
|
||||
private pollDialog: AssignmentPollDialogService,
|
||||
private assignmentPollService: AssignmentPollService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
this.subscriptions.push(
|
||||
@ -306,11 +308,15 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
* Creates a new Poll
|
||||
*/
|
||||
public openDialog(): void {
|
||||
this.pollDialog.openDialog({
|
||||
// TODO: That is not really a ViewObject
|
||||
const dialogData = {
|
||||
collectionString: ViewAssignmentPoll.COLLECTIONSTRING,
|
||||
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>
|
||||
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<h2 translate>Result</h2>
|
||||
|
||||
<div class="result-wrapper">
|
||||
<div [class]="chartType === 'horizontalBar' ? 'result-wrapper-bar-chart' : 'result-wrapper-pie-chart'">
|
||||
<!-- Result Table -->
|
||||
<mat-table class="result-table" [dataSource]="poll.tableData">
|
||||
<ng-container matColumnDef="user" sticky>
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
|
||||
</ng-container>
|
||||
<div *ngIf="!isVotedPoll">
|
||||
<ng-container matColumnDef="yes">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Yes' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
|
||||
</ng-container>
|
||||
<table class="assignment-result-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th translate>Candidates</th>
|
||||
<th translate>Votes</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of poll.tableData">
|
||||
<td>
|
||||
<span>
|
||||
{{ 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">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'No' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.no }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="abstain">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'Abstain' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.abstain }}</mat-cell>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isVotedPoll">
|
||||
<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>
|
||||
<span>
|
||||
{{ vote.amount | parsePollNumber }}
|
||||
<span *ngIf="vote.showPercent">
|
||||
{{ vote.amount | pollPercentBase: poll }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Result Chart -->
|
||||
<os-charts
|
||||
class="result-chart"
|
||||
class="assignment-result-chart"
|
||||
[ngClass]="chartType === 'doughnut' ? 'pie-chart' : ''"
|
||||
*ngIf="chartDataSubject.value"
|
||||
[type]="chartType"
|
||||
[labels]="candidatesLabels"
|
||||
@ -105,7 +110,8 @@
|
||||
[ngClass]="voteOptionStyle[vote.votes[option.user_id].value].css"
|
||||
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>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@ -123,17 +129,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Meta Infos -->
|
||||
<div class="poll-content small">
|
||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||
<div class="assignment-poll-meta">
|
||||
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||
{{ 'Groups' | translate }}:
|
||||
|
||||
<span *ngFor="let group of poll.groups; let i = index">
|
||||
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||
</span>
|
||||
</div>
|
||||
</small>
|
||||
|
||||
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
||||
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
||||
<small *ngIf="poll.onehundred_percent_base">
|
||||
{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@ -141,7 +148,7 @@
|
||||
<!-- More Menu -->
|
||||
<mat-menu #pollDetailMenu="matMenu">
|
||||
<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>
|
||||
<span translate>Edit</span>
|
||||
</button>
|
||||
|
@ -1,9 +1,22 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.result-wrapper {
|
||||
%assignment-result-wrapper {
|
||||
margin-top: 2em;
|
||||
display: grid;
|
||||
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:
|
||||
'chart'
|
||||
'results'
|
||||
@ -11,7 +24,7 @@
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
.result-wrapper {
|
||||
.result-wrapper-pie-chart {
|
||||
grid-template-areas:
|
||||
'results chart'
|
||||
'names names';
|
||||
@ -19,13 +32,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.result-table {
|
||||
.assignment-result-table {
|
||||
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;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
max-width: 300px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.named-result-table {
|
||||
@ -36,27 +70,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.poll-content {
|
||||
.assignment-poll-meta {
|
||||
display: grid;
|
||||
text-align: right;
|
||||
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 {
|
||||
display: block;
|
||||
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 { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
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 { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
@ -43,13 +44,6 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
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';
|
||||
|
||||
public constructor(
|
||||
@ -130,9 +124,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
}
|
||||
|
||||
this.setVotesData(Object.values(votes));
|
||||
|
||||
this.candidatesLabels = this.pollService.getChartLabels(this.poll);
|
||||
|
||||
this.isReady = true;
|
||||
}
|
||||
|
||||
@ -157,4 +149,17 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
protected hasPerms(): boolean {
|
||||
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>
|
||||
|
||||
<!-- Analog voting -->
|
||||
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
||||
<!-- Candidate values -->
|
||||
<form [formGroup]="dialogVoteForm">
|
||||
<!-- Candidates -->
|
||||
<div formGroupName="options">
|
||||
<div *ngFor="let option of options" class="votes-grid">
|
||||
<div>
|
||||
@ -33,36 +35,30 @@
|
||||
></os-check-input>
|
||||
</div>
|
||||
</form>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Publish Check -->
|
||||
<div class="spacer-top-20">
|
||||
<mat-checkbox
|
||||
[(ngModel)]="publishImmediately"
|
||||
(change)="publishStateChanged($event.checked)"
|
||||
>
|
||||
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
|
||||
<span translate>Publish immediately</span>
|
||||
</mat-checkbox>
|
||||
<mat-error *ngIf="!dialogVoteForm.valid" translate>
|
||||
If you want to publish after creating, you have to fill at least one of the fields.
|
||||
</mat-error>
|
||||
</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>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- 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>
|
||||
</button>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<button mat-button [mat-dialog-close]="false">
|
||||
<span translate>Cancel</span>
|
||||
</button>
|
||||
|
@ -11,9 +11,7 @@ import { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/mod
|
||||
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.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 { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
type OptionsObject = { user_id: number; user: ViewUser }[];
|
||||
@ -26,7 +24,7 @@ type OptionsObject = { user_id: number; user: ViewUser }[];
|
||||
templateUrl: './assignment-poll-dialog.component.html',
|
||||
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
|
||||
*/
|
||||
@ -41,7 +39,7 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent imple
|
||||
public specialValues: [number, string][];
|
||||
|
||||
@ViewChild('pollForm', { static: true })
|
||||
protected pollForm: PollFormComponent;
|
||||
protected pollForm: PollFormComponent<ViewAssignmentPoll>;
|
||||
|
||||
/**
|
||||
* vote entries for each option in this component. Is empty if method
|
||||
@ -65,13 +63,14 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent imple
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent>,
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent<ViewAssignmentPoll>>,
|
||||
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewAssignmentPoll>
|
||||
) {
|
||||
super(title, translate, matSnackbar, dialogRef);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
// TODO: not solid.
|
||||
// on new poll creation, poll.options does not exist, so we have to build a substitute from the assignment candidates
|
||||
this.options = this.pollData.options
|
||||
? this.pollData.options
|
||||
@ -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
|
||||
*
|
||||
|
@ -1,8 +1,4 @@
|
||||
<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)">
|
||||
<!-- TODO: Someone should make this pretty -->
|
||||
|
@ -50,7 +50,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.defineVoteOptions();
|
||||
if (this.poll) {
|
||||
this.defineVoteOptions();
|
||||
}
|
||||
|
||||
this.subscriptions.push(
|
||||
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
||||
this.votes = votes;
|
||||
@ -91,8 +94,6 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
}
|
||||
|
||||
protected updateVotes(): void {
|
||||
console.log('currentVotes: ', this.currentVotes);
|
||||
|
||||
if (this.user && this.votes && this.poll) {
|
||||
const filtered = this.votes.filter(
|
||||
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()">
|
||||
<div class="assignment-poll-wrapper">
|
||||
<div class="assignment-poll-title-header">
|
||||
<div>
|
||||
<!-- Title -->
|
||||
<mat-card-title>
|
||||
<a routerLink="/assignments/polls/{{ poll.id }}">
|
||||
{{ poll.title }}
|
||||
</a>
|
||||
</mat-card-title>
|
||||
<div class="poll-properties">
|
||||
<mat-chip
|
||||
*osPerms="'assignments.can_manage_polls'"
|
||||
class="poll-state active"
|
||||
[disableRipple]="true"
|
||||
[matMenuTriggerFor]="triggerMenu"
|
||||
[class]="poll.stateVerbose.toLowerCase()"
|
||||
[ngClass]="{ disabled: !poll.getNextStates() }"
|
||||
>
|
||||
|
||||
<!-- Type and State -->
|
||||
<div>
|
||||
<span *ngIf="poll.type !== 'analog'"> {{ poll.typeVerbose | translate }} · </span>
|
||||
<span>
|
||||
{{ poll.stateVerbose | translate }}
|
||||
</mat-chip>
|
||||
<span *ngIf="poll.type !== 'analog'">
|
||||
{{ poll.typeVerbose | translate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="poll-menu">
|
||||
<!-- Buttons -->
|
||||
<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"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</button>
|
||||
</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 *ngIf="hasVotes">
|
||||
<os-charts
|
||||
[class]="chartType === 'doughnut' ? 'doughnut-chart' : 'bar-chart'"
|
||||
[type]="chartType"
|
||||
[labels]="candidatesLabels"
|
||||
[data]="chartDataSubject"
|
||||
[hasPadding]="false"
|
||||
></os-charts>
|
||||
</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>
|
||||
<div class="poll-detail-button-wrapper">
|
||||
<a mat-button routerLink="/assignments/polls/{{ poll.id }}">
|
||||
{{ 'More' | translate }}
|
||||
<a mat-icon-button routerLink="/assignments/polls/{{ poll.id }}" matTooltip="{{ 'More' | translate }}">
|
||||
<mat-icon class="small-icon">
|
||||
visibility
|
||||
</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<button mat-menu-item (click)="openDialog()">
|
||||
@ -75,6 +86,11 @@
|
||||
<span translate>Ballot paper</span>
|
||||
</button>
|
||||
<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()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.assignment-poll-wrapper {
|
||||
@import '~assets/styles/poll-common-styles.scss';
|
||||
position: relative;
|
||||
margin: 0 15px;
|
||||
|
||||
@ -9,37 +10,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
margin: auto 0;
|
||||
@ -47,4 +17,23 @@
|
||||
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 { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||
import { AssignmentPollVoteComponent } from '../assignment-poll-vote/assignment-poll-vote.component';
|
||||
import { AssignmentPollComponent } from './assignment-poll.component';
|
||||
|
||||
@ -11,8 +12,8 @@ describe('AssignmentPollComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AssignmentPollComponent, AssignmentPollVoteComponent],
|
||||
imports: [E2EImportsModule]
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [AssignmentPollComponent, AssignmentPollVoteComponent, PollProgressComponent]
|
||||
}).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 { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
/**
|
||||
@ -52,9 +51,6 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
public descriptionForm: FormGroup;
|
||||
|
||||
/**
|
||||
* permission checks.
|
||||
* TODO stub
|
||||
*
|
||||
* @returns true if the user is permitted to do operations
|
||||
*/
|
||||
public get canManage(): boolean {
|
||||
@ -93,9 +89,6 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
/*this.majorityChoice =
|
||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
||||
null;*/
|
||||
this.descriptionForm = this.formBuilder.group({
|
||||
description: this.poll ? this.poll.description : ''
|
||||
});
|
||||
@ -115,70 +108,4 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
(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 { 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 { 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 { ViewAssignmentOption } from './view-assignment-option';
|
||||
|
||||
@ -35,7 +34,6 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
||||
}
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
// TODO: update to new voting system?
|
||||
return {
|
||||
getBasicProjectorElement: options => ({
|
||||
name: AssignmentPoll.COLLECTIONSTRING,
|
||||
@ -49,27 +47,36 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
||||
}
|
||||
|
||||
public generateTableData(): PollTableData[] {
|
||||
const data = this.options
|
||||
.map(candidate => ({
|
||||
yes: candidate.yes,
|
||||
no: candidate.no,
|
||||
abstain: candidate.abstain,
|
||||
user: candidate.user.full_name,
|
||||
showPercent: true
|
||||
const tableData: PollTableData[] = this.options.map(candidate => ({
|
||||
votingOption: candidate.user.short_name,
|
||||
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
|
||||
|
||||
value: this.voteTableKeys.map(
|
||||
key =>
|
||||
({
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
return tableData;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
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 { ViewUser } from 'app/site/users/models/view-user';
|
||||
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[];
|
||||
polls: ViewAssignmentPoll[];
|
||||
tags?: ViewTag[];
|
||||
attachments?: ViewMediafile[];
|
||||
}
|
||||
|
@ -185,11 +185,11 @@ export class AssignmentPdfService {
|
||||
const tableData = poll.generateTableData();
|
||||
|
||||
for (const pollResult of tableData) {
|
||||
const voteOption = this.translate.instant(this.pollKeyVerbose.transform(pollResult.votingOption));
|
||||
const resultLine = this.getPollResult(pollResult, poll);
|
||||
|
||||
const tableLine = [
|
||||
{
|
||||
text: pollResult.user
|
||||
text: voteOption
|
||||
},
|
||||
{
|
||||
text: resultLine
|
||||
@ -217,11 +217,13 @@ export class AssignmentPdfService {
|
||||
* Converts pollData to a printable string representation
|
||||
*/
|
||||
private getPollResult(votingResult: PollTableData, poll: ViewAssignmentPoll): string {
|
||||
const resultList = poll.pollmethodFields.map(field => {
|
||||
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(field));
|
||||
const resultValue = this.parsePollNumber.transform(votingResult[field]);
|
||||
const resultInPercent = this.pollPercentBase.transform(votingResult[field], poll);
|
||||
return `${votingKey}: ${resultValue} ${resultInPercent ? resultInPercent : ''}`;
|
||||
const resultList = votingResult.value.map(singleResult => {
|
||||
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(singleResult.vote));
|
||||
const resultValue = this.parsePollNumber.transform(singleResult.amount);
|
||||
const resultInPercent = this.pollPercentBase.transform(singleResult.amount, poll);
|
||||
return `${votingKey}${!!votingKey ? ': ' : ''}${resultValue} ${
|
||||
singleResult.showPercent && resultInPercent ? resultInPercent : ''
|
||||
}`;
|
||||
});
|
||||
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 { 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 { AssignmentPollService } from './assignment-poll.service';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
|
||||
/**
|
||||
@ -16,7 +15,7 @@ import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
export class AssignmentPollDialogService extends BasePollDialogService<ViewAssignmentPoll> {
|
||||
protected dialogComponent = AssignmentPollDialogComponent;
|
||||
|
||||
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService, service: AssignmentPollService) {
|
||||
super(dialog, mapper, service);
|
||||
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService) {
|
||||
super(dialog, mapper);
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,6 @@ export class AssignmentPollPdfService extends PollPdfService {
|
||||
* @param title The identifier of the motion
|
||||
* @param subtitle The actual motion title
|
||||
*/
|
||||
// TODO: typing of result
|
||||
protected createBallot(data: AbstractPollData): object {
|
||||
return {
|
||||
columns: [
|
||||
@ -137,7 +136,6 @@ export class AssignmentPollPdfService extends PollPdfService {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: typing of result
|
||||
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
||||
const candidates = poll.options.sort((a, b) => {
|
||||
return a.weight - b.weight;
|
||||
@ -147,15 +145,23 @@ export class AssignmentPollPdfService extends PollPdfService {
|
||||
? this.createBallotOption(cand.user.full_name)
|
||||
: this.createYNBallotEntry(cand.user.full_name, poll.pollmethod);
|
||||
});
|
||||
|
||||
if (poll.pollmethod === 'votes') {
|
||||
const noEntry = this.createBallotOption(this.translate.instant('No'));
|
||||
noEntry.margin[1] = 25;
|
||||
resultObject.push(noEntry);
|
||||
if (poll.global_no) {
|
||||
const noEntry = this.createBallotOption(this.translate.instant('No'));
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO: typing of result
|
||||
private createYNBallotEntry(option: string, method: AssignmentPollMethods): object {
|
||||
const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
||||
const columnstack = choices.map(choice => {
|
||||
@ -182,7 +188,6 @@ export class AssignmentPollPdfService extends PollPdfService {
|
||||
* @param poll
|
||||
* @returns pdfMake definitions
|
||||
*/
|
||||
// TODO: typing of result
|
||||
private createPollHint(poll: ViewAssignmentPoll): object {
|
||||
return {
|
||||
text: poll.description || '',
|
||||
|
@ -5,11 +5,9 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { Collection } from 'app/shared/models/base/collection';
|
||||
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
||||
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -25,6 +23,10 @@ export class AssignmentPollService extends PollService {
|
||||
*/
|
||||
public defaultMajorityMethod: MajorityMethod;
|
||||
|
||||
public defaultGroupIds: number[];
|
||||
|
||||
public defaultPollMethod: AssignmentPollMethods;
|
||||
|
||||
/**
|
||||
* Constructor. Subscribes to the configuration values needed
|
||||
* @param config ConfigService
|
||||
@ -42,16 +44,21 @@ export class AssignmentPollService extends PollService {
|
||||
config
|
||||
.get<MajorityMethod>('motion_poll_default_majority_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 {
|
||||
super.fillDefaultPollData(poll);
|
||||
public getDefaultPollData(): AssignmentPoll {
|
||||
const poll = new AssignmentPoll(super.getDefaultPollData());
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.assignment_id === poll.assignment_id)
|
||||
.length;
|
||||
|
||||
poll.title = !length ? this.translate.instant('Ballot') : `${this.translate.instant('Ballot')} (${length + 1})`;
|
||||
poll.pollmethod = AssignmentPollMethods.YN;
|
||||
poll.assignment_id = poll.assignment_id;
|
||||
poll.pollmethod = this.defaultPollMethod;
|
||||
|
||||
return poll;
|
||||
}
|
||||
|
||||
private sumOptionsYN(poll: PollData): number {
|
||||
|
@ -1,9 +1,8 @@
|
||||
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 { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
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';
|
||||
|
||||
export interface MotionPollTitleInformation {
|
||||
@ -15,58 +14,12 @@ export const MotionPollMethodsVerbose = {
|
||||
YNA: 'Yes/No/Abstain'
|
||||
};
|
||||
|
||||
interface TableKey {
|
||||
vote: string;
|
||||
icon?: string;
|
||||
canHide: boolean;
|
||||
showPercent: boolean;
|
||||
}
|
||||
|
||||
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
|
||||
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
||||
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
||||
|
||||
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 {
|
||||
return this.options[0];
|
||||
}
|
||||
@ -80,22 +33,30 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
||||
}
|
||||
|
||||
public generateTableData(): PollTableData[] {
|
||||
let tableData = this.options.flatMap(vote =>
|
||||
this.tableKeys.map(key => ({
|
||||
key: key.vote,
|
||||
value: vote[key.vote],
|
||||
canHide: key.canHide,
|
||||
icon: key.icon,
|
||||
showPercent: key.showPercent
|
||||
}))
|
||||
let tableData: PollTableData[] = this.options.flatMap(vote =>
|
||||
this.voteTableKeys.map(key => this.createTableDataEntry(key, vote))
|
||||
);
|
||||
tableData.push(
|
||||
...this.voteKeys.map(key => ({ key: key.vote, value: this[key.vote], showPercent: key.showPercent }))
|
||||
);
|
||||
tableData = tableData.filter(entry => entry.canHide === false || entry.value || entry.value !== -2);
|
||||
tableData.push(...this.sumTableKeys.map(key => this.createTableDataEntry(key)));
|
||||
|
||||
tableData = tableData.filter(localeTableData => !localeTableData.value.some(result => result.hide));
|
||||
|
||||
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 {
|
||||
return {
|
||||
getBasicProjectorElement: options => ({
|
||||
@ -116,16 +77,6 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
||||
public anySpecialVotes(): boolean {
|
||||
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 {
|
||||
|
@ -2,6 +2,7 @@ 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 { ManageSubmittersComponent } from '../manage-submitters/manage-submitters.component';
|
||||
import { MotionCommentsComponent } from '../motion-comments/motion-comments.component';
|
||||
import { MotionDetailDiffComponent } from '../motion-detail-diff/motion-detail-diff.component';
|
||||
@ -26,7 +27,8 @@ describe('MotionDetailComponent', () => {
|
||||
MotionPollComponent,
|
||||
MotionDetailOriginalChangeRecommendationsComponent,
|
||||
MotionDetailDiffComponent,
|
||||
MotionPollVoteComponent
|
||||
MotionPollVoteComponent,
|
||||
PollProgressComponent
|
||||
]
|
||||
}).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 { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.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 { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
@ -467,7 +468,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
private motionFilterService: MotionFilterListService,
|
||||
private amendmentFilterService: AmendmentFilterListService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private pollDialog: MotionPollDialogService
|
||||
private pollDialog: MotionPollDialogService,
|
||||
private motionPollService: MotionPollService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
}
|
||||
@ -1625,9 +1627,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public openDialog(poll?: ViewMotionPoll): void {
|
||||
this.pollDialog.openDialog(
|
||||
poll ? poll : { collectionString: ViewMotionPoll.COLLECTIONSTRING, motion_id: this.motion.id }
|
||||
);
|
||||
public openDialog(): void {
|
||||
// TODO: Could be simpler, requires a lot of refactoring
|
||||
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-container *ngIf="poll">
|
||||
<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.stateHasVotes">
|
||||
<h2 translate>Result</h2>
|
||||
|
||||
<div class="result-wrapper" *ngIf="poll.hasVotes">
|
||||
<!-- Chart -->
|
||||
<os-charts
|
||||
class="result-chart"
|
||||
*ngIf="chartDataSubject.value"
|
||||
[type]="chartType"
|
||||
[data]="chartDataSubject"
|
||||
></os-charts>
|
||||
|
||||
<!-- result table -->
|
||||
<table class="result-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th translate>Votes</th>
|
||||
<th colspan="2" translate>Votes</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of poll.tableData">
|
||||
<tr *ngFor="let row of poll.tableData" [class]="row.votingOption">
|
||||
<!-- YNA/Valid etc -->
|
||||
<td>
|
||||
<os-icon-container *ngIf="row.icon" [icon]="row.icon">
|
||||
{{ row.key | pollKeyVerbose | translate }}
|
||||
<os-icon-container *ngIf="row.value[0].icon" [icon]="row.value[0].icon">
|
||||
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||
</os-icon-container>
|
||||
<span *ngIf="!row.icon">
|
||||
{{ row.key | pollKeyVerbose | translate }}
|
||||
<span *ngIf="!row.value[0].icon">
|
||||
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Percent numbers -->
|
||||
<td class="result-cell-definition">
|
||||
{{ row.value | parsePollNumber }}
|
||||
<span *ngIf="row.showPercent">
|
||||
{{ row.value | pollPercentBase: poll }}
|
||||
<span *ngIf="row.value[0].showPercent">
|
||||
{{ row.value[0].amount | pollPercentBase: poll }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Voices -->
|
||||
<td class="result-cell-definition">
|
||||
{{ row.value[0].amount | parsePollNumber }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Named table: only show if votes are present -->
|
||||
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
||||
<h3>{{ 'Single votes' | translate }}</h3>
|
||||
<os-list-view-table
|
||||
[listObservable]="votesDataObservable"
|
||||
[columns]="columnDefinition"
|
||||
[filterProps]="filterProps"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="false"
|
||||
[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>
|
||||
<!-- Chart -->
|
||||
<div class="doughnut-chart">
|
||||
<os-charts
|
||||
*ngIf="chartDataSubject.value"
|
||||
[type]="chartType"
|
||||
[data]="chartDataSubject"
|
||||
[showLegend]="false"
|
||||
[hasPadding]="false"
|
||||
></os-charts>
|
||||
</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 class="poll-content small">
|
||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||
<div class="poll-content">
|
||||
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||
{{ 'Groups' | translate }}:
|
||||
|
||||
<span *ngFor="let group of poll.groups; let i = index">
|
||||
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||
</span>
|
||||
</div>
|
||||
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
||||
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
||||
</small>
|
||||
<small>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@ -116,7 +132,7 @@
|
||||
<!-- More Menu -->
|
||||
<mat-menu #pollDetailMenu="matMenu">
|
||||
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog()">
|
||||
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog(poll)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span translate>Edit</span>
|
||||
</button>
|
||||
|
@ -2,53 +2,51 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.poll-content {
|
||||
text-align: right;
|
||||
display: grid;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.result-wrapper {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-areas:
|
||||
'chart'
|
||||
'results'
|
||||
'names';
|
||||
}
|
||||
grid-gap: 2em;
|
||||
margin: 2em;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
|
||||
@include desktop {
|
||||
.result-wrapper {
|
||||
grid-template-areas:
|
||||
'results chart'
|
||||
'names names';
|
||||
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-table {
|
||||
// display: block;
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
.result-cell-definition {
|
||||
text-align: center;
|
||||
tr {
|
||||
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 {
|
||||
grid-area: chart;
|
||||
max-width: 300px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
.doughnut-chart {
|
||||
display: block;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.named-result-table {
|
||||
|
@ -1,12 +1,4 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@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';
|
||||
|
||||
fdescribe('MotionPollDialogComponent', () => {
|
||||
describe('MotionPollDialogComponent', () => {
|
||||
let component: 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',
|
||||
styleUrls: ['./motion-poll-dialog.component.scss']
|
||||
})
|
||||
export class MotionPollDialogComponent extends BasePollDialogComponent {
|
||||
export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotionPoll> {
|
||||
public motionPollMethods = { YNA: MotionPollMethodsVerbose.YNA };
|
||||
|
||||
@ViewChild('pollForm', { static: false })
|
||||
protected pollForm: PollFormComponent;
|
||||
protected pollForm: PollFormComponent<ViewMotionPoll>;
|
||||
|
||||
public constructor(
|
||||
private fb: FormBuilder,
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent>,
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent<ViewMotionPoll>>,
|
||||
@Inject(MAT_DIALOG_DATA) public pollData: Partial<ViewMotionPoll>
|
||||
) {
|
||||
super(title, translate, matSnackbar, dialogRef);
|
||||
|
@ -1,10 +1,10 @@
|
||||
<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>
|
||||
</div>
|
||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||
<div *ngIf="vmanager.canVote(poll)" class="vote-button-grid">
|
||||
<!-- Voting -->
|
||||
<p *ngFor="let option of voteOptions">
|
||||
<div class="vote-button" *ngFor="let option of voteOptions">
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveVote(option.vote)"
|
||||
@ -13,11 +13,6 @@
|
||||
<mat-icon> {{ option.icon }}</mat-icon>
|
||||
</button>
|
||||
<span class="vote-label"> {{ option.label | translate }} </span>
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<!-- TODO most of the messages are not making sense -->
|
||||
<!-- <ng-container *ngIf="!vmanager.canVote(poll)">
|
||||
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
||||
</ng-container> -->
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -1,17 +1,33 @@
|
||||
@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 {
|
||||
background-color: $votes-yes-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
background-color: $votes-no-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
background-color: $votes-abstain-color;
|
||||
}
|
||||
|
||||
.vote-label {
|
||||
margin-left: 1em;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ 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 { MotionPollVoteComponent } from './motion-poll-vote.component';
|
||||
|
||||
describe('MotionPollVoteComponent', () => {
|
||||
@ -11,7 +12,7 @@ describe('MotionPollVoteComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [MotionPollVoteComponent]
|
||||
declarations: [MotionPollVoteComponent, PollProgressComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
@ -1,44 +1,68 @@
|
||||
<div class="poll-preview-wrapper" *ngIf="poll && showPoll()">
|
||||
<mat-card class="motion-poll-wrapper" *ngIf="poll">
|
||||
<!-- Poll Infos -->
|
||||
<div class="poll-title-wrapper">
|
||||
<!-- Title -->
|
||||
<a class="poll-title" [routerLink]="pollLink">
|
||||
{{ poll.title }}
|
||||
</a>
|
||||
<!-- Title Area -->
|
||||
<div class="poll-title-area">
|
||||
<!-- Title -->
|
||||
<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 -->
|
||||
<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">
|
||||
<mat-icon class="small-icon">more_horiz</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- State chip -->
|
||||
<div class="poll-properties" *osPerms="'motions.can_manage_polls'">
|
||||
<div *ngIf="pollService.isElectronicVotingEnabled && poll.type !== 'analog'">
|
||||
{{ poll.typeVerbose | translate }}
|
||||
</div>
|
||||
|
||||
<mat-chip
|
||||
disableRipple
|
||||
[matMenuTriggerFor]="triggerMenu"
|
||||
class="poll-state active"
|
||||
[class]="poll.stateVerbose.toLowerCase()"
|
||||
[ngClass]="{ 'disabled': !poll.getNextStates() }"
|
||||
>
|
||||
{{ poll.stateVerbose }}
|
||||
</mat-chip>
|
||||
</div>
|
||||
<!-- Change state button -->
|
||||
<div *osPerms="'motions.can_manage_polls'">
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngIf="!poll.isPublished"
|
||||
[ngClass]="pollStateActions[poll.state].css"
|
||||
(click)="changeState(poll.nextState)"
|
||||
>
|
||||
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
|
||||
<span class="next-state-label">
|
||||
{{ poll.nextStateActionVerbose | translate }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||
</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>
|
||||
<div [routerLink]="pollLink" class="poll-link-wrapper">
|
||||
<div class="poll-link-wrapper">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"
|
||||
></ng-container>
|
||||
@ -46,7 +70,28 @@
|
||||
</ng-template>
|
||||
|
||||
<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">
|
||||
<os-charts
|
||||
*ngIf="showChart"
|
||||
@ -57,31 +102,13 @@
|
||||
>
|
||||
</os-charts>
|
||||
</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 class="poll-detail-button-wrapper">
|
||||
<a mat-button [routerLink]="pollLink">
|
||||
{{ 'More' | translate }}
|
||||
</a>
|
||||
|
||||
<!-- In Progress hint -->
|
||||
<div class="motion-couting-in-progress-hint" *osPerms="'motions.can_manage_polls'; complement: true">
|
||||
<span *ngIf="poll.isFinished">
|
||||
{{ 'Counting is in progress' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@ -104,18 +131,16 @@
|
||||
</button>
|
||||
<div *osPerms="'motions.can_manage_polls'">
|
||||
<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()">
|
||||
<mat-icon color="warn">delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
||||
<!-- Select state menu -->
|
||||
<mat-menu #triggerMenu="matMenu">
|
||||
<ng-container *ngIf="poll">
|
||||
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.getNextStates() | keyvalue">
|
||||
<span translate>{{ state.key }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
|
@ -4,70 +4,47 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.poll-preview-wrapper {
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
.motion-poll-wrapper {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.poll-title {
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.poll-title-wrapper {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-areas: 'title actions';
|
||||
grid-template-columns: auto min-content;
|
||||
|
||||
.poll-title-actions {
|
||||
float: right;
|
||||
}
|
||||
.poll-title-area {
|
||||
grid-area: title;
|
||||
margin-top: 1em;
|
||||
|
||||
.poll-properties {
|
||||
margin: 4px 0;
|
||||
|
||||
.mat-chip {
|
||||
margin: 0 4px;
|
||||
|
||||
&.active {
|
||||
cursor: pointer;
|
||||
.poll-title {
|
||||
font-size: 125%;
|
||||
}
|
||||
}
|
||||
|
||||
.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-actions {
|
||||
grid-area: actions;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-chart-wrapper {
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-areas: 'placeholder chart legend';
|
||||
grid-template-columns: auto minmax(50px, 20%) auto;
|
||||
grid-gap: 20px;
|
||||
margin: 2em;
|
||||
// try to find max scale
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
|
||||
.doughnut-chart {
|
||||
grid-area: chart;
|
||||
display: block;
|
||||
max-width: 200px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.vote-legend {
|
||||
grid-area: legend;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
div + div {
|
||||
margin-top: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.votes-yes {
|
||||
@ -93,16 +70,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.poll-preview-meta-info {
|
||||
span {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.next-state-label {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.poll-content {
|
||||
padding-bottom: 8px;
|
||||
.start-poll-button {
|
||||
color: green !important;
|
||||
}
|
||||
|
||||
.poll-footer {
|
||||
text-align: end;
|
||||
.stop-poll-button {
|
||||
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';
|
||||
|
||||
@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 { PollProgressComponent } from 'app/site/polls/components/poll-progress/poll-progress.component';
|
||||
import { MotionPollVoteComponent } from '../motion-poll-vote/motion-poll-vote.component';
|
||||
import { MotionPollComponent } from './motion-poll.component';
|
||||
|
||||
@ -11,7 +12,7 @@ describe('MotionPollComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [MotionPollComponent, MotionPollVoteComponent]
|
||||
declarations: [MotionPollComponent, MotionPollVoteComponent, PollProgressComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
|
@ -4,7 +4,6 @@ import { Title } from '@angular/platform-browser';
|
||||
|
||||
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 { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
@ -114,22 +113,12 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
public pollRepo: MotionPollRepositoryService,
|
||||
pollDialog: MotionPollDialogService,
|
||||
public pollService: PollService,
|
||||
private operator: OperatorService,
|
||||
private pdfService: MotionPollPdfService
|
||||
) {
|
||||
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 {
|
||||
console.log('picture_as_pdf');
|
||||
this.pdfService.printBallots(this.poll);
|
||||
}
|
||||
|
||||
|
@ -371,13 +371,17 @@ export class MotionPdfService {
|
||||
motion.polls.forEach(poll => {
|
||||
if (poll.hasVotes) {
|
||||
const tableData = poll.generateTableData();
|
||||
|
||||
tableData.forEach(votingResult => {
|
||||
const resultKey = this.translate.instant(this.pollKeyVerbose.transform(votingResult.key));
|
||||
const resultValue = this.parsePollNumber.transform(votingResult.value);
|
||||
column1.push(`${resultKey}:`);
|
||||
const votingOption = this.translate.instant(
|
||||
this.pollKeyVerbose.transform(votingResult.votingOption)
|
||||
);
|
||||
const value = votingResult.value[0];
|
||||
const resultValue = this.parsePollNumber.transform(value.amount);
|
||||
column1.push(`${votingOption}:`);
|
||||
column2.push(resultValue);
|
||||
if (votingResult.showPercent) {
|
||||
const resultInPercent = this.pollPercentBase.transform(votingResult.value, poll);
|
||||
if (value.showPercent) {
|
||||
const resultInPercent = this.pollPercentBase.transform(value.amount, poll);
|
||||
column3.push(resultInPercent);
|
||||
}
|
||||
});
|
||||
|
@ -4,7 +4,6 @@ import { MatDialog } from '@angular/material';
|
||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.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 { MotionPollService } from './motion-poll.service';
|
||||
import { ViewMotionPoll } from '../models/view-motion-poll';
|
||||
|
||||
/**
|
||||
@ -16,7 +15,7 @@ import { ViewMotionPoll } from '../models/view-motion-poll';
|
||||
export class MotionPollDialogService extends BasePollDialogService<ViewMotionPoll> {
|
||||
protected dialogComponent = MotionPollDialogComponent;
|
||||
|
||||
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService, service: MotionPollService) {
|
||||
super(dialog, mapper, service);
|
||||
public constructor(dialog: MatDialog, mapper: CollectionStringMapperService) {
|
||||
super(dialog, mapper);
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,8 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { Collection } from 'app/shared/models/base/collection';
|
||||
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-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';
|
||||
|
||||
interface PollResultData {
|
||||
@ -34,6 +32,8 @@ export class MotionPollService extends PollService {
|
||||
*/
|
||||
public defaultMajorityMethod: MajorityMethod;
|
||||
|
||||
public defaultGroupIds: number[];
|
||||
|
||||
/**
|
||||
* Constructor. Subscribes to the configuration values needed
|
||||
* @param config ConfigService
|
||||
@ -51,15 +51,18 @@ export class MotionPollService extends PollService {
|
||||
config
|
||||
.get<MajorityMethod>('motion_poll_default_majority_method')
|
||||
.subscribe(method => (this.defaultMajorityMethod = method));
|
||||
|
||||
config.get<number[]>(MotionPoll.defaultGroupsConfig).subscribe(ids => (this.defaultGroupIds = ids));
|
||||
}
|
||||
|
||||
public fillDefaultPollData(poll: Partial<ViewMotionPoll> & Collection): void {
|
||||
super.fillDefaultPollData(poll);
|
||||
public getDefaultPollData(): MotionPoll {
|
||||
const poll = new MotionPoll(super.getDefaultPollData());
|
||||
const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === poll.motion_id).length;
|
||||
|
||||
poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`;
|
||||
poll.pollmethod = MotionPollMethods.YNA;
|
||||
poll.motion_id = poll.motion_id;
|
||||
|
||||
return poll;
|
||||
}
|
||||
|
||||
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 { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.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 { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||
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;
|
||||
|
||||
/**
|
||||
* The breadcrumbs for the poll-states.
|
||||
*/
|
||||
public breadcrumbs: Breadcrumb[] = [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public openDialog(): void {
|
||||
this.pollDialog.openDialog(this.poll);
|
||||
public openDialog(viewPoll: V): void {
|
||||
this.pollDialog.openDialog(viewPoll);
|
||||
}
|
||||
|
||||
protected onDeleted(): void {}
|
||||
@ -198,7 +191,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
this.repo.getViewModelObservable(params.id).subscribe(poll => {
|
||||
if (poll) {
|
||||
this.poll = poll;
|
||||
this.updateBreadcrumbs();
|
||||
this.onPollLoaded();
|
||||
this.waitForOptions();
|
||||
this.checkData();
|
||||
@ -218,81 +210,4 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
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 { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { PollFormComponent } from './poll-form/poll-form.component';
|
||||
import { ViewBasePoll } from '../models/view-base-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;
|
||||
|
||||
protected pollForm: PollFormComponent;
|
||||
protected pollForm: PollFormComponent<T>;
|
||||
|
||||
public dialogVoteForm: FormGroup;
|
||||
|
||||
@ -23,7 +24,7 @@ export abstract class BasePollDialogComponent extends BaseViewComponent {
|
||||
title: Title,
|
||||
protected translate: TranslateService,
|
||||
matSnackbar: MatSnackBar,
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent>
|
||||
public dialogRef: MatDialogRef<BasePollDialogComponent<T>>
|
||||
) {
|
||||
super(title, translate, matSnackbar);
|
||||
}
|
||||
|
@ -18,6 +18,21 @@ export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseView
|
||||
|
||||
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(
|
||||
titleService: Title,
|
||||
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
|
||||
*/
|
||||
|
@ -7,6 +7,8 @@
|
||||
</h2>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<!-- TODO: rather disable forms than duplicate them -->
|
||||
<div *ngIf="data && data.state > 1" class="poll-preview-meta-info">
|
||||
<span class="short-description" *ngFor="let value of pollValues">
|
||||
<span class="short-description-label subtitle" translate>
|
||||
|
@ -5,8 +5,8 @@ import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { PollFormComponent } from './poll-form.component';
|
||||
|
||||
describe('PollFormComponent', () => {
|
||||
let component: PollFormComponent;
|
||||
let fixture: ComponentFixture<PollFormComponent>;
|
||||
let component: PollFormComponent<any>;
|
||||
let fixture: ComponentFixture<PollFormComponent<any>>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
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 { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import {
|
||||
MajorityMethodVerbose,
|
||||
PercentBaseVerbose,
|
||||
@ -28,7 +29,7 @@ import { PollService } from '../../services/poll.service';
|
||||
templateUrl: './poll-form.component.html',
|
||||
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.
|
||||
*/
|
||||
@ -44,7 +45,7 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
||||
public pollMethods: { [key: string]: string };
|
||||
|
||||
@Input()
|
||||
public data: Partial<ViewBasePoll>;
|
||||
public data: Partial<T>;
|
||||
|
||||
/**
|
||||
* The different types the poll can accept.
|
||||
@ -103,18 +104,15 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
||||
public ngOnInit(): void {
|
||||
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.groups_id) {
|
||||
if (this.data.collectionString === ViewAssignmentPoll.COLLECTIONSTRING) {
|
||||
this.data.groups_id = this.configService.instant('assignment_poll_default_groups');
|
||||
} else {
|
||||
this.data.groups_id = this.configService.instant('motion_poll_default_groups');
|
||||
if (this.data instanceof ViewAssignmentPoll) {
|
||||
if (this.data.assignment && !this.data.votes_amount) {
|
||||
this.data.votes_amount = this.data.assignment.open_posts;
|
||||
}
|
||||
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 => {
|
||||
@ -193,26 +191,31 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
||||
* @param data Passing the properties of the poll.
|
||||
*/
|
||||
private updatePollValues(data: { [key: string]: any }): void {
|
||||
this.pollValues = [
|
||||
[this.pollService.getVerboseNameForKey('type'), this.pollService.getVerboseNameForValue('type', data.type)]
|
||||
];
|
||||
// show pollmethod only for assignment polls
|
||||
if (this.data.pollClassType === PollClassType.Assignment) {
|
||||
this.pollValues.push([
|
||||
this.pollService.getVerboseNameForKey('pollmethod'),
|
||||
this.pollService.getVerboseNameForValue('pollmethod', data.pollmethod)
|
||||
]);
|
||||
}
|
||||
if (data.type !== 'analog') {
|
||||
this.pollValues.push([
|
||||
this.pollService.getVerboseNameForKey('groups'),
|
||||
this.groupRepo.getNameForIds(...([] || (data && data.groups_id)))
|
||||
]);
|
||||
}
|
||||
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]);
|
||||
if (this.data) {
|
||||
this.pollValues = [
|
||||
[
|
||||
this.pollService.getVerboseNameForKey('type'),
|
||||
this.pollService.getVerboseNameForValue('type', data.type)
|
||||
]
|
||||
];
|
||||
// show pollmethod only for assignment polls
|
||||
if (this.data.pollClassType === PollClassType.Assignment) {
|
||||
this.pollValues.push([
|
||||
this.pollService.getVerboseNameForKey('pollmethod'),
|
||||
this.pollService.getVerboseNameForValue('pollmethod', data.pollmethod)
|
||||
]);
|
||||
}
|
||||
if (data.type !== 'analog') {
|
||||
this.pollValues.push([
|
||||
this.pollService.getVerboseNameForKey('groups'),
|
||||
this.groupRepo.getNameForIds(...([] || (data && data.groups_id)))
|
||||
]);
|
||||
}
|
||||
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>
|
||||
{{ 'Casted votes' | translate }}: {{ poll.votescast }} / {{ max }},
|
||||
{{ 'valid votes' | translate}}: {{ poll.votesvalid }} / {{ max }},
|
||||
{{ 'invalid votes' | translate}}: {{ poll.votesinvalid }} / {{ max }}
|
||||
</span>
|
||||
<div *ngIf="poll" class="poll-progress-wrapper">
|
||||
<div class="motion-vote-number" *ngIf="poll.pollClassType === 'motion'">
|
||||
<span>{{ poll.votescast }} / {{ max }}</span>
|
||||
</div>
|
||||
|
||||
<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(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [PollProgressComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
@ -30,7 +30,11 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.userRepo
|
||||
.getViewModelListObservable()
|
||||
.pipe(
|
||||
map(users =>
|
||||
users.filter(user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length)
|
||||
if (this.poll) {
|
||||
this.userRepo
|
||||
.getViewModelListObservable()
|
||||
.pipe(
|
||||
map(users =>
|
||||
users.filter(user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(users => {
|
||||
this.max = users.length;
|
||||
});
|
||||
.subscribe(users => {
|
||||
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 { BaseProjectableViewModel } from 'app/site/base/base-projectable-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.
|
||||
*/
|
||||
export interface PollTableData {
|
||||
key?: string;
|
||||
value?: number;
|
||||
yes?: number;
|
||||
no?: number;
|
||||
abstain?: number;
|
||||
user?: string;
|
||||
canHide?: boolean;
|
||||
votingOption: string;
|
||||
votingOptionSubtitle?: string;
|
||||
value: VotingResult[];
|
||||
}
|
||||
|
||||
export interface VotingResult {
|
||||
vote?: 'yes' | 'no' | 'abstain' | 'votesvalid' | 'votesinvalid' | 'votescast';
|
||||
amount?: number;
|
||||
icon?: string;
|
||||
hide?: boolean;
|
||||
showPercent?: boolean;
|
||||
}
|
||||
|
||||
@ -35,7 +37,7 @@ export const PollClassTypeVerbose = {
|
||||
export const PollStateVerbose = {
|
||||
1: 'Created',
|
||||
2: 'Started',
|
||||
3: 'Finished',
|
||||
3: 'Finished (unpublished)',
|
||||
4: 'Published'
|
||||
};
|
||||
|
||||
@ -85,6 +87,41 @@ export const PercentBaseVerbose = {
|
||||
export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends BaseProjectableViewModel<M> {
|
||||
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[] {
|
||||
if (!this._tableData.length) {
|
||||
this._tableData = this.generateTableData();
|
||||
@ -108,6 +145,10 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
||||
return PollStateVerbose[this.state];
|
||||
}
|
||||
|
||||
public get nextStateActionVerbose(): string {
|
||||
return PollStateChangeActionVerbose[this.nextState];
|
||||
}
|
||||
|
||||
public get typeVerbose(): string {
|
||||
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];
|
||||
}
|
||||
|
||||
public get showAbstainPercent(): boolean {
|
||||
return this.onehundred_percent_base === PercentBase.YNA;
|
||||
}
|
||||
|
||||
public abstract readonly pollClassType: 'motion' | 'assignment';
|
||||
|
||||
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 {
|
||||
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 { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
|
||||
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 { 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 {
|
||||
MajorityMethodVerbose,
|
||||
PercentBaseVerbose,
|
||||
PollPropertyVerbose,
|
||||
PollTypeVerbose,
|
||||
ViewBasePoll
|
||||
PollTypeVerbose
|
||||
} from 'app/site/polls/models/view-base-poll';
|
||||
import { ConstantsService } from '../../../core/core-services/constants.service';
|
||||
|
||||
@ -129,6 +127,11 @@ export abstract class PollService {
|
||||
*/
|
||||
public abstract defaultMajorityMethod: MajorityMethod;
|
||||
|
||||
/**
|
||||
* Per default entitled to vote
|
||||
*/
|
||||
public abstract defaultGroupIds: number[];
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param poll the poll/object to fill
|
||||
*/
|
||||
public fillDefaultPollData(poll: Partial<ViewBasePoll> & Collection): void {
|
||||
poll.onehundred_percent_base = this.defaultPercentBase;
|
||||
poll.majority_method = this.defaultMajorityMethod;
|
||||
poll.type = PollType.Analog;
|
||||
public getDefaultPollData(): Partial<BasePoll> {
|
||||
return {
|
||||
onehundred_percent_base: this.defaultPercentBase,
|
||||
majority_method: this.defaultMajorityMethod,
|
||||
groups_id: this.defaultGroupIds,
|
||||
type: PollType.Analog
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
public getFullName: () => string;
|
||||
public getShortName: () => string;
|
||||
public getLevelAndNumber: () => string;
|
||||
|
||||
/**
|
||||
* Formats the category for search
|
||||
|
@ -5,7 +5,6 @@ export interface AssignmentSlideData {
|
||||
open_posts: number;
|
||||
assignment_related_users: {
|
||||
user: string;
|
||||
elected: boolean;
|
||||
}[];
|
||||
number_poll_candidates: boolean;
|
||||
}
|
||||
|
@ -11,13 +11,11 @@
|
||||
<ol *ngIf="data.data.number_poll_candidates">
|
||||
<li *ngFor="let candidate of data.data.assignment_related_users">
|
||||
{{ candidate.user }}
|
||||
<mat-icon *ngIf="candidate.elected">star</mat-icon>
|
||||
</li>
|
||||
</ol>
|
||||
<ul *ngIf="!data.data.number_poll_candidates">
|
||||
<li *ngFor="let candidate of data.data.assignment_related_users">
|
||||
{{ candidate.user }}
|
||||
<mat-icon *ngIf="candidate.elected">star</mat-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
@ -99,6 +99,10 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.user-subtitle {
|
||||
color: mat-color($foreground, secondary-text);
|
||||
}
|
||||
|
||||
mat-card-header {
|
||||
background-color: mat-color($background, app-bar);
|
||||
}
|
||||
@ -162,10 +166,6 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.mat-progress-bar-buffer {
|
||||
background-color: mat-color($background, card) !important;
|
||||
}
|
||||
|
||||
.primary-foreground {
|
||||
color: mat-color($primary);
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Define the colors used for yes, no and abstain
|
||||
*/
|
||||
$votes-yes-color: #9fd773;
|
||||
$votes-yes-color: #4caf50;
|
||||
$votes-no-color: #cc6c5b;
|
||||
$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/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/assignments/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
||||
|
||||
/** fonts */
|
||||
@import './assets/styles/fonts.scss';
|
||||
@ -60,6 +61,7 @@ $narrow-spacing: (
|
||||
@include os-banner-style($theme);
|
||||
@include os-motion-poll-style($theme);
|
||||
@include os-motion-poll-detail-style($theme);
|
||||
@include os-assignment-poll-detail-style($theme);
|
||||
}
|
||||
|
||||
/** Load projector specific SCSS values */
|
||||
|
@ -51,12 +51,26 @@ def get_config_variables():
|
||||
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(
|
||||
name="assignment_poll_add_candidates_to_list_of_speakers",
|
||||
default_value=True,
|
||||
input_type="boolean",
|
||||
label="Put all candidates on the list of speakers",
|
||||
weight=415,
|
||||
weight=420,
|
||||
group="Elections",
|
||||
subgroup="Voting",
|
||||
)
|
||||
|
@ -175,7 +175,12 @@ class Migration(migrations.Migration):
|
||||
model_name="assignmentpoll",
|
||||
name="pollmethod",
|
||||
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(
|
||||
|
@ -20,4 +20,5 @@ class Migration(migrations.Migration):
|
||||
migrations.RemoveField(model_name="assignmentpoll", name="votesabstain"),
|
||||
migrations.RemoveField(model_name="assignmentpoll", name="votesno"),
|
||||
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.
|
||||
"""
|
||||
|
||||
elected = models.BooleanField(default=False)
|
||||
"""
|
||||
Saves the election state of each user
|
||||
"""
|
||||
|
||||
weight = models.IntegerField(default=0)
|
||||
"""
|
||||
The sort order of the candidates.
|
||||
@ -141,7 +136,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
||||
settings.AUTH_USER_MODEL, through="AssignmentRelatedUser"
|
||||
)
|
||||
"""
|
||||
Users that are candidates or elected.
|
||||
Users that are candidates.
|
||||
|
||||
See AssignmentRelatedUser for more information.
|
||||
"""
|
||||
@ -180,14 +175,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
||||
"""
|
||||
Queryset that represents the candidates for the assignment.
|
||||
"""
|
||||
return self.related_users.filter(assignmentrelateduser__elected=False)
|
||||
|
||||
@property
|
||||
def elected(self):
|
||||
"""
|
||||
Queryset that represents all elected users for the assignment.
|
||||
"""
|
||||
return self.related_users.filter(assignmentrelateduser__elected=True)
|
||||
return self.related_users.all()
|
||||
|
||||
def is_candidate(self, user):
|
||||
"""
|
||||
@ -197,14 +185,6 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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"]
|
||||
or 0
|
||||
)
|
||||
defaults = {"elected": False, "weight": weight + 1}
|
||||
defaults = {"weight": weight + 1}
|
||||
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):
|
||||
"""
|
||||
Delete the connection from the assignment to the user.
|
||||
@ -348,7 +320,11 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
POLLMETHOD_YN = "YN"
|
||||
POLLMETHOD_YNA = "YNA"
|
||||
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)
|
||||
|
||||
PERCENT_BASE_YN = "YN"
|
||||
@ -404,7 +380,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
def create_options(self, skip_autoupdate=False):
|
||||
related_users = AssignmentRelatedUser.objects.filter(
|
||||
assignment__id=self.assignment.id
|
||||
).exclude(elected=True)
|
||||
)
|
||||
|
||||
for related_user in related_users:
|
||||
option = AssignmentOption(
|
||||
|
@ -19,10 +19,7 @@ async def assignment_slide(
|
||||
assignment = get_model(all_data, "assignments/assignment", element.get("id"))
|
||||
|
||||
assignment_related_users: List[Dict[str, Any]] = [
|
||||
{
|
||||
"user": await get_user_name(all_data, aru["user_id"]),
|
||||
"elected": aru["elected"],
|
||||
}
|
||||
{"user": await get_user_name(all_data, aru["user_id"])}
|
||||
for aru in sorted(
|
||||
assignment["assignment_related_users"], key=lambda aru: aru["weight"]
|
||||
)
|
||||
|
@ -45,7 +45,7 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AssignmentRelatedUser
|
||||
fields = ("id", "user", "elected", "weight")
|
||||
fields = ("id", "user", "weight")
|
||||
|
||||
|
||||
class AssignmentVoteSerializer(BaseVoteSerializer):
|
||||
|
@ -32,8 +32,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||
API endpoint for assignments.
|
||||
|
||||
There are the following views: metadata, list, retrieve, create,
|
||||
partial_update, update, destroy, candidature_self, candidature_other,
|
||||
mark_elected and create_poll.
|
||||
partial_update, update, destroy, candidature_self, candidature_other and create_poll.
|
||||
"""
|
||||
|
||||
access_permissions = AssignmentAccessPermissions()
|
||||
@ -53,7 +52,6 @@ class AssignmentViewSet(ModelViewSet):
|
||||
"partial_update",
|
||||
"update",
|
||||
"destroy",
|
||||
"mark_elected",
|
||||
"sort_related_users",
|
||||
):
|
||||
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||
@ -81,8 +79,6 @@ class AssignmentViewSet(ModelViewSet):
|
||||
candidature (DELETE).
|
||||
"""
|
||||
assignment = self.get_object()
|
||||
if assignment.is_elected(request.user):
|
||||
raise ValidationError({"detail": "You are already elected."})
|
||||
if request.method == "POST":
|
||||
message = self.nominate_self(request, assignment)
|
||||
else:
|
||||
@ -132,8 +128,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||
def get_user_from_request_data(self, request):
|
||||
"""
|
||||
Helper method to get a specific user from request data (not the
|
||||
request.user) so that the views self.candidature_other or
|
||||
self.mark_elected can play with it.
|
||||
request.user) so that the view self.candidature_other can play with it.
|
||||
"""
|
||||
if not isinstance(request.data, dict):
|
||||
raise ValidationError(
|
||||
@ -172,10 +167,6 @@ class AssignmentViewSet(ModelViewSet):
|
||||
return self.delete_other(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:
|
||||
raise ValidationError(
|
||||
{
|
||||
@ -209,7 +200,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||
"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(
|
||||
{
|
||||
"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_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"])
|
||||
def sort_related_users(self, request, pk=None):
|
||||
"""
|
||||
|
@ -401,52 +401,3 @@ class CandidatureOther(TestCase):
|
||||
)
|
||||
|
||||
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