Merge pull request #4640 from MaximilianKrambach/pollCalculations
Poll calculations
This commit is contained in:
commit
a09269cf2a
@ -1,6 +1,11 @@
|
||||
import { Deserializer } from '../base/deserializer';
|
||||
import { PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||
|
||||
export interface AssignmentOptionVote {
|
||||
weight: number;
|
||||
value: PollVoteValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a poll option
|
||||
*
|
||||
@ -11,10 +16,7 @@ export class AssignmentPollOption extends Deserializer {
|
||||
public id: number; // The AssignmentUser id of the candidate
|
||||
public candidate_id: number; // the User id of the candidate
|
||||
public is_elected: boolean;
|
||||
public votes: {
|
||||
weight: number; // represented as a string because it's a decimal field
|
||||
value: PollVoteValue;
|
||||
}[];
|
||||
public votes: AssignmentOptionVote[];
|
||||
public poll_id: number;
|
||||
public weight: number; // weight to order the display
|
||||
|
||||
|
@ -98,11 +98,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
this._assignment = assignment;
|
||||
|
||||
this.filterCandidates();
|
||||
if (this.assignment.polls.length) {
|
||||
this.assignment.polls.forEach(poll => {
|
||||
poll.pollBase = this.pollService.getBaseAmount(poll);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,10 +7,9 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
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 { 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 { OperatorService } from 'app/core/core-services/operator.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 dialog: MatDialog,
|
||||
private promptService: PromptService,
|
||||
private formBuilder: FormBuilder,
|
||||
private config: ConfigService
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
@ -148,13 +146,6 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
this.descriptionForm = this.formBuilder.group({
|
||||
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 {
|
||||
const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes';
|
||||
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;
|
||||
}
|
||||
|
||||
@ -263,7 +258,11 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
*/
|
||||
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 quorum = this.pollService.yesQuorum(
|
||||
this.majorityChoice,
|
||||
this.pollService.calculationDataFromPoll(this.poll),
|
||||
option
|
||||
);
|
||||
const isReached = this.quorumReached(option)
|
||||
? this.translate.instant('reached')
|
||||
: this.translate.instant('not reached');
|
||||
|
@ -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 { Updateable } from 'app/site/base/updateable';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
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)
|
||||
@ -40,10 +40,7 @@ export class ViewAssignmentPollOption implements Identifiable, Updateable {
|
||||
return this.option.is_elected;
|
||||
}
|
||||
|
||||
public get votes(): {
|
||||
weight: number;
|
||||
value: PollVoteValue;
|
||||
}[] {
|
||||
public get votes(): AssignmentOptionVote[] {
|
||||
return this.option.votes.sort((a, b) => votesOrder.indexOf(a.value) - votesOrder.indexOf(b.value));
|
||||
}
|
||||
|
||||
|
@ -78,13 +78,6 @@ export class ViewAssignmentPoll implements Identifiable, Updateable, Projectable
|
||||
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[]) {
|
||||
this._assignmentPoll = assignmentPoll;
|
||||
this._assignmentPollOptions = assignmentPollOptions;
|
||||
|
@ -243,8 +243,11 @@ export class AssignmentPdfService {
|
||||
const conclusionLabel = this.translate.instant(this.pollService.getLabel(key));
|
||||
const specialLabel = this.translate.instant(this.pollService.getSpecialLabel(poll[key]));
|
||||
let percentLabel = '';
|
||||
if (!this.pollService.isAbstractValue(poll, key)) {
|
||||
percentLabel = ` (${this.pollService.getValuePercent(poll, key)}%)`;
|
||||
if (!this.pollService.isAbstractValue(this.pollService.calculationDataFromPoll(poll), key)) {
|
||||
percentLabel = ` (${this.pollService.getValuePercent(
|
||||
this.pollService.calculationDataFromPoll(poll),
|
||||
key
|
||||
)}%)`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
@ -302,10 +305,17 @@ export class AssignmentPdfService {
|
||||
let resultString = '';
|
||||
const label = this.translate.instant(this.pollService.getLabel(optionLabel));
|
||||
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}`;
|
||||
if (percentNr && !this.pollService.isAbstractOption(poll, pollOption, optionLabel)) {
|
||||
if (
|
||||
percentNr &&
|
||||
!this.pollService.isAbstractOption(this.pollService.calculationDataFromPoll(poll), pollOption, optionLabel)
|
||||
) {
|
||||
resultString += ` (${percentNr}%)`;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 {
|
||||
PollService,
|
||||
@ -15,6 +16,28 @@ type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno';
|
||||
export type AssignmentPollMethod = 'yn' | 'yna' | 'votes';
|
||||
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)
|
||||
*/
|
||||
@ -75,17 +98,18 @@ export class AssignmentPollService extends PollService {
|
||||
* 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)
|
||||
*
|
||||
* @param poll
|
||||
* @param data
|
||||
* @returns The amount of votes indicating the 100% base
|
||||
*/
|
||||
public getBaseAmount(poll: ViewAssignmentPoll): number | null {
|
||||
switch (this.percentBase) {
|
||||
public getBaseAmount(data: CalculationData): number | null {
|
||||
const percentBase = data.percentBase || this.percentBase;
|
||||
switch (percentBase) {
|
||||
case 'DISABLED':
|
||||
return null;
|
||||
case 'YES_NO':
|
||||
case 'YES_NO_ABSTAIN':
|
||||
if (poll.pollmethod === 'votes') {
|
||||
const yes = poll.options.map(option => {
|
||||
if (data.pollMethod === 'votes') {
|
||||
const yes = data.pollOptions.map(option => {
|
||||
const yesValue = option.votes.find(v => v.value === 'Votes');
|
||||
return yesValue ? yesValue.weight : -99;
|
||||
});
|
||||
@ -99,9 +123,9 @@ export class AssignmentPollService extends PollService {
|
||||
return null;
|
||||
}
|
||||
case 'CAST':
|
||||
return poll.votescast > 0 && poll.votesinvalid >= 0 ? poll.votescast : null;
|
||||
return data.votescast > 0 && data.votesinvalid >= 0 ? data.votescast : null;
|
||||
case 'VALID':
|
||||
return poll.votesvalid > 0 ? poll.votesvalid : null;
|
||||
return data.votesvalid > 0 ? data.votesvalid : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -111,25 +135,25 @@ export class AssignmentPollService extends PollService {
|
||||
* Get the percentage for an option
|
||||
*
|
||||
* @param poll
|
||||
* @param option
|
||||
* @param value
|
||||
* @param data
|
||||
* @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;
|
||||
if (this.percentBase === 'DISABLED') {
|
||||
if (percentBase === 'DISABLED') {
|
||||
return null;
|
||||
} else if (this.percentBase === 'VALID') {
|
||||
base = poll.votesvalid;
|
||||
} else if (this.percentBase === 'CAST') {
|
||||
base = poll.votescast;
|
||||
} else if (percentBase === 'VALID') {
|
||||
base = data.votesvalid;
|
||||
} else if (percentBase === 'CAST') {
|
||||
base = data.votescast;
|
||||
} 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) {
|
||||
return null;
|
||||
}
|
||||
const vote = option.votes.find(v => v.value === value);
|
||||
const vote = option.votes.find(v => v.value === key);
|
||||
if (!vote) {
|
||||
return null;
|
||||
}
|
||||
@ -140,39 +164,53 @@ export class AssignmentPollService extends PollService {
|
||||
* 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 data
|
||||
* @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) {
|
||||
public getValuePercent(data: CalculationData, value: CalculablePollKey): number | null {
|
||||
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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const baseAmount = this.getBaseAmount(data);
|
||||
if (!baseAmount) {
|
||||
return null;
|
||||
}
|
||||
const amount = poll[value];
|
||||
const amount = data[value];
|
||||
if (amount === undefined || amount < 0) {
|
||||
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)
|
||||
*
|
||||
* @param poll
|
||||
* @param data
|
||||
* @param option
|
||||
* @param key (optional) the key to calculate
|
||||
* @returns true if the poll has no percentages, the poll option is a special value,
|
||||
* or if the calculations are disabled in the config
|
||||
*/
|
||||
public isAbstractOption(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption, key?: PollVoteValue): boolean {
|
||||
if (this.percentBase === 'DISABLED' || !option.votes || !option.votes.length) {
|
||||
public isAbstractOption(data: CalculationData, option: ViewAssignmentPollOption, key?: PollVoteValue): boolean {
|
||||
const percentBase = data.percentBase || this.percentBase;
|
||||
if (percentBase === 'DISABLED' || !option.votes || !option.votes.length) {
|
||||
return true;
|
||||
}
|
||||
if (key === 'Abstain' && this.percentBase === 'YES_NO') {
|
||||
if (key === 'Abstain' && percentBase === 'YES_NO') {
|
||||
return true;
|
||||
}
|
||||
if (poll.pollmethod === 'votes') {
|
||||
return poll.pollBase ? false : true;
|
||||
if (data.pollMethod === 'votes') {
|
||||
return this.getBaseAmount(data) > 0 ? false : true;
|
||||
} else {
|
||||
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
|
||||
* 'meta' values
|
||||
*
|
||||
* @param poll
|
||||
* @param data
|
||||
* @param value
|
||||
* @returns true if percentages cannot be calculated
|
||||
* TODO: Yes, No, etc. in an option will always return true.
|
||||
* Use {@link isAbstractOption} for these
|
||||
*/
|
||||
public isAbstractValue(poll: ViewAssignmentPoll, value: CalculablePollKey): boolean {
|
||||
if (this.percentBase === 'DISABLED' || !poll.pollBase || !this.pollValues.includes(value)) {
|
||||
public isAbstractValue(data: CalculationData, value: CalculablePollKey): boolean {
|
||||
const percentBase = data.percentBase || this.percentBase;
|
||||
if (percentBase === 'DISABLED' || !this.getBaseAmount(data) || !this.pollValues.includes(value)) {
|
||||
return true;
|
||||
}
|
||||
if (this.percentBase === 'CAST' && poll[value] >= 0) {
|
||||
if (percentBase === 'CAST' && data[value] >= 0) {
|
||||
return false;
|
||||
} else if (this.percentBase === 'VALID' && value === 'votesvalid' && poll[value] > 0) {
|
||||
} else if (percentBase === 'VALID' && value === 'votesvalid' && data[value] > 0) {
|
||||
return false;
|
||||
}
|
||||
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'
|
||||
*
|
||||
* @param data
|
||||
* @param option
|
||||
* @returns an positive integer to be used as percentage base, or null
|
||||
*/
|
||||
private getOptionBaseAmount(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption): number | null {
|
||||
if (this.percentBase === 'DISABLED' || poll.pollmethod === 'votes') {
|
||||
private getOptionBaseAmount(data: CalculationData, option: CalculationOption): number | null {
|
||||
const percentBase = data.percentBase || this.percentBase;
|
||||
if (percentBase === 'DISABLED' || data.pollMethod === 'votes') {
|
||||
return null;
|
||||
} else if (this.percentBase === 'CAST') {
|
||||
return poll.votescast > 0 ? poll.votescast : null;
|
||||
} else if (this.percentBase === 'VALID') {
|
||||
return poll.votesvalid > 0 ? poll.votesvalid : null;
|
||||
} else if (percentBase === 'CAST') {
|
||||
return data.votescast > 0 ? data.votescast : null;
|
||||
} else if (percentBase === 'VALID') {
|
||||
return data.votesvalid > 0 ? data.votesvalid : null;
|
||||
}
|
||||
const yes = option.votes.find(v => v.value === 'Yes');
|
||||
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) {
|
||||
return 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');
|
||||
if (!abstain || abstain.weight === undefined) {
|
||||
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
|
||||
*
|
||||
* @param method
|
||||
* @param poll
|
||||
* @param data
|
||||
* @param option
|
||||
* @returns a positive integer number; may return null if quorum is not calculable
|
||||
*/
|
||||
public yesQuorum(
|
||||
method: MajorityMethod,
|
||||
poll: ViewAssignmentPoll,
|
||||
option: ViewAssignmentPollOption
|
||||
): number | null {
|
||||
const baseAmount = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option);
|
||||
public yesQuorum(method: MajorityMethod, data: CalculationData, option: ViewAssignmentPollOption): number | null {
|
||||
const baseAmount =
|
||||
data.pollMethod === 'votes' ? this.getBaseAmount(data) : this.getOptionBaseAmount(data, option);
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,8 @@ export interface PollSlideOption {
|
||||
user: string;
|
||||
is_elected: boolean;
|
||||
votes: {
|
||||
weight: PollVoteValue;
|
||||
value: string;
|
||||
percent?: string;
|
||||
weight: string;
|
||||
value: PollVoteValue;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
@ -24,39 +24,18 @@
|
||||
</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>
|
||||
<span *ngIf="vote.value !== 'Votes'">{{ getLabel(vote.value) }}:</span>
|
||||
<span> {{ getVotePercent(vote.value, option) }}</span>
|
||||
</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">
|
||||
<span translate>Valid votes</span>
|
||||
<span>{{ getLabel(value) }}</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 }}
|
||||
{{ getPollPercent(value) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,9 +1,15 @@
|
||||
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 { 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 { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'os-poll-slide',
|
||||
@ -13,85 +19,78 @@ import { SlideData } from 'app/core/core-services/projector-data.service';
|
||||
export class PollSlideComponent extends BaseSlideComponent<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()
|
||||
public set data(data: SlideData<PollSlideData>) {
|
||||
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> {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
public constructor(private pollService: AssignmentPollService) {
|
||||
public constructor(private pollService: AssignmentPollService, private translate: TranslateService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getVoteString(rawValue: string): string {
|
||||
const num = parseFloat(rawValue);
|
||||
if (!isNaN(num)) {
|
||||
return this.pollService.getSpecialLabel(num);
|
||||
}
|
||||
return '-';
|
||||
/**
|
||||
* get a vote's numerical or special label, including percent values if these are to
|
||||
* be displayed
|
||||
*
|
||||
* @param key
|
||||
* @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 {
|
||||
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}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
public getPollPercent(key: CalculablePollKey): string {
|
||||
const percent = this.pollService.getValuePercent(this.calculationData, key);
|
||||
const number = this.translate.instant(this.pollService.getSpecialLabel(this.calculationData[key]));
|
||||
return percent === null ? number : `${number} (${percent}%)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns a translated label for a key
|
||||
*/
|
||||
public labelValue(input: string): string {
|
||||
return this.pollService.getSpecialLabel(parseFloat(input));
|
||||
public getLabel(key: CalculablePollKey): string {
|
||||
return this.translate.instant(this.pollService.getLabel(key));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user