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 * Takes an HTML string, converts to HTML using a DOM parser and recursivly parses
* the content into pdfmake compatible doc definition * 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 { ConfigService } from './config.service';
import { HttpService } from '../core-services/http.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 * Custom PDF error class to handle errors in a safer way
*/ */
@ -594,4 +602,92 @@ export class PdfDocumentService {
pdfMake.vfs[url] = base64; 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> <h1>{{ assignment.getTitle() }}</h1>
</div> </div>
<div *ngIf="assignment"> <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>
<div class="meta-info-grid"> <div class="meta-info-grid">
<div class="number-of-elected"> <div class="number-of-elected">

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router'; 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 { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
@ -181,7 +181,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
private tagRepo: TagRepositoryService, private tagRepo: TagRepositoryService,
private promptService: PromptService, private promptService: PromptService,
private pdfService: AssignmentPdfExportService, private pdfService: AssignmentPdfExportService,
private mediafileRepo: MediafileRepositoryService private mediafileRepo: MediafileRepositoryService,
private sanitizer: DomSanitizer
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.subscriptions.push( this.subscriptions.push(
@ -501,4 +502,15 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
.sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment) .sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment)
.then(null, this.raiseError); .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> <span translate>Deselect all</span>
</button> </button>
<mat-divider></mat-divider> <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 <button
mat-menu-item mat-menu-item
class="red-warning-text" 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 { PromptService } from 'app/core/ui-services/prompt.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment'; import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
import { AssignmentPdfExportService } from '../../services/assignment-pdf-export.service';
/** /**
* List view for the assignments * List view for the assignments
@ -55,6 +56,7 @@ export class AssignmentListComponent
private promptService: PromptService, private promptService: PromptService,
public filterService: AssignmentFilterListService, public filterService: AssignmentFilterListService,
public sortService: AssignmentSortListService, public sortService: AssignmentSortListService,
private pdfService: AssignmentPdfExportService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
private router: Router, private router: Router,
public operator: OperatorService public operator: OperatorService
@ -93,10 +95,12 @@ export class AssignmentListComponent
/** /**
* Function to download the assignment list * 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 { public downloadAssignmentButton(assignments?: ViewAssignment[]): void {
this.raiseError('TODO: assignment download not yet implemented'); 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 { ViewAssignment } from '../models/view-assignment';
import { AssignmentPdfService } from './assignment-pdf.service'; import { AssignmentPdfService } from './assignment-pdf.service';
import { TranslateService } from '@ngx-translate/core'; 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 * Controls PDF export for assignments
@ -37,4 +37,78 @@ export class AssignmentPdfExportService {
}; };
this.pdfDocumentService.download(doc, filename, metadata); 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 { private createDescription(assignment: ViewAssignment): object {
if (assignment.description) { if (assignment.description) {
const assignmentHtml = this.htmlToPdfService.convertHtml(assignment.description); const descriptionDocDef = this.htmlToPdfService.addPlainText(assignment.description);
const descriptionText = `${this.translate.instant('Description')}: `; const descriptionText = `${this.translate.instant('Description')}: `;
const description = [ const description = [
@ -111,11 +111,7 @@ export class AssignmentPdfService {
bold: true, bold: true,
style: 'textItem' style: 'textItem'
}, },
{ descriptionDocDef
text: assignmentHtml,
style: 'textItem',
margin: [10, 0, 0, 0]
}
]; ];
return description; return description;
} else { } else {

View File

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

View File

@ -564,7 +564,7 @@ export class MotionPdfService {
// columnWidth = '100%'; // columnWidth = '100%';
// } // }
reason.push(this.addHtml(motion.reason)); reason.push(this.htmlToPdfService.addPlainText(motion.reason));
return reason; return reason;
} else { } else {
@ -707,25 +707,9 @@ export class MotionPdfService {
const section = motion.getCommentForSection(viewComment); const section = motion.getCommentForSection(viewComment);
if (section && section.comment) { if (section && section.comment) {
result.push({ text: viewComment.name, style: 'heading3', margin: [0, 25, 0, 10] }); 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; 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)
}
]
};
}
} }