Merge pull request #4617 from tsiegleauq/assignment-pdf
Create assignment PDF service
This commit is contained in:
commit
23103362a5
@ -44,6 +44,25 @@ export class PdfDocumentService {
|
||||
*/
|
||||
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
|
||||
*
|
||||
@ -502,6 +521,10 @@ export class PdfDocumentService {
|
||||
listChild: {
|
||||
fontSize: 12,
|
||||
margin: [0, 5]
|
||||
},
|
||||
textItem: {
|
||||
fontSize: 11,
|
||||
margin: [0, 7]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
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 { AssignmentPollService } from '../../services/assignment-poll.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,
|
||||
private agendaRepo: ItemRepositoryService,
|
||||
private tagRepo: TagRepositoryService,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
private pdfService: AssignmentPdfExportService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
this.subscriptions.push(
|
||||
@ -406,7 +408,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
||||
}
|
||||
|
||||
public onDownloadPdf(): void {
|
||||
// TODO: Download summary pdf
|
||||
this.pdfService.exportSingleAssignment(this.assignment);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,7 +67,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
||||
* used in this poll (e.g.)
|
||||
*/
|
||||
public get pollValues(): CalculablePollKey[] {
|
||||
return this.pollService.pollValues.filter(name => this.poll[name] !== undefined);
|
||||
return this.pollService.getVoteOptionsByPoll(this.poll);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,6 +57,14 @@ export class ViewAssignment extends BaseAgendaViewModel {
|
||||
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[] {
|
||||
return this._assignmentRelatedUsers.map(aru => aru.user);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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`;
|
||||
}
|
||||
}
|
@ -67,6 +67,10 @@ export class AssignmentPollService extends PollService {
|
||||
.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
|
||||
* (e.g. yes/no/abstain may have a different percentage base and will return null here)
|
||||
|
@ -13,6 +13,7 @@ import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-mo
|
||||
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
||||
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
|
||||
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
|
||||
@ -52,6 +53,7 @@ export class MotionPdfService {
|
||||
* @param statuteRepo To get formated stature paragraphs
|
||||
* @param changeRecoRepo to get the change recommendations
|
||||
* @param configService Read config variables
|
||||
* @param pdfDocumentService Global PDF Functions
|
||||
* @param htmlToPdfService To convert HTML text into pdfmake doc def
|
||||
* @param pollService MotionPollService for rendering the polls
|
||||
* @param linenumberingService Line numbers
|
||||
@ -63,6 +65,7 @@ export class MotionPdfService {
|
||||
private statuteRepo: StatuteParagraphRepositoryService,
|
||||
private changeRecoRepo: ChangeRecommendationRepositoryService,
|
||||
private configService: ConfigService,
|
||||
private pdfDocumentService: PdfDocumentService,
|
||||
private htmlToPdfService: HtmlToPdfService,
|
||||
private pollService: MotionPollService,
|
||||
private linenumberingService: LinenumberingService,
|
||||
@ -652,17 +655,7 @@ export class MotionPdfService {
|
||||
dontBreakRows: true,
|
||||
body: callListTableBody.concat(callListRows)
|
||||
},
|
||||
layout: {
|
||||
hLineWidth: rowIndex => {
|
||||
return rowIndex === 1;
|
||||
},
|
||||
vLineWidth: () => {
|
||||
return 0;
|
||||
},
|
||||
fillColor: rowIndex => {
|
||||
return rowIndex % 2 === 0 ? '#EEEEEE' : null;
|
||||
}
|
||||
}
|
||||
layout: this.pdfDocumentService.switchColorTableLayout
|
||||
};
|
||||
return [title, table];
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
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
|
||||
@ -24,8 +25,13 @@ export class UserPdfService {
|
||||
*
|
||||
* @param translate handle translations
|
||||
* @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
|
||||
@ -252,17 +258,7 @@ export class UserPdfService {
|
||||
headerRows: 1,
|
||||
body: userTableBody.concat(this.getListUsers(users))
|
||||
},
|
||||
layout: {
|
||||
hLineWidth: rowIndex => {
|
||||
return rowIndex === 1;
|
||||
},
|
||||
vLineWidth: () => {
|
||||
return 0;
|
||||
},
|
||||
fillColor: rowIndex => {
|
||||
return rowIndex % 2 === 0 ? '#EEEEEE' : null;
|
||||
}
|
||||
}
|
||||
layout: this.pdfDocumentService.switchColorTableLayout
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user