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 { 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
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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');
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const baseAmount = this.getBaseAmount(data);
|
||||||
|
if (!baseAmount) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const amount = poll[value];
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user