Merge pull request #4709 from GabrielInTheWorld/utilities

Implements the export of assignment-list as pdf
This commit is contained in:
Emanuel Schütze 2019-05-27 14:42:58 +02:00 committed by GitHub
commit 7e2045aa76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 244 additions and 123 deletions

View File

@ -136,6 +136,19 @@ export class HtmlToPdfService {
}
}
/**
* Function to convert plain html text without linenumbering.
*
* @param text The html text that should be converted to PDF.
*
* @returns {object} The converted html as DocDef.
*/
public addPlainText(text: string): object {
return {
columns: [{ stack: this.convertHtml(text, LineNumberingMode.None) }]
};
}
/**
* Takes an HTML string, converts to HTML using a DOM parser and recursivly parses
* the content into pdfmake compatible doc definition

View File

@ -8,6 +8,14 @@ import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from './config.service';
import { HttpService } from '../core-services/http.service';
/**
* Enumeration to define possible values for the styling.
*/
export enum StyleType {
DEFAULT = 'tocEntry',
CATEGORY_SECTION = 'tocCategorySection'
}
/**
* Custom PDF error class to handle errors in a safer way
*/
@ -594,4 +602,92 @@ export class PdfDocumentService {
pdfMake.vfs[url] = base64;
}
}
/**
* Creates the title for the motion list as pdfmake doc definition
*
* @returns The motion list title for the PDF document
*/
public createTitle(configVariable: string): object {
const titleText = this.translate.instant(this.configService.instant<string>(configVariable));
return {
text: titleText,
style: 'title'
};
}
/**
* Creates the preamble for the motion list as pdfmake doc definition
*
* @returns The motion list preamble for the PDF document
*/
public createPreamble(configVariable: string): object {
const preambleText = this.configService.instant<string>(configVariable);
if (preambleText) {
return {
text: preambleText,
style: 'preamble'
};
} else {
return {};
}
}
public getPageBreak(): Object {
return {
text: '',
pageBreak: 'after'
};
}
/**
* Generates the table definition for the TOC
*
* @param tocBody the body of the table
* @returns The table of contents as doc definition
*/
public createTocTableDef(tocBody: object, style: StyleType = StyleType.DEFAULT): object {
return {
table: {
widths: ['auto', '*', 'auto'],
body: tocBody
},
layout: 'noBorders',
style: style
};
}
/**
* Function, that creates a line for the 'Table of contents'
*
* @param identifier The identifier/prefix for the line
* @param title The name of the line
* @param pageReference Defaults to the page, where the object begins
* @param style Optional style. Defaults to `'tocEntry'`
*
* @returns A line for the toc
*/
public createTocLine(
identifier: string,
title: string,
pageReference: string,
style: StyleType = StyleType.DEFAULT
): Object {
return [
{
text: identifier,
style: style
},
{
text: title,
style: 'tocEntry'
},
{
pageReference: pageReference,
style: 'tocEntry',
alignment: 'right'
}
];
}
}

View File

@ -61,7 +61,7 @@
<h1>{{ assignment.getTitle() }}</h1>
</div>
<div *ngIf="assignment">
<div *ngIf="assignment.assignment.description" [innerHTML]="assignment.assignment.description"></div>
<div *ngIf="assignment.assignment.description" [innerHTML]="getSanitizedText(assignment.assignment.description)"></div>
</div>
<div class="meta-info-grid">
<div class="number-of-elected">

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { Title, DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';
@ -181,7 +181,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
private tagRepo: TagRepositoryService,
private promptService: PromptService,
private pdfService: AssignmentPdfExportService,
private mediafileRepo: MediafileRepositoryService
private mediafileRepo: MediafileRepositoryService,
private sanitizer: DomSanitizer
) {
super(title, translate, matSnackBar);
this.subscriptions.push(
@ -501,4 +502,15 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
.sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment)
.then(null, this.raiseError);
}
/**
* Sanitize the text.
*
* @param text {string} The text to display.
*
* @returns {SafeHtml} the sanitized text.
*/
public getSanitizedText(text: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(text);
}
}

View File

@ -111,6 +111,15 @@
<span translate>Deselect all</span>
</button>
<mat-divider></mat-divider>
<button
*osPerms="'assignment.can_manage'"
mat-menu-item
[disabled]="!selectedRows.length"
(click)="downloadAssignmentButton(selectedRows)">
<mat-icon>archive</mat-icon>
<span>{{ 'Export selected elections' | translate }}</span>
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="red-warning-text"

View File

@ -14,6 +14,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
/**
* List view for the assignments
@ -55,6 +56,7 @@ export class AssignmentListComponent
private promptService: PromptService,
public filterService: AssignmentFilterListService,
public sortService: AssignmentSortListService,
private pdfService: AssignmentPdfExportService,
protected route: ActivatedRoute,
private router: Router,
public operator: OperatorService
@ -93,10 +95,12 @@ export class AssignmentListComponent
/**
* Function to download the assignment list
* TODO: Not yet implemented
*
* @param assignments Optional parameter: If given, the chosen list will be exported,
* otherwise the whole list of assignments is exported.
*/
public downloadAssignmentButton(): void {
this.raiseError('TODO: assignment download not yet implemented');
public downloadAssignmentButton(assignments?: ViewAssignment[]): void {
this.pdfService.exportMultipleAssignments(assignments ? assignments : this.repo.getSortedViewModelList());
}
/**

View File

@ -2,7 +2,7 @@ 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';
import { PdfDocumentService, PdfError } from 'app/core/ui-services/pdf-document.service';
/**
* Controls PDF export for assignments
@ -37,4 +37,78 @@ export class AssignmentPdfExportService {
};
this.pdfDocumentService.download(doc, filename, metadata);
}
/**
* Generates a pdf document for a list of assignments
*
* @param assignments The list of assignments that should be exported as pdf.
*/
public exportMultipleAssignments(assignments: ViewAssignment[]): void {
const doc = this.createDocOfMultipleAssignments(assignments);
const filename = this.translate.instant('Elections');
const metaData = {
title: filename
};
this.pdfDocumentService.download(doc, filename, metaData);
}
/**
* Helper to generate from a list of assignments a document for the pdf export.
*
* @param assignments The list of assignments
*
* @returns doc definition as object
*/
private createDocOfMultipleAssignments(assignments: ViewAssignment[]): object {
const doc = [];
const fileList = assignments.map((assignment, index) => {
try {
const assignmentDocDef = this.assignmentPdfService.assignmentToDocDef(assignment);
assignmentDocDef[0].id = `${assignment.id}`;
return index < assignments.length - 1
? [assignmentDocDef, this.pdfDocumentService.getPageBreak()]
: [assignmentDocDef];
} catch (error) {
const errorText = `${this.translate.instant('Error during PDF creation of assignment:')} ${
assignment.title
}`;
console.error(`${errorText}\nDebugInfo:\n`, error);
throw new PdfError(errorText);
}
});
if (assignments.length > 1) {
doc.push(
this.pdfDocumentService.createTitle('assignments_pdf_title'),
this.pdfDocumentService.createPreamble('assignments_pdf_preamble'),
this.createToc(assignments)
);
}
doc.push(fileList);
return doc;
}
/**
* Function to create the 'Table of contents'
*
* @param assignments All the assignments, who should be exported as PDF.
*
* @returns The toc as
*/
private createToc(assignments: ViewAssignment[]): Object {
const toc = [];
const tocTitle = {
text: this.translate.instant('Table of contents'),
style: 'heading2'
};
const tocBody = assignments.map((assignment, index) =>
this.pdfDocumentService.createTocLine(`${index + 1}`, assignment.title, `${assignment.id}`)
);
toc.push(this.pdfDocumentService.createTocTableDef(tocBody));
return [tocTitle, toc, this.pdfDocumentService.getPageBreak()];
}
}

View File

@ -102,7 +102,7 @@ export class AssignmentPdfService {
*/
private createDescription(assignment: ViewAssignment): object {
if (assignment.description) {
const assignmentHtml = this.htmlToPdfService.convertHtml(assignment.description);
const descriptionDocDef = this.htmlToPdfService.addPlainText(assignment.description);
const descriptionText = `${this.translate.instant('Description')}: `;
const description = [
@ -111,11 +111,7 @@ export class AssignmentPdfService {
bold: true,
style: 'textItem'
},
{
text: assignmentHtml,
style: 'textItem',
margin: [10, 0, 0, 0]
}
descriptionDocDef
];
return description;
} else {

View File

@ -6,7 +6,7 @@ import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-mo
import { MotionPdfService, InfoToExport } from './motion-pdf.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { ViewCategory } from '../models/view-category';
import { PdfError } from 'app/core/ui-services/pdf-document.service';
import { PdfError, PdfDocumentService, StyleType } from 'app/core/ui-services/pdf-document.service';
/**
* Service to export a list of motions.
@ -20,14 +20,6 @@ import { PdfError } from 'app/core/ui-services/pdf-document.service';
providedIn: 'root'
})
export class MotionPdfCatalogService {
/**
* Helper to add page breaks to documents
*/
private pageBreak = {
text: '',
pageBreak: 'after'
};
/**
* Constructor
*
@ -38,7 +30,8 @@ export class MotionPdfCatalogService {
public constructor(
private translate: TranslateService,
private configService: ConfigService,
private motionPdfService: MotionPdfService
private motionPdfService: MotionPdfService,
private pdfService: PdfDocumentService
) {}
/**
@ -81,7 +74,7 @@ export class MotionPdfCatalogService {
motionDocList.push(motionDocDef);
if (motionIndex < motions.length - 1) {
motionDocList.push(this.pageBreak);
motionDocList.push(this.pdfService.getPageBreak());
}
} catch (err) {
const errorText = `${this.translate.instant('Error during PDF creation of motion:')} ${
@ -94,7 +87,11 @@ export class MotionPdfCatalogService {
// print extra data (title, preamble, categories, toc) only if there are more than 1 motion
if (motions.length > 1) {
doc.push(this.createTitle(), this.createPreamble(), this.createToc(motions));
doc.push(
this.pdfService.createTitle('motions_export_title'),
this.pdfService.createPreamble('motions_export_preamble'),
this.createToc(motions)
);
}
doc = doc.concat(motionDocList);
@ -102,37 +99,6 @@ export class MotionPdfCatalogService {
return doc;
}
/**
* Creates the title for the motion list as pdfmake doc definition
*
* @returns The motion list title for the PDF document
*/
private createTitle(): object {
const titleText = this.translate.instant(this.configService.instant<string>('motions_export_title'));
return {
text: titleText,
style: 'title'
};
}
/**
* Creates the preamble for the motion list as pdfmake doc definition
*
* @returns The motion list preamble for the PDF document
*/
private createPreamble(): object {
const preambleText = this.configService.instant<string>('motions_export_preamble');
if (preambleText) {
return {
text: preambleText,
style: 'preamble'
};
} else {
return {};
}
}
/**
* Creates the table of contents for the motion book.
* Considers sorting by categories and no sorting.
@ -177,71 +143,38 @@ export class MotionPdfCatalogService {
const tocBody = motions
.filter(motion => category === motion.category)
.map(motion => this.createTocLine(motion, 'tocCategoryEntry'));
.map(motion =>
this.pdfService.createTocLine(
`${motion.identifier}`,
motion.title,
`${motion.id}`,
StyleType.CATEGORY_SECTION
)
);
catTocBody.push(this.createTocTableDef(tocBody));
catTocBody.push(this.pdfService.createTocTableDef(tocBody, StyleType.CATEGORY_SECTION));
}
// handle those without category
const uncatTocBody = motions
.filter(motion => !motion.category)
.map(motion => this.createTocLine(motion, 'tocEntry'));
.map(motion => this.pdfService.createTocLine(`${motion.identifier}`, motion.title, `${motion.id}`));
// only push this array if there is at least one entry
if (uncatTocBody.length > 0) {
catTocBody.push(this.createTocTableDef(uncatTocBody));
catTocBody.push(this.pdfService.createTocTableDef(uncatTocBody, StyleType.CATEGORY_SECTION));
}
toc.push(catTocBody);
} else {
// all motions in the same table
const tocBody = motions.map(motion => this.createTocLine(motion, 'tocEntry'));
toc.push(this.createTocTableDef(tocBody));
const tocBody = motions.map(motion =>
this.pdfService.createTocLine(`${motion.identifier}`, motion.title, `${motion.id}`)
);
toc.push(this.pdfService.createTocTableDef(tocBody, StyleType.CATEGORY_SECTION));
}
return [tocTitle, toc, this.pageBreak];
}
/**
* Generates the table definition for the TOC
*
* @param tocBody the body of the table
* @returns The table of contents as doc definition
*/
private createTocTableDef(tocBody: object): object {
return {
table: {
widths: ['auto', '*', 'auto'],
body: tocBody
},
layout: 'noBorders',
style: 'tocCategorySection'
};
}
/**
* Generates a line in the TOC as list object
*
* @param motion motion to make a toc entry to
* @param style the desired style
*/
private createTocLine(motion: ViewMotion, style: string): object {
const firstColumn = motion.identifier;
return [
{
text: firstColumn,
style: style
},
{
text: motion.title,
style: 'tocEntry'
},
{
pageReference: `${motion.id}`,
style: 'tocEntry',
alignment: 'right'
}
];
return [tocTitle, toc, this.pdfService.getPageBreak()];
}
/**

View File

@ -564,7 +564,7 @@ export class MotionPdfService {
// columnWidth = '100%';
// }
reason.push(this.addHtml(motion.reason));
reason.push(this.htmlToPdfService.addPlainText(motion.reason));
return reason;
} else {
@ -707,25 +707,9 @@ export class MotionPdfService {
const section = motion.getCommentForSection(viewComment);
if (section && section.comment) {
result.push({ text: viewComment.name, style: 'heading3', margin: [0, 25, 0, 10] });
result.push(this.addHtml(section.comment));
result.push(this.htmlToPdfService.addPlainText(section.comment));
}
}
return result;
}
/**
* Helper function to add simple rendered HTML in a new column-stack object.
* Prevents all kinds of malformation
*
* @param content the HTML content
*/
private addHtml(content: string): object {
return {
columns: [
{
stack: this.htmlToPdfService.convertHtml(content, LineNumberingMode.None)
}
]
};
}
}