Merge pull request #4625 from MaximilianKrambach/assignmentSlide
Assignment/Assignment poll slide
This commit is contained in:
commit
ddd4588a1c
@ -169,6 +169,7 @@ export abstract class PollService {
|
||||
public getSpecialLabel(value: number): string {
|
||||
if (value >= 0) {
|
||||
return value.toString();
|
||||
// TODO: toLocaleString(lang); but translateService is not usable here, thus lang is not well defined
|
||||
}
|
||||
const vote = this.specialPollVotes.find(special => special[0] === value);
|
||||
return vote ? vote[1] : 'Undocumented special (negative) value';
|
||||
|
@ -28,14 +28,22 @@
|
||||
<span translate>PDF</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<!-- Delete -->
|
||||
</div>
|
||||
<!-- Project -->
|
||||
<os-projector-button
|
||||
[object]="assignment"
|
||||
[menuItem]="true"
|
||||
*osPerms="'core.can_manage_projector'"
|
||||
></os-projector-button>
|
||||
|
||||
<!-- Delete -->
|
||||
<div *ngIf="assignment && hasPerms('manage')">
|
||||
<button mat-menu-item class="red-warning-text" (click)="onDeleteAssignmentButton()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</mat-menu>
|
||||
</os-head-bar>
|
||||
|
||||
|
@ -36,8 +36,8 @@
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- projector column -->
|
||||
<ng-container matColumnDef="projector">
|
||||
<!-- projector column -->
|
||||
<ng-container matColumnDef="projector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment">
|
||||
<os-projector-button [object]="assignment"></os-projector-button>
|
||||
|
@ -0,0 +1,4 @@
|
||||
/** Title */
|
||||
.mat-column-title {
|
||||
padding-left: 10px;
|
||||
}
|
@ -3,17 +3,12 @@ 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 { AssignmentPollService, SummaryPollKey } from '../../services/assignment-poll.service';
|
||||
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||
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 summaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid';
|
||||
|
||||
/**
|
||||
* A dialog for updating the values of an assignment-related poll.
|
||||
*/
|
||||
@ -26,7 +21,7 @@ export class AssignmentPollDialogComponent {
|
||||
/**
|
||||
* The summary values that will have fields in the dialog
|
||||
*/
|
||||
public sumValues: summaryPollKey[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
public sumValues: SummaryPollKey[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
|
||||
/**
|
||||
* List of accepted special non-numerical values.
|
||||
@ -148,7 +143,7 @@ export class AssignmentPollDialogComponent {
|
||||
* @param value
|
||||
* @returns integer or undefined
|
||||
*/
|
||||
public getSumValue(value: summaryPollKey): number | undefined {
|
||||
public getSumValue(value: SummaryPollKey): number | undefined {
|
||||
return this.data[value] || undefined;
|
||||
}
|
||||
|
||||
@ -158,7 +153,7 @@ export class AssignmentPollDialogComponent {
|
||||
* @param value
|
||||
* @param weight
|
||||
*/
|
||||
public setSumValue(value: summaryPollKey, weight: string): void {
|
||||
public setSumValue(value: SummaryPollKey, weight: string): void {
|
||||
this.data[value] = parseFloat(weight);
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,11 @@ type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno';
|
||||
export type AssignmentPollMethod = 'yn' | 'yna' | 'votes';
|
||||
export type AssignmentPercentBase = 'YES_NO_ABSTAIN' | 'YES_NO' | 'VALID' | 'CAST' | 'DISABLED';
|
||||
|
||||
/**
|
||||
* Vote entries included once for summary (e.g. total votes cast)
|
||||
*/
|
||||
export type SummaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid' | 'votesno' | 'votesabstain';
|
||||
|
||||
/**
|
||||
* Service class for assignment polls.
|
||||
*/
|
||||
|
@ -1,3 +1,16 @@
|
||||
<div *ngIf="data">
|
||||
<h1>TODO</h1>
|
||||
<div class="slidetitle">
|
||||
<h1>{{ data.data.title }}</h1>
|
||||
<h2 translate>Election</h2>
|
||||
</div>
|
||||
|
||||
<div *ngIf="data.data && data.data.description" [innerHTML]="data.data.description"></div>
|
||||
|
||||
<h3 translate>Candidates</h3>
|
||||
<ul *ngIf="data.data.assignment_related_users && data.data.assignment_related_users.length">
|
||||
<li *ngFor="let candidate of data.data.assignment_related_users">
|
||||
{{ candidate.user }}
|
||||
<mat-icon *ngIf="candidate.elected">star</mat-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -17,12 +17,11 @@ export class AssignmentSlideComponent extends BaseSlideComponent<AssignmentSlide
|
||||
@Input()
|
||||
public set data(data: SlideData<AssignmentSlideData>) {
|
||||
this._data = data;
|
||||
console.log('Data: ', data);
|
||||
}
|
||||
|
||||
public get data(): SlideData<AssignmentSlideData> {
|
||||
return this._data;
|
||||
}
|
||||
// UNTIL HERE
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
@ -1,4 +1,15 @@
|
||||
import { AssignmentPercentBase, AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||
|
||||
export interface PollSlideOption {
|
||||
user: string;
|
||||
is_elected: boolean;
|
||||
votes: {
|
||||
weight: PollVoteValue;
|
||||
value: string;
|
||||
percent?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PollSlideData {
|
||||
title: string;
|
||||
@ -13,5 +24,6 @@ export interface PollSlideData {
|
||||
votesvalid?: string;
|
||||
votesinvalid?: string;
|
||||
votescast?: string;
|
||||
options?: PollSlideOption[];
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,64 @@
|
||||
<div *ngIf="data">
|
||||
<h1>TODO</h1>
|
||||
<div class="slidetitle">
|
||||
<h1>{{ data.data.title }}</h1>
|
||||
<h2 translate>Election result</h2>
|
||||
</div>
|
||||
|
||||
<div class="spacer-top-10"></div>
|
||||
<div *ngIf="!data.data.poll.published"><span translate>Waiting for results</span><span>...</span></div>
|
||||
<div *ngIf="data.data.poll.published">
|
||||
<div *ngIf="data.data.poll.has_votes" class="result-table">
|
||||
<div class="row">
|
||||
<div class="option-name heading">
|
||||
<h3 translate>Candidates</h3>
|
||||
</div>
|
||||
<div class="option-percents heading">
|
||||
<h3 translate>Votes</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngFor="let option of data.data.poll.options">
|
||||
<div class="row">
|
||||
<div class="option-name">
|
||||
<span class="bold">{{ option.user }}</span>
|
||||
<mat-icon *ngIf="option.is_elected">star</mat-icon>
|
||||
</div>
|
||||
<div class="option-percents">
|
||||
<div *ngFor="let vote of option.votes" class="bold">
|
||||
<span *ngIf="vote.value !== 'Votes'">{{ vote.value | translate }}:</span>
|
||||
<span>
|
||||
{{ labelValue(vote.weight) | translate }}
|
||||
</span>
|
||||
<span *ngIf="vote.percent">
|
||||
({{ vote.percent }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="data.data.poll.votesvalid !== null" class="row">
|
||||
<div class="option-name grey">
|
||||
<span translate>Valid votes</span>
|
||||
</div>
|
||||
<div class="option-percents grey">
|
||||
{{ labelValue(data.data.poll.votesvalid) | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="data.data.poll.votesinvalid !== null" class="row">
|
||||
<div class="option-name grey">
|
||||
<span translate>Invalid votes</span>
|
||||
</div>
|
||||
<div class="option-percents grey">
|
||||
{{ labelValue(data.data.poll.votesinvalid) | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="data.data.poll.votescast !== null" class="row">
|
||||
<div class="option-name grey">
|
||||
<span translate>Total votes cast</span>
|
||||
</div>
|
||||
<div class="option-percents grey">
|
||||
{{ labelValue(data.data.poll.votescast) | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,31 @@
|
||||
.row {
|
||||
border-top: 1px solid #ddd;
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
||||
.heading {
|
||||
text-transform: uppercase;
|
||||
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.option-name {
|
||||
display: table-cell;
|
||||
padding: 5px;
|
||||
vertical-align: middle;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.option-percents {
|
||||
display: table-cell;
|
||||
padding: 5px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.grey {
|
||||
background-color: #ddd !important;
|
||||
color: rgba(0, 0, 0, 0.87) !important;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { AssignmentPollService, SummaryPollKey } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { PollSlideData } from './poll-slide-data';
|
||||
import { SlideData } from 'app/core/core-services/projector-data.service';
|
||||
@ -10,21 +11,87 @@ import { SlideData } from 'app/core/core-services/projector-data.service';
|
||||
styleUrls: ['./poll-slide.component.scss']
|
||||
})
|
||||
export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
|
||||
// TODO: Remove the following block, if not needed.
|
||||
// This is just for debugging to get a console statement with all recieved
|
||||
// data from the server
|
||||
private _data: SlideData<PollSlideData>;
|
||||
|
||||
public pollValues: SummaryPollKey[] = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast'];
|
||||
|
||||
@Input()
|
||||
public set data(data: SlideData<PollSlideData>) {
|
||||
this._data = data;
|
||||
console.log('Data: ', data);
|
||||
this.setPercents();
|
||||
}
|
||||
|
||||
public get data(): SlideData<PollSlideData> {
|
||||
return this._data;
|
||||
}
|
||||
// UNTIL HERE
|
||||
|
||||
public constructor() {
|
||||
public constructor(private pollService: AssignmentPollService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getVoteString(rawValue: string): string {
|
||||
const num = parseFloat(rawValue);
|
||||
if (!isNaN(num)) {
|
||||
return this.pollService.getSpecialLabel(num);
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
private setPercents(): void {
|
||||
if (
|
||||
this.data.data.assignments_poll_100_percent_base === 'DISABLED' ||
|
||||
!this.data.data.poll.has_votes ||
|
||||
!this.data.data.poll.options.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (const option of this.data.data.poll.options) {
|
||||
for (const vote of option.votes) {
|
||||
const voteweight = parseFloat(vote.weight);
|
||||
if (isNaN(voteweight) || voteweight < 0) {
|
||||
return;
|
||||
}
|
||||
let base: number;
|
||||
switch (this.data.data.assignments_poll_100_percent_base) {
|
||||
case 'CAST':
|
||||
base = this.data.data.poll.votescast ? parseFloat(this.data.data.poll.votescast) : 0;
|
||||
break;
|
||||
case 'VALID':
|
||||
base = this.data.data.poll.votesvalid ? parseFloat(this.data.data.poll.votesvalid) : 0;
|
||||
break;
|
||||
case 'YES_NO':
|
||||
case 'YES_NO_ABSTAIN':
|
||||
const yesOption = option.votes.find(v => v.value === 'Yes');
|
||||
const yes = yesOption ? parseFloat(yesOption.weight) : -1;
|
||||
const noOption = option.votes.find(v => v.value === 'No');
|
||||
const no = noOption ? parseFloat(noOption.weight) : -1;
|
||||
const absOption = option.votes.find(v => v.value === 'Abstain');
|
||||
const abs = absOption ? parseFloat(absOption.weight) : -1;
|
||||
if (this.data.data.assignments_poll_100_percent_base === 'YES_NO_ABSTAIN') {
|
||||
base = yes >= 0 && no >= 0 && abs >= 0 ? yes + no + abs : 0;
|
||||
} else {
|
||||
if (vote.value !== 'Abstain') {
|
||||
base = yes >= 0 && no >= 0 ? yes + no : 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (base) {
|
||||
vote.percent = `${Math.round(((parseFloat(vote.weight) * 100) / base) * 100) / 100}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number-like string to a simpler, more readable version
|
||||
*
|
||||
* @param input a server-sent string representing a numerical value
|
||||
* @returns either the special label or a cleaned-up number of the inpu string
|
||||
*/
|
||||
public labelValue(input: string): string {
|
||||
return this.pollService.getSpecialLabel(parseFloat(input));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
#sidebox {
|
||||
width: 260px;
|
||||
right: 0;
|
||||
margin-top: 95px;
|
||||
margin-top: 94px;
|
||||
background: #d3d3d3;
|
||||
border-radius: 7px 0 0 7px;
|
||||
padding: 3px 7px 10px 10px;
|
||||
|
@ -22,18 +22,19 @@
|
||||
}
|
||||
|
||||
.slidetitle {
|
||||
border-bottom: 5px solid #d3d3d3;
|
||||
border-bottom: 4px solid #d3d3d3;
|
||||
margin-bottom: 40px;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #9a9898;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
@ -48,4 +49,8 @@
|
||||
hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ async def poll_slide(
|
||||
|
||||
poll_data["options"] = [
|
||||
{
|
||||
"user": await get_user_name(all_data, option["user_id"]),
|
||||
"user": await get_user_name(all_data, option["candidate_id"]),
|
||||
"is_elected": option["is_elected"],
|
||||
"votes": option["votes"],
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user