Merge pull request #4625 from MaximilianKrambach/assignmentSlide

Assignment/Assignment poll slide
This commit is contained in:
Emanuel Schütze 2019-04-25 22:18:31 +02:00 committed by GitHub
commit ddd4588a1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 227 additions and 26 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 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> </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,64 @@
<div *ngIf="data"> <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> </div>

View File

@ -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;
}
}

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

@ -7,7 +7,7 @@
#sidebox { #sidebox {
width: 260px; width: 260px;
right: 0; right: 0;
margin-top: 95px; margin-top: 94px;
background: #d3d3d3; background: #d3d3d3;
border-radius: 7px 0 0 7px; border-radius: 7px 0 0 7px;
padding: 3px 7px 10px 10px; padding: 3px 7px 10px 10px;

View File

@ -22,18 +22,19 @@
} }
.slidetitle { .slidetitle {
border-bottom: 5px solid #d3d3d3; border-bottom: 4px solid #d3d3d3;
margin-bottom: 40px; margin-bottom: 40px;
h1 { h1 {
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
margin-top: 20px;
} }
h2 { h2 {
color: #9a9898; color: #9a9898;
margin-top: 10px; margin-top: 10px;
margin-bottom: 5px; margin-bottom: 2px;
font-size: 28px; font-size: 28px;
font-weight: normal; font-weight: normal;
display: block; display: block;
@ -48,4 +49,8 @@
hr { hr {
margin: 10px 0; margin: 10px 0;
} }
.bold {
font-weight: 500;
}
} }

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"],
} }