diff --git a/CHANGELOG b/CHANGELOG index 118efc367..4fb48f183 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ Motions: - Fixed empty motion comment field in motion update form [#3194]. - Removed server side image to base64 transformation and added local transformation [#2705] +- Added support for export motions in a zip archive [#3189]. Core: - No reload on logoff. OpenSlides is now a full single page diff --git a/bower.json b/bower.json index 04bf56eb6..94b64c79a 100644 --- a/bower.json +++ b/bower.json @@ -28,6 +28,7 @@ "jquery.cookie": "~1.4.1", "js-data": "~2.9.0", "js-data-angular": "~3.2.1", + "jszip": "~3.1.3", "lodash": "~4.16.0", "ng-dialog": "~0.6.4", "ng-file-upload": "~11.2.3", diff --git a/openslides/core/static/js/core/pdf.js b/openslides/core/static/js/core/pdf.js index e518607e4..45cd8bbda 100644 --- a/openslides/core/static/js/core/pdf.js +++ b/openslides/core/static/js/core/pdf.js @@ -859,10 +859,11 @@ angular.module('OpenSlidesApp.core.pdf', []) .factory('PdfCreate', [ '$timeout', + '$q', 'gettextCatalog', 'FileSaver', 'Messaging', - function ($timeout, gettextCatalog, FileSaver, Messaging) { + function ($timeout, $q, gettextCatalog, FileSaver, Messaging) { var filenameMessageMap = {}; var b64toBlob = function(b64Data) { var byteCharacters = atob(b64Data); @@ -898,19 +899,28 @@ angular.module('OpenSlidesApp.core.pdf', []) }, 1); }; return { + getBase64FromDocument: function (pdfDocument) { + return $q(function (resolve, reject) { + var pdfWorker = new Worker('/static/js/workers/pdf-worker.js'); + pdfWorker.addEventListener('message', function (event) { + resolve(event.data); + }); + pdfWorker.addEventListener('error', function (event) { + reject(event); + }); + pdfWorker.postMessage(JSON.stringify(pdfDocument)); + }); + }, download: function (pdfDocument, filename) { stateChange('info', filename); - var pdfWorker = new Worker('/static/js/workers/pdf-worker.js'); - pdfWorker.addEventListener('message', function (event) { - var blob = b64toBlob(event.data); + this.getBase64FromDocument(pdfDocument).then(function (data) { + var blob = b64toBlob(data); stateChange('success', filename); FileSaver.saveAs(blob, filename); + }, function (error) { + stateChange('error', filename, error.message); }); - pdfWorker.addEventListener('error', function (event) { - stateChange('error', filename, event.message); - }); - pdfWorker.postMessage(JSON.stringify(pdfDocument)); }, }; } diff --git a/openslides/motions/static/js/motions/pdf.js b/openslides/motions/static/js/motions/pdf.js index 144178b0e..e11b196c9 100644 --- a/openslides/motions/static/js/motions/pdf.js +++ b/openslides/motions/static/js/motions/pdf.js @@ -288,7 +288,6 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) for (var i = 0; i < fields.length; i++) { if (motion.comments[i] && canSeeComment(i)) { var title = gettextCatalog.getString('Comment') + ' ' + fields[i].name; - console.log(fields[i]); if (!fields[i].public) { title += ' (' + gettextCatalog.getString('internal') + ')'; } @@ -602,6 +601,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) .factory('MotionPdfExport', [ '$http', + '$q', 'Config', 'gettextCatalog', 'MotionChangeRecommendation', @@ -614,15 +614,14 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) 'PdfMakeBallotPaperProvider', 'PdfCreate', 'PDFLayout', - '$q', - function ($http, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer, PdfMakeConverter, + 'Messaging', + 'FileSaver', + function ($http, $q, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer, PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeDocumentProvider, PollContentProvider, - PdfMakeBallotPaperProvider, PdfCreate, PDFLayout, $q) { + PdfMakeBallotPaperProvider, PdfCreate, PDFLayout, Messaging, FileSaver) { return { - export: function (motions, params, singleMotion) { - if (!params) { - params = {}; - } + getDocumentProvider: function (motions, params, singleMotion) { + params = _.clone(params || {}); // Clone this to avoid sideeffects. _.defaults(params, { filename: gettextCatalog.getString('motions') + '.pdf', changeRecommendationMode: Config.get('motions_recommendation_text_mode').value, @@ -643,11 +642,11 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) angular.forEach(motions, function (motion) { if (singleMotion) { motion.changeRecommendations = MotionChangeRecommendation.filter({ - 'where': {'motion_version_id': {'==': motion.active_version}} + 'where': {'motion_version_id': {'==': params.version}} }); } else { motion.changeRecommendations = MotionChangeRecommendation.filter({ - 'where': {'motion_version_id': {'==': params.version}} + 'where': {'motion_version_id': {'==': motion.active_version}} }); } var text = motion.getTextByMode(params.changeRecommendationMode, null); @@ -659,43 +658,93 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) image_sources = image_sources.concat(tmp_image_sources); }); - var image_map = {}; - _.forEach(image_sources, function (image_source) { - image_map[image_source] = PDFLayout.imageURLtoBase64(image_source); + var imageMap = {}; + var imagePromises = _.map(image_sources, function (image_source) { + return PDFLayout.imageURLtoBase64(image_source).then(function (base64Str) { + imageMap[image_source] = base64Str; + }); }); - var image_promises = Object.keys(image_map).map(function( key ) { - return image_map[key]; + return $q(function (resolve) { + //resolve promises to get base64 + $q.all(imagePromises).then(function(base64Str) { + var converter = PdfMakeConverter.createInstance(imageMap); + var motionContentProviderArray = []; + + //convert all motions to motionContentProviders + angular.forEach(motions, function (motion) { + var version = (singleMotion ? params.version : motion.active_version); + motionContentProviderArray.push(MotionContentProvider.createInstance( + converter, motion, version, params.changeRecommendationMode, + motion.changeRecommendations, params.lineNumberMode, + params.includeReason, params.includeComments + )); + }); + + var documentProvider; + if (singleMotion) { + documentProvider = PdfMakeDocumentProvider.createInstance(motionContentProviderArray[0]); + } else { + var motionCatalogContentProvider = MotionCatalogContentProvider.createInstance(motionContentProviderArray); + documentProvider = PdfMakeDocumentProvider.createInstance(motionCatalogContentProvider); + } + + resolve(documentProvider); + }); }); - - //resolv promises to get base64 - $q.all(image_promises).then(function(base64Str) { - Object.keys(image_map).map(function(key, i) { - image_map[key] = base64Str[i]; - }); - - var converter = PdfMakeConverter.createInstance(image_map); - var motionContentProviderArray = []; - - //convert all motions to motionContentProviders - angular.forEach(motions, function (motion) { - var version = (singleMotion ? params.version : motion.active_version); - motionContentProviderArray.push(MotionContentProvider.createInstance( - converter, motion, version, params.changeRecommendationMode, - motion.changeRecommendations, params.lineNumberMode, - params.includeReason, params.includeComments - )); - }); - - var documentProvider; - if (singleMotion) { - documentProvider = PdfMakeDocumentProvider.createInstance(motionContentProviderArray[0]); - } else { - var motionCatalogContentProvider = MotionCatalogContentProvider.createInstance(motionContentProviderArray); - documentProvider = PdfMakeDocumentProvider.createInstance(motionCatalogContentProvider); + }, + export: function (motions, params, singleMotion) { + _.defaults(params, { + filename: gettextCatalog.getString('motions') + '.pdf', + }); + this.getDocumentProvider(motions, params, singleMotion).then( + function (documentProvider) { + PdfCreate.download(documentProvider.getDocument(), params.filename); } + ); + }, + exportZip: function (motions, params) { + var messageId = Messaging.addMessage('' + + gettextCatalog.getString('Generating PDFs and ZIP archive') + ' ...', 'info'); + var zipFilename = params.filename || gettextCatalog.getString('motions') + '.zip'; + params.filename = void 0; // clear this, so we do not override the default filenames for each pdf. - PdfCreate.download(documentProvider.getDocument(), params.filename); + var self = this; + var pdfs = {}; + var pdfPromises = _.map(motions, function (motion) { + var identifier = motion.identifier ? '-' + motion.identifier : ''; + var filename = gettextCatalog.getString('Motion') + identifier + '.pdf'; + + return $q(function (resolve, reject) { + // get documentProvider for every motion. + self.getDocumentProvider(motion, params, true).then(function (documentProvider) { + var doc = documentProvider.getDocument(); + + PdfCreate.getBase64FromDocument(doc).then(function (data) { + pdfs[filename] = data; + resolve(); + }, function (error) { + reject(error); + }); + }); + }); + }); + + // Wait for all documents to be generated. Then put them into a zip and download it. + $q.all(pdfPromises).then(function () { + var zip = new JSZip(); + _.forEach(pdfs, function (data, filename) { + zip.file(filename, data, {base64: true}); + }); + Messaging.createOrEditMessage(messageId, '' + + gettextCatalog.getString('ZIP successfully generated.'), 'success', {timeout: 3000}); + zip.generateAsync({type: 'blob'}).then(function (content) { + FileSaver.saveAs(content, zipFilename); + }); + }, function (error) { + Messaging.createOrEditMessage(messageId, '' + gettextCatalog.getString('Error while generating ZIP file') + + ': ' + error + '', 'error'); }); }, createPollPdf: function (motion, version) { diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index a500bdc8c..d06d080f8 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -733,9 +733,23 @@ angular.module('OpenSlidesApp.motions.site', [ ], }, hideExpression: "model.format !== 'pdf'", - }); + }); } } + if (!singleMotion) { + fields.push({ + key: 'pdfFormat', + type: 'select-radio', + templateOptions: { + label: gettextCatalog.getString('PDF format'), + options: [ + {name: gettextCatalog.getString('One PDF'), value: 'pdf'}, + {name: gettextCatalog.getString('Multiple PDFs in a zip arcive'), value: 'zip'}, + ], + }, + hideExpression: "model.format !== 'pdf'", + }); + } return fields; }, }; @@ -761,6 +775,7 @@ angular.module('OpenSlidesApp.motions.site', [ $scope.params = params || {}; _.defaults($scope.params, { format: 'pdf', + pdfFormat: 'pdf', changeRecommendationMode: Config.get('motions_recommendation_text_mode').value, lineNumberMode: Config.get('motions_default_line_numbering').value, includeReason: true, @@ -772,7 +787,11 @@ angular.module('OpenSlidesApp.motions.site', [ $scope.export = function () { switch ($scope.params.format) { case 'pdf': - MotionPdfExport.export(motions, $scope.params, singleMotion); + if ($scope.params.pdfFormat === 'pdf') { + MotionPdfExport.export(motions, $scope.params, singleMotion); + } else { + MotionPdfExport.exportZip(motions, $scope.params); + } break; case 'csv': MotionCsvExport.export(motions, $scope.params);