Merge pull request #4617 from tsiegleauq/assignment-pdf

Create assignment PDF service
This commit is contained in:
Emanuel Schütze 2019-04-26 21:46:04 +02:00 committed by GitHub
commit 23103362a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 440 additions and 26 deletions

View File

@ -44,6 +44,25 @@ export class PdfDocumentService {
*/ */
private imageUrls: string[] = []; private imageUrls: string[] = [];
/**
* Table layout that switches the background color every other row.
* @example
* ```ts
* layout: this.pdfDocumentService.switchColorTableLayout
* ```
*/
public switchColorTableLayout = {
hLineWidth: rowIndex => {
return rowIndex === 1;
},
vLineWidth: () => {
return 0;
},
fillColor: rowIndex => {
return rowIndex % 2 === 0 ? '#EEEEEE' : null;
}
};
/** /**
* Constructor * Constructor
* *
@ -502,6 +521,10 @@ export class PdfDocumentService {
listChild: { listChild: {
fontSize: 12, fontSize: 12,
margin: [0, 5] margin: [0, 5]
},
textItem: {
fontSize: 11,
margin: [0, 7]
} }
}; };
} }

View File

@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { Assignment } from 'app/shared/models/assignments/assignment'; import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { AssignmentPollService } 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';
@ -172,7 +173,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
public pollService: AssignmentPollService, public pollService: AssignmentPollService,
private agendaRepo: ItemRepositoryService, private agendaRepo: ItemRepositoryService,
private tagRepo: TagRepositoryService, private tagRepo: TagRepositoryService,
private promptService: PromptService private promptService: PromptService,
private pdfService: AssignmentPdfExportService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.subscriptions.push( this.subscriptions.push(
@ -406,7 +408,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
} }
public onDownloadPdf(): void { public onDownloadPdf(): void {
// TODO: Download summary pdf this.pdfService.exportSingleAssignment(this.assignment);
} }
/** /**

View File

@ -67,7 +67,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
* used in this poll (e.g.) * used in this poll (e.g.)
*/ */
public get pollValues(): CalculablePollKey[] { public get pollValues(): CalculablePollKey[] {
return this.pollService.pollValues.filter(name => this.poll[name] !== undefined); return this.pollService.getVoteOptionsByPoll(this.poll);
} }
/** /**

View File

@ -57,6 +57,14 @@ export class ViewAssignment extends BaseAgendaViewModel {
return this.assignment.title; return this.assignment.title;
} }
public get open_posts(): number {
return this.assignment.open_posts;
}
public get description(): string {
return this.assignment.description;
}
public get candidates(): ViewUser[] { public get candidates(): ViewUser[] {
return this._assignmentRelatedUsers.map(aru => aru.user); return this._assignmentRelatedUsers.map(aru => aru.user);
} }

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { AssignmentPdfExportService } from './assignment-pdf-export.service';
import { E2EImportsModule } from 'e2e-imports.module';
describe('AssignmentPdfExportService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: AssignmentPdfExportService = TestBed.get(AssignmentPdfExportService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { ViewAssignment } from '../models/view-assignment';
import { AssignmentPdfService } from './assignment-pdf.service';
import { TranslateService } from '@ngx-translate/core';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
/**
* Controls PDF export for assignments
*/
@Injectable({
providedIn: 'root'
})
export class AssignmentPdfExportService {
/**
* Constructor
*
* @param translate Translate
* @param assignmentPdfService Service for single assignment details
* @param pdfDocumentService Service for PDF document generation
*/
public constructor(
private translate: TranslateService,
private assignmentPdfService: AssignmentPdfService,
private pdfDocumentService: PdfDocumentService
) {}
/**
* Generates an pdf out of a given assignment and saves it as file
*
* @param assignment the assignment to export
*/
public exportSingleAssignment(assignment: ViewAssignment): void {
const doc = this.assignmentPdfService.assignmentToDocDef(assignment);
const filename = `${this.translate.instant('Assignments')} ${assignment.title}`;
const metadata = {
title: filename
};
this.pdfDocumentService.download(doc, filename, metadata);
}
}

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { AssignmentPdfService } from './assignment-pdf.service';
import { E2EImportsModule } from 'e2e-imports.module';
describe('AssignmentPdfService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: AssignmentPdfService = TestBed.get(AssignmentPdfService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,314 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { AssignmentPollService } from './assignment-poll.service';
import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
import { PollVoteValue } from 'app/core/ui-services/poll.service';
import { ViewAssignment } from '../models/view-assignment';
import { ViewAssignmentPollOption } from '../models/view-assignment-poll-option';
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
/**
* Creates a PDF document from a single assignment
*/
@Injectable({
providedIn: 'root'
})
export class AssignmentPdfService {
/**
* Will be set to `true` of a person was elected.
* Determines that in indicator is shown under the table
*/
private showIsElected = false;
/**
* Constructor
*
* @param translate Translate
* @param pollService Get poll information
* @param pdfDocumentService PDF functions
* @param htmlToPdfService Convert the assignment detail html text to pdf
*/
public constructor(
private translate: TranslateService,
private pollService: AssignmentPollService,
private pdfDocumentService: PdfDocumentService,
private htmlToPdfService: HtmlToPdfService
) {}
/**
* Main function to control the pdf generation.
* Calls all other functions to generate the PDF in multiple steps
*
* @param assignment the ViewAssignment to create the document for
* @returns a pdfmake compatible document as document
*/
public assignmentToDocDef(assignment: ViewAssignment): object {
const title = this.createTitle(assignment);
const preamble = this.createPreamble(assignment);
const description = this.createDescription(assignment);
const candidateList = this.createCandidateList(assignment);
const pollResult = this.createPollResultTable(assignment);
return [title, preamble, description, candidateList, pollResult];
}
/**
* Creates the title for PDF
* TODO: Cleanup. Should be reused from time to time. Can be in another service
*
* @param assignment the ViewAssignment to create the document for
* @returns the title part of the document
*/
private createTitle(assignment: ViewAssignment): object {
return {
text: assignment.title,
style: 'title'
};
}
/**
* Creates the preamble, usually just contains "Number of persons to be elected"
*
* @param assignment the ViewAssignment to create the document for
* @returns the preamble part of the pdf document
*/
private createPreamble(assignment: ViewAssignment): object {
const preambleText = `${this.translate.instant('Number of persons to be elected')}: `;
const memberNumber = '' + assignment.open_posts;
const preamble = {
text: [
{
text: preambleText,
bold: true,
style: 'textItem'
},
{
text: memberNumber,
style: 'textItem'
}
]
};
return preamble;
}
/**
* Creates the description part of the document. Also converts the parts of an assignment to PDF
*
* @param assignment the ViewAssignment to create the document for
* @returns the description of the assignment
*/
private createDescription(assignment: ViewAssignment): object {
if (assignment.description) {
const assignmentHtml = this.htmlToPdfService.convertHtml(assignment.description);
const descriptionText = `${this.translate.instant('Description')}: `;
const description = [
{
text: descriptionText,
bold: true,
style: 'textItem'
},
{
text: assignmentHtml,
style: 'textItem',
margin: [10, 0, 0, 0]
}
];
return description;
} else {
return {};
}
}
/**
* Creates the assignment list
*
* @param assignment the ViewAssignment to create the document for
* @returns the assignment list as PDF document
*/
private createCandidateList(assignment: ViewAssignment): object {
if (assignment.phase !== 2) {
const candidates = assignment.assignmentRelatedUsers.sort((a, b) => a.weight - b.weight);
const candidatesText = `${this.translate.instant('Candidates')}: `;
const userList = candidates.map(candidate => {
return {
text: candidate.user.full_name,
margin: [0, 0, 0, 10]
};
});
return {
columns: [
{
text: candidatesText,
bold: true,
width: '25%',
style: 'textItem'
},
{
ul: userList,
style: 'textItem'
}
]
};
} else {
return {};
}
}
/**
* 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
*/
private electedCandidateLine(candidateName: string, pollOption: ViewAssignmentPollOption): 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
*/
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 pollTableBody = [];
resultBody.push({
text: `${this.translate.instant('Ballot')} ${pollIndex + 1}`,
bold: true,
style: 'textItem',
margin: [0, 15, 0, 0]
});
pollTableBody.push([
{
text: this.translate.instant('Candidates'),
style: 'tableHeader'
},
{
text: this.translate.instant('Votes'),
style: 'tableHeader'
}
]);
for (let optionIndex = 0; optionIndex < poll.options.length; optionIndex++) {
const pollOption = poll.options[optionIndex];
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));
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(poll, key)) {
percentLabel = ` (${this.pollService.getValuePercent(poll, key)}%)`;
}
return [
{
text: conclusionLabel,
style: 'tableConclude'
},
{
text: specialLabel + percentLabel,
style: 'tableConclude'
}
];
});
pollTableBody.push(...summaryLine);
resultBody.push({
table: {
widths: ['64%', '33%'],
headerRows: 1,
body: pollTableBody
},
layout: this.pdfDocumentService.switchColorTableLayout
});
}
}
// 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;
}
/**
* 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 pollOption the corresponding poll option
* @returns a string a nicer number representation: "Yes 25 (22,2%)" or just "10"
*/
private parseVoteValue(
optionLabel: PollVoteValue,
value: number,
poll: ViewAssignmentPoll,
pollOption: ViewAssignmentPollOption
): string {
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);
resultString += `${label} ${valueString}`;
if (percentNr && !this.pollService.isAbstractOption(poll, pollOption, optionLabel)) {
resultString += ` (${percentNr}%)`;
}
return `${resultString}\n`;
}
}

View File

@ -67,6 +67,10 @@ export class AssignmentPollService extends PollService {
.subscribe(base => (this.percentBase = base)); .subscribe(base => (this.percentBase = base));
} }
public getVoteOptionsByPoll(poll: ViewAssignmentPoll): CalculablePollKey[] {
return this.pollValues.filter(name => poll[name] !== undefined);
}
/** /**
* 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)

View File

@ -13,6 +13,7 @@ import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-mo
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service'; import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
/** /**
* Type declaring which strings are valid options for metainfos to be exported into a pdf * Type declaring which strings are valid options for metainfos to be exported into a pdf
@ -52,6 +53,7 @@ export class MotionPdfService {
* @param statuteRepo To get formated stature paragraphs * @param statuteRepo To get formated stature paragraphs
* @param changeRecoRepo to get the change recommendations * @param changeRecoRepo to get the change recommendations
* @param configService Read config variables * @param configService Read config variables
* @param pdfDocumentService Global PDF Functions
* @param htmlToPdfService To convert HTML text into pdfmake doc def * @param htmlToPdfService To convert HTML text into pdfmake doc def
* @param pollService MotionPollService for rendering the polls * @param pollService MotionPollService for rendering the polls
* @param linenumberingService Line numbers * @param linenumberingService Line numbers
@ -63,6 +65,7 @@ export class MotionPdfService {
private statuteRepo: StatuteParagraphRepositoryService, private statuteRepo: StatuteParagraphRepositoryService,
private changeRecoRepo: ChangeRecommendationRepositoryService, private changeRecoRepo: ChangeRecommendationRepositoryService,
private configService: ConfigService, private configService: ConfigService,
private pdfDocumentService: PdfDocumentService,
private htmlToPdfService: HtmlToPdfService, private htmlToPdfService: HtmlToPdfService,
private pollService: MotionPollService, private pollService: MotionPollService,
private linenumberingService: LinenumberingService, private linenumberingService: LinenumberingService,
@ -652,17 +655,7 @@ export class MotionPdfService {
dontBreakRows: true, dontBreakRows: true,
body: callListTableBody.concat(callListRows) body: callListTableBody.concat(callListRows)
}, },
layout: { layout: this.pdfDocumentService.switchColorTableLayout
hLineWidth: rowIndex => {
return rowIndex === 1;
},
vLineWidth: () => {
return 0;
},
fillColor: rowIndex => {
return rowIndex % 2 === 0 ? '#EEEEEE' : null;
}
}
}; };
return [title, table]; return [title, table];
} }

View File

@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { ViewUser } from '../models/view-user'; import { ViewUser } from '../models/view-user';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
/** /**
* Creates a pdf for a user, containing greetings and initial login information * Creates a pdf for a user, containing greetings and initial login information
@ -24,8 +25,13 @@ export class UserPdfService {
* *
* @param translate handle translations * @param translate handle translations
* @param configService Read config variables * @param configService Read config variables
* @param pdfDocumentService Global PDF Functions
*/ */
public constructor(private translate: TranslateService, private configService: ConfigService) {} public constructor(
private translate: TranslateService,
private configService: ConfigService,
private pdfDocumentService: PdfDocumentService
) {}
/** /**
* Converts a user to PdfMake doc definition, containing access information * Converts a user to PdfMake doc definition, containing access information
@ -252,17 +258,7 @@ export class UserPdfService {
headerRows: 1, headerRows: 1,
body: userTableBody.concat(this.getListUsers(users)) body: userTableBody.concat(this.getListUsers(users))
}, },
layout: { layout: this.pdfDocumentService.switchColorTableLayout
hLineWidth: rowIndex => {
return rowIndex === 1;
},
vLineWidth: () => {
return 0;
},
fillColor: rowIndex => {
return rowIndex % 2 === 0 ? '#EEEEEE' : null;
}
}
}; };
} }