diff --git a/CHANGELOG b/CHANGELOG index 118e79ecf..7258b1961 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ Core: Motions: - Added origin field. - Added button to sort and number all motions in a category. +- Introduced pdfMake for clientside generation of PDFs. Users: - Added field is_committee and new default group Committees. diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index 789fc43db..33af57eb2 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -37,8 +37,15 @@ angular.module('OpenSlidesApp.core.site', [ color: '#555', fontSize: 10, margin: [80, 50, 80, 0], //margin: [left, top, right, bottom] - columns: ['OpenSlides | Presentation and assembly system', { + columns: [ + { + text: 'OpenSlides | Presentation and assembly system', + fontSize:10, + width: '70%' + }, + { fontSize: 6, + width: '30%', text: 'Stand: ' + date.toLocaleDateString() + " " + date.toLocaleTimeString(), alignment: 'right' }] diff --git a/openslides/core/urls.py b/openslides/core/urls.py index 76ea25daa..3dbc1b614 100644 --- a/openslides/core/urls.py +++ b/openslides/core/urls.py @@ -19,6 +19,10 @@ urlpatterns = [ views.SearchView.as_view(), name='core_search'), + url(r'^core/encode_media/$', + views.MediaEncoder.as_view(), + name="core_mediaencoding"), + url(r'^angular_js/(?Psite|projector)/$', views.AppsJsView.as_view(), name='core_apps_js'), @@ -26,8 +30,8 @@ urlpatterns = [ # View for the projectors are handelt by angular. url(r'^projector.*$', views.ProjectorView.as_view()), - # Main entry point for all angular pages. # Has to be the last entry in the urls.py url(r'^.*$', views.IndexView.as_view()), + ] diff --git a/openslides/core/views.py b/openslides/core/views.py index ac6ee5412..7c38e257d 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -1,3 +1,6 @@ +import base64 +import json +import os import re import uuid from collections import OrderedDict @@ -606,3 +609,143 @@ class SearchView(utils_views.APIView): return super().get_context_data( elements=search(unquote(query)), **context) + + +class MediaEncoder(utils_views.APIView): + """ + MediaEncoder is a class based view to prepare encoded media for pdfMake + """ + http_method_names = ['post'] + + def post(self, request, *args, **kwargs): + """ + Encode_image is used in the context of PDF-Generation + 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: + :return: Response of the resulting dictionary + + Calling e.g. + $.ajax({ type: "POST", url: "/motions/encode_images/", + data: JSON.stringify(["$FILEPATH"]), + success: function(data){ console.log(data); }, + dataType: 'application/json' }); + """ + 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 + }) + + 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.SITE_ROOT, '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 + :param file_path: + :return: + """ + path = os.path.join(settings.MEDIA_ROOT, 'file', os.path.basename(file_path)) + try: + with open(path, "rb") as file: + string_representation = "data:image/{};base64,{}".format(os.path.splitext(file_path)[1][1:], + base64.b64encode(file.read()).decode()) + except Exception: + # If any error occurs ignore it and return an empty string + return "" + else: + return string_representation diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index a9928c9b2..c8ae39fd6 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -49,9 +49,11 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) */ signment = function(motion, $scope, User) { var label = converter.createElement("text", gettextCatalog.getString('Submitter') + ':\nStatus:'); + var state = converter.createElement("text", User.get(motion.submitters_id[0]).full_name + '\n'+gettextCatalog.getString(motion.state.name)); + state.width = "70%"; label.width = "30%"; label.bold = true; - var signment = converter.createElement("stack", [label]); + var signment = converter.createElement("columns", [label, state]); signment.margin = [10, 20, 0, 10]; signment.lineHeight = 2.5; return signment; @@ -778,10 +780,11 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) 'MotionContentProvider', 'PdfMakeConverter', 'PdfMakeDocumentProvider', + 'gettextCatalog', function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, motion, - SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider) { + SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, gettextCatalog) { Motion.bindOne(motion.id, $scope, 'motion'); Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); @@ -794,13 +797,14 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) $scope.makePDF = function(){ var content = motion.getText($scope.version) + motion.getReason($scope.version), + id = motion.identifier, slice = Function.prototype.call.bind([].slice), map = Function.prototype.call.bind([].map), image_sources = map($(content).find("img"), function(element) { return element.getAttribute("src"); }); - $http.post('/motions/encode_media/', JSON.stringify(image_sources)).success(function(data) { + $http.post('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) { /** * Converter for use with pdfMake * @constructor @@ -812,8 +816,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake), motionContentProvider = MotionContentProvider.createInstance(converter), contentProvider = SingleMotionContentProvider.createInstance(motionContentProvider, motion, $scope, User), - documentProvider = PdfMakeDocumentProvider.createInstance(contentProvider, data.defaultFont); - pdfMake.createPdf(documentProvider.getDocument()).open(); + documentProvider = PdfMakeDocumentProvider.createInstance(contentProvider, data.defaultFont), + filename = gettextCatalog.getString("Motion") + " " + id + ".pdf"; + 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 2061abd98..db5bb7e37 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -5,13 +5,9 @@ All motions - - - PDF - - PDFmake + PDF diff --git a/openslides/motions/urls.py b/openslides/motions/urls.py index f0d89ef51..9b37d60d8 100644 --- a/openslides/motions/urls.py +++ b/openslides/motions/urls.py @@ -14,6 +14,4 @@ urlpatterns = [ url(r'^poll/(?P\d+)/print/$', views.MotionPollPDF.as_view(), name='motionpoll_pdf'), - - url(r'^encode_media/', views.encode_media, name="media_encoding") ] diff --git a/openslides/motions/views.py b/openslides/motions/views.py index e34285a89..703558528 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -1,16 +1,10 @@ -import base64 -import json -import os - -from django.conf import settings from django.db import transaction -from django.http import Http404, JsonResponse +from django.http import Http404 from django.utils.text import slugify from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop from reportlab.platypus import SimpleDocTemplate from rest_framework import status -from rest_framework.decorators import api_view from openslides.core.config import config from openslides.utils.rest_api import ( @@ -472,142 +466,3 @@ class MotionPDFView(SingleObjectMixin, PDFView): motions_to_pdf(pdf, motions) else: motion_to_pdf(pdf, self.get_object()) - - -@api_view(["POST"]) -def encode_media(request): - """ - Encode_image is used in the context of PDF-Generation - Takes an array of IMG.src - Paths - Retrieves the according images - Encodes the images - 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: - :return: JsonResponse of the resulting dictionary - - Calling e.g. - $.ajax({ type: "POST", url: "/motions/encode_images/", - data: JSON.stringify(["$FILEPATH"]), - success: function(data){ console.log(data); }, - dataType: 'application/json' }); - """ - body_unicode = request.body.decode('utf-8') - file_paths = json.loads(body_unicode) - images = {file_path: encode_image_from(file_path) for file_path in file_paths} - fonts = encoded_fonts() - default_font = get_default_font() - return JsonResponse({ - "images": images, - "fonts": fonts, - "defaultFont": default_font - }) - - -def get_default_font(): - """ - For development purposes this is hard coded - :return: the name of the default Font - """ - return "OpenSans" - - -def encoded_fonts(): - """ - Generate font encoding for pdfMake - :return: list of Font Encodings - """ - - fonts = get_configured_fonts() - - enc_fonts = [encode_font(name, files) for name, files in fonts.items()] - - return enc_fonts - - -def get_configured_fonts(): - """ - 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(fontName, 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: encode_font_from(file_path) for type, file_path in font_files.items()} - return {fontName: encoded_files} - - -def encode_font_from(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.SITE_ROOT, '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(file_path): - """ - Returns the BASE64 encoded version of an image-file for a given path - :param file_path: - :return: - """ - path = os.path.join(settings.MEDIA_ROOT, 'file', os.path.basename(file_path)) - try: - with open(path, "rb") as file: - string_representation = "data:image/{};base64,{}".format(os.path.splitext(file_path)[1][1:], - base64.b64encode(file.read()).decode()) - except: - return "" - else: - return string_representation