Assignment slides

This commit is contained in:
Maximilian Krambach 2019-04-25 15:34:42 +02:00
parent 6800f99ef0
commit 1b1499a660
13 changed files with 204 additions and 23 deletions

View File

@ -169,6 +169,7 @@ export abstract class PollService {
public getSpecialLabel(value: number): string { public getSpecialLabel(value: number): string {
if (value >= 0) { if (value >= 0) {
return value.toString(); 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); const vote = this.specialPollVotes.find(special => special[0] === value);
return vote ? vote[1] : 'Undocumented special (negative) value'; return vote ? vote[1] : 'Undocumented special (negative) value';

View File

@ -28,14 +28,22 @@
<span translate>PDF</span> <span translate>PDF</span>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<!-- Delete -->
</div> </div>
<!-- Project -->
<os-projector-button
[object]="assignment"
[menuItem]="true"
*osPerms="'core.can_manage_projector'"
></os-projector-button>
<!-- Delete -->
<div *ngIf="assignment && hasPerms('manage')"> <div *ngIf="assignment && hasPerms('manage')">
<button mat-menu-item class="red-warning-text" (click)="onDeleteAssignmentButton()"> <button mat-menu-item class="red-warning-text" (click)="onDeleteAssignmentButton()">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>
</div> </div>
</mat-menu> </mat-menu>
</os-head-bar> </os-head-bar>

View File

@ -0,0 +1,4 @@
/** Title */
.mat-column-title {
padding-left: 10px;
}

View File

@ -3,17 +3,12 @@ import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option'; 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 { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll'; import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option'; 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. * 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 * 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. * List of accepted special non-numerical values.
@ -148,7 +143,7 @@ export class AssignmentPollDialogComponent {
* @param value * @param value
* @returns integer or undefined * @returns integer or undefined
*/ */
public getSumValue(value: summaryPollKey): number | undefined { public getSumValue(value: SummaryPollKey): number | undefined {
return this.data[value] || undefined; return this.data[value] || undefined;
} }
@ -158,7 +153,7 @@ export class AssignmentPollDialogComponent {
* @param value * @param value
* @param weight * @param weight
*/ */
public setSumValue(value: summaryPollKey, weight: string): void { public setSumValue(value: SummaryPollKey, weight: string): void {
this.data[value] = parseFloat(weight); this.data[value] = parseFloat(weight);
} }

View File

@ -15,6 +15,11 @@ type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno';
export type AssignmentPollMethod = 'yn' | 'yna' | 'votes'; export type AssignmentPollMethod = 'yn' | 'yna' | 'votes';
export type AssignmentPercentBase = 'YES_NO_ABSTAIN' | 'YES_NO' | 'VALID' | 'CAST' | 'DISABLED'; 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. * Service class for assignment polls.
*/ */

View File

@ -1,3 +1,16 @@
<div *ngIf="data"> <div *ngIf="data">
<h1>TODO</h1> <div>
<h1> {{ data.data.title }} </h1>
</div>
<mat-divider></mat-divider>
<div *ngIf="data.data && data.data.description" [innerHTML]="data.data.description"></div>
<mat-divider></mat-divider>
<h3 translate>Candidates</h3>
<div *ngIf="data.data.assignment_related_users && data.data.assignment_related_users.length">
<div *ngFor="let candidate of data.data.assignment_related_users">
{{ candidate.user }}
<mat-icon>{{ candidate.elected ? 'star' : 'star_border' }}</mat-icon>
</div>
</div>
</div> </div>

View File

@ -17,12 +17,11 @@ export class AssignmentSlideComponent extends BaseSlideComponent<AssignmentSlide
@Input() @Input()
public set data(data: SlideData<AssignmentSlideData>) { public set data(data: SlideData<AssignmentSlideData>) {
this._data = data; this._data = data;
console.log('Data: ', data);
} }
public get data(): SlideData<AssignmentSlideData> { public get data(): SlideData<AssignmentSlideData> {
return this._data; return this._data;
} }
// UNTIL HERE
public constructor() { public constructor() {
super(); super();

View File

@ -1,4 +1,15 @@
import { AssignmentPercentBase, AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service'; 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 { export interface PollSlideData {
title: string; title: string;
@ -13,5 +24,6 @@ export interface PollSlideData {
votesvalid?: string; votesvalid?: string;
votesinvalid?: string; votesinvalid?: string;
votescast?: string; votescast?: string;
options?: PollSlideOption[];
}; };
} }

View File

@ -1,3 +1,60 @@
<div *ngIf="data"> <div *ngIf="data">
<h1>TODO</h1> <h1>{{ data.data.title }}</h1>
<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">
<div class="row">
<div class="option-name">
<h3 translate>Candidates</h3>
</div>
<div class="option-percents">
<h3 translate>Votes</h3>
</div>
</div>
<div *ngFor="let option of data.data.poll.options">
<div class="row">
<div class="option-name">
<mat-icon *ngIf="option.is_elected">star</mat-icon>
<span [ngClass]="option.is_elected ? 'bold': ''">{{ option.user }}</span>
</div>
<div class="option-percents">
<div *ngFor="let vote of option.votes">
<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> </div>

View File

@ -0,0 +1,20 @@
.row {
display: table;
width: 100%;
.option-name {
display: table-cell;
padding: 5px;
border: 1px solid grey;
vertical-align: middle;
width: 70%;
.bold {
font-weight: bold;
}
}
.option-percents {
display: table-cell;
padding: 5px;
border: 1px solid grey;
width: 30%;
}
}

View File

@ -1,5 +1,6 @@
import { Component, Input } from '@angular/core'; 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 { BaseSlideComponent } from 'app/slides/base-slide-component';
import { PollSlideData } from './poll-slide-data'; import { PollSlideData } from './poll-slide-data';
import { SlideData } from 'app/core/core-services/projector-data.service'; 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'] styleUrls: ['./poll-slide.component.scss']
}) })
export class PollSlideComponent extends BaseSlideComponent<PollSlideData> { 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>; private _data: SlideData<PollSlideData>;
public pollValues: SummaryPollKey[] = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast'];
@Input() @Input()
public set data(data: SlideData<PollSlideData>) { public set data(data: SlideData<PollSlideData>) {
this._data = data; this._data = data;
console.log('Data: ', data); this.setPercents();
} }
public get data(): SlideData<PollSlideData> { public get data(): SlideData<PollSlideData> {
return this._data; return this._data;
} }
// UNTIL HERE
public constructor() { public constructor(private pollService: AssignmentPollService) {
super(); 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));
}
} }

View File

@ -87,7 +87,7 @@ async def poll_slide(
poll_data["options"] = [ 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"], "is_elected": option["is_elected"],
"votes": option["votes"], "votes": option["votes"],
} }