From 4e1fdc6b22e73d9745b776b49e86fe1e03bd64da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Sch=C3=BCtze?= Date: Wed, 2 Nov 2016 22:45:43 +0100 Subject: [PATCH] Updated PDF layout - use default font for pdf from vfs_font.js remove base64 encoding function on server side - use recommendation config value in motion detail template --- bower.json | 5 +- openslides/agenda/static/js/agenda/pdf.js | 10 +- openslides/agenda/static/js/agenda/site.js | 2 +- .../assignments/static/js/assignments/pdf.js | 63 ++- openslides/core/static/js/core/pdf.js | 388 +++++++++--------- openslides/core/views.py | 90 +--- openslides/motions/static/js/motions/base.js | 5 +- .../static/js/motions/motion-services.js | 4 +- openslides/motions/static/js/motions/pdf.js | 361 +++++++++------- openslides/motions/static/js/motions/site.js | 4 +- .../templates/motions/motion-detail.html | 6 +- openslides/users/static/js/users/pdf.js | 18 +- 12 files changed, 483 insertions(+), 473 deletions(-) diff --git a/bower.json b/bower.json index 942150772..39c4dee98 100644 --- a/bower.json +++ b/bower.json @@ -40,7 +40,10 @@ }, "overrides": { "pdfmake-dist-dist": { - "main": "build/pdfmake.min.js" + "main": [ + "build/pdfmake.min.js", + "build/vfs_fonts.js" + ] }, "pdfjs-dist": { "main": "build/pdf.combined.js" diff --git a/openslides/agenda/static/js/agenda/pdf.js b/openslides/agenda/static/js/agenda/pdf.js index 6aee41d9b..841ba0364 100644 --- a/openslides/agenda/static/js/agenda/pdf.js +++ b/openslides/agenda/static/js/agenda/pdf.js @@ -6,15 +6,15 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf']) .factory('AgendaContentProvider', [ 'gettextCatalog', - 'PdfPredefinedFunctions', - function(gettextCatalog, PdfPredefinedFunctions) { + 'PDFLayout', + function(gettextCatalog, PDFLayout) { var createInstance = function(items) { - //use the Predefined Functions to create the title - var title = PdfPredefinedFunctions.createTitle(gettextCatalog.getString("Agenda")); + // page title + var title = PDFLayout.createTitle(gettextCatalog.getString("Agenda")); - //function to generate the item list out of the given "items" object + // generate the item list with all subitems var createItemList = function() { var agenda_items = []; angular.forEach(items, function (item) { diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js index 8bd801e51..cc3162b90 100644 --- a/openslides/agenda/static/js/agenda/site.js +++ b/openslides/agenda/static/js/agenda/site.js @@ -315,7 +315,7 @@ angular.module('OpenSlidesApp.agenda.site', [ }; $scope.makePDF = function() { - var filename = gettextCatalog.getString("Agenda")+".pdf"; + var filename = gettextCatalog.getString('Agenda') + '.pdf'; var agendaContentProvider = AgendaContentProvider.createInstance($scope.items); var documentProvider = PdfMakeDocumentProvider.createInstance(agendaContentProvider); pdfMake.createPdf(documentProvider.getDocument()).download(filename); diff --git a/openslides/assignments/static/js/assignments/pdf.js b/openslides/assignments/static/js/assignments/pdf.js index 5c8012447..7150ce823 100644 --- a/openslides/assignments/static/js/assignments/pdf.js +++ b/openslides/assignments/static/js/assignments/pdf.js @@ -6,15 +6,15 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) .factory('AssignmentContentProvider', [ 'gettextCatalog', - 'PdfPredefinedFunctions', - function(gettextCatalog, PdfPredefinedFunctions) { + 'PDFLayout', + function(gettextCatalog, PDFLayout) { var createInstance = function(assignment) { - //use the Predefined Functions to create the title - var title = PdfPredefinedFunctions.createTitle(assignment.title); + // page title + var title = PDFLayout.createTitle(assignment.title); - //create the preamble + // number of posts var createPreamble = function() { var preambleText = gettextCatalog.getString("Number of posts to be elected") + ": "; var memberNumber = ""+assignment.open_posts; @@ -34,7 +34,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) return preamble; }; - //adds the description if present in the assignment + // description var createDescription = function() { if (assignment.description) { var descriptionText = gettextCatalog.getString("Description") + ":"; @@ -56,7 +56,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) } }; - //creates the candidate list in columns if the assignment phase is 'voting' + // show candidate list (if assignment phase is not 'finished') var createCandidateList = function() { if (assignment.phase != 2) { var candidatesText = gettextCatalog.getString("Candidates") + ": "; @@ -90,23 +90,23 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) } }; - //handles the case if a candidate is elected or not + // handles the case if a candidate is elected or not var electedCandidateLine = function(candidateName, pollOption, pollTableBody) { if (pollOption.is_elected) { return { text: candidateName + "*", bold: true, - style: PdfPredefinedFunctions.flipTableRowStyle(pollTableBody.length) + style: PDFLayout.flipTableRowStyle(pollTableBody.length) }; } else { return { text: candidateName, - style: PdfPredefinedFunctions.flipTableRowStyle(pollTableBody.length) + style: PDFLayout.flipTableRowStyle(pollTableBody.length) }; } }; - //creates the pull result table + // creates the election result table var createPollResultTable = function() { var resultBody = []; angular.forEach(assignment.polls, function(poll, pollIndex) { @@ -144,7 +144,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) electedCandidateLine(candidateName, pollOption, pollTableBody), { text: votes[0].value + " " + votes[0].percentStr, - style: PdfPredefinedFunctions.flipTableRowStyle(pollTableBody.length) + style: PDFLayout.flipTableRowStyle(pollTableBody.length) } ]); } else if (poll.pollmethod == 'yn') { @@ -163,7 +163,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) votes[1].percentStr } ], - style: PdfPredefinedFunctions.flipTableRowStyle(pollTableBody.length) + style: PDFLayout.flipTableRowStyle(pollTableBody.length) } ]); } else if (poll.pollmethod == 'yna') { @@ -187,7 +187,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) votes[2].percentStr } ], - style: PdfPredefinedFunctions.flipTableRowStyle(pollTableBody.length) + style: PDFLayout.flipTableRowStyle(pollTableBody.length) } ]); } @@ -247,7 +247,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) } }); - //Add the legend to the result body + // add the legend to the result body if (assignment.polls.length > 0) { resultBody.push({ text: "* = " + gettextCatalog.getString("is elected"), @@ -281,12 +281,12 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) .factory('BallotContentProvider', [ 'gettextCatalog', - 'PdfPredefinedFunctions', - function(gettextCatalog, PdfPredefinedFunctions) { + 'PDFLayout', + function(gettextCatalog, PDFLayout) { var createInstance = function(scope, poll, pollNumber) { - // use the Predefined Functions to create the title + // page title var createTitle = function() { return { text: scope.assignment.title, @@ -294,7 +294,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) }; }; - //function to create the poll hint + // poll description var createPollHint = function() { var description = poll.description ? ': ' + poll.description : ''; return { @@ -303,19 +303,19 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) }; }; - //function to create the selection entries + // election entries var createYNBallotEntry = function(decision) { var YNColumn = [ { width: "auto", stack: [ - PdfPredefinedFunctions.createBallotEntry(gettextCatalog.getString("Yes")) + PDFLayout.createBallotEntry(gettextCatalog.getString("Yes")) ] }, { width: "auto", stack: [ - PdfPredefinedFunctions.createBallotEntry(gettextCatalog.getString("No")) + PDFLayout.createBallotEntry(gettextCatalog.getString("No")) ] }, ]; @@ -324,7 +324,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) YNColumn.push({ width: "auto", stack: [ - PdfPredefinedFunctions.createBallotEntry(gettextCatalog.getString("Abstain")) + PDFLayout.createBallotEntry(gettextCatalog.getString("Abstain")) ] }); } @@ -346,7 +346,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) if (poll.pollmethod == 'votes') { angular.forEach(poll.options, function(option) { var candidate = option.candidate.get_full_name(); - candidateBallotList.push(PdfPredefinedFunctions.createBallotEntry(candidate)); + candidateBallotList.push(PDFLayout.createBallotEntry(candidate)); }); } else { angular.forEach(poll.options, function(option) { @@ -361,7 +361,6 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) // since it is not possible to give a column a fixed height, we draw an "empty" column // with a one px width and a fixed top-margin - return { columns : [ { @@ -431,7 +430,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) widths: ['50%', '50%'], body: tableBody }, - layout: PdfPredefinedFunctions.getBallotLayoutLines() + layout: PDFLayout.getBallotLayoutLines() }]; }; @@ -451,13 +450,13 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) .factory('AssignmentCatalogContentProvider', [ 'gettextCatalog', - 'PdfPredefinedFunctions', + 'PDFLayout', 'Config', - function(gettextCatalog, PdfPredefinedFunctions, Config) { + function(gettextCatalog, PDFLayout, Config) { var createInstance = function(allAssignmnets) { - var title = PdfPredefinedFunctions.createTitle( + var title = PDFLayout.createTitle( gettextCatalog.getString(Config.get('assignments_pdf_title').value) ); @@ -476,7 +475,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) var createTOContent = function(assignmentTitles) { var heading = { text: gettextCatalog.getString("Table of contents"), - style: "heading", + style: "heading2", }; var toc = []; @@ -490,7 +489,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) return [ heading, toc, - PdfPredefinedFunctions.addPageBreak() + PDFLayout.addPageBreak() ]; }; @@ -503,7 +502,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) assignmentTitles.push(assignment.title); assignmentContent.push(assignment.getContent()); if (key < allAssignmnets.length - 1) { - assignmentContent.push(PdfPredefinedFunctions.addPageBreak()); + assignmentContent.push(PDFLayout.addPageBreak()); } }); diff --git a/openslides/core/static/js/core/pdf.js b/openslides/core/static/js/core/pdf.js index f75288ac4..3caa714b4 100644 --- a/openslides/core/static/js/core/pdf.js +++ b/openslides/core/static/js/core/pdf.js @@ -4,23 +4,51 @@ angular.module('OpenSlidesApp.core.pdf', []) -.factory('PdfPredefinedFunctions', [ +/* + * General layout functions for building PDFs with pdfmake. + */ +.factory('PDFLayout', [ function() { - var PdfPredefinedFunctions = {}; + var PDFLayout = {}; var BallotCircleDimensions = { yDistance: 6, size: 8 }; - PdfPredefinedFunctions.createTitle = function(titleString) { + // Set and return default font family. + // + // To use custom ttf font files you have to replace the vfs_fonts.js file. + // See https://github.com/pdfmake/pdfmake/wiki/Custom-Fonts---client-side + PDFLayout.getFontName = function() { + pdfMake.fonts = { + Roboto: { + normal: 'Roboto-Regular.ttf', + bold: 'Roboto-Medium.ttf', + italics: 'Roboto-Italic.ttf', + bolditalics: 'Roboto-Italic.ttf' + } + }; + return "Roboto"; + }; + + // page title + PDFLayout.createTitle = function(title) { return { - text: titleString, + text: title, style: "title" }; }; - // function to apply a pagebreak-keyword - PdfPredefinedFunctions.addPageBreak = function() { + // page subtitle + PDFLayout.createSubtitle = function(subtitle) { + return { + text: subtitle, + style: "subtitle" + }; + }; + + // pagebreak + PDFLayout.addPageBreak = function() { return [ { text: '', @@ -29,7 +57,8 @@ angular.module('OpenSlidesApp.core.pdf', []) ]; }; - PdfPredefinedFunctions.flipTableRowStyle = function(currentTableSize) { + // table row style + PDFLayout.flipTableRowStyle = function(currentTableSize) { if (currentTableSize % 2 === 0) { return "tableEven"; } else { @@ -37,8 +66,8 @@ angular.module('OpenSlidesApp.core.pdf', []) } }; - //draws a circle - PdfPredefinedFunctions.drawCircle = function(y, size) { + // draws a circle + PDFLayout.drawCircle = function(y, size) { return [ { type: 'ellipse', @@ -51,14 +80,15 @@ angular.module('OpenSlidesApp.core.pdf', []) ]; }; - //Returns an entry in the ballot with a circle to draw into - PdfPredefinedFunctions.createBallotEntry = function(decision) { + // returns an entry in the ballot with a circle to draw into + PDFLayout.createBallotEntry = function(decision) { return { margin: [40+BallotCircleDimensions.size, 10, 0, 0], columns: [ { width: 15, - canvas: PdfPredefinedFunctions.drawCircle(BallotCircleDimensions.yDistance, BallotCircleDimensions.size) + canvas: PDFLayout.drawCircle(BallotCircleDimensions.yDistance, + BallotCircleDimensions.size) }, { width: "auto", @@ -68,7 +98,8 @@ angular.module('OpenSlidesApp.core.pdf', []) }; }; - PdfPredefinedFunctions.getBallotLayoutLines = function() { + // crop marks for ballot papers + PDFLayout.getBallotLayoutLines = function() { return { hLineWidth: function(i, node) { return (i === 0 || i === node.table.body.length) ? 0 : 0.5; @@ -85,10 +116,11 @@ angular.module('OpenSlidesApp.core.pdf', []) }; }; - return PdfPredefinedFunctions; + return PDFLayout; } ]) + .factory('HTMLValidizer', function() { var HTMLValidizer = {}; @@ -111,138 +143,163 @@ angular.module('OpenSlidesApp.core.pdf', []) return HTMLValidizer; }) + .factory('PdfMakeDocumentProvider', [ 'gettextCatalog', 'Config', - function(gettextCatalog, Config) { + 'PDFLayout', + function(gettextCatalog, Config, PDFLayout) { /** - * Provides the global Document + * Provides the global document * @constructor - * @param {object} contentProvider - Object with on method `getContent`, which returns an array for content - * @param {string} defaultFont - Default font for the document + * @param {object} contentProvider - Object with on method `getContent`, which + * returns an array for content */ - var createInstance = function(contentProvider, defaultFont) { - /** - * Generates header for PDF - * @constructor - */ + var createInstance = function(contentProvider) { + // PDF header var header = function() { - var date = new Date(); - return { - // alignment: 'center', - color: '#555', - fontSize: 10, - margin: [80, 50, 80, 0], //margin: [left, top, right, bottom] - columns: [ - { - text: Config.get('general_event_name').value + ' · ' + Config.get('general_event_description').value , - fontSize:10, - width: '70%' - }, - { - fontSize: 6, - width: '30%', - text: gettextCatalog.getString('As of') + " " + date.toLocaleDateString() + " " + date.toLocaleTimeString(), - alignment: 'right' - }] - }; - }, - /** - * Generates footer line - * @function - * @param {object} currentPage - An object representing the current page - * @param {number} pageCount - number for pages - */ - footer = function(currentPage, pageCount) { - return { - alignment: 'center', - fontSize: 8, - color: '#555', - text: gettextCatalog.getString('Page') + ' ' + currentPage.toString() + ' / ' + pageCount.toString() - }; - }, - /** - * Generates the document(definition) for pdfMake - * @function - */ - getDocument = function() { - var content = contentProvider.getContent(); - return { - pageSize: 'A4', - pageMargins: [80, 90, 80, 60], - defaultStyle: { - font: defaultFont, - fontSize: 10 - }, - header: header, - footer: footer, - content: content, - styles: { - title: { - fontSize: 30, - margin: [0,0,0,20], - bold: true - }, - preamble: { - fontSize: 12, - margin: [0,0,0,10], - }, - userDataTitle: { - fontSize: 26, - margin: [0,0,0,0], - bold: true - }, - textItem: { - fontSize: 11, - margin: [0,7] - }, - heading: { - fontSize: 16, - margin: [0,0,0,10], - bold: true - }, - userDataHeading: { - fontSize: 14, - margin: [0,10], - bold: true - }, - userDataTopic: { - fontSize: 12, - margin: [0,5] - }, - userDataValue: { - fontSize: 12, - margin: [15,5] - }, - tableofcontent: { - fontSize: 12, - margin: [0,3] - }, - listParent: { - fontSize: 14, - margin: [0,5] - }, - listChild: { - fontSize: 11, - margin: [0,5] - }, - tableHeader: { - bold: true, - fillColor: 'white' - }, - tableEven: { - fillColor: 'white' - }, - tableOdd: { - fillColor: '#eee' - }, - tableConclude: { - fillColor: '#ddd', - bold: true - } - } - }; + var date = new Date(); + var columns = []; + + // add here your custom logo (which has to be added to a custom vfs_fonts.js) + // see https://github.com/pdfmake/pdfmake/wiki/Custom-Fonts---client-side + /* + columns.push({ + image: 'logo.png', + fit: [180,40] + });*/ + + var line1 = [ + Config.get('general_event_name').value, + Config.get('general_event_description').value + ].join(' – '); + var line2 = [ + Config.get('general_event_location').value, + Config.get('general_event_date').value + ].join(', '); + var text = [line1, line2].join('\n'); + columns.push({ + text: text, + fontSize:10, + width: '100%' + }); + return { + color: '#555', + fontSize: 9, + margin: [80, 30, 80, 10], // [left, top, right, bottom] + columns: columns, + columnGap: 10 }; + }; + + // PDF footer + var footer = function(currentPage, pageCount) { + return { + alignment: 'center', + fontSize: 8, + color: '#555', + text: gettextCatalog.getString('Page') + ' ' + currentPage.toString() + + ' / ' + pageCount.toString() + }; + }; + // Generates the document(definition) for pdfMake + var getDocument = function() { + var content = contentProvider.getContent(); + return { + pageSize: 'A4', + pageMargins: [80, 90, 80, 60], + defaultStyle: { + font: PDFLayout.getFontName(), + fontSize: 10 + }, + header: header, + footer: footer, + content: content, + styles: { + title: { + fontSize: 18, + margin: [0,0,0,20], + bold: true + }, + subtitle: { + fontSize: 9, + margin: [0,-20,0,20], + }, + preamble: { + fontSize: 10, + margin: [0,0,0,10], + }, + userDataTitle: { + fontSize: 26, + margin: [0,0,0,0], + bold: true + }, + textItem: { + fontSize: 11, + margin: [0,7] + }, + heading2: { + fontSize: 14, + margin: [0,0,0,10], + bold: true + }, + heading3: { + fontSize: 12, + margin: [0,10,0,0], + bold: true + }, + userDataHeading: { + fontSize: 14, + margin: [0,10], + bold: true + }, + userDataTopic: { + fontSize: 12, + margin: [0,5] + }, + userDataValue: { + fontSize: 12, + margin: [15,5] + }, + tableofcontent: { + fontSize: 12, + margin: [0,3] + }, + listParent: { + fontSize: 12, + margin: [0,5] + }, + listChild: { + fontSize: 10, + margin: [0,5] + }, + tableHeader: { + bold: true, + fillColor: 'white' + }, + tableEven: { + fillColor: 'white' + }, + tableOdd: { + fillColor: '#eee' + }, + tableConclude: { + fillColor: '#ddd', + bold: true + }, + grey: { + fillColor: '#ddd', + }, + lightgrey: { + fillColor: '#aaa', + }, + bold: { + bold: true, + } + } + }; + }; + return { getDocument: getDocument }; @@ -256,14 +313,14 @@ angular.module('OpenSlidesApp.core.pdf', []) .factory('PdfMakeBallotPaperProvider', [ 'gettextCatalog', 'Config', - function(gettextCatalog, Config) { + 'PDFLayout', + function(gettextCatalog, Config, PDFLayout) { /** * Provides the global Document * @constructor * @param {object} contentProvider - Object with on method `getContent`, which returns an array for content - * @param {string} defaultFont - Default font for the document */ - var createInstance = function(contentProvider, defaultFont) { + var createInstance = function(contentProvider) { /** * Generates the document(definition) for pdfMake * @function @@ -274,7 +331,7 @@ angular.module('OpenSlidesApp.core.pdf', []) pageSize: 'A4', pageMargins: [0, 0, 0, 0], defaultStyle: { - font: defaultFont, + font: PDFLayout.getFontName(), fontSize: 10 }, content: content, @@ -308,10 +365,9 @@ angular.module('OpenSlidesApp.core.pdf', []) * Converter component for HTML->JSON for pdfMake * @constructor * @param {object} images - Key-Value structure representing image.src/BASE64 of images - * @param {object} fonts - Key-Value structure representing fonts (detailed description below) * @param {object} pdfMake - the converter component enhances pdfMake */ - var createInstance = function(images, fonts, pdfMake) { + var createInstance = function(images, pdfMake) { var slice = Function.prototype.call.bind([].slice), map = Function.prototype.call.bind([].map), @@ -319,47 +375,6 @@ angular.module('OpenSlidesApp.core.pdf', []) DIFF_MODE_INSERT = 1, DIFF_MODE_DELETE = 2, - /** - * Adds a custom font to pdfMake.vfs - * @function - * @param {object} fontFiles - object with Files to add to pdfMake.vfs - * { - * normal: $Filename - * bold: $Filename - * italics: $Filename - * bolditalics: $Filename - * } - */ - addFontToVfs = function(fontFiles) { - Object.keys(fontFiles).forEach(function(name) { - var file = fontFiles[name]; - pdfMake.vfs[file.name] = file.content; - }); - }, - /** - * Adds custom fonts to pdfMake - * @function - * @param {object} fontInfo - Font configuration from Backend - * { - * $FontName : { - * normal: $Filename - * bold: $Filename - * italics: $Filename - * bolditalics: $Filename - * } - * } - */ - registerFont = function(fontInfo) { - Object.keys(fontInfo).forEach(function(name) { - var font = fontInfo[name]; - addFontToVfs(font); - pdfMake.fonts = pdfMake.fonts || {}; - pdfMake.fonts[name] = Object.keys(font).reduce(function(fontDefinition, style) { - fontDefinition[style] = font[style].name; - return fontDefinition; - }, {}); - }); - }, /** * Convertes HTML for use with pdfMake * @function @@ -726,9 +741,6 @@ angular.module('OpenSlidesApp.core.pdf', []) o[name] = content; return o; }; - fonts.forEach(function(fontInfo) { - registerFont(fontInfo); - }); return { convertHTML: convertHTML, createElement: create diff --git a/openslides/core/views.py b/openslides/core/views.py index f1721ad40..859ebb4c8 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -788,22 +788,12 @@ class MediaEncoder(utils_views.APIView): Takes an array of IMG.src - Paths Retrieves the according images Encodes the images to BASE64 - Add configured fonts Puts it into a key-value structure { "images": { "media/file/ubuntu.png":"$ENCODED_IMAGE" - }, - "fonts": [{ - $FontName : { - normal: $Filename - bold: $Filename - italics: $Filename - bolditalics: $Filename - } - }], - "default_font": "$DEFAULTFONT" + } } :param request: @@ -818,86 +808,10 @@ class MediaEncoder(utils_views.APIView): body_unicode = request.body.decode('utf-8') file_paths = json.loads(body_unicode) images = {file_path: self.encode_image_from(file_path) for file_path in file_paths} - fonts = self.encoded_fonts() - default_font = self.get_default_font() return Response({ - "images": images, - "fonts": fonts, - "defaultFont": default_font + "images": images }) - def get_default_font(self): - """ - Returns the default font for pdfMake. - - Note: For development purposes this is hard coded. - - :return: the name of the default Font - """ - return 'OpenSans' - - def encoded_fonts(self): - """ - Generate font encoding for pdfMake - :return: list of Font Encodings - """ - fonts = self.get_configured_fonts() - enc_fonts = [self.encode_font(name, files) for name, files in fonts.items()] - return enc_fonts - - def get_configured_fonts(self): - """ - Returns the configured fonts - - Note: For development purposes, the current font definition is hard coded - - The form is { - $FontName : { - normal: $Filename - bold: $Filename - italics: $Filename - bolditalics: $Filename - } - } - This structure is required according to PDFMake specs. - :return: - """ - fonts = { - 'OpenSans': { - 'normal': 'OpenSans-Regular.ttf', - 'bold': 'OpenSans-Bold.ttf', - 'italics': 'OpenSans-Italic.ttf', - 'bolditalics': 'OpenSans-BoldItalic.ttf' - } - } - return fonts - - def encode_font(self, font_name, font_files): - """ - Responsible to encode a single font - :param fontName: name of the font - :param font_files: files for different weighs - :return: dictionary with encoded font - """ - encoded_files = {type: self.encode_font_from(file_path) for type, file_path in font_files.items()} - return {font_name: encoded_files} - - def encode_font_from(self, file_path): - """ - Returns the BASE64 encoded version of an image-file for a given path - :param file_path: - :return: dictionary with the string representation (content) and the name of the file - for the pdfMake.vfs structure - """ - path = os.path.join(settings.MODULE_DIR, 'static', 'fonts', os.path.basename(file_path)) - try: - with open(path, "rb") as file: - string_representation = "{}".format(base64.b64encode(file.read()).decode()) - except: - return "" - else: - return {"content": string_representation, "name": file_path} - def encode_image_from(self, file_path): """ Returns the BASE64 encoded version of an image-file for a given path diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index a16ecccce..9f79cdd73 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -109,7 +109,7 @@ angular.module('OpenSlidesApp.motions', [ } // calculate percent value var config = Config.get('motions_poll_100_percent_base').value; - var percentStr; + var percentStr = ''; var percentNumber = null; var base = null; if (!impossible) { @@ -137,7 +137,8 @@ angular.module('OpenSlidesApp.motions', [ return { 'value': value, 'percentStr': percentStr, - 'percentNumber': percentNumber + 'percentNumber': percentNumber, + 'display': value + ' ' + percentStr }; } } diff --git a/openslides/motions/static/js/motions/motion-services.js b/openslides/motions/static/js/motions/motion-services.js index 8ec7b21df..ce436eedb 100644 --- a/openslides/motions/static/js/motions/motion-services.js +++ b/openslides/motions/static/js/motions/motion-services.js @@ -30,9 +30,9 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions', }); $http.post('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) { - var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake); + var converter = PdfMakeConverter.createInstance(data.images, pdfMake); var motionContentProvider = MotionContentProvider.createInstance(converter, $scope.motion, $scope, User, $http); - var documentProvider = PdfMakeDocumentProvider.createInstance(motionContentProvider, data.defaultFont); + var documentProvider = PdfMakeDocumentProvider.createInstance(motionContentProvider); var filename = gettextCatalog.getString("Motion") + "-" + $scope.motion.identifier + ".pdf"; pdfMake.createPdf(documentProvider.getDocument()).download(filename); }); diff --git a/openslides/motions/static/js/motions/pdf.js b/openslides/motions/static/js/motions/pdf.js index 01b3b7619..dd76217a1 100644 --- a/openslides/motions/static/js/motions/pdf.js +++ b/openslides/motions/static/js/motions/pdf.js @@ -6,21 +6,199 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) .factory('MotionContentProvider', [ 'gettextCatalog', - 'PdfPredefinedFunctions', - function(gettextCatalog, PdfPredefinedFunctions) { + 'PDFLayout', + 'Category', + 'Config', + function(gettextCatalog, PDFLayout, Category, Config) { /** * Provides the content as JS objects for Motions in pdfMake context * @constructor */ - var createInstance = function(converter, motion, $scope, User) { + var createInstance = function(converter, motion, $scope) { + // title var identifier = motion.identifier ? ' ' + motion.identifier : ''; - var header = PdfPredefinedFunctions.createTitle(gettextCatalog.getString("Motion") + identifier + - ': ' + motion.getTitle($scope.version)); + var title = PDFLayout.createTitle( + gettextCatalog.getString('Motion') + identifier + ': ' + + motion.getTitle($scope.version) + ); - // generates the text of the motion. Also septerates between line-numbers - var textContent = function() { + // subtitle + var subtitle = PDFLayout.createSubtitle( + gettextCatalog.getString('Sequential number') + ': ' + motion.id + ); + + // meta data table + var metaTable = function() { + var metaTableBody = []; + + // submitters + var submitters = _.map(motion.submitters, function (submitter) { + return submitter.get_full_name(); + }).join(', '); + metaTableBody.push([ + { + text: gettextCatalog.getString('Submitters') + ':', + style: ['bold', 'grey'] + }, + { + text: submitters, + style: 'grey' + } + ]); + + // state + metaTableBody.push([ + { + text: gettextCatalog.getString('State') + ':', + style: ['bold', 'grey'] + }, + { + text: motion.getStateName(), + style: 'grey' + } + ]); + + // recommendation + if (motion.getRecommendationName()) { + metaTableBody.push([ + { + text: Config.get('motions_recommendations_by').value + ':', + style: ['bold', 'grey'] + }, + { + text: motion.getRecommendationName(), + style: 'grey' + } + ]); + } + + // category + if (motion.category) { + metaTableBody.push([ + { + text: gettextCatalog.getString('Category') + ':', + style: ['bold', 'grey'] + }, + { + text: motion.category.name, + style: 'grey' + } + ]); + } + + // voting result + var column1 = []; + var column2 = []; + var column3 = []; + motion.polls.map(function(poll, index) { + var votenumber = ''; + if (motion.polls.length > 1) { + votenumber = index + 1 + '. ' + gettextCatalog.getString('Vote'); + } + + // yes + var yes = poll.getVote(poll.yes, 'yes'); + column1.push(gettextCatalog.getString('Yes') + ':'); + column2.push(yes.value); + column3.push(yes.percentStr); + // no + var no = poll.getVote(poll.no, 'no'); + column1.push(gettextCatalog.getString('No') + ':'); + column2.push(no.value); + column3.push(no.percentStr); + // abstain + var abstain = poll.getVote(poll.abstain, 'abstain'); + column1.push(gettextCatalog.getString('Abstain') + ':'); + column2.push(abstain.value); + column3.push(abstain.percentStr); + // votes valid + if (poll.votesvalid) { + var valid = poll.getVote(poll.votesvalid, 'votesvalid'); + column1.push(gettextCatalog.getString('Valid votes') + ':'); + column2.push(valid.value); + column3.push(valid.percentStr); + } + // votes invalid + if (poll.votesvalid) { + var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid'); + column1.push(gettextCatalog.getString('Invalid votes') + ':'); + column2.push(invalid.value); + column3.push(invalid.percentStr); + } + // votes cast + if (poll.votescast) { + var cast = poll.getVote(poll.votescast, 'votescast'); + column1.push(gettextCatalog.getString('Votes cast') + ':'); + column2.push(cast.value); + column3.push(cast.percentStr); + } + }); + metaTableBody.push([ + { + text: gettextCatalog.getString('Voting result') + ':', + style: ['bold', 'grey'] + }, + { + columns: [ + { + text: column1.join('\n'), + width: 'auto' + }, + { + text: column2.join('\n'), + width: 'auto', + alignment: 'right' + }, + { + text: column3.join('\n'), + width: 'auto', + alignment: 'right' + }, + ], + columnGap: 7, + style: 'grey' + } + ]); + + + // build table + var metaTableJsonString = { + table: { + widths: ['30%','70%'], + body: metaTableBody, + }, + margin: [0, 0, 0, 20], + layout: { + hLineWidth: function(i, node) { + return (i === 0 || i === node.table.body.length) ? 0 : 0.5; + }, + vLineWidth: function(i, node) { + return (i === 0 || i === node.table.widths.length) ? 0 : 0; + }, + hLineColor: function(i, node) { + return (i === 0 || i === node.table.body.length) ? '' : 'white'; + }, + vLineColor: function(i, node) { + return (i === 0 || i === node.table.widths.length) ? '' : 'white'; + } + } + }; + return metaTableJsonString; + }; + + // motion title + var motionTitle = function() { + return [{ + text: motion.getTitle($scope.version), + style: 'heading3' + }]; + }; + + + // motion text (with line-numbers) + var motionText = function() { if ($scope.lineNumberMode == "inline" || $scope.lineNumberMode == "outside") { /* in order to distinguish between the line-number-types we need to pass the scope * to the convertHTML function. @@ -35,109 +213,18 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) } }; - // Generate text of reason - var reasonContent = function() { - return converter.convertHTML(motion.getReason($scope.version), $scope); + // motion reason heading + var motionReason = function() { + var reason = [{ + text: gettextCatalog.getString('Reason'), + style: 'heading3' + }]; + reason.push(converter.convertHTML(motion.getReason($scope.version), $scope)); + return reason; }; - // Generate text of signment - var signment = function() { - var label = converter.createElement("text", gettextCatalog.getString('Submitter') + ':\nStatus:'); - var state = converter.createElement("text", User.get(motion.submitters_id[0]).full_name + '\n' + motion.getStateName()); - state.width = "70%"; - label.width = "30%"; - label.bold = true; - var signment = converter.createElement("columns", [label, state]); - signment.margin = [10, 20, 0, 10]; - signment.lineHeight = 2.5; - return signment; - }; - // Generates polls - var polls = function() { - if (!motion.polls.length) return {}; - var pollLabel = converter.createElement("text", gettextCatalog.getString('Voting result') + ":"), - results = function() { - return motion.polls.map(function(poll, index) { - var id = index + 1, - yes = poll.yes ? poll.yes : '-', // if no poll.yes is given set it to '-' - yesRelative = poll.getVote(poll.yes, 'yes').percentStr, - no = poll.no ? poll.no : '-', - noRelative = poll.getVote(poll.no, 'no').percentStr, - abstain = poll.abstain ? poll.abstain : '-', - abstainrelativeGet = poll.getVote(poll.abstain, 'abstain').percentStr, - abstainRelative = abstainrelativeGet ? abstainrelativeGet : '', - valid = poll.votesvalid ? poll.votesvalid : '-', - validRelative = poll.getVote(poll.votesvalid, 'votesvalid').percentStr, - number = { - text: id + ".", - width: "5%" - }, - headerText = { - text: gettextCatalog.getString('Vote'), - width: "15%" - }, - /** - * Generates a part (consisting of different columns) of the polls - * - * Example Ja 100 ( 90% ) - * - * @function - * @param {string} name - E.g. "Ja" - * @param {number} value - E.g.100 - * @param {number} relValue - E.g. 90 - */ - createPart = function(name, value, relValue) { - var indexColumn = converter.createElement("text"); - var nameColumn = converter.createElement("text", "" + name); - var valueColumn = converter.createElement("text", "" + value); - var relColumn = converter.createElement("text", relValue); - valueColumn.width = "40%"; - indexColumn.width = "5%"; - valueColumn.width = "5%"; - valueColumn.alignment = "right"; - relColumn.margin = [5, 0, 0, 0]; - return [indexColumn, nameColumn, valueColumn, relColumn]; - }, - yesPart = converter.createElement("columns", createPart(gettextCatalog.getString("Yes"), yes, yesRelative)), - noPart = converter.createElement("columns", createPart(gettextCatalog.getString("No"), no, noRelative)), - abstainPart = converter.createElement("columns", createPart(gettextCatalog.getString("Abstain"), abstain, abstainRelative)), - totalPart = converter.createElement("columns", createPart(gettextCatalog.getString("Valid votes"), valid, validRelative)), - heading = converter.createElement("columns", [number, headerText]), - pollResult = converter.createElement("stack", [ - heading, yesPart, noPart, abstainPart, totalPart - ]); - - return pollResult; - }, {}); - }; - pollLabel.width = '35%'; - pollLabel.bold = true; - var result = converter.createElement("columns", [pollLabel, results()]); - result.margin = [10, 0, 0, 10]; - result.lineHeight = 1; - return result; - }; - - //Generates title section for motion - var titleSection = function() { - var title = converter.createElement("text", motion.getTitle($scope.version)); - title.bold = true; - title.fontSize = 14; - title.margin = [0, 0, 0, 10]; - return title; - }; - - // Generates reason section for polls - var reason = function() { - var r = converter.createElement("text", gettextCatalog.getString("Reason") + ":"); - r.bold = true; - r.fontSize = 14; - r.margin = [0, 30, 0, 10]; - return r; - }; - - //getters + // getters var getTitle = function() { return motion.getTitle($scope.verion); }; @@ -152,25 +239,17 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) // Generates content as a pdfmake consumable var getContent = function() { - if (reasonContent().length === 0 ) { - return [ - header, - signment(), - polls(), - titleSection(), - textContent(), - ]; - } else { - return [ - header, - signment(), - polls(), - titleSection(), - textContent(), - reason(), - reasonContent() - ]; + var content = [ + title, + subtitle, + metaTable(), + motionTitle(), + motionText(), + ]; + if (motionReason()) { + content.push(motionReason()); } + return content; }; return { getContent: getContent, @@ -186,8 +265,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) }]) .factory('PollContentProvider', [ - 'PdfPredefinedFunctions', - function(PdfPredefinedFunctions) { + 'PDFLayout', + function(PDFLayout) { /** * Generates a content provider for polls * @constructor @@ -211,9 +290,9 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) text: title, style: 'description' }, - PdfPredefinedFunctions.createBallotEntry(gettextCatalog.getString("Yes")), - PdfPredefinedFunctions.createBallotEntry(gettextCatalog.getString("No")), - PdfPredefinedFunctions.createBallotEntry(gettextCatalog.getString("Abstain")), + PDFLayout.createBallotEntry(gettextCatalog.getString("Yes")), + PDFLayout.createBallotEntry(gettextCatalog.getString("No")), + PDFLayout.createBallotEntry(gettextCatalog.getString("Abstain")), ], margin: [0, 0, 0, sheetend] }; @@ -237,7 +316,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) [createSection(), createSection()] ], }, - layout: PdfPredefinedFunctions.getBallotLayoutLines() + layout: PDFLayout.getBallotLayoutLines() }]; }; @@ -252,20 +331,20 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) .factory('MotionCatalogContentProvider', [ 'gettextCatalog', - 'PdfPredefinedFunctions', + 'PDFLayout', + 'Category', 'Config', - function(gettextCatalog, PdfPredefinedFunctions, Config) { + function(gettextCatalog, PDFLayout, Category, Config) { /** * Constructor * @function * @param {object} allMotions - A sorted array of all motions to parse * @param {object} $scope - Current $scope - * @param {object} User - Current user */ - var createInstance = function(allMotions, $scope, User, Category) { + var createInstance = function(allMotions, $scope) { - var title = PdfPredefinedFunctions.createTitle( + var title = PDFLayout.createTitle( gettextCatalog.getString(Config.get('motions_export_title').value) ); @@ -284,7 +363,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) var createTOContent = function() { var heading = { text: gettextCatalog.getString("Table of contents"), - style: "heading" + style: "heading2" }; var toc = []; @@ -310,7 +389,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) return [ heading, toc, - PdfPredefinedFunctions.addPageBreak() + PDFLayout.addPageBreak() ]; }; @@ -319,7 +398,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) if (Category.getAll().length > 0) { var heading = { text: gettextCatalog.getString("Categories"), - style: "heading" + style: "heading2" }; var toc = []; @@ -344,7 +423,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) return [ heading, toc, - PdfPredefinedFunctions.addPageBreak() + PDFLayout.addPageBreak() ]; } else { // if there are no categories, return "empty string" @@ -359,7 +438,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) angular.forEach(allMotions, function(motion, key) { motionContent.push(motion.getContent()); if (key < allMotions.length - 1) { - motionContent.push(PdfPredefinedFunctions.addPageBreak()); + motionContent.push(PDFLayout.addPageBreak()); } }); diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index 4f0534610..109bb1399 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -926,7 +926,7 @@ angular.module('OpenSlidesApp.motions.site', [ //post-request to convert the images. Async. $http.post('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) { - var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake); + var converter = PdfMakeConverter.createInstance(data.images, pdfMake); var motionContentProviderArray = []; //convert the filtered motions to motionContentProviders @@ -934,7 +934,7 @@ angular.module('OpenSlidesApp.motions.site', [ motionContentProviderArray.push(MotionContentProvider.createInstance(converter, motion, $scope, User, $http)); }); var motionCatalogContentProvider = MotionCatalogContentProvider.createInstance(motionContentProviderArray, $scope, User, Category); - var documentProvider = PdfMakeDocumentProvider.createInstance(motionCatalogContentProvider, data.defaultFont); + var documentProvider = PdfMakeDocumentProvider.createInstance(motionCatalogContentProvider); pdfMake.createPdf(documentProvider.getDocument()).download(filename); }); }; diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index 3b4465f47..e3c7a04e6 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -132,11 +132,13 @@
-

Recommendation

+

+ {{ config('motions_recommendations_by') }} +

- Recommendation + {{ config('motions_recommendations_by') }}