Result PDF for Voting

- Add result PDF for Motion and Assignments
- Add "getPercentBase" for Assignment
This commit is contained in:
Sean Engelhardt 2020-02-12 15:03:27 +01:00 committed by FinnStutzenstein
parent 6044c63c28
commit 93dc78c7d6
9 changed files with 124 additions and 314 deletions

View File

@ -1,3 +1,4 @@
import { CalculablePollKey } from 'app/site/polls/services/poll.service';
import { AssignmentOption } from './assignment-option';
import { BasePoll } from '../poll/base-poll';
@ -23,6 +24,16 @@ export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
public global_abstain: boolean;
public description: string;
public get pollmethodFields(): CalculablePollKey[] {
if (this.pollmethod === AssignmentPollMethods.YN) {
return ['yes', 'no'];
} else if (this.pollmethod === AssignmentPollMethods.YNA) {
return ['yes', 'no', 'abstain'];
} else if (this.pollmethod === AssignmentPollMethods.Votes) {
return ['yes'];
}
}
public constructor(input?: any) {
super(AssignmentPoll.COLLECTIONSTRING, input);
}

View File

@ -1,3 +1,4 @@
import { CalculablePollKey } from 'app/site/polls/services/poll.service';
import { BasePoll } from '../poll/base-poll';
import { MotionOption } from './motion-option';
@ -16,6 +17,15 @@ export class MotionPoll extends BasePoll<MotionPoll, MotionOption> {
public motion_id: number;
public pollmethod: MotionPollMethods;
public get pollmethodFields(): CalculablePollKey[] {
const ynField: CalculablePollKey[] = ['yes', 'no'];
if (this.pollmethod === MotionPollMethods.YN) {
return ynField;
} else if (this.pollmethod === MotionPollMethods.YNA) {
return ynField.concat(['abstain']);
}
}
public constructor(input?: any) {
super(MotionPoll.COLLECTIONSTRING, input);
}

View File

@ -24,7 +24,6 @@
<div *ngIf="assignment">
<!-- PDF -->
<button mat-menu-item (click)="onDownloadPdf()">
<!-- TODO: results or description. Results if published -->
<mat-icon>picture_as_pdf</mat-icon>
<span translate>PDF</span>
</button>

View File

@ -2,7 +2,7 @@ import { BehaviorSubject } from 'rxjs';
import { ChartData } from 'app/shared/components/charts/charts.component';
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
import { PollColor, PollState } from 'app/shared/models/poll/base-poll';
import { PercentBase, PollColor, PollState } from 'app/shared/models/poll/base-poll';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { PollData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
@ -80,7 +80,8 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
yes: candidate.yes,
no: candidate.no,
abstain: candidate.abstain,
user: candidate.user.full_name
user: candidate.user.full_name,
showPercent: true
}))
.sort((a, b) => b.yes - a.yes);
@ -97,8 +98,41 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
return super.getNextStates();
}
private sumOptionsYN(): number {
return this.options.reduce((o, n) => {
o += n.yes > 0 ? n.yes : 0;
o += n.no > 0 ? n.no : 0;
return o;
}, 0);
}
private sumOptionsYNA(): number {
return this.options.reduce((o, n) => {
o += n.abstain > 0 ? n.abstain : 0;
return o;
}, this.sumOptionsYN());
}
public getPercentBase(): number {
return 0;
const base: PercentBase = this.poll.onehundred_percent_base;
let totalByBase: number;
switch (base) {
case PercentBase.YN:
totalByBase = this.sumOptionsYN();
break;
case PercentBase.YNA:
totalByBase = this.sumOptionsYNA();
break;
case PercentBase.Valid:
totalByBase = this.poll.votesvalid;
break;
case PercentBase.Cast:
totalByBase = this.poll.votescast;
break;
default:
break;
}
return totalByBase;
}
}

View File

@ -3,7 +3,12 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HtmlToPdfService } from 'app/core/pdf-services/html-to-pdf.service';
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
import { PollPercentBasePipe } from 'app/shared/pipes/poll-percent-base.pipe';
import { PollData } from 'app/site/polls/models/view-base-poll';
import { ViewAssignment } from '../models/view-assignment';
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
/**
* Creates a PDF document from a single assignment
@ -20,7 +25,13 @@ export class AssignmentPdfService {
* @param pdfDocumentService PDF functions
* @param htmlToPdfService Convert the assignment detail html text to pdf
*/
public constructor(private translate: TranslateService, private htmlToPdfService: HtmlToPdfService) {}
public constructor(
private translate: TranslateService,
private htmlToPdfService: HtmlToPdfService,
private pollKeyVerbose: PollKeyVerbosePipe,
private parsePollNumber: ParsePollNumberPipe,
private pollPercentBase: PollPercentBasePipe
) {}
/**
* Main function to control the pdf generation.
@ -140,44 +151,20 @@ export class AssignmentPdfService {
}
}
/**
* Creates a candidate line in the results table
*
* @param candidateName The name of the candidate
* @param pollOption the poll options (yes, no, maybe [...])
* @returns a line in the table
*/
// TODO: type the result.
/*private electedCandidateLine(candidateName: string, pollOption: ViewAssignmentOption): object {
if (pollOption.is_elected) {
this.showIsElected = true;
return {
text: candidateName + '*',
bold: true
};
} else {
return {
text: candidateName
};
}*
}*/
/**
* Creates the poll result table for all published polls
*
* @param assignment the ViewAssignment to create the document for
* @returns the table as pdfmake object
*/
// TODO: type the result
private createPollResultTable(assignment: ViewAssignment): object {
/*const resultBody = [];
for (let pollIndex = 0; pollIndex < assignment.polls.length; pollIndex++) {
const poll = assignment.polls[pollIndex];
if (poll.published) {
const resultBody = [];
for (const poll of assignment.polls) {
if (poll.isPublished) {
const pollTableBody = [];
resultBody.push({
text: `${this.translate.instant('Ballot')} ${pollIndex + 1}`,
text: poll.title,
bold: true,
style: 'textItem',
margin: [0, 15, 0, 0]
@ -194,56 +181,22 @@ export class AssignmentPdfService {
}
]);
for (let optionIndex = 0; optionIndex < poll.options.length; optionIndex++) {
const pollOption = poll.options[optionIndex];
const tableData = poll.generateTableData();
const candidateName = pollOption.user.full_name;
const votes = pollOption.votes; // 0 = yes, 1 = no, 2 = abstain0 = yes, 1 = no, 2 = abstain
const tableLine = [];
tableLine.push(this.electedCandidateLine(candidateName, pollOption));
for (const pollResult of tableData) {
const resultLine = this.getPollResult(pollResult, poll);
if (poll.pollmethod === 'votes') {
tableLine.push({
text: this.parseVoteValue(votes[0].value, votes[0].weight, poll, pollOption)
});
} else {
const resultBlock = votes.map(vote =>
this.parseVoteValue(vote.value, vote.weight, poll, pollOption)
);
tableLine.push({
text: resultBlock
});
}
pollTableBody.push(tableLine);
}
// push the result lines
const summaryLine = this.pollService.getVoteOptionsByPoll(poll).map(key => {
// TODO: Refractor into pollService to make this easier.
// Return an object with untranslated lable: string, specialLabel: string and (opt) percent: number
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(this.pollService.calculationDataFromPoll(poll), key)) {
percentLabel = ` (${this.pollService.getValuePercent(
this.pollService.calculationDataFromPoll(poll),
key
)}%)`;
}
return [
const tableLine = [
{
text: conclusionLabel,
style: 'tableConclude'
text: pollResult.user
},
{
text: specialLabel + percentLabel,
style: 'tableConclude'
text: resultLine
}
];
});
pollTableBody.push(...summaryLine);
pollTableBody.push(tableLine);
}
resultBody.push({
table: {
@ -256,52 +209,19 @@ export class AssignmentPdfService {
}
}
// add the legend to the result body
// if (assignment.polls.length > 0 && isElectedSemaphore) {
if (assignment.polls.length > 0 && this.showIsElected) {
resultBody.push({
text: `* = ${this.translate.instant('is elected')}`,
margin: [0, 5, 0, 0]
});
}
return resultBody;*/
throw new Error('TODO');
return resultBody;
}
/**
* Creates a translated voting result with numbers and percent-value depending in the polloptions
* I.e: "Yes 25 (22,2%)" or just "10"
*
* @param optionLabel Usually Yes or No
* @param value the amount of votes
* @param poll the specific poll
* @param option the corresponding poll option
* @returns a string a nicer number representation: "Yes 25 (22,2%)" or just "10"
* Converts pollData to a printable string representation
*/
/*private parseVoteValue(
optionLabel: PollVoteValue,
value: number,
poll: ViewAssignmentPoll,
option: ViewAssignmentOption
): string {
let resultString = '';
const label = this.translate.instant(this.pollService.getLabel(optionLabel));
const valueString = this.pollService.getSpecialLabel(value);
const percentNr = this.pollService.getPercent(
this.pollService.calculationDataFromPoll(poll),
option,
optionLabel
);
resultString += `${label} ${valueString}`;
if (
percentNr &&
!this.pollService.isAbstractOption(this.pollService.calculationDataFromPoll(poll), option, optionLabel)
) {
resultString += ` (${percentNr}%)`;
}
return `${resultString}\n`;
}*/
private getPollResult(votingResult: PollData, poll: ViewAssignmentPoll): string {
const resultList = poll.pollmethodFields.map(field => {
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(field));
const resultValue = this.parsePollNumber.transform(votingResult[field]);
const resultInPercent = this.pollPercentBase.transform(votingResult[field], poll);
return `${votingKey}: ${resultValue} ${resultInPercent ? resultInPercent : ''}`;
});
return resultList.join('\n');
}
}

View File

@ -68,8 +68,12 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
}
];
public get result(): ViewMotionOption {
return this.options[0];
}
public get hasVotes(): boolean {
return !!this.options[0].votes.length;
return !!this.result.votes.length;
}
public initChartLabels(): string[] {
@ -130,7 +134,7 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
}
public anySpecialVotes(): boolean {
return this.options[0].yes < 0 || this.options[0].no < 0 || this.options[0].abstain < 0;
return this.result.yes < 0 || this.result.no < 0 || this.result.abstain < 0;
}
/**
@ -145,23 +149,22 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
public getPercentBase(): number {
const base: PercentBase = this.poll.onehundred_percent_base;
const options = this.options[0];
let totalByBase: number;
switch (base) {
case PercentBase.YN:
if (options.yes >= 0 && options.no >= 0) {
totalByBase = options.sumYN();
if (this.result.yes >= 0 && this.result.no >= 0) {
totalByBase = this.result.sumYN();
}
break;
case PercentBase.YNA:
if (options.yes >= 0 && options.no >= 0 && options.abstain >= 0) {
totalByBase = options.sumYNA();
if (this.result.yes >= 0 && this.result.no >= 0 && this.result.abstain >= 0) {
totalByBase = this.result.sumYNA();
}
break;
case PercentBase.Valid:
// auslagern
if (options.yes >= 0 && options.no >= 0 && options.abstain >= 0) {
if (this.result.yes >= 0 && this.result.no >= 0 && this.result.abstain >= 0) {
totalByBase = this.poll.votesvalid;
}
break;

View File

@ -55,7 +55,6 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
public openDialog(): void {
this.pollDialog.openDialog(this.poll);
console.log('this.poll: ', this.poll.hasVotes);
}
protected onDeleted(): void {

View File

@ -11,6 +11,9 @@ import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions
import { ConfigService } from 'app/core/ui-services/config.service';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
import { PollPercentBasePipe } from 'app/shared/pipes/poll-percent-base.pipe';
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
import { MotionExportInfo } from './motion-export.service';
import { ChangeRecoMode, InfoToExport, LineNumberingMode, PERSONAL_NOTE_ID } from '../motions.constants';
@ -61,7 +64,10 @@ export class MotionPdfService {
private pdfDocumentService: PdfDocumentService,
private htmlToPdfService: HtmlToPdfService,
private linenumberingService: LinenumberingService,
private commentRepo: MotionCommentSectionRepositoryService
private commentRepo: MotionCommentSectionRepositoryService,
private pollKeyVerbose: PollKeyVerbosePipe,
private pollPercentBase: PollPercentBasePipe,
private parsePollNumber: ParsePollNumberPipe
) {}
/**
@ -362,31 +368,20 @@ export class MotionPdfService {
const column1 = [];
const column2 = [];
const column3 = [];
motion.polls.map((poll, index) => {
/*if (poll.has_votes) {
if (motion.motion.polls.length > 1) {
column1.push(index + 1 + '. ' + this.translate.instant('Vote'));
column2.push('');
column3.push('');
}
const values: CalculablePollKey[] = ['yes', 'no', 'abstain'];
if (poll.votesvalid) {
values.push('votesvalid');
}
if (poll.votesinvalid) {
values.push('votesinvalid');
}
if (poll.votescast) {
values.push('votescast');
}
values.map(value => {
column1.push(`${this.translate.instant(this.pollService.getLabel(value))}:`);
column2.push(`${this.translate.instant(this.pollService.getSpecialLabel(poll[value]))}`);
this.pollService.isAbstractValue(poll, value)
? column3.push('')
: column3.push(`(${this.pollService.calculatePercentage(poll, value)} %)`);
motion.polls.forEach(poll => {
if (poll.hasVotes) {
const tableData = poll.generateTableData();
tableData.forEach(votingResult => {
const resultKey = this.translate.instant(this.pollKeyVerbose.transform(votingResult.key));
const resultValue = this.parsePollNumber.transform(votingResult.value);
column1.push(`${resultKey}:`);
column2.push(resultValue);
if (votingResult.showPercent) {
const resultInPercent = this.pollPercentBase.transform(votingResult.value, poll);
column3.push(resultInPercent);
}
});
}*/
}
});
metaTableBody.push([
{
@ -653,15 +648,6 @@ export class MotionPdfService {
margin: [0, 25, 0, 10]
});
// determine the width of the reason depending on line numbering
// currently not used
// let columnWidth: string;
// if (lnMode === LineNumberingMode.Outside) {
// columnWidth = '80%';
// } else {
// columnWidth = '100%';
// }
reason.push(this.htmlToPdfService.addPlainText(motion.reason));
return reason;

View File

@ -132,24 +132,6 @@ export abstract class PollService {
.subscribe(settings => (this.isElectronicVotingEnabled = settings.ENABLE_ELECTRONIC_VOTING));
}
/**
* retrieve special labels for a poll value
* {@link specialPollVotes}. Positive values will return as string
* representation of themselves
*
* @param value check value for special numbers
* @returns the label for a non-positive value, according to
*/
public getSpecialLabel(value: number): string {
// if (value >= 0) {
// return value.toString();
// // TODO: toLocaleString(lang); but translateService is not usable here, thus lang is not well defined
// }
// const vote = this.specialPollVotes.find(special => special[0] === value);
// return vote ? vote[1] : 'Undocumented special (negative) value';
return '';
}
/**
* Assigns the default poll data to the object. To be extended in subclasses
* @param poll the poll/object to fill
@ -160,140 +142,6 @@ export abstract class PollService {
poll.type = PollType.Analog;
}
/**
* Calculates the percentage the given key reaches.
*
* @param poll
* @param key
* @returns a percentage number with two digits, null if the value cannot be calculated (consider 0 !== null)
*/
public calculatePercentage(poll: ViewBasePoll, key: CalculablePollKey): number | null {
const baseNumber = this.getBaseAmount(poll);
if (!baseNumber) {
return null;
}
switch (key) {
case 'abstain':
if (poll.onehundred_percent_base === PercentBase.YN) {
return null;
}
break;
case 'votesinvalid':
if (poll.onehundred_percent_base !== PercentBase.Cast) {
return null;
}
break;
case 'votesvalid':
if (![PercentBase.Cast, PercentBase.Valid].includes(poll.onehundred_percent_base)) {
return null;
}
break;
case 'votescast':
if (poll.onehundred_percent_base !== PercentBase.Cast) {
return null;
}
}
return Math.round(((poll[key] * 100) / baseNumber) * 100) / 100;
}
/**
* Gets the number representing 100 percent for a given MotionPoll, depending
* on the configuration and the votes given.
*
* @param poll
* @returns the positive number representing 100 percent of the poll, 0 if
* the base cannot be calculated
*/
public getBaseAmount(poll: ViewBasePoll): number {
/*if (!poll) {
return 0;
}
switch (this.percentBase) {
case 'CAST':
if (!poll.votescast) {
return 0;
}
if (poll.votesinvalid < 0) {
return 0;
}
return poll.votescast;
case 'VALID':
if (poll.yes < 0 || poll.no < 0 || poll.abstain < 0) {
return 0;
}
return poll.votesvalid ? poll.votesvalid : 0;
case 'YES_NO_ABSTAIN':
if (poll.yes < 0 || poll.no < 0 || poll.abstain < 0) {
return 0;
}
return poll.yes + poll.no + poll.abstain;
case 'YES_NO':
if (poll.yes < 0 || poll.no < 0 || poll.abstain === -1) {
// It is not allowed to set 'Abstain' to 'majority' but exclude it from calculation.
// Setting 'Abstain' to 'undocumented' is possible, of course.
return 0;
}
return poll.yes + poll.no;
}*/
return 0;
}
/**
* Calculates which number is needed for the quorum to be surpassed
* TODO: Methods still hard coded to mirror the server's.
*
* @param poll
* @param method (optional) majority calculation method. If none is given,
* the default as set in the config will be used.
* @returns the first integer number larger than the required majority,
* undefined if a quorum cannot be calculated.
*/
public calculateQuorum(poll: ViewBasePoll, method?: string): number {
if (!method) {
method = this.defaultMajorityMethod;
}
const baseNumber = this.getBaseAmount(poll);
if (!baseNumber) {
return undefined;
}
const calc = PollMajorityMethod.find(m => m.value === method);
return calc && calc.calc ? calc.calc(baseNumber) : null;
}
/**
* Determines if a value is abstract (percentages cannot be calculated)
*
* @param poll
* @param value
* @returns true if the percentages should not be calculated
*/
public isAbstractValue(poll: ViewBasePoll, value: CalculablePollKey): boolean {
// if (this.getBaseAmount(poll) === 0) {
// return true;
// }
// switch (this.percentBase) {
// case 'YES_NO':
// if (['votescast', 'votesinvalid', 'votesvalid', 'abstain'].includes(value)) {
// return true;
// }
// break;
// case 'YES_NO_ABSTAIN':
// if (['votescast', 'votesinvalid', 'votesvalid'].includes(value)) {
// return true;
// }
// break;
// case 'VALID':
// if (['votesinvalid', 'votescast'].includes(value)) {
// return true;
// }
// break;
// }
// if (poll[value] < 0) {
// return true;
// }
return false;
}
public getVerboseNameForValue(key: string, value: string): string {
switch (key) {
case 'majority_method':