diff --git a/client/src/app/core/marked-translations.ts b/client/src/app/core/marked-translations.ts index 3c91bd041..c89164e72 100644 --- a/client/src/app/core/marked-translations.ts +++ b/client/src/app/core/marked-translations.ts @@ -151,9 +151,11 @@ _('Number of all delegates'); _('Number of all participants'); _('Use the following custom number'); _('Custom number of ballot papers'); -// subgroup export -_('Title for PDF and DOCX documents (all motions)'); -_('Preamble text for PDF and DOCX documents (all motions)'); +// subgroup PDF export +_('PDF export'); +_('Title for PDF documents of motions'); +_('Preamble text for PDF documents of motions'); +_('Show submitters and recommendation in table of contents'); _('Sort categories by'); _('Sort motions by'); _('Include the sequential number in PDF and DOCX'); diff --git a/client/src/app/core/ui-services/pdf-document.service.ts b/client/src/app/core/ui-services/pdf-document.service.ts index b19d89784..fceefb169 100644 --- a/client/src/app/core/ui-services/pdf-document.service.ts +++ b/client/src/app/core/ui-services/pdf-document.service.ts @@ -13,9 +13,20 @@ import { HttpService } from '../core-services/http.service'; */ export enum StyleType { DEFAULT = 'tocEntry', + SUBTITLE = 'subtitle', + SUB_ENTRY = 'tocSubEntry', CATEGORY_SECTION = 'tocCategorySection' } +/** + * Enumeration to describe the type of borders. + */ +export enum BorderType { + DEFAULT = 'noBorders', + LIGHT_HORIZONTAL_LINES = 'lightHorizontalLines', + HEADER_ONLY = 'headerLineOnly' +} + /** * Custom PDF error class to handle errors in a safer way */ @@ -445,9 +456,10 @@ export class PdfDocumentService { * @returns an object that contains all pdf styles */ private getStandardPaperStyles(): object { + const pageSize = this.configService.instant('general_export_pdf_pagesize'); return { title: { - fontSize: 18, + fontSize: pageSize === 'A5' ? 14 : 16, margin: [0, 0, 0, 20], bold: true }, @@ -475,12 +487,12 @@ export class PdfDocumentService { fontSize: 8 }, heading2: { - fontSize: 14, + fontSize: pageSize === 'A5' ? 12 : 14, margin: [0, 0, 0, 10], bold: true }, heading3: { - fontSize: 12, + fontSize: pageSize === 'A5' ? 10 : 12, margin: [0, 10, 0, 0], bold: true }, @@ -498,17 +510,25 @@ export class PdfDocumentService { margin: [15, 5] }, tocEntry: { - fontSize: 12, + fontSize: pageSize === 'A5' ? 10 : 11, margin: [0, 0, 0, 0], bold: false }, + tocHeaderRow: { + fontSize: 8, + italics: true + }, + tocSubEntry: { + fontSize: pageSize === 'A5' ? 9 : 10, + color: '#404040' + }, tocCategoryEntry: { - fontSize: 12, + fontSize: pageSize === 'A5' ? 10 : 11, margin: [10, 0, 0, 0], bold: false }, tocCategoryTitle: { - fontSize: 12, + fontSize: pageSize === 'A5' ? 10 : 11, margin: [0, 0, 0, 4], bold: true }, @@ -610,6 +630,7 @@ export class PdfDocumentService { */ public createTitle(configVariable: string): object { const titleText = this.translate.instant(this.configService.instant(configVariable)); + return { text: titleText, style: 'title' @@ -647,13 +668,21 @@ export class PdfDocumentService { * @param tocBody the body of the table * @returns The table of contents as doc definition */ - public createTocTableDef(tocBody: object, style: StyleType = StyleType.DEFAULT): object { + public createTocTableDef( + tocBody: object[], + style: StyleType = StyleType.DEFAULT, + borderStyle: BorderType = BorderType.DEFAULT, + ...header: object[] + ): object { return { table: { + headerRows: header[0] ? header.length : 0, + keepWithHeaderRows: header[0] ? header.length : 0, + dontBreakRows: true, widths: ['auto', '*', 'auto'], - body: tocBody + body: header[0] ? [...header, ...tocBody] : tocBody }, - layout: 'noBorders', + layout: borderStyle, style: style }; } @@ -672,15 +701,16 @@ export class PdfDocumentService { identifier: string, title: string, pageReference: string, - style: StyleType = StyleType.DEFAULT - ): Object { + style: StyleType = StyleType.DEFAULT, + ...subTitle: object[] + ): Array { return [ { text: identifier, style: style }, { - text: title, + text: [title, ...subTitle], style: 'tocEntry' }, { @@ -690,4 +720,19 @@ export class PdfDocumentService { } ]; } + + /** + * Function to create an inline line in the toc. + * + * @param text The text for the line. + * @param bold Optional boolean, if the text should be bold - defaults to `false`. + * + * @returns {Object} An object for `DocDefinition` for `pdf-make`. + */ + public createTocLineInline(text: string): Object { + return { + text: '\n' + text, + style: StyleType.SUB_ENTRY + }; + } } diff --git a/client/src/app/site/motions/services/motion-pdf-catalog.service.ts b/client/src/app/site/motions/services/motion-pdf-catalog.service.ts index d61c14d94..6e0b5fbf2 100644 --- a/client/src/app/site/motions/services/motion-pdf-catalog.service.ts +++ b/client/src/app/site/motions/services/motion-pdf-catalog.service.ts @@ -6,7 +6,8 @@ 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, PdfDocumentService, StyleType } from 'app/core/ui-services/pdf-document.service'; +import { PdfError, PdfDocumentService, StyleType, BorderType } from 'app/core/ui-services/pdf-document.service'; +import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; /** * Service to export a list of motions. @@ -31,7 +32,8 @@ export class MotionPdfCatalogService { private translate: TranslateService, private configService: ConfigService, private motionPdfService: MotionPdfService, - private pdfService: PdfDocumentService + private pdfService: PdfDocumentService, + private motionRepo: MotionRepositoryService ) {} /** @@ -116,13 +118,15 @@ export class MotionPdfCatalogService { text: this.translate.instant('Table of contents'), style: 'heading2' }; + const exportSubmitterRecommendation = this.configService.instant( + 'motions_export_submitter_recommendation' + ); - if (!sorting) { - sorting = this.configService.instant('motions_export_category_sorting'); - } - const exportCategory = sorting === 'identifier' || sorting === 'prefix'; + // Initialize the header and the layout for border-style. + const header = exportSubmitterRecommendation ? this.getTocHeaderDefinition() : undefined; + const layout = exportSubmitterRecommendation ? BorderType.LIGHT_HORIZONTAL_LINES : BorderType.DEFAULT; - if (exportCategory && categories) { + if (categories && categories.length) { const catTocBody = []; for (const category of categories) { // push the name of the category @@ -138,27 +142,38 @@ export class MotionPdfCatalogService { ] ] }, - layout: 'noBorders' + layout: exportSubmitterRecommendation ? 'lightHorizontalLines' : 'noBorders' }); - const tocBody = motions - .filter(motion => category === motion.category) - .map(motion => - this.pdfService.createTocLine( - `${motion.identifier}`, - motion.title, - `${motion.id}`, - StyleType.CATEGORY_SECTION - ) - ); + const tocBody = []; + for (const motion of motions.filter(motionIn => category === motionIn.category)) { + if (exportSubmitterRecommendation) { + tocBody.push(this.appendSubmittersAndRecommendation(motion, StyleType.CATEGORY_SECTION)); + } else { + tocBody.push( + this.pdfService.createTocLine( + `${motion.identifier ? motion.identifier : ''}`, + motion.title, + `${motion.id}`, + StyleType.CATEGORY_SECTION + ) + ); + } + } - catTocBody.push(this.pdfService.createTocTableDef(tocBody, StyleType.CATEGORY_SECTION)); + catTocBody.push(this.pdfService.createTocTableDef(tocBody, StyleType.CATEGORY_SECTION, layout, header)); } // handle those without category const uncatTocBody = motions .filter(motion => !motion.category) - .map(motion => this.pdfService.createTocLine(`${motion.identifier}`, motion.title, `${motion.id}`)); + .map(motion => + this.pdfService.createTocLine( + `${motion.identifier ? motion.identifier : ''}`, + motion.title, + `${motion.id}` + ) + ); // only push this array if there is at least one entry if (uncatTocBody.length > 0) { @@ -168,15 +183,46 @@ export class MotionPdfCatalogService { toc.push(catTocBody); } else { // all motions in the same table - const tocBody = motions.map(motion => - this.pdfService.createTocLine(`${motion.identifier}`, motion.title, `${motion.id}`) - ); - toc.push(this.pdfService.createTocTableDef(tocBody, StyleType.CATEGORY_SECTION)); + const tocBody = []; + for (const motion of motions) { + if (exportSubmitterRecommendation) { + tocBody.push(this.appendSubmittersAndRecommendation(motion)); + } else { + tocBody.push( + this.pdfService.createTocLine( + `${motion.identifier ? motion.identifier : ''}`, + motion.title, + `${motion.id}` + ) + ); + } + } + toc.push(this.pdfService.createTocTableDef(tocBody, StyleType.CATEGORY_SECTION, layout, header)); } return [tocTitle, toc, this.pdfService.getPageBreak()]; } + /** + * Function to get the definition for the header + * for exporting motion-list as PDF. Needed, if submitters + * and recommendation should also be exported to the `Table of Contents`. + * + * @returns {object} The DocDefinition for `pdfmake.js`. + */ + private getTocHeaderDefinition(): object { + return [ + { text: this.translate.instant('Identifier'), style: 'tocHeaderRow' }, + { + text: `${this.translate.instant('Title')} · ${this.translate.instant( + 'Submitters' + )} · ${this.translate.instant('Recommendation')}`, + style: 'tocHeaderRow' + }, + { text: this.translate.instant('Page'), style: 'tocHeaderRow', alignment: 'right' } + ]; + } + /** * Extract the used categories from the given motion list. * @@ -198,4 +244,31 @@ export class MotionPdfCatalogService { return categories; } + + /** + * Creates lines for the `Table of contents` containing submitters and recommendation. + * + * @param motion The motion containing the information + * @param style Optional `StyleType`. Defaults to `tocEntry`. + * + * @returns {Array} An array containing the `DocDefinitions` for `pdf-make`. + */ + private appendSubmittersAndRecommendation(motion: ViewMotion, style: StyleType = StyleType.DEFAULT): Array { + const recommendation = this.motionRepo.getExtendedRecommendationLabel(motion); + let submitterList = ''; + for (let i = 0; i < motion.submitters.length; ++i) { + submitterList += + i !== motion.submitters.length - 1 + ? motion.submitters[i].getTitle() + ', ' + : motion.submitters[i].getTitle(); + } + return this.pdfService.createTocLine( + `${motion.identifier ? motion.identifier : ''}`, + motion.title, + `${motion.id}`, + style, + this.pdfService.createTocLineInline(submitterList), + this.pdfService.createTocLineInline(recommendation) + ); + } } diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py index b3c0f73c0..8f58db0af 100644 --- a/openslides/motions/config_variables.py +++ b/openslides/motions/config_variables.py @@ -344,22 +344,32 @@ def get_config_variables(): validators=(MinValueValidator(1),), ) - # PDF and DOCX export + # PDF export yield ConfigVariable( name="motions_export_title", default_value="Motions", - label="Title for PDF and DOCX documents (all motions)", + label="Title for PDF document of motions", weight=370, group="Motions", - subgroup="Export", + subgroup="PDF export", ) yield ConfigVariable( name="motions_export_preamble", default_value="", - label="Preamble text for PDF and DOCX documents (all motions)", + label="Preamble text for PDF documents of motions", weight=375, group="Motions", - subgroup="Export", + subgroup="PDF export", + ) + + yield ConfigVariable( + name="motions_export_submitter_recommendation", + default_value=False, + label="Show submitters and recommendation in table of contents", + input_type="boolean", + weight=378, + group="Motions", + subgroup="PDF export", )