Export a list of motions

Ports the "Motion Catalog Content Provider" to OpenSlides 3.
Categories and Prefixes are considered as before.
Updated to Code to ES6/Typescript.
Allows to export all motions from the motion list view.
This commit is contained in:
Sean Engelhardt 2019-01-24 14:40:05 +01:00
parent c48148fc01
commit 0e77ec79c2
9 changed files with 329 additions and 7 deletions

View File

@ -93,7 +93,7 @@ export class HtmlToPdfService {
const parser = new DOMParser(); const parser = new DOMParser();
const parsedHtml = parser.parseFromString(htmlText, 'text/html'); const parsedHtml = parser.parseFromString(htmlText, 'text/html');
// Since the spread operator did not work for HTMLCollection, use Array.from // Since the spread operator did not work for HTMLCollection, use Array.from
const htmlArray = Array.from(parsedHtml.body.children); const htmlArray = Array.from(parsedHtml.body.childNodes);
// Parse the children of the current HTML element // Parse the children of the current HTML element
for (const child of htmlArray) { for (const child of htmlArray) {
@ -219,6 +219,7 @@ export class HtmlToPdfService {
} }
} else { } else {
const children = this.parseChildren(element, styles); const children = this.parseChildren(element, styles);
newParagraph = { newParagraph = {
...this.create('text'), ...this.create('text'),
...this.computeStyle(styles) ...this.computeStyle(styles)

View File

@ -308,13 +308,18 @@ export class PdfDocumentService {
} }
/** /**
* Downloads a pdf. Does not seem to work. * Downloads a pdf.
* *
* @param docDefinition the structure of the PDF document * @param docDefinition the structure of the PDF document
*/ */
public download(docDefinition: object, filename: string, metadata?: object): void { public async download(docDefinition: object, filename: string, metadata?: object): Promise<void> {
this.getStandardPaper(docDefinition, metadata).then(doc => { const doc = await this.getStandardPaper(docDefinition, metadata);
pdfMake.createPdf(doc).getBlob(blob => saveAs(blob, `${filename}.pdf`, { autoBOM: true })); await new Promise<boolean>(resolve => {
const pdf = pdfMake.createPdf(doc);
pdf.getBlob(blob => {
saveAs(blob, `${filename}.pdf`, { autoBOM: true });
resolve(true);
});
}); });
} }
@ -347,6 +352,9 @@ export class PdfDocumentService {
margin: [0, -20, 0, 20], margin: [0, -20, 0, 20],
color: 'grey' color: 'grey'
}, },
preamble: {
margin: [0, 0, 0, 10]
},
headerText: { headerText: {
fontSize: 10, fontSize: 10,
margin: [0, 10, 0, 0] margin: [0, 10, 0, 0]
@ -362,10 +370,33 @@ export class PdfDocumentService {
smallText: { smallText: {
fontSize: 8 fontSize: 8
}, },
heading2: {
fontSize: 14,
margin: [0, 0, 0, 10],
bold: true
},
heading3: { heading3: {
fontSize: 12, fontSize: 12,
margin: [0, 10, 0, 0], margin: [0, 10, 0, 0],
bold: true bold: true
},
tocEntry: {
fontSize: 12,
margin: [0, 0, 0, 0],
bold: false
},
tocCategoryEntry: {
fontSize: 12,
margin: [10, 0, 0, 0],
bold: false
},
tocCategoryTitle: {
fontSize: 12,
margin: [0, 0, 0, 4],
bold: true
},
tocCategorySection: {
margin: [0, 0, 0, 10]
} }
}; };
} }

View File

@ -178,6 +178,10 @@
<mat-icon>archive</mat-icon> <mat-icon>archive</mat-icon>
<span translate>Export as CSV</span> <span translate>Export as CSV</span>
</button> </button>
<button mat-menu-item (click)="onExportAsPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Export all as PDF</span>
</button>
<button mat-menu-item routerLink="import"> <button mat-menu-item routerLink="import">
<mat-icon>save_alt</mat-icon> <mat-icon>save_alt</mat-icon>
<span translate>Import</span><span>&nbsp;...</span> <span translate>Import</span><span>&nbsp;...</span>

View File

@ -22,6 +22,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewWorkflow } from '../../models/view-workflow'; import { ViewWorkflow } from '../../models/view-workflow';
import { WorkflowState } from '../../../../shared/models/motions/workflow-state'; import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
import { WorkflowRepositoryService } from '../../services/workflow-repository.service'; import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
import { MotionPdfExportService } from '../../services/motion-pdf-export.service';
/** /**
* Component that displays all the motions in a Table using DataSource. * Component that displays all the motions in a Table using DataSource.
@ -74,6 +75,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* @param categoryRepo: Repo for categories. Used to define filters * @param categoryRepo: Repo for categories. Used to define filters
* @param workflowRepo: Repo for Workflows. Used to define filters * @param workflowRepo: Repo for Workflows. Used to define filters
* @param motionCsvExport * @param motionCsvExport
* @param pdfExport To export motions as PDF
* @param multiselectService Service for the multiSelect actions * @param multiselectService Service for the multiSelect actions
* @param userRepo * @param userRepo
* @param sortService * @param sortService
@ -93,6 +95,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
private workflowRepo: WorkflowRepositoryService, private workflowRepo: WorkflowRepositoryService,
private motionRepo: MotionRepositoryService, private motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService, private motionCsvExport: MotionCsvExportService,
private pdfExport: MotionPdfExportService,
public multiselectService: MotionMultiselectService, public multiselectService: MotionMultiselectService,
public sortService: MotionSortListService, public sortService: MotionSortListService,
public filterService: MotionFilterListService, public filterService: MotionFilterListService,
@ -192,6 +195,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
this.motionCsvExport.exportMotionList(this.dataSource.data); this.motionCsvExport.exportMotionList(this.dataSource.data);
} }
/**
* Exports motions as PDF.
*/
public onExportAsPdf(): void {
this.pdfExport.exportMotionCatalog(this.dataSource.data);
}
/** /**
* Returns current definitions for the listView table * Returns current definitions for the listView table
*/ */

View File

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

View File

@ -0,0 +1,240 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ViewMotion } from '../models/view-motion';
import { MotionPdfService } from './motion-pdf.service';
import { ConfigService } from 'app/core/services/config.service';
import { Category } from 'app/shared/models/motions/category';
/**
* Service to export a list of motions.
*
* @example
* ```ts
* const docDef = this.motionPdfCatalogService.motionListToDocDef(myListOfViewMotions);
* ```
*/
@Injectable({
providedIn: 'root'
})
export class MotionPdfCatalogService {
/**
* Helper to add page breaks to documents
*/
private pageBreak = {
text: '',
pageBreak: 'after'
};
/**
* Constructor
*
* @param translate handle translations
* @param configService read out config variables
* @param motionPdfService handle motion to pdf conversion
*/
public constructor(
private translate: TranslateService,
private configService: ConfigService,
private motionPdfService: MotionPdfService
) {}
/**
* Converts the list of motions to pdfmake doc definition.
* Public entry point to conversion of multiple motions
*
* @param motions the list of view motions to convert
* @returns pdfmake doc definition as object
*/
public motionListToDocDef(motions: ViewMotion[]): object {
let doc = [];
const motionDocList = [];
for (let motionIndex = 0; motionIndex < motions.length; ++motionIndex) {
const motionDocDef: any = this.motionPdfService.motionToDocDef(motions[motionIndex]);
// add id field to the first page of a motion to make it findable over TOC
motionDocDef[0].id = `${motions[motionIndex].id}`;
motionDocList.push(motionDocDef);
if (motionIndex < motions.length - 1) {
motionDocList.push(this.pageBreak);
}
}
// 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 = doc.concat(motionDocList);
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.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.
*
* @param motions The motions to add in the TOC
* @param sorting The optional sorting strategy
* @returns the table of contents as document definition
*/
private createToc(motions: ViewMotion[], sorting?: string): object {
const toc = [];
const categories: Category[] = this.getUniqueCategories(motions);
// Create the toc title
const tocTitle = {
text: this.translate.instant('Table of contents'),
style: 'heading2'
};
if (!sorting) {
sorting = this.configService.instant<string>('motions_export_category_sorting');
}
const exportCategory = sorting === 'identifier' || sorting === 'prefix';
if (exportCategory && categories) {
const catTocBody = [];
for (const category of categories) {
// push the name of the category
// make a table for correct alignment
catTocBody.push({
table: {
body: [
[
{
text: category.prefix + ' - ' + category.name,
style: 'tocCategoryTitle'
}
]
]
},
layout: 'noBorders'
});
const tocBody = motions
.filter(motion => category === motion.category)
.map(motion => this.createTocLine(motion, 'tocCategoryEntry'));
catTocBody.push(this.createTocTableDef(tocBody));
}
// handle those without category
const uncatTocBody = motions
.filter(motion => !motion.category)
.map(motion => this.createTocLine(motion, 'tocEntry'));
// only push this array if there is at least one entry
if (uncatTocBody.length > 0) {
catTocBody.push(this.createTocTableDef(uncatTocBody));
}
toc.push(catTocBody);
} else {
// all motions in the same table
const tocBody = motions.map(motion => this.createTocLine(motion, 'tocEntry'));
toc.push(this.createTocTableDef(tocBody));
}
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'
}
];
}
/**
* Extract the used categories from the given motion list.
*
* @param motions the list of motions
* @returns Unique list of categories
*/
private getUniqueCategories(motions: ViewMotion[]): Category[] {
const categories: Category[] = motions
// remove motions without category
.filter(motion => (motion.category ? motion : null))
// map motions their categories
.map(motion => motion.category)
// remove redundancies
.filter(
(category, index, self) =>
index ===
self.findIndex(compare => compare.prefix === category.prefix && compare.name === category.name)
);
return categories;
}
}

View File

@ -5,6 +5,8 @@ import { TranslateService } from '@ngx-translate/core';
import { MotionPdfService } from './motion-pdf.service'; import { MotionPdfService } from './motion-pdf.service';
import { PdfDocumentService } from 'app/core/services/pdf-document.service'; import { PdfDocumentService } from 'app/core/services/pdf-document.service';
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion'; import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
import { ConfigService } from 'app/core/services/config.service';
import { MotionPdfCatalogService } from './motion-pdf-catalog.service';
/** /**
* Export service to handle various kind of exporting necessities. * Export service to handle various kind of exporting necessities.
@ -17,12 +19,15 @@ export class MotionPdfExportService {
* Constructor * Constructor
* *
* @param translate handle translations * @param translate handle translations
* @param configService Read out Config variables
* @param motionPdfService Converting actual motions to PDF * @param motionPdfService Converting actual motions to PDF
* @param pdfDocumentService Actual pdfmake functions and global doc definitions * @param pdfDocumentService Actual pdfmake functions and global doc definitions
*/ */
public constructor( public constructor(
private translate: TranslateService, private translate: TranslateService,
private configService: ConfigService,
private motionPdfService: MotionPdfService, private motionPdfService: MotionPdfService,
private pdfCatalogService: MotionPdfCatalogService,
private pdfDocumentService: PdfDocumentService private pdfDocumentService: PdfDocumentService
) {} ) {}
@ -41,4 +46,18 @@ export class MotionPdfExportService {
}; };
this.pdfDocumentService.download(doc, filename, metadata); this.pdfDocumentService.download(doc, filename, metadata);
} }
/**
* Exports multiple motions to a collection of PDFs
*
* @param motions
*/
public exportMotionCatalog(motions: ViewMotion[]): void {
const doc = this.pdfCatalogService.motionListToDocDef(motions);
const filename = this.translate.instant(this.configService.instant<string>('motions_export_title'));
const metadata = {
title: filename
};
this.pdfDocumentService.download(doc, filename, metadata);
}
} }

View File

@ -57,7 +57,7 @@ export class MotionPdfService {
// determine the default crMode if not explicitly given // determine the default crMode if not explicitly given
if (!crMode) { if (!crMode) {
lnMode = this.configService.instant('motions_recommendation_text_mode'); crMode = this.configService.instant('motions_recommendation_text_mode');
} }
const title = this.createTitle(motion); const title = this.createTitle(motion);

View File

@ -20,7 +20,7 @@ describe('FullscreenProjectorComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
fit('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });