Merge pull request #4640 from MaximilianKrambach/pollCalculations

Poll calculations
This commit is contained in:
Maximilian Krambach 2019-05-20 11:59:31 +02:00 committed by GitHub
commit a09269cf2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 180 deletions

View File

@ -1,6 +1,11 @@
import { Deserializer } from '../base/deserializer'; import { Deserializer } from '../base/deserializer';
import { PollVoteValue } from 'app/core/ui-services/poll.service'; import { PollVoteValue } from 'app/core/ui-services/poll.service';
export interface AssignmentOptionVote {
weight: number;
value: PollVoteValue;
}
/** /**
* Representation of a poll option * Representation of a poll option
* *
@ -11,10 +16,7 @@ export class AssignmentPollOption extends Deserializer {
public id: number; // The AssignmentUser id of the candidate public id: number; // The AssignmentUser id of the candidate
public candidate_id: number; // the User id of the candidate public candidate_id: number; // the User id of the candidate
public is_elected: boolean; public is_elected: boolean;
public votes: { public votes: AssignmentOptionVote[];
weight: number; // represented as a string because it's a decimal field
value: PollVoteValue;
}[];
public poll_id: number; public poll_id: number;
public weight: number; // weight to order the display public weight: number; // weight to order the display

View File

@ -98,11 +98,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
this._assignment = assignment; this._assignment = assignment;
this.filterCandidates(); this.filterCandidates();
if (this.assignment.polls.length) {
this.assignment.polls.forEach(poll => {
poll.pollBase = this.pollService.getBaseAmount(poll);
});
}
} }
/** /**

View File

@ -7,10 +7,9 @@ import { TranslateService } from '@ngx-translate/core';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component'; import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component';
import { AssignmentPollService, AssignmentPercentBase } from '../../services/assignment-poll.service'; import { AssignmentPollService } from '../../services/assignment-poll.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { ConfigService } from 'app/core/ui-services/config.service';
import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service'; import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
@ -132,8 +131,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
public translate: TranslateService, public translate: TranslateService,
public dialog: MatDialog, public dialog: MatDialog,
private promptService: PromptService, private promptService: PromptService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder
private config: ConfigService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
} }
@ -148,13 +146,6 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
this.descriptionForm = this.formBuilder.group({ this.descriptionForm = this.formBuilder.group({
description: this.poll ? this.poll.description : '' description: this.poll ? this.poll.description : ''
}); });
this.subscriptions.push(
this.config.get<AssignmentPercentBase>('assignments_poll_100_percent_base').subscribe(() => {
if (this.poll) {
this.poll.pollBase = this.pollService.getBaseAmount(this.poll);
}
})
);
} }
/** /**
@ -188,7 +179,11 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
public quorumReached(option: ViewAssignmentPollOption): boolean { public quorumReached(option: ViewAssignmentPollOption): boolean {
const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes'; const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes';
const amount = option.votes.find(v => v.value === yesValue).weight; const amount = option.votes.find(v => v.value === yesValue).weight;
const yesQuorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option); const yesQuorum = this.pollService.yesQuorum(
this.majorityChoice,
this.pollService.calculationDataFromPoll(this.poll),
option
);
return yesQuorum && amount >= yesQuorum; return yesQuorum && amount >= yesQuorum;
} }
@ -263,7 +258,11 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
*/ */
public getQuorumReachedString(option: ViewAssignmentPollOption): string { public getQuorumReachedString(option: ViewAssignmentPollOption): string {
const name = this.translate.instant(this.majorityChoice.display_name); const name = this.translate.instant(this.majorityChoice.display_name);
const quorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option); const quorum = this.pollService.yesQuorum(
this.majorityChoice,
this.pollService.calculationDataFromPoll(this.poll),
option
);
const isReached = this.quorumReached(option) const isReached = this.quorumReached(option)
? this.translate.instant('reached') ? this.translate.instant('reached')
: this.translate.instant('not reached'); : this.translate.instant('not reached');

View File

@ -1,9 +1,9 @@
import { ViewUser } from 'app/site/users/models/view-user'; import { AssignmentPollOption, AssignmentOptionVote } from 'app/shared/models/assignments/assignment-poll-option';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { Updateable } from 'app/site/base/updateable';
import { Identifiable } from 'app/shared/models/base/identifiable'; import { Identifiable } from 'app/shared/models/base/identifiable';
import { PollVoteValue } from 'app/core/ui-services/poll.service'; import { PollVoteValue } from 'app/core/ui-services/poll.service';
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option'; import { Updateable } from 'app/site/base/updateable';
import { ViewUser } from 'app/site/users/models/view-user';
/** /**
* Defines the order the option's votes are sorted in (server might send raw data in any order) * Defines the order the option's votes are sorted in (server might send raw data in any order)
@ -40,10 +40,7 @@ export class ViewAssignmentPollOption implements Identifiable, Updateable {
return this.option.is_elected; return this.option.is_elected;
} }
public get votes(): { public get votes(): AssignmentOptionVote[] {
weight: number;
value: PollVoteValue;
}[] {
return this.option.votes.sort((a, b) => votesOrder.indexOf(a.value) - votesOrder.indexOf(b.value)); return this.option.votes.sort((a, b) => votesOrder.indexOf(a.value) - votesOrder.indexOf(b.value));
} }

View File

@ -78,13 +78,6 @@ export class ViewAssignmentPoll implements Identifiable, Updateable, Projectable
return this.poll.assignment_id; return this.poll.assignment_id;
} }
/**
* storing the base values for percentage calculations,
* to avoid recalculating pollBases too often
* (the calculation iterates through all pollOptions in some use cases)
*/
public pollBase: number;
public constructor(assignmentPoll: AssignmentPoll, assignmentPollOptions: ViewAssignmentPollOption[]) { public constructor(assignmentPoll: AssignmentPoll, assignmentPollOptions: ViewAssignmentPollOption[]) {
this._assignmentPoll = assignmentPoll; this._assignmentPoll = assignmentPoll;
this._assignmentPollOptions = assignmentPollOptions; this._assignmentPollOptions = assignmentPollOptions;

View File

@ -243,8 +243,11 @@ export class AssignmentPdfService {
const conclusionLabel = this.translate.instant(this.pollService.getLabel(key)); const conclusionLabel = this.translate.instant(this.pollService.getLabel(key));
const specialLabel = this.translate.instant(this.pollService.getSpecialLabel(poll[key])); const specialLabel = this.translate.instant(this.pollService.getSpecialLabel(poll[key]));
let percentLabel = ''; let percentLabel = '';
if (!this.pollService.isAbstractValue(poll, key)) { if (!this.pollService.isAbstractValue(this.pollService.calculationDataFromPoll(poll), key)) {
percentLabel = ` (${this.pollService.getValuePercent(poll, key)}%)`; percentLabel = ` (${this.pollService.getValuePercent(
this.pollService.calculationDataFromPoll(poll),
key
)}%)`;
} }
return [ return [
{ {
@ -302,10 +305,17 @@ export class AssignmentPdfService {
let resultString = ''; let resultString = '';
const label = this.translate.instant(this.pollService.getLabel(optionLabel)); const label = this.translate.instant(this.pollService.getLabel(optionLabel));
const valueString = this.pollService.getSpecialLabel(value); const valueString = this.pollService.getSpecialLabel(value);
const percentNr = this.pollService.getPercent(poll, pollOption, optionLabel); const percentNr = this.pollService.getPercent(
this.pollService.calculationDataFromPoll(poll),
pollOption,
optionLabel
);
resultString += `${label} ${valueString}`; resultString += `${label} ${valueString}`;
if (percentNr && !this.pollService.isAbstractOption(poll, pollOption, optionLabel)) { if (
percentNr &&
!this.pollService.isAbstractOption(this.pollService.calculationDataFromPoll(poll), pollOption, optionLabel)
) {
resultString += ` (${percentNr}%)`; resultString += ` (${percentNr}%)`;
} }

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AssignmentOptionVote } from 'app/shared/models/assignments/assignment-poll-option';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { import {
PollService, PollService,
@ -15,6 +16,28 @@ 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';
/**
* interface common to data in a ViewAssignmentPoll and PollSlideData
*
* TODO: simplify
*/
export interface CalculationData {
pollMethod: AssignmentPollMethod;
votesno: number;
votesabstain: number;
votescast: number;
votesvalid: number;
votesinvalid: number;
percentBase?: AssignmentPercentBase;
pollOptions?: {
votes: AssignmentOptionVote[];
}[];
}
interface CalculationOption {
votes: AssignmentOptionVote[];
}
/** /**
* Vote entries included once for summary (e.g. total votes cast) * Vote entries included once for summary (e.g. total votes cast)
*/ */
@ -75,17 +98,18 @@ export class AssignmentPollService extends PollService {
* Get the base amount for the 100% calculations. Note that some poll methods * Get the base amount for the 100% calculations. Note that some poll methods
* (e.g. yes/no/abstain may have a different percentage base and will return null here) * (e.g. yes/no/abstain may have a different percentage base and will return null here)
* *
* @param poll * @param data
* @returns The amount of votes indicating the 100% base * @returns The amount of votes indicating the 100% base
*/ */
public getBaseAmount(poll: ViewAssignmentPoll): number | null { public getBaseAmount(data: CalculationData): number | null {
switch (this.percentBase) { const percentBase = data.percentBase || this.percentBase;
switch (percentBase) {
case 'DISABLED': case 'DISABLED':
return null; return null;
case 'YES_NO': case 'YES_NO':
case 'YES_NO_ABSTAIN': case 'YES_NO_ABSTAIN':
if (poll.pollmethod === 'votes') { if (data.pollMethod === 'votes') {
const yes = poll.options.map(option => { const yes = data.pollOptions.map(option => {
const yesValue = option.votes.find(v => v.value === 'Votes'); const yesValue = option.votes.find(v => v.value === 'Votes');
return yesValue ? yesValue.weight : -99; return yesValue ? yesValue.weight : -99;
}); });
@ -99,9 +123,9 @@ export class AssignmentPollService extends PollService {
return null; return null;
} }
case 'CAST': case 'CAST':
return poll.votescast > 0 && poll.votesinvalid >= 0 ? poll.votescast : null; return data.votescast > 0 && data.votesinvalid >= 0 ? data.votescast : null;
case 'VALID': case 'VALID':
return poll.votesvalid > 0 ? poll.votesvalid : null; return data.votesvalid > 0 ? data.votesvalid : null;
default: default:
return null; return null;
} }
@ -111,25 +135,25 @@ export class AssignmentPollService extends PollService {
* Get the percentage for an option * Get the percentage for an option
* *
* @param poll * @param poll
* @param option * @param data
* @param value
* @returns a percentage number with two digits, null if the value cannot be calculated * @returns a percentage number with two digits, null if the value cannot be calculated
*/ */
public getPercent(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption, value: PollVoteValue): number | null { public getPercent(data: CalculationData, option: CalculationOption, key: PollVoteValue): number | null {
const percentBase = data.percentBase || this.percentBase;
let base = 0; let base = 0;
if (this.percentBase === 'DISABLED') { if (percentBase === 'DISABLED') {
return null; return null;
} else if (this.percentBase === 'VALID') { } else if (percentBase === 'VALID') {
base = poll.votesvalid; base = data.votesvalid;
} else if (this.percentBase === 'CAST') { } else if (percentBase === 'CAST') {
base = poll.votescast; base = data.votescast;
} else { } else {
base = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option); base = data.pollMethod === 'votes' ? this.getBaseAmount(data) : this.getOptionBaseAmount(data, option);
} }
if (!base || base < 0) { if (!base || base < 0) {
return null; return null;
} }
const vote = option.votes.find(v => v.value === value); const vote = option.votes.find(v => v.value === key);
if (!vote) { if (!vote) {
return null; return null;
} }
@ -140,39 +164,53 @@ export class AssignmentPollService extends PollService {
* get the percentage for a non-abstract per-poll value * get the percentage for a non-abstract per-poll value
* TODO: similar code to getPercent. Mergeable? * TODO: similar code to getPercent. Mergeable?
* *
* @param poll the poll this value refers to * @param data
* @param value a per-poll value (e.g. 'votesvalid') * @param value a per-poll value (e.g. 'votesvalid')
* @returns a percentage number with two digits, null if the value cannot be calculated * @returns a percentage number with two digits, null if the value cannot be calculated
*/ */
public getValuePercent(poll: ViewAssignmentPoll, value: CalculablePollKey): number | null { public getValuePercent(data: CalculationData, value: CalculablePollKey): number | null {
if (!poll.pollBase) { const percentBase = data.percentBase || this.percentBase;
switch (percentBase) {
case 'YES_NO':
case 'YES_NO_ABSTAIN':
case 'DISABLED':
return null;
case 'VALID':
if (value === 'votesinvalid' || value === 'votescast') {
return null; return null;
} }
const amount = poll[value]; break;
}
const baseAmount = this.getBaseAmount(data);
if (!baseAmount) {
return null;
}
const amount = data[value];
if (amount === undefined || amount < 0) { if (amount === undefined || amount < 0) {
return null; return null;
} }
return Math.round(((amount * 100) / poll.pollBase) * 100) / 100; return Math.round(((amount * 100) / baseAmount) * 100) / 100;
} }
/** /**
* Check if the option in a poll is abstract (percentages should not be calculated) * Check if the option in a poll is abstract (percentages should not be calculated)
* *
* @param poll * @param data
* @param option * @param option
* @param key (optional) the key to calculate * @param key (optional) the key to calculate
* @returns true if the poll has no percentages, the poll option is a special value, * @returns true if the poll has no percentages, the poll option is a special value,
* or if the calculations are disabled in the config * or if the calculations are disabled in the config
*/ */
public isAbstractOption(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption, key?: PollVoteValue): boolean { public isAbstractOption(data: CalculationData, option: ViewAssignmentPollOption, key?: PollVoteValue): boolean {
if (this.percentBase === 'DISABLED' || !option.votes || !option.votes.length) { const percentBase = data.percentBase || this.percentBase;
if (percentBase === 'DISABLED' || !option.votes || !option.votes.length) {
return true; return true;
} }
if (key === 'Abstain' && this.percentBase === 'YES_NO') { if (key === 'Abstain' && percentBase === 'YES_NO') {
return true; return true;
} }
if (poll.pollmethod === 'votes') { if (data.pollMethod === 'votes') {
return poll.pollBase ? false : true; return this.getBaseAmount(data) > 0 ? false : true;
} else { } else {
return option.votes.some(v => v.weight < 0); return option.votes.some(v => v.weight < 0);
} }
@ -182,19 +220,20 @@ export class AssignmentPollService extends PollService {
* Check for abstract (not usable as percentage) options in non-option * Check for abstract (not usable as percentage) options in non-option
* 'meta' values * 'meta' values
* *
* @param poll * @param data
* @param value * @param value
* @returns true if percentages cannot be calculated * @returns true if percentages cannot be calculated
* TODO: Yes, No, etc. in an option will always return true. * TODO: Yes, No, etc. in an option will always return true.
* Use {@link isAbstractOption} for these * Use {@link isAbstractOption} for these
*/ */
public isAbstractValue(poll: ViewAssignmentPoll, value: CalculablePollKey): boolean { public isAbstractValue(data: CalculationData, value: CalculablePollKey): boolean {
if (this.percentBase === 'DISABLED' || !poll.pollBase || !this.pollValues.includes(value)) { const percentBase = data.percentBase || this.percentBase;
if (percentBase === 'DISABLED' || !this.getBaseAmount(data) || !this.pollValues.includes(value)) {
return true; return true;
} }
if (this.percentBase === 'CAST' && poll[value] >= 0) { if (percentBase === 'CAST' && data[value] >= 0) {
return false; return false;
} else if (this.percentBase === 'VALID' && value === 'votesvalid' && poll[value] > 0) { } else if (percentBase === 'VALID' && value === 'votesvalid' && data[value] > 0) {
return false; return false;
} }
return true; return true;
@ -203,24 +242,27 @@ export class AssignmentPollService extends PollService {
/** /**
* Calculate the base amount inside an option. Only useful if poll method is not 'votes' * Calculate the base amount inside an option. Only useful if poll method is not 'votes'
* *
* @param data
* @param option
* @returns an positive integer to be used as percentage base, or null * @returns an positive integer to be used as percentage base, or null
*/ */
private getOptionBaseAmount(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption): number | null { private getOptionBaseAmount(data: CalculationData, option: CalculationOption): number | null {
if (this.percentBase === 'DISABLED' || poll.pollmethod === 'votes') { const percentBase = data.percentBase || this.percentBase;
if (percentBase === 'DISABLED' || data.pollMethod === 'votes') {
return null; return null;
} else if (this.percentBase === 'CAST') { } else if (percentBase === 'CAST') {
return poll.votescast > 0 ? poll.votescast : null; return data.votescast > 0 ? data.votescast : null;
} else if (this.percentBase === 'VALID') { } else if (percentBase === 'VALID') {
return poll.votesvalid > 0 ? poll.votesvalid : null; return data.votesvalid > 0 ? data.votesvalid : null;
} }
const yes = option.votes.find(v => v.value === 'Yes'); const yes = option.votes.find(v => v.value === 'Yes');
const no = option.votes.find(v => v.value === 'No'); const no = option.votes.find(v => v.value === 'No');
if (this.percentBase === 'YES_NO') { if (percentBase === 'YES_NO') {
if (!yes || yes.weight === undefined || !no || no.weight === undefined) { if (!yes || yes.weight === undefined || !no || no.weight === undefined) {
return null; return null;
} }
return yes.weight >= 0 && no.weight >= 0 ? yes.weight + no.weight : null; return yes.weight >= 0 && no.weight >= 0 ? yes.weight + no.weight : null;
} else if (this.percentBase === 'YES_NO_ABSTAIN') { } else if (percentBase === 'YES_NO_ABSTAIN') {
const abstain = option.votes.find(v => v.value === 'Abstain'); const abstain = option.votes.find(v => v.value === 'Abstain');
if (!abstain || abstain.weight === undefined) { if (!abstain || abstain.weight === undefined) {
return null; return null;
@ -235,16 +277,32 @@ export class AssignmentPollService extends PollService {
* Get the minimum amount of votes needed for an option to pass the quorum * Get the minimum amount of votes needed for an option to pass the quorum
* *
* @param method * @param method
* @param poll * @param data
* @param option * @param option
* @returns a positive integer number; may return null if quorum is not calculable * @returns a positive integer number; may return null if quorum is not calculable
*/ */
public yesQuorum( public yesQuorum(method: MajorityMethod, data: CalculationData, option: ViewAssignmentPollOption): number | null {
method: MajorityMethod, const baseAmount =
poll: ViewAssignmentPoll, data.pollMethod === 'votes' ? this.getBaseAmount(data) : this.getOptionBaseAmount(data, option);
option: ViewAssignmentPollOption
): number | null {
const baseAmount = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option);
return method.calc(baseAmount); return method.calc(baseAmount);
} }
/**
* helper function to tuirn a Poll into calculation data for this service
* TODO: temp until better method to normalize Poll ans PollSlideData is implemented
*
* @param poll
* @returns calculationData ready to be used
*/
public calculationDataFromPoll(poll: ViewAssignmentPoll): CalculationData {
return {
pollMethod: poll.pollmethod,
votesno: poll.votesno,
votesabstain: poll.votesabstain,
votescast: poll.votescast,
votesinvalid: poll.votesinvalid,
votesvalid: poll.votesvalid,
pollOptions: poll.options
};
}
} }

View File

@ -5,9 +5,8 @@ export interface PollSlideOption {
user: string; user: string;
is_elected: boolean; is_elected: boolean;
votes: { votes: {
weight: PollVoteValue; weight: string;
value: string; value: PollVoteValue;
percent?: string;
}[]; }[];
} }

View File

@ -24,39 +24,18 @@
</div> </div>
<div class="option-percents"> <div class="option-percents">
<div *ngFor="let vote of option.votes" class="bold"> <div *ngFor="let vote of option.votes" class="bold">
<span *ngIf="vote.value !== 'Votes'">{{ vote.value | translate }}:</span> <span *ngIf="vote.value !== 'Votes'">{{ getLabel(vote.value) }}:</span>
<span> <span> {{ getVotePercent(vote.value, option) }}</span>
{{ labelValue(vote.weight) | translate }}
</span>
<span *ngIf="vote.percent">
({{ vote.percent }})
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div *ngIf="data.data.poll.votesvalid !== null" class="row"> <div *ngFor="let value of pollValues" class="row">
<div class="option-name grey"> <div class="option-name grey">
<span translate>Valid votes</span> <span>{{ getLabel(value) }}</span>
</div> </div>
<div class="option-percents grey"> <div class="option-percents grey">
{{ labelValue(data.data.poll.votesvalid) | translate }} {{ getPollPercent(value) }}
</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

@ -1,9 +1,15 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { AssignmentPollService, SummaryPollKey } from 'app/site/assignments/services/assignment-poll.service'; import {
AssignmentPollService,
CalculationData,
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, PollSlideOption } from './poll-slide-data';
import { PollVoteValue, CalculablePollKey } from 'app/core/ui-services/poll.service';
import { SlideData } from 'app/core/core-services/projector-data.service'; import { SlideData } from 'app/core/core-services/projector-data.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'os-poll-slide', selector: 'os-poll-slide',
@ -13,85 +19,78 @@ import { SlideData } from 'app/core/core-services/projector-data.service';
export class PollSlideComponent extends BaseSlideComponent<PollSlideData> { export class PollSlideComponent extends BaseSlideComponent<PollSlideData> {
private _data: SlideData<PollSlideData>; private _data: SlideData<PollSlideData>;
public pollValues: SummaryPollKey[] = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast']; private calculationData: CalculationData;
public get pollValues(): SummaryPollKey[] {
if (!this.data) {
return [];
}
const values: SummaryPollKey[] = ['votesno', 'votesabstain', 'votesvalid', 'votesinvalid', 'votescast'];
return values.filter(val => this.data.data.poll[val] !== null);
}
@Input() @Input()
public set data(data: SlideData<PollSlideData>) { public set data(data: SlideData<PollSlideData>) {
this._data = data; this._data = data;
this.setPercents(); this.calculationData = {
pollMethod: data.data.poll.pollmethod,
votesno: parseFloat(data.data.poll.votesno),
votesabstain: parseFloat(data.data.poll.votesabstain),
votescast: parseFloat(data.data.poll.votescast),
votesvalid: parseFloat(data.data.poll.votesvalid),
votesinvalid: parseFloat(data.data.poll.votesinvalid),
pollOptions: data.data.poll.options.map(opt => {
return {
votes: opt.votes.map(vote => {
return {
weight: parseFloat(vote.weight),
value: vote.value
};
})
};
}),
percentBase: data.data.assignments_poll_100_percent_base
};
} }
public get data(): SlideData<PollSlideData> { public get data(): SlideData<PollSlideData> {
return this._data; return this._data;
} }
public constructor(private pollService: AssignmentPollService) { public constructor(private pollService: AssignmentPollService, private translate: TranslateService) {
super(); super();
} }
public getVoteString(rawValue: string): string { /**
const num = parseFloat(rawValue); * get a vote's numerical or special label, including percent values if these are to
if (!isNaN(num)) { * be displayed
return this.pollService.getSpecialLabel(num); *
} * @param key
return '-'; * @param option
*/
public getVotePercent(key: PollVoteValue, option: PollSlideOption): string {
const calcOption = {
votes: option.votes.map(vote => {
return { weight: parseFloat(vote.weight), value: vote.value };
})
};
const percent = this.pollService.getPercent(this.calculationData, calcOption, key);
const number = this.translate.instant(
this.pollService.getSpecialLabel(parseFloat(option.votes.find(v => v.value === key).weight))
);
return percent === null ? number : `${number} (${percent}%)`;
} }
private setPercents(): void { public getPollPercent(key: CalculablePollKey): string {
if ( const percent = this.pollService.getValuePercent(this.calculationData, key);
this.data.data.assignments_poll_100_percent_base === 'DISABLED' || const number = this.translate.instant(this.pollService.getSpecialLabel(this.calculationData[key]));
!this.data.data.poll.has_votes || return percent === null ? number : `${number} (${percent}%)`;
!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 * @returns a translated label for a key
*
* @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 { public getLabel(key: CalculablePollKey): string {
return this.pollService.getSpecialLabel(parseFloat(input)); return this.translate.instant(this.pollService.getLabel(key));
} }
} }