assignment improvements
- layouting - fix one-vote polls - filter candidate list - candidate sorting - fix adding/removing of candidates - avoid ui jumping - fix quorum calculations
This commit is contained in:
parent
0f1df91915
commit
9dfac94099
@ -93,33 +93,40 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
||||
private createViewAssignmentRelatedUsers(
|
||||
assignmentRelatedUsers: AssignmentRelatedUser[]
|
||||
): ViewAssignmentRelatedUser[] {
|
||||
return assignmentRelatedUsers.map(aru => {
|
||||
const user = this.viewModelStoreService.get(ViewUser, aru.user_id);
|
||||
return new ViewAssignmentRelatedUser(aru, user);
|
||||
});
|
||||
return assignmentRelatedUsers
|
||||
.map(aru => {
|
||||
const user = this.viewModelStoreService.get(ViewUser, aru.user_id);
|
||||
return new ViewAssignmentRelatedUser(aru, user);
|
||||
})
|
||||
.sort((a, b) => a.weight - b.weight);
|
||||
}
|
||||
|
||||
private createViewAssignmentPolls(assignmentPolls: AssignmentPoll[]): ViewAssignmentPoll[] {
|
||||
return assignmentPolls.map(poll => {
|
||||
const options = poll.options.map(option => {
|
||||
const user = this.viewModelStoreService.get(ViewUser, option.candidate_id);
|
||||
return new ViewAssignmentPollOption(option, user);
|
||||
});
|
||||
const options = poll.options
|
||||
.map(option => {
|
||||
const user = this.viewModelStoreService.get(ViewUser, option.candidate_id);
|
||||
return new ViewAssignmentPollOption(option, user);
|
||||
})
|
||||
.sort((a, b) => a.weight - b.weight);
|
||||
return new ViewAssignmentPoll(poll, options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another user as a candidate
|
||||
* Adds/removes another user to/from the candidates list of an assignment
|
||||
*
|
||||
* @param userId User id of a candidate
|
||||
* @param user A ViewUser
|
||||
* @param assignment The assignment to add the candidate to
|
||||
* @param adding optional boolean to force an add (true)/ remove (false)
|
||||
* of the candidate. Else, the candidate will be added if not on the list,
|
||||
* and removed if on the list
|
||||
*/
|
||||
public async changeCandidate(userId: number, assignment: ViewAssignment): Promise<void> {
|
||||
const data = { user: userId };
|
||||
if (assignment.candidates.some(candidate => candidate.id === userId)) {
|
||||
public async changeCandidate(user: ViewUser, assignment: ViewAssignment, adding?: boolean): Promise<void> {
|
||||
const data = { user: user.id };
|
||||
if (assignment.candidates.some(candidate => candidate.id === user.id) && adding !== true) {
|
||||
await this.httpService.delete(this.restPath + assignment.id + this.candidatureOtherPath, data);
|
||||
} else {
|
||||
} else if (adding !== false) {
|
||||
await this.httpService.post(this.restPath + assignment.id + this.candidatureOtherPath, data);
|
||||
}
|
||||
}
|
||||
@ -149,6 +156,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
||||
*/
|
||||
public async addPoll(assignment: ViewAssignment): Promise<void> {
|
||||
await this.httpService.post(this.restPath + assignment.id + this.createPollPath);
|
||||
// TODO: change current tab to new poll
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +193,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
||||
const votes = poll.options.map(option => {
|
||||
switch (poll.pollmethod) {
|
||||
case 'votes':
|
||||
return { Votes: option.votes.find(v => v.value === 'Yes').weight };
|
||||
return { Votes: option.votes.find(v => v.value === 'Votes').weight };
|
||||
case 'yn':
|
||||
return {
|
||||
Yes: option.votes.find(v => v.value === 'Yes').weight,
|
||||
@ -232,16 +240,14 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting the candidates
|
||||
* TODO untested stub
|
||||
* Sends a request to sort an assignment's candidates
|
||||
*
|
||||
* @param sortedCandidates
|
||||
* @param sortedCandidates the id of the assignment related users (note: NOT viewUsers)
|
||||
* @param assignment
|
||||
*/
|
||||
public async sortCandidates(sortedCandidates: any[], assignment: ViewAssignment): Promise<void> {
|
||||
throw Error('TODO');
|
||||
// const restPath = `/rest/assignments/assignment/${assignment.id}/sort_related_users`;
|
||||
// const data = { related_users: sortedCandidates };
|
||||
// await this.httpService.post(restPath, data);
|
||||
public async sortCandidates(sortedCandidates: number[], assignment: ViewAssignment): Promise<void> {
|
||||
const restPath = `/rest/assignments/assignment/${assignment.id}/sort_related_users/`;
|
||||
const data = { related_users: sortedCandidates };
|
||||
await this.httpService.post(restPath, data);
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ import { _ } from 'app/core/translate/translation-marker';
|
||||
|
||||
/**
|
||||
* The possible keys of a poll object that represent numbers.
|
||||
* TODO Should be 'key of MotionPoll if type of key is number'
|
||||
* TODO: normalize MotionPoll model and other poll models
|
||||
* TODO Should be 'key of MotionPoll|AssinmentPoll if type of key is number'
|
||||
*/
|
||||
export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'yes' | 'no' | 'abstain';
|
||||
|
||||
@ -13,12 +12,12 @@ export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'y
|
||||
* TODO: may be obsolete if the server switches to lower case only
|
||||
* (lower case variants are already in CalculablePollKey)
|
||||
*/
|
||||
export type PollVoteValue = 'Yes' | 'No' | 'Abstain';
|
||||
export type PollVoteValue = 'Yes' | 'No' | 'Abstain' | 'Votes';
|
||||
|
||||
/**
|
||||
* Interface representing possible majority calculation methods. The implementing
|
||||
* calc function should return an integer number that must be reached for the
|
||||
* option to reach the quorum, or null if disabled
|
||||
* option to successfully fulfill the quorum, or null if disabled
|
||||
*/
|
||||
export interface MajorityMethod {
|
||||
value: string;
|
||||
@ -33,17 +32,26 @@ export const PollMajorityMethod: MajorityMethod[] = [
|
||||
{
|
||||
value: 'simple_majority',
|
||||
display_name: 'Simple majority',
|
||||
calc: base => Math.ceil(base * 0.5)
|
||||
calc: base => {
|
||||
const q = base * 0.5;
|
||||
return Number.isInteger(q) ? q + 1 : Math.ceil(q);
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 'two-thirds_majority',
|
||||
display_name: 'Two-thirds majority',
|
||||
calc: base => Math.ceil((base / 3) * 2)
|
||||
calc: base => {
|
||||
const q = (base / 3) * 2;
|
||||
return Number.isInteger(q) ? q + 1 : Math.ceil(q);
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 'three-quarters_majority',
|
||||
display_name: 'Three-quarters majority',
|
||||
calc: base => Math.ceil((base / 4) * 3)
|
||||
calc: base => {
|
||||
const q = (base / 4) * 3;
|
||||
return Number.isInteger(q) ? q + 1 : Math.ceil(q);
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 'disabled',
|
||||
@ -95,6 +103,7 @@ export abstract class PollService {
|
||||
|
||||
/**
|
||||
* empty constructor
|
||||
*
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
|
@ -8,10 +8,7 @@
|
||||
>
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="assignment && !newAssignment">
|
||||
<span *ngIf="!editAssignment">{{ assignment.getTitle() }}</span>
|
||||
<span *ngIf="editAssignment">{{ assignmentForm.get('title').value }}</span>
|
||||
</h2>
|
||||
<h2 *ngIf="!newAssignment" translate>Election</h2>
|
||||
<h2 *ngIf="newAssignment" translate>New election</h2>
|
||||
</div>
|
||||
|
||||
@ -23,17 +20,17 @@
|
||||
</div>
|
||||
|
||||
<mat-menu #assignmentDetailMenu="matMenu">
|
||||
<!-- delete -->
|
||||
<!-- print, edit, delete -->
|
||||
<div *ngIf="assignment">
|
||||
<!-- PDF -->
|
||||
<button mat-menu-item (click)="onDownloadPdf()">
|
||||
<!-- TODO: results or descritipon. Results if published -->
|
||||
<!-- TODO: results or description. Results if published -->
|
||||
<mat-icon>picture_as_pdf</mat-icon>
|
||||
<span translate>PDF</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<!-- Delete -->
|
||||
</div>
|
||||
<div *ngIf="assignment && hasPerms('manage')">
|
||||
<button mat-menu-item class="red-warning-text" (click)="onDeleteAssignmentButton()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
@ -43,12 +40,6 @@
|
||||
</os-head-bar>
|
||||
|
||||
<div class="content-container">
|
||||
<!-- Title -->
|
||||
<div class="title on-transition-fade" *ngIf="assignment && !editAssignment">
|
||||
<div class="title-line">
|
||||
<h1>{{ assignment.getTitle() }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container>
|
||||
</div>
|
||||
|
||||
@ -57,119 +48,130 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #desktopView>
|
||||
<mat-card class="os-card">
|
||||
<div *ngIf="editAssignment">
|
||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||
</div>
|
||||
<div *ngIf="!editAssignment">
|
||||
<mat-card class="os-card">
|
||||
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
||||
</mat-card>
|
||||
<mat-card class="os-card">
|
||||
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-card>
|
||||
<div *ngIf="editAssignment">
|
||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||
</div>
|
||||
<div *ngIf="!editAssignment">
|
||||
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #metaInfoTemplate>
|
||||
<span translate>Meta information</span>
|
||||
<div *ngIf="assignment; "flex-spaced"">
|
||||
<mat-card class="os-card" *ngIf="assignment">
|
||||
<h1>{{ assignment.getTitle() }}</h1>
|
||||
<div *ngIf="assignment">
|
||||
<div *ngIf="assignment.assignment.description" [innerHTML]="assignment.assignment.description"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span translate>Number of persons to be elected</span>:
|
||||
<span>{{ assignment.assignment.open_posts }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span> {{ phaseString | translate }}</span>
|
||||
<mat-form-field>
|
||||
<mat-label translate>Phase</mat-label>
|
||||
<mat-select
|
||||
class="selection"
|
||||
[disabled]="!hasPerms('manage')"
|
||||
(selectionChange)="setPhase($event)"
|
||||
[value]="assignment.phase"
|
||||
>
|
||||
<mat-option *ngFor="let option of phaseOptions" [value]="option.value">
|
||||
{{ option.display_name | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<span translate>Phase</span>:
|
||||
<mat-basic-chip *ngIf="hasPerms('manage')" [matMenuTriggerFor]="phaseMenu" class="bluegrey" disableRipple>
|
||||
{{ assignment.phaseString | translate }}
|
||||
</mat-basic-chip>
|
||||
<mat-basic-chip *ngIf="!hasPerms('manage')" class="bluegrey" disableRipple>
|
||||
{{ assignment.phaseString | translate }}
|
||||
</mat-basic-chip>
|
||||
<mat-menu #phaseMenu="matMenu">
|
||||
<button *ngFor="let option of phaseOptions" mat-menu-item (click)="onSetPhaseButton(option.value)">
|
||||
{{ option.display_name | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #contentTemplate>
|
||||
<ng-container *ngIf="assignment && assignment.phase !== 2" [ngTemplateOutlet]="candidatesTemplate">
|
||||
</ng-container>
|
||||
<!-- TODO related agenda item to create/updade: internal status; parent_id ? -->
|
||||
<ng-container [ngTemplateOutlet]="pollTemplate"></ng-container>
|
||||
<!-- TODO different status/display if finished -->
|
||||
<mat-card class="os-card">
|
||||
<ng-container *ngIf="assignment && !assignment.isFinished" [ngTemplateOutlet]="candidatesTemplate">
|
||||
</ng-container>
|
||||
<!-- TODO related agenda item to create/updade: internal status; parent_id ? -->
|
||||
<ng-container [ngTemplateOutlet]="pollTemplate"></ng-container>
|
||||
<!-- TODO different status/display if finished -->
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
|
||||
<!-- poll template -->
|
||||
<ng-template #pollTemplate>
|
||||
<div>
|
||||
<!-- TODO: header. New poll button to the right, and with icon -->
|
||||
<!-- new poll button -->
|
||||
<button type="button" mat-button *ngIf="hasPerms('createPoll')" (click)="createPoll()">
|
||||
<span translate>Create poll</span>
|
||||
</button>
|
||||
</div>
|
||||
<mat-tab-group (selectedTabChange)="onTabChange()" *ngIf="assignment && assignment.polls">
|
||||
<mat-tab-group
|
||||
(selectedTabChange)="onTabChange()"
|
||||
*ngIf="assignment && assignment.polls && assignment.polls.length"
|
||||
>
|
||||
<!-- TODO avoid animation/switching on update -->
|
||||
<mat-tab *ngFor="let poll of assignment.polls; let i = index" [label]="getPollLabel(poll, i)">
|
||||
<mat-tab
|
||||
*ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex"
|
||||
[label]="getPollLabel(poll, i)"
|
||||
>
|
||||
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #candidatesTemplate>
|
||||
<!-- TODO two columns here: Left candidates; right add/remove -->
|
||||
<div>
|
||||
<div>
|
||||
<div *ngIf="assignment && assignment.candidates">
|
||||
<!-- TODO: Sorting -->
|
||||
<div *ngFor="let candidate of assignment.candidates">
|
||||
<span>{{ candidate.full_name }}</span>
|
||||
<button mat-button *ngIf="hasPerms('addOthers')" (click)="removeUser(candidate)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div *ngIf="hasPerms('addOthers')">
|
||||
<!-- candidates: if open: Select list, sortable (like list of speakers) -->
|
||||
<os-search-value-selector
|
||||
*ngIf="hasPerms('addOthers')"
|
||||
ngDefaultControl
|
||||
placeholder="Add candidate"
|
||||
[formControl]="candidatesForm.get('candidate')"
|
||||
[InputListValues]="availableCandidates"
|
||||
[form]="candidatesForm"
|
||||
>
|
||||
</os-search-value-selector>
|
||||
<button mat-button [disabled]="!candidatesForm.get('candidate').value" (click)="addUser()">
|
||||
<span translate>add</span>
|
||||
</button>
|
||||
<!-- TODO disable if no user set; filter out users already in list of candidates -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- class="spacer-top-10" -->
|
||||
<button
|
||||
type="button"
|
||||
mat-button
|
||||
color="primary"
|
||||
*ngIf="hasPerms('addSelf') && !isSelfCandidate"
|
||||
(click)="addSelf()"
|
||||
<!-- candidates -->
|
||||
<div class="candidates-list" *ngIf="assignment && assignment.assignmentRelatedUsers && assignment.assignmentRelatedUsers.length > 0">
|
||||
<os-sorting-list
|
||||
[input]="assignment.assignmentRelatedUsers"
|
||||
[live]="true"
|
||||
[count]="true"
|
||||
[enable]="hasPerms('addOthers')"
|
||||
(sortEvent)="onSortingChange($event)"
|
||||
>
|
||||
<span class="upper" translate>Add me</span>
|
||||
</button>
|
||||
<br />
|
||||
<button type="button" mat-button *ngIf="hasPerms('addSelf') && isSelfCandidate" (click)="removeSelf()">
|
||||
<span translate>Remove me</span>
|
||||
</button>
|
||||
<br />
|
||||
<!-- implicit item references into the component using ng-template slot -->
|
||||
<ng-template let-item>
|
||||
<span *ngIf="hasPerms('addOthers')">
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Remove candidate' | translate }}"
|
||||
(click)="removeUser(item)"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</ng-template>
|
||||
</os-sorting-list>
|
||||
</div>
|
||||
<!-- Search for candidates -->
|
||||
<div *ngIf="hasPerms('addOthers')">
|
||||
<form *ngIf="filteredCandidates && filteredCandidates.value.length > 0" [formGroup]="candidatesForm">
|
||||
<os-search-value-selector
|
||||
*ngIf="hasPerms('addOthers')"
|
||||
ngDefaultControl
|
||||
listname="{{ 'Select a new candidate' | translate }}"
|
||||
[formControl]="candidatesForm.get('userId')"
|
||||
[InputListValues]="filteredCandidates"
|
||||
[form]="candidatesForm"
|
||||
[multiple]="false"
|
||||
>
|
||||
<!-- TODO: better class here: wider -->
|
||||
<!-- TODO: Performance check: something seems off here with filteredCandidates -->
|
||||
</os-search-value-selector>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Add me and remove me if OP has correct permission -->
|
||||
<div *ngIf="assignment && hasPerms('addSelf') && assignment.candidates" class="add-self-buttons">
|
||||
<div>
|
||||
<button mat-stroked-button (click)="addSelf()" *ngIf="!isSelfCandidate">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span translate>Add me</span>
|
||||
</button>
|
||||
<button mat-stroked-button (click)="removeSelf()" *ngIf="isSelfCandidate">
|
||||
<mat-icon>remove</mat-icon>
|
||||
<span translate>Remove me</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="assignment && hasPerms('createPoll')">
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-button (click)="createPoll()">
|
||||
<mat-icon color="primary">poll</mat-icon>
|
||||
<span translate>New poll</span>
|
||||
</button>
|
||||
</div>
|
||||
<mat-divider *ngIf="assignment && assignment.polls && assignment.polls.length"> </mat-divider>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #assignmentFormTemplate>
|
||||
@ -180,15 +182,17 @@
|
||||
(keydown)="onKeyDown($event)"
|
||||
*ngIf="assignment && editAssignment"
|
||||
>
|
||||
<!-- title -->
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Title' | translate }}"
|
||||
formControlName="title"
|
||||
[value]="assignmentCopy.title || ''"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<!-- title -->
|
||||
<mat-form-field class="full-width">
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Title' | translate }}"
|
||||
formControlName="title"
|
||||
[value]="assignmentCopy.getTitle() || ''"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- description: HTML Editor -->
|
||||
<editor
|
||||
@ -209,7 +213,7 @@
|
||||
</div>
|
||||
|
||||
<!-- searchValueSelector: tags -->
|
||||
<div class="content-field">
|
||||
<div class="content-field" *ngIf="tagsAvailable">
|
||||
<os-search-value-selector
|
||||
ngDefaultControl
|
||||
[form]="assignmentForm"
|
||||
@ -222,7 +226,7 @@
|
||||
</div>
|
||||
|
||||
<!-- searchValueSelector:agendaItem -->
|
||||
<div class="content-field">
|
||||
<div class="content-field" *ngIf="parentsAvailable">
|
||||
<os-search-value-selector
|
||||
ngDefaultControl
|
||||
[form]="assignmentForm"
|
||||
@ -235,23 +239,27 @@
|
||||
</div>
|
||||
|
||||
<!-- poll_description_default -->
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Default comment on the ballot paper' | translate }}"
|
||||
formControlName="poll_description_default"
|
||||
[value]="assignmentCopy.assignment.poll_description_default || ''"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Default comment on the ballot paper' | translate }}"
|
||||
formControlName="poll_description_default"
|
||||
[value]="assignmentCopy.assignment.poll_description_default || ''"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- open posts: number -->
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Number of persons to be elected' | translate }}"
|
||||
formControlName="open_posts"
|
||||
[value]="assignmentCopy.assignment.open_posts || null"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Number of persons to be elected' | translate }}"
|
||||
formControlName="open_posts"
|
||||
[value]="assignmentCopy.assignment.open_posts || null"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- TODO searchValueSelector: Parent -->
|
||||
</form>
|
||||
</div>
|
||||
|
@ -0,0 +1,33 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.candidates {
|
||||
width: 60%;
|
||||
}
|
||||
.candidate {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid lightgray;
|
||||
vertical-align: top;
|
||||
.name {
|
||||
word-wrap: break-word;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.top-aligned {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
// TODO: same as .waiting-list in list-of-speakers
|
||||
.candidates-list {
|
||||
padding: 10px 25px 0 25px;
|
||||
}
|
||||
|
||||
// TODO: duplicate in list-of-speakers
|
||||
.add-self-buttons {
|
||||
padding: 15px 0 20px 25px;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar, MatSelectChange } from '@angular/material';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@ -8,22 +8,22 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ViewAssignment, AssignmentPhase } from '../../models/view-assignment';
|
||||
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
|
||||
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
|
||||
/**
|
||||
* Component for the assignment detail view
|
||||
@ -47,16 +47,17 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
/**
|
||||
* The different phases of an assignment. Info is fetched from server
|
||||
*/
|
||||
public phaseOptions: AssignmentPhase[] = [];
|
||||
public phaseOptions = AssignmentPhases;
|
||||
|
||||
/**
|
||||
* List of users (used in searchValueSelector for candidates)
|
||||
* TODO Candidates already in the list should be filtered out
|
||||
* List of users available as candidates (used as raw data for {@link filteredCandidates})
|
||||
*/
|
||||
public availableCandidates = new BehaviorSubject<ViewUser[]>([]);
|
||||
private availableCandidates = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
/**
|
||||
* TODO a filtered list (excluding users already in this.assignment.candidates)
|
||||
* A BehaviourSubject with a filtered list of users (excluding users already
|
||||
* in the list of candidates). It is updated each time {@link filterCandidates}
|
||||
* is called (triggered by autoupdates)
|
||||
*/
|
||||
public filteredCandidates = new BehaviorSubject<ViewUser[]>([]);
|
||||
|
||||
@ -88,6 +89,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
*/
|
||||
public set assignment(assignment: ViewAssignment) {
|
||||
this._assignment = assignment;
|
||||
|
||||
this.filterCandidates();
|
||||
if (this.assignment.polls.length) {
|
||||
this.assignment.polls.forEach(poll => {
|
||||
poll.pollBase = this.pollService.getBaseAmount(poll);
|
||||
@ -122,17 +125,21 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the current assignment phase as string
|
||||
*
|
||||
* @returns a matching string (untranslated)
|
||||
* Checks if there are any tags available
|
||||
*/
|
||||
public get phaseString(): string {
|
||||
const mapping = this.phaseOptions.find(ph => ph.value === this.assignment.phase);
|
||||
return mapping ? mapping.display_name : '';
|
||||
public get tagsAvailable(): boolean {
|
||||
return this.tagsObserver.getValue().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Build forms and subscribe to needed configs, constants and updates
|
||||
* Checks if there are any tags available
|
||||
*/
|
||||
public get parentsAvailable(): boolean {
|
||||
return this.agendaObserver.getValue().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Build forms and subscribe to needed configs and updates
|
||||
*
|
||||
* @param title
|
||||
* @param translate
|
||||
@ -145,7 +152,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
* @param formBuilder
|
||||
* @param repo
|
||||
* @param userRepo
|
||||
* @param constants
|
||||
* @param pollService
|
||||
* @param agendaRepo
|
||||
* @param tagRepo
|
||||
@ -163,17 +169,19 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
formBuilder: FormBuilder,
|
||||
public repo: AssignmentRepositoryService,
|
||||
private userRepo: UserRepositoryService,
|
||||
private constants: ConstantsService,
|
||||
public pollService: AssignmentPollService,
|
||||
private agendaRepo: ItemRepositoryService,
|
||||
private tagRepo: TagRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
/* Server side constants for phases */
|
||||
this.constants.get<AssignmentPhase[]>('AssignmentPhases').subscribe(phases => (this.phaseOptions = phases));
|
||||
/* List of eligible users */
|
||||
this.userRepo.getViewModelListObservable().subscribe(users => this.availableCandidates.next(users));
|
||||
this.subscriptions.push(
|
||||
/* List of eligible users */
|
||||
this.userRepo.getViewModelListObservable().subscribe(users => {
|
||||
this.availableCandidates.next(users);
|
||||
this.filterCandidates();
|
||||
})
|
||||
);
|
||||
this.assignmentForm = formBuilder.group({
|
||||
phase: null,
|
||||
tags_id: [],
|
||||
@ -184,7 +192,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
agenda_item_id: '' // create agenda item
|
||||
});
|
||||
this.candidatesForm = formBuilder.group({
|
||||
candidate: null
|
||||
userId: null
|
||||
});
|
||||
}
|
||||
|
||||
@ -201,9 +209,9 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
* Permission check for interactions.
|
||||
*
|
||||
* Current operations supported:
|
||||
* - addSelf: the user can add themself to the list of candidates
|
||||
* - addOthers: the user can add other candidates
|
||||
* - createPoll: the user can add/edit election poll (requires candidates to be present)
|
||||
* - addSelf: the user can add/remove themself to the list of candidates
|
||||
* - addOthers: the user can add/remove other candidates
|
||||
* - createPoll: the user can add/edit an election poll (requires candidates to be present)
|
||||
* - manage: the user has general manage permissions (i.e. editing the assignment metaInfo)
|
||||
*
|
||||
* @param operation the action requested
|
||||
@ -213,20 +221,28 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
const isManager = this.operator.hasPerms('assignments.can_manage');
|
||||
switch (operation) {
|
||||
case 'addSelf':
|
||||
if (isManager && this.assignment.phase !== 2) {
|
||||
if (isManager && !this.assignment.isFinished) {
|
||||
return true;
|
||||
} else {
|
||||
return this.assignment.phase === 0 && this.operator.hasPerms('assignments.can_nominate_self');
|
||||
return (
|
||||
this.assignment.isSearchingForCandidates &&
|
||||
this.operator.hasPerms('assignments.can_nominate_self') &&
|
||||
!this.assignment.isFinished
|
||||
);
|
||||
}
|
||||
case 'addOthers':
|
||||
if (isManager && this.assignment.phase !== 2) {
|
||||
if (isManager && !this.assignment.isFinished) {
|
||||
return true;
|
||||
} else {
|
||||
return this.assignment.phase === 0 && this.operator.hasPerms('assignments.can_nominate_others');
|
||||
return (
|
||||
this.assignment.isSearchingForCandidates &&
|
||||
this.operator.hasPerms('assignments.can_nominate_others') &&
|
||||
!this.assignment.isFinished
|
||||
);
|
||||
}
|
||||
case 'createPoll':
|
||||
return (
|
||||
isManager && this.assignment && this.assignment.phase !== 2 && this.assignment.candidateAmount > 0
|
||||
isManager && this.assignment && !this.assignment.isFinished && this.assignment.candidateAmount > 0
|
||||
);
|
||||
case 'manage':
|
||||
return isManager;
|
||||
@ -261,6 +277,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
private patchForm(assignment: ViewAssignment): void {
|
||||
this.assignmentCopy = assignment;
|
||||
this.assignmentForm.patchValue({
|
||||
title: assignment.title || '',
|
||||
tags_id: assignment.assignment.tags_id || [],
|
||||
agendaItem: assignment.assignment.agenda_item_id || null,
|
||||
phase: assignment.phase, // todo default: 0?
|
||||
@ -304,23 +321,24 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a user to the list of candidates
|
||||
* Adds the user from the candidates form to the list of candidates
|
||||
*
|
||||
* @param userId the id of a ViewUser
|
||||
*/
|
||||
public async addUser(): Promise<void> {
|
||||
const candId = this.candidatesForm.get('candidate').value;
|
||||
this.candidatesForm.setValue({ candidate: null });
|
||||
if (candId) {
|
||||
await this.repo.changeCandidate(candId, this.assignment).then(null, this.raiseError);
|
||||
public async addUser(userId: number): Promise<void> {
|
||||
const user = this.userRepo.getViewModel(userId);
|
||||
if (user) {
|
||||
await this.repo.changeCandidate(user, this.assignment, true).then(null, this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a user from the list of candidates
|
||||
*
|
||||
* @param user Assignment User
|
||||
* @param candidate A ViewAssignmentUser currently in the list of related users
|
||||
*/
|
||||
public async removeUser(user: ViewUser): Promise<void> {
|
||||
await this.repo.changeCandidate(user.id, this.assignment).then(null, this.raiseError);
|
||||
public async removeUser(candidate: ViewAssignmentRelatedUser): Promise<void> {
|
||||
await this.repo.changeCandidate(candidate.user, this.assignment, false).then(null, this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -342,6 +360,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
}
|
||||
})
|
||||
);
|
||||
this.subscriptions.push(
|
||||
this.candidatesForm.valueChanges.subscribe(formResult => {
|
||||
// resetting a form triggers a form.next(null) - check if data is present
|
||||
if (formResult && formResult.userId) {
|
||||
this.addUser(formResult.userId);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.newAssignment = true;
|
||||
// TODO set defaults?
|
||||
@ -357,8 +383,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
public async onDeleteAssignmentButton(): Promise<void> {
|
||||
const title = this.translate.instant('Are you sure you want to delete this election?');
|
||||
if (await this.promptService.open(title, this.assignment.getTitle())) {
|
||||
await this.repo.delete(this.assignment);
|
||||
this.router.navigate(['../assignments/']);
|
||||
this.repo.delete(this.assignment).then(() => this.router.navigate(['../']), this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -374,12 +399,10 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
* TODO: only with existing assignments, else it should fail
|
||||
* TODO check permissions and conditions
|
||||
*
|
||||
* @param event
|
||||
* @param value the phase to set
|
||||
*/
|
||||
public async setPhase(event: MatSelectChange): Promise<void> {
|
||||
if (!this.newAssignment && this.phaseOptions.find(option => option.value === event.value)) {
|
||||
this.repo.update({ phase: event.value }, this.assignment).then(null, this.raiseError);
|
||||
}
|
||||
public async onSetPhaseButton(value: number): Promise<void> {
|
||||
this.repo.update({ phase: value }, this.assignment).then(null, this.raiseError);
|
||||
}
|
||||
|
||||
public onDownloadPdf(): void {
|
||||
@ -426,11 +449,48 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
|
||||
/**
|
||||
* Assemble a meaningful label for the poll
|
||||
* TODO (currently e.g. 'Ballot 10 (unublished)')
|
||||
* Published polls will look like 'Ballot 2 (published)'
|
||||
* other polls will be named 'Ballot 2' for normal users, with the hint
|
||||
* '(unpulished)' appended for manager users
|
||||
*
|
||||
* @param poll
|
||||
* @param index the index of the poll relative to the assignment
|
||||
*/
|
||||
public getPollLabel(poll: AssignmentPoll, index: number): string {
|
||||
const pubState = poll.published ? this.translate.instant('published') : this.translate.instant('unpublished');
|
||||
const title = this.translate.instant('Ballot');
|
||||
return `${title} ${index + 1} (${pubState})`;
|
||||
const title = `${this.translate.instant('Ballot')} ${index + 1}`;
|
||||
if (poll.published) {
|
||||
return title + ` (${this.translate.instant('published')})`;
|
||||
} else {
|
||||
if (this.hasPerms('manage')) {
|
||||
return title + ` (${this.translate.instant('unpublished')})`;
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update of the filter for the list of available candidates
|
||||
* (triggered on an autoupdate of either users or the assignment)
|
||||
*/
|
||||
private filterCandidates(): void {
|
||||
if (!this.assignment || !this.assignment.candidates) {
|
||||
this.filteredCandidates.next(this.availableCandidates.getValue());
|
||||
} else {
|
||||
this.filteredCandidates.next(
|
||||
this.availableCandidates
|
||||
.getValue()
|
||||
.filter(u => !this.assignment.candidates.some(cand => cand.id === u.id))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update of the sorting.
|
||||
*/
|
||||
public onSortingChange(listInNewOrder: ViewAssignmentRelatedUser[]): void {
|
||||
this.repo
|
||||
.sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment)
|
||||
.then(null, this.raiseError);
|
||||
}
|
||||
}
|
||||
|
@ -40,12 +40,12 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment">{{ assignment.getListTitle() }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- pahse column-->
|
||||
<!-- phase column-->
|
||||
<ng-container matColumnDef="phase">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Phase</mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment">
|
||||
<mat-chip-list>
|
||||
<mat-chip color="primary" selected>{{ assignment.phase }}</mat-chip>
|
||||
<mat-chip color="primary" selected>{{ assignment.phaseString | translate }}</mat-chip>
|
||||
</mat-chip-list>
|
||||
</mat-cell>
|
||||
<button mat-menu-item (click)="selectAll()">
|
||||
@ -62,7 +62,7 @@
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Candidates</mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment">
|
||||
<mat-chip-list>
|
||||
<mat-chip color="accent" selected>{{ assignment.candidateAmount }}</mat-chip>
|
||||
<mat-chip color="accent" selected matTooltip="{{ 'Number of candidates' | translate }}">{{ assignment.candidateAmount }}</mat-chip>
|
||||
</mat-chip-list>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AssignmentListComponent } from './assignment-list.component';
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('AssignmentListComponent', () => {
|
||||
let component: AssignmentListComponent;
|
||||
|
@ -13,7 +13,7 @@ import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewAssignment } from '../../models/view-assignment';
|
||||
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
|
||||
|
||||
/**
|
||||
* List view for the assignments
|
||||
@ -24,6 +24,11 @@ import { ViewAssignment } from '../../models/view-assignment';
|
||||
styleUrls: ['./assignment-list.component.scss']
|
||||
})
|
||||
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment, Assignment> implements OnInit {
|
||||
/**
|
||||
* The different phases of an assignment. Info is fetched from server
|
||||
*/
|
||||
public phaseOptions = AssignmentPhases;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
@ -1,73 +1,40 @@
|
||||
<h2 translate>Voting result</h2>
|
||||
<div class="meta-text">
|
||||
<span translate>Special values</span>:<br />
|
||||
<mat-chip>-1</mat-chip> = <span translate>majority</span><br />
|
||||
<mat-chip>-1</mat-chip> = <span translate>majority</span>
|
||||
<mat-chip color="accent">-2</mat-chip> =
|
||||
<span translate>undocumented</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex-spaced" *ngFor="let candidate of data.poll.options">
|
||||
<div>
|
||||
{{ getName(candidate.candidate_id) }}
|
||||
<div class="spacer-top-10"></div>
|
||||
<div class="width-600">
|
||||
<!-- Candidate values -->
|
||||
<div [ngClass]="getGridClass()" *ngFor="let candidate of data.options">
|
||||
<div class="candidate-name">
|
||||
{{ candidate.user.full_name }}
|
||||
</div>
|
||||
<div class="votes">
|
||||
<div *ngFor="let key of optionPollKeys" class="votes">
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[value]="getValue('Yes', candidate)"
|
||||
(change)="setValue('Yes', candidate, $event.target.value)"
|
||||
/>
|
||||
<mat-label *ngIf="data.poll.pollmethod !== 'votes'" translate>Yes</mat-label>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="data.poll.pollmethod !== 'votes'">
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[value]="getValue('No', candidate)"
|
||||
(change)="setValue('No', candidate, $event.target.value)"
|
||||
/>
|
||||
<mat-label translate>No</mat-label>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="data.poll.pollmethod === 'yna'">
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[value]="getValue('Abstain', candidate)"
|
||||
(change)="setValue('Abstain', candidate, $event.target.value)"
|
||||
/>
|
||||
<mat-label translate>Abstain</mat-label>
|
||||
<input type="number"
|
||||
matInput
|
||||
[value]="getValue(key, candidate)"
|
||||
(change)="setValue(key, candidate, $event.target.value)"
|
||||
/>
|
||||
<mat-label> {{ key | translate }}</mat-label>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-divider *ngIf="data.poll.pollmethod !== 'votes'"></mat-divider>
|
||||
</div>
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[value]="getSumValue('votesvalid')"
|
||||
(change)="setSumValue('votesvalid', $event.target.value)"
|
||||
/>
|
||||
<mat-label translate>Valid votes</mat-label>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[value]="getSumValue('votesinvalid')"
|
||||
(change)="setSumValue('votesinvalid', $event.target.value)"
|
||||
/>
|
||||
<mat-label translate>Invalid votes</mat-label>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[value]="getSumValue('votescast')"
|
||||
(change)="setSumValue('votescast', $event.target.value)"
|
||||
/>
|
||||
<mat-label translate>Total votes</mat-label>
|
||||
</mat-form-field>
|
||||
<!-- 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) }}</mat-label>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="submit-buttons">
|
||||
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>
|
||||
|
@ -13,7 +13,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
.votes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.candidate-name {
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
.votes-grid-1 {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
grid-template-columns: auto 60px;
|
||||
align-items: center;
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: more elegant way. Only grid-template-columns is different
|
||||
.votes-grid-2 {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
grid-template-columns: auto 60px 60px;
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: more elegant way. Only grid-template-columns is different
|
||||
.votes-grid-3 {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
grid-template-columns: auto 60px 60px 60px;
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sum-value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.width-600 {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
@ -2,16 +2,17 @@ import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
||||
|
||||
/**
|
||||
* Vote entries included once for summary (e.g. total votes cast)
|
||||
*/
|
||||
type summaryPollKeys = 'votescast' | 'votesvalid' | 'votesinvalid';
|
||||
type summaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid';
|
||||
|
||||
/**
|
||||
* A dialog for updating the values of an assignment-related poll.
|
||||
@ -22,6 +23,11 @@ type summaryPollKeys = 'votescast' | 'votesvalid' | 'votesinvalid';
|
||||
styleUrls: ['./assignment-poll-dialog.component.scss']
|
||||
})
|
||||
export class AssignmentPollDialogComponent {
|
||||
/**
|
||||
* The summary values that will have fields in the dialog
|
||||
*/
|
||||
public sumValues: summaryPollKey[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
|
||||
/**
|
||||
* List of accepted special non-numerical values.
|
||||
* See {@link PollService.specialPollVotes}
|
||||
@ -40,15 +46,16 @@ export class AssignmentPollDialogComponent {
|
||||
*/
|
||||
public constructor(
|
||||
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { poll: AssignmentPoll; users: ViewUser[] },
|
||||
@Inject(MAT_DIALOG_DATA) public data: ViewAssignmentPoll,
|
||||
private matSnackBar: MatSnackBar,
|
||||
private translate: TranslateService,
|
||||
private pollService: AssignmentPollService
|
||||
public pollService: AssignmentPollService,
|
||||
private userRepo: UserRepositoryService
|
||||
) {
|
||||
this.specialValues = this.pollService.specialPollVotes;
|
||||
switch (this.data.poll.pollmethod) {
|
||||
switch (this.data.pollmethod) {
|
||||
case 'votes':
|
||||
this.optionPollKeys = ['Yes'];
|
||||
this.optionPollKeys = ['Votes'];
|
||||
break;
|
||||
case 'yn':
|
||||
this.optionPollKeys = ['Yes', 'No'];
|
||||
@ -73,7 +80,7 @@ export class AssignmentPollDialogComponent {
|
||||
* TODO better validation
|
||||
*/
|
||||
public submit(): void {
|
||||
const error = this.data.poll.options.find(dataoption => {
|
||||
const error = this.data.options.find(dataoption => {
|
||||
for (const key of this.optionPollKeys) {
|
||||
const keyValue = dataoption.votes.find(o => o.value === key);
|
||||
if (!keyValue || keyValue.weight === undefined) {
|
||||
@ -90,7 +97,7 @@ export class AssignmentPollDialogComponent {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.dialogRef.close(this.data.poll);
|
||||
this.dialogRef.close(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,18 +111,6 @@ export class AssignmentPollDialogComponent {
|
||||
return this.pollService.getLabel(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the (full) name of a pollOption candidate
|
||||
*
|
||||
* @param the id of the candidate
|
||||
* @returns the full_name property
|
||||
*/
|
||||
public getName(candidateId: number): string {
|
||||
const user = this.data.users.find(c => c.id === candidateId);
|
||||
return user ? user.full_name : 'unknown user';
|
||||
// TODO error handling
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a vote value
|
||||
*
|
||||
@ -123,14 +118,14 @@ export class AssignmentPollDialogComponent {
|
||||
* @param candidate the candidate for whom to update the value
|
||||
* @param newData the new value
|
||||
*/
|
||||
public setValue(value: PollVoteValue, candidate: AssignmentPollOption, newData: string): void {
|
||||
public setValue(value: PollVoteValue, candidate: ViewAssignmentPollOption, newData: string): void {
|
||||
const vote = candidate.votes.find(v => v.value === value);
|
||||
if (vote) {
|
||||
vote.weight = parseInt(newData, 10);
|
||||
vote.weight = parseFloat(newData);
|
||||
} else {
|
||||
candidate.votes.push({
|
||||
value: value,
|
||||
weight: parseInt(newData, 10)
|
||||
weight: parseFloat(newData)
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -153,8 +148,8 @@ export class AssignmentPollDialogComponent {
|
||||
* @param value
|
||||
* @returns integer or undefined
|
||||
*/
|
||||
public getSumValue(value: summaryPollKeys): number | undefined {
|
||||
return this.data.poll[value] || undefined;
|
||||
public getSumValue(value: summaryPollKey): number | undefined {
|
||||
return this.data[value] || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,7 +158,23 @@ export class AssignmentPollDialogComponent {
|
||||
* @param value
|
||||
* @param weight
|
||||
*/
|
||||
public setSumValue(value: summaryPollKeys, weight: string): void {
|
||||
this.data.poll[value] = parseInt(weight, 10);
|
||||
public setSumValue(value: summaryPollKey, weight: string): void {
|
||||
this.data[value] = parseFloat(weight);
|
||||
}
|
||||
|
||||
public getGridClass(): string {
|
||||
return `votes-grid-${this.optionPollKeys.length}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the name for a poll option
|
||||
* TODO: observable. Note that the assignment.related_user may not contain the user (anymore?)
|
||||
*
|
||||
* @param option Any poll option
|
||||
* @returns the full_name for the candidate
|
||||
*/
|
||||
public getCandidateName(option: AssignmentPollOption): string {
|
||||
const user = this.userRepo.getViewModel(option.candidate_id);
|
||||
return user ? user.full_name : '';
|
||||
}
|
||||
}
|
||||
|
@ -1,119 +1,197 @@
|
||||
<!-- label: 'Ballot 1 ...' -->
|
||||
<h3 translate>Ballot</h3>
|
||||
<mat-card class="os-card">
|
||||
<div class="flex-spaced">
|
||||
<div>
|
||||
<!-- *ngIf="poll.description"> -->
|
||||
Description
|
||||
<!-- {{ poll.description}} -->
|
||||
</div>
|
||||
<mat-card class="os-card" *ngIf="poll">
|
||||
<div class="flex-spaced poll-menu">
|
||||
<!-- Buttons -->
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<button mat-button (click)="printBallot()">
|
||||
<span translate>Print ballot paper</span>
|
||||
</button>
|
||||
<button mat-button [disabled]="assignment.phase == 2" (click)="enterVotes()">
|
||||
<span translate>Enter votes</span>
|
||||
</button>
|
||||
<button mat-button (click)="togglePublished()">
|
||||
<span *ngIf="!poll.published" translate>Publish</span>
|
||||
<span *ngIf="poll.published" translate>Unpublish</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *osPerms="'core.can_manage_projector'">
|
||||
<button mat-button>
|
||||
<span translate>Project</span>
|
||||
<!-- os-projector-button ?-->
|
||||
</button>
|
||||
</div>
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<button mat-button class="red-warning-text" (click)="onDeletePoll(poll)">
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- text input 'set hint for pballot paper' -->
|
||||
<!-- submit: setBallotHint(poll) -->
|
||||
</div>
|
||||
|
||||
<div *ngIf="pollService.majorityMethods && majorityChoice">
|
||||
<span translate>Majority method</span>
|
||||
|
||||
<mat-basic-chip *ngIf="canManage" [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple>
|
||||
{{ majorityChoice.display_name | translate }}
|
||||
</mat-basic-chip>
|
||||
<mat-basic-chip *ngIf="!canManage" class="grey" disableRipple>
|
||||
{{ majorityChoice.display_name | translate }}
|
||||
</mat-basic-chip>
|
||||
<mat-menu #majorityMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let method of pollService.majorityMethods" (click)="setMajority(method)">
|
||||
<mat-icon *ngIf="method.value === majorityChoice.value">
|
||||
check
|
||||
</mat-icon>
|
||||
{{ method.display_name | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
*osPerms="'assignments.can_manage'; "core.can_manage_projector""
|
||||
[matMenuTriggerFor]="pollItemMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #pollItemMenu="matMenu">
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<button mat-menu-item (click)="printBallot()">
|
||||
<mat-icon>local_printshop</mat-icon>
|
||||
<span translate>Print ballot paper</span>
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="!assignment.isFinished" (click)="enterVotes()">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span translate>Enter votes</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="togglePublished()">
|
||||
<mat-icon>
|
||||
{{ poll.published ? 'visibility_off' : 'visibility' }}
|
||||
</mat-icon>
|
||||
<span *ngIf="!poll.published" translate>Publish</span>
|
||||
<span *ngIf="poll.published" translate>Unpublish</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *osPerms="'core.can_manage_projector'">
|
||||
<button mat-menu-item>
|
||||
<mat-icon>videocam</mat-icon>
|
||||
<span translate>Project</span>
|
||||
<!-- os-projector-button ?-->
|
||||
</button>
|
||||
</div>
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<div class="on-transition-fade" *ngIf="poll && poll.options">
|
||||
<div *ngFor="let option of poll.options" class="flex-spaced">
|
||||
<div *ngIf="poll.published">
|
||||
<mat-checkbox [checked]="option.is_elected" (change)="toggleElected(option)">
|
||||
<!-- TODO mark/unkmark elected osPerms -->
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<!-- candidate Name -->
|
||||
<div>
|
||||
{{ option.user.full_name }}
|
||||
</div>
|
||||
<!-- Votes -->
|
||||
<div *ngIf="poll.published && poll.has_votes">
|
||||
<div *ngFor="let vote of option.votes">
|
||||
<div class="poll-progress on-transition-fade">
|
||||
<span>{{ pollService.getLabel(vote.value) | translate }}:</span>
|
||||
{{ pollService.getSpecialLabel(vote.weight) }}
|
||||
<span *ngIf="!pollService.isAbstractOption(poll, option)"
|
||||
>({{ pollService.getPercent(poll, option, vote.value) }}%)</span
|
||||
>
|
||||
<div>
|
||||
<h4>
|
||||
<span translate>Poll description</span>
|
||||
</h4>
|
||||
<div [formGroup]="descriptionForm">
|
||||
<mat-form-field class="wide">
|
||||
<input matInput formControlName="description" [disabled]="!canManage" />
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="!dirtyDescription"
|
||||
*ngIf="canManage"
|
||||
(click)="onEditDescriptionButton()"
|
||||
>
|
||||
<mat-icon inline>edit</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="spacer-bottom-10">
|
||||
<h4 translate>Poll method</h4>
|
||||
<span>{{ pollMethodName | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="on-transition-fade" *ngIf="poll.options">
|
||||
<div *ngIf="pollData">
|
||||
<div class="poll-grid">
|
||||
<div></div>
|
||||
<div><span class="table-view-list-title" translate>Candidate</span></div>
|
||||
<div><span class="table-view-list-title" translate>Votes</span></div>
|
||||
<div *ngIf="pollService.majorityMethods && majorityChoice">
|
||||
<div>
|
||||
<span class="table-view-list-title" translate>Quorum</span>
|
||||
</div>
|
||||
<div *ngIf="!pollService.isAbstractOption(poll, option)" class="poll-progress-bar">
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="pollService.getPercent(poll, option, vote.value)"
|
||||
[ngClass]="pollService.getProgressBarColor(vote.value)"
|
||||
>
|
||||
</mat-progress-bar>
|
||||
<div>
|
||||
<!-- manager majority chip (menu trigger) -->
|
||||
<mat-basic-chip *ngIf="canManage" [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple>
|
||||
{{ majorityChoice.display_name | translate }}
|
||||
</mat-basic-chip>
|
||||
<!-- non-manager (menuless) majority chip -->
|
||||
<mat-basic-chip *ngIf="!canManage" class="grey" disableRipple>
|
||||
{{ majorityChoice.display_name | translate }}
|
||||
</mat-basic-chip>
|
||||
<!-- menu for selecting quorum choices -->
|
||||
<mat-menu #majorityMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let method of pollService.majorityMethods"
|
||||
(click)="setMajority(method)"
|
||||
>
|
||||
<mat-icon *ngIf="method.value === majorityChoice.value">
|
||||
check
|
||||
</mat-icon>
|
||||
{{ method.display_name | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="
|
||||
poll.has_votes &&
|
||||
poll.published &&
|
||||
majorityChoice &&
|
||||
majorityChoice.value !== 'disabled' &&
|
||||
!pollService.isAbstractOption(poll, option)
|
||||
"
|
||||
>
|
||||
<span>{{ pollService.yesQuorum(majorityChoice, poll, option) }}</span>
|
||||
<span *ngIf="quorumReached(option)" class="green-text">
|
||||
<mat-icon>done</mat-icon>
|
||||
</span>
|
||||
<span *ngIf="!quorumReached(option)" class="red-warning-text">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</span>
|
||||
<div *ngFor="let option of poll.options" class="poll-grid poll-border">
|
||||
<div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
(click)="toggleElected(option)"
|
||||
[disabled]="!canManage || assignment.isFinished"
|
||||
disableRipple
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="option.is_elected"
|
||||
class="top-aligned green-text"
|
||||
matTooltip="{{ 'Elected' | translate }}"
|
||||
>check_box</mat-icon
|
||||
>
|
||||
<mat-icon
|
||||
*ngIf="!option.is_elected && canManage && !assignment.isFinished"
|
||||
class="top-aligned primary"
|
||||
matTooltip="{{ 'Not elected' | translate }}"
|
||||
>
|
||||
check_box_outline_blank</mat-icon
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- candidate Name -->
|
||||
<div class="candidate-name">
|
||||
{{ option.user.full_name }}
|
||||
</div>
|
||||
<!-- Votes -->
|
||||
<div>
|
||||
<div *ngIf="poll.has_votes">
|
||||
<div *ngFor="let vote of option.votes" class="spacer-bottom-10">
|
||||
<div class="poll-progress on-transition-fade">
|
||||
<span *ngIf="vote.value !== 'Votes'"
|
||||
>{{ pollService.getLabel(vote.value) | translate }}:</span
|
||||
>
|
||||
{{ pollService.getSpecialLabel(vote.weight) }}
|
||||
<span *ngIf="!pollService.isAbstractOption(poll, option)"
|
||||
>({{ pollService.getPercent(poll, option, vote.value) }}%)</span
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="!pollService.isAbstractOption(poll, option)" class="poll-progress-bar">
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="pollService.getPercent(poll, option, vote.value)"
|
||||
[ngClass]="pollService.getProgressBarColor(vote.value)"
|
||||
>
|
||||
</mat-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
*ngIf="
|
||||
poll.has_votes &&
|
||||
majorityChoice &&
|
||||
majorityChoice.value !== 'disabled' &&
|
||||
!pollService.isAbstractOption(poll, option)
|
||||
"
|
||||
class="poll-quorum"
|
||||
>
|
||||
<span>{{ pollService.yesQuorum(majorityChoice, poll, option) }}</span>
|
||||
<span [ngClass]="quorumReached(option) ? 'green-text' : 'red-warning-text'"
|
||||
matTooltip="{{ getQuorumReachedString(option) }}"
|
||||
>
|
||||
<mat-icon *ngIf="quorumReached(option)">{{ pollService.getIcon('yes') }}</mat-icon>
|
||||
<mat-icon *ngIf="!quorumReached(option)">{{ pollService.getIcon('no') }}</mat-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- summary -->
|
||||
<div>
|
||||
<div *ngFor="let key of pollValues" class="poll-grid">
|
||||
<div></div>
|
||||
<div class="candidate-name">
|
||||
<span>{{ pollService.getLabel(key) | translate }}</span
|
||||
>:
|
||||
</div>
|
||||
<div>
|
||||
{{ pollService.getSpecialLabel(poll[key]) | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- summary -->
|
||||
<div *ngFor="let key of pollValues">
|
||||
<div>
|
||||
<span>{{ key | translate }}</span>:
|
||||
</div>
|
||||
<div>
|
||||
{{ poll[key] | precisionPipe }}
|
||||
{{ pollService.getSpecialLabel(poll[key]) }}
|
||||
</div>
|
||||
<div *ngIf="!pollData">
|
||||
<h4 translate>Candidates</h4>
|
||||
<div *ngFor="let option of poll.options">
|
||||
<span class="accent"> {{ option.user.full_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,3 +59,36 @@
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-grid {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
padding: 5px;
|
||||
grid-template-columns: 30px auto 250px 150px;
|
||||
.candidate-name {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
.poll-border {
|
||||
border: 1px solid lightgray;
|
||||
}
|
||||
.poll-menu {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.poll-quorum {
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
mat-icon {
|
||||
vertical-align: middle;
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
.top-aligned {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 90%;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -6,15 +7,15 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component';
|
||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewAssignment } from '../../models/view-assignment';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
||||
import { ViewAssignment } from '../../models/view-assignment';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
||||
|
||||
/**
|
||||
* Component for a single assignment poll. Used in assignment detail view
|
||||
@ -37,6 +38,11 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
@Input()
|
||||
public poll: ViewAssignmentPoll;
|
||||
|
||||
/**
|
||||
* Form for updating the poll's description
|
||||
*/
|
||||
public descriptionForm: FormGroup;
|
||||
|
||||
/**
|
||||
* The selected Majority method to display quorum calculations. Will be
|
||||
* set/changed by the user
|
||||
@ -63,6 +69,23 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
return this.pollService.pollValues.filter(name => this.poll[name] !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the description on the form differs from the poll's description
|
||||
*/
|
||||
public get dirtyDescription(): boolean {
|
||||
return this.descriptionForm.get('description').value !== this.poll.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if vote results can be seen by the user
|
||||
*/
|
||||
public get pollData(): boolean {
|
||||
if (!this.poll.has_votes) {
|
||||
return false;
|
||||
}
|
||||
return this.poll.published || this.canManage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translated poll method name
|
||||
*
|
||||
@ -90,6 +113,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
/**
|
||||
* constructor. Does nothing
|
||||
*
|
||||
* @param titleService
|
||||
* @param matSnackBar
|
||||
* @param pollService poll related calculations
|
||||
* @param operator permission checks
|
||||
* @param assignmentRepo The repository to the assignments
|
||||
@ -105,7 +130,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
private assignmentRepo: AssignmentRepositoryService,
|
||||
public translate: TranslateService,
|
||||
public dialog: MatDialog,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
@ -117,6 +143,9 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
this.majorityChoice =
|
||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
||||
null;
|
||||
this.descriptionForm = this.formBuilder.group({
|
||||
description: this.poll ? this.poll.description : ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -159,14 +188,9 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
* closes successfully (validation is done there)
|
||||
*/
|
||||
public enterVotes(): void {
|
||||
// TODO deep copy of this.poll (JSON parse is ugly workaround)
|
||||
// or sending just copy of the options
|
||||
const data = {
|
||||
poll: JSON.parse(JSON.stringify(this.poll)),
|
||||
users: this.assignment.candidates // used to get the names of the users
|
||||
};
|
||||
const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
|
||||
data: data,
|
||||
// TODO deep copy of this.poll (JSON parse is ugly workaround) or sending just copy of the options
|
||||
data: this.poll.copy(),
|
||||
maxHeight: '90vh',
|
||||
minWidth: '300px',
|
||||
maxWidth: '80vw',
|
||||
@ -213,4 +237,27 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
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).then(null, this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a tooltip string about the quorum
|
||||
* @param option
|
||||
* @returns a translated
|
||||
*/
|
||||
public getQuorumReachedString(option: ViewAssignmentPollOption): string {
|
||||
const name = this.translate.instant(this.majorityChoice.display_name);
|
||||
const quorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option);
|
||||
const isReached = this.quorumReached(option)
|
||||
? this.translate.instant('reached')
|
||||
: this.translate.instant('not reached');
|
||||
return `${name} (${quorum}) ${isReached}`;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { AssignmentPollMethod } from '../services/assignment-poll.service';
|
||||
import { ViewAssignmentPollOption } from './view-assignment-poll-option';
|
||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
||||
|
||||
export class ViewAssignmentPoll implements Identifiable, Updateable {
|
||||
private _assignmentPoll: AssignmentPoll;
|
||||
@ -37,14 +38,25 @@ export class ViewAssignmentPoll implements Identifiable, Updateable {
|
||||
return this.poll.votesvalid;
|
||||
}
|
||||
|
||||
public set votesvalid(amount: number) {
|
||||
this.poll.votesvalid = amount;
|
||||
}
|
||||
|
||||
public get votesinvalid(): number {
|
||||
return this.poll.votesinvalid;
|
||||
}
|
||||
public set votesinvalid(amount: number) {
|
||||
this.poll.votesinvalid = amount;
|
||||
}
|
||||
|
||||
public get votescast(): number {
|
||||
return this.poll.votescast;
|
||||
}
|
||||
|
||||
public set votescast(amount: number) {
|
||||
this.poll.votescast = amount;
|
||||
}
|
||||
|
||||
public get has_votes(): boolean {
|
||||
return this.poll.has_votes;
|
||||
}
|
||||
@ -68,4 +80,22 @@ export class ViewAssignmentPoll implements Identifiable, Updateable {
|
||||
public updateDependencies(update: BaseViewModel): void {
|
||||
this.options.forEach(option => option.updateDependencies(update));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy with deep-copy on all changing numerical values,
|
||||
* but intact uncopied references to the users
|
||||
*
|
||||
* TODO check and review
|
||||
*/
|
||||
public copy(): ViewAssignmentPoll {
|
||||
return new ViewAssignmentPoll(
|
||||
new AssignmentPoll(JSON.parse(JSON.stringify(this._assignmentPoll))),
|
||||
this._assignmentPollOptions.map(option => {
|
||||
return new ViewAssignmentPollOption(
|
||||
new AssignmentPollOption(JSON.parse(JSON.stringify(option.option))),
|
||||
option.user
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { Updateable } from 'app/site/base/updateable';
|
||||
import { Displayable } from 'app/site/base/displayable';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
import { Updateable } from 'app/site/base/updateable';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
|
||||
export class ViewAssignmentRelatedUser implements Updateable, Identifiable {
|
||||
export class ViewAssignmentRelatedUser implements Updateable, Identifiable, Displayable {
|
||||
private _assignmentRelatedUser: AssignmentRelatedUser;
|
||||
private _user?: ViewUser;
|
||||
|
||||
@ -46,4 +47,12 @@ export class ViewAssignmentRelatedUser implements Updateable, Identifiable {
|
||||
this._user = update;
|
||||
}
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return this.user ? this.user.getTitle() : '';
|
||||
}
|
||||
|
||||
public getListTitle(): string {
|
||||
return this.getTitle();
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,28 @@ import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { ViewAssignmentRelatedUser } from './view-assignment-related-user';
|
||||
import { ViewAssignmentPoll } from './view-assignment-poll';
|
||||
|
||||
export interface AssignmentPhase {
|
||||
value: number;
|
||||
display_name: string;
|
||||
}
|
||||
/**
|
||||
* A constant containing all possible assignment phases and their different
|
||||
* representations as numerical value, string as used in server, and the display
|
||||
* name.
|
||||
*/
|
||||
export const AssignmentPhases: { name: string; value: number; display_name: string }[] = [
|
||||
{
|
||||
name: 'PHASE_SEARCH',
|
||||
value: 0,
|
||||
display_name: 'Searching for candidates'
|
||||
},
|
||||
{
|
||||
name: 'PHASE_VOTING',
|
||||
value: 1,
|
||||
display_name: 'Voting'
|
||||
},
|
||||
{
|
||||
name: 'PHASE_FINISHED',
|
||||
value: 2,
|
||||
display_name: 'Finished'
|
||||
}
|
||||
];
|
||||
|
||||
export class ViewAssignment extends BaseAgendaViewModel {
|
||||
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
|
||||
@ -62,12 +80,37 @@ export class ViewAssignment extends BaseAgendaViewModel {
|
||||
return this.assignment.phase;
|
||||
}
|
||||
|
||||
public get phaseString(): string {
|
||||
const phase = AssignmentPhases.find(ap => ap.value === this.assignment.phase);
|
||||
return phase ? phase.display_name : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the assignment is in the 'finished' state
|
||||
* (not accepting votes or candidates anymore)
|
||||
*/
|
||||
public get isFinished(): boolean {
|
||||
const finishedState = AssignmentPhases.find(ap => ap.name === 'PHASE_FINISHED');
|
||||
return this.phase === finishedState.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the assignment is in the 'searching' state
|
||||
*/
|
||||
public get isSearchingForCandidates(): boolean {
|
||||
const searchState = AssignmentPhases.find(ap => ap.name === 'PHASE_SEARCH');
|
||||
return this.phase === searchState.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the amount of candidates in the assignment's candidate list
|
||||
*/
|
||||
public get candidateAmount(): number {
|
||||
return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is set by the repository
|
||||
* Constructor. Is set by the repository
|
||||
*/
|
||||
public getVerboseName;
|
||||
public getAgendaTitle;
|
||||
|
@ -4,8 +4,7 @@ import { AssignmentRepositoryService } from 'app/core/repositories/assignments/a
|
||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { ViewAssignment, AssignmentPhase } from '../models/view-assignment';
|
||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { ViewAssignment, AssignmentPhases } from '../models/view-assignment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -37,11 +36,7 @@ export class AssignmentFilterListService extends BaseFilterListService<Assignmen
|
||||
* @param assignmentRepo Repository
|
||||
* @param constants the openslides constant service to get the assignment options
|
||||
*/
|
||||
public constructor(
|
||||
store: StorageService,
|
||||
assignmentRepo: AssignmentRepositoryService,
|
||||
private constants: ConstantsService
|
||||
) {
|
||||
public constructor(store: StorageService, assignmentRepo: AssignmentRepositoryService) {
|
||||
super(store, assignmentRepo);
|
||||
this.createPhaseOptions();
|
||||
}
|
||||
@ -51,14 +46,8 @@ export class AssignmentFilterListService extends BaseFilterListService<Assignmen
|
||||
* constants
|
||||
*/
|
||||
private createPhaseOptions(): void {
|
||||
this.constants.get<AssignmentPhase[]>('AssignmentPhases').subscribe(phases => {
|
||||
this.phaseFilter.options = phases.map(ph => {
|
||||
return {
|
||||
label: ph.display_name,
|
||||
condition: ph.value,
|
||||
isActive: false
|
||||
};
|
||||
});
|
||||
this.phaseFilter.options = AssignmentPhases.map(ap => {
|
||||
return { label: ap.display_name, condition: ap.value, isActive: false };
|
||||
});
|
||||
this.updateFilterDefinitions(this.filterOptions);
|
||||
}
|
||||
|
@ -117,6 +117,25 @@ export class AssignmentPollService extends PollService {
|
||||
return Math.round(((vote.weight * 100) / base) * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the percentage for a non-abstract per-poll value
|
||||
* TODO: similar code to getPercent. Mergeable?
|
||||
*
|
||||
* @param poll the poll this value refers to
|
||||
* @param value a per-poll value (e.g. 'votesvalid')
|
||||
* @returns a percentage number with two digits, null if the value cannot be calculated
|
||||
*/
|
||||
public getValuePercent(poll: ViewAssignmentPoll, value: CalculablePollKey): number | null {
|
||||
if (!poll.pollBase) {
|
||||
return null;
|
||||
}
|
||||
const amount = poll[value];
|
||||
if (amount === undefined || amount < 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(((amount * 100) / poll.pollBase) * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the option in a poll is abstract (percentages should not be calculated)
|
||||
*
|
||||
|
@ -26,7 +26,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'assignments',
|
||||
loadChildren: './assignments/assignments.module#AssignmentsModule',
|
||||
data: { basePerm: 'assignment.can_see' }
|
||||
data: { basePerm: 'assignments.can_see' }
|
||||
},
|
||||
{
|
||||
path: 'mediafiles',
|
||||
|
@ -1,7 +1,6 @@
|
||||
from typing import Any, Dict, List, Set
|
||||
from typing import Any, Dict, Set
|
||||
|
||||
from django.apps import AppConfig
|
||||
from mypy_extensions import TypedDict
|
||||
|
||||
|
||||
class AssignmentsAppConfig(AppConfig):
|
||||
@ -51,14 +50,6 @@ class AssignmentsAppConfig(AppConfig):
|
||||
"""
|
||||
yield self.get_model("Assignment")
|
||||
|
||||
def get_angular_constants(self):
|
||||
assignment = self.get_model("Assignment")
|
||||
Item = TypedDict("Item", {"value": int, "display_name": str})
|
||||
phases: List[Item] = []
|
||||
for phase in assignment.PHASES:
|
||||
phases.append({"value": phase[0], "display_name": phase[1]})
|
||||
return {"AssignmentPhases": phases}
|
||||
|
||||
|
||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user