Merge pull request #2381 from FinnStutzenstein/Issue2346
Motions docx export with docxtemplater
This commit is contained in:
commit
66aa42021a
@ -9,6 +9,7 @@
|
|||||||
"angular-bootstrap-colorpicker": "~3.0.25",
|
"angular-bootstrap-colorpicker": "~3.0.25",
|
||||||
"angular-chosen-localytics": "~1.5.0",
|
"angular-chosen-localytics": "~1.5.0",
|
||||||
"angular-csv-import": "~0.0.36",
|
"angular-csv-import": "~0.0.36",
|
||||||
|
"angular-file-saver": "~1.1.2",
|
||||||
"angular-formly": "~8.4.0",
|
"angular-formly": "~8.4.0",
|
||||||
"angular-formly-templates-bootstrap": "~6.2.0",
|
"angular-formly-templates-bootstrap": "~6.2.0",
|
||||||
"angular-gettext": "~2.3.7",
|
"angular-gettext": "~2.3.7",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"angular-ui-tinymce": "~0.0.17",
|
"angular-ui-tinymce": "~0.0.17",
|
||||||
"angular-ui-tree": "~2.22.0",
|
"angular-ui-tree": "~2.22.0",
|
||||||
"bootstrap-css-only": "~3.3.6",
|
"bootstrap-css-only": "~3.3.6",
|
||||||
|
"docxtemplater": "~2.1.5",
|
||||||
"font-awesome-bower": "~4.5.0",
|
"font-awesome-bower": "~4.5.0",
|
||||||
"jquery.cookie": "~1.4.1",
|
"jquery.cookie": "~1.4.1",
|
||||||
"js-data": "~2.9.0",
|
"js-data": "~2.9.0",
|
||||||
|
@ -13,6 +13,7 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
'localytics.directives',
|
'localytics.directives',
|
||||||
'ngBootbox',
|
'ngBootbox',
|
||||||
'ngDialog',
|
'ngDialog',
|
||||||
|
'ngFileSaver',
|
||||||
'ngMessages',
|
'ngMessages',
|
||||||
'ngCsvImport',
|
'ngCsvImport',
|
||||||
'ui.tinymce',
|
'ui.tinymce',
|
||||||
|
@ -211,24 +211,24 @@ def get_config_variables():
|
|||||||
subgroup='Voting and ballot papers',
|
subgroup='Voting and ballot papers',
|
||||||
validators=(MinValueValidator(1),))
|
validators=(MinValueValidator(1),))
|
||||||
|
|
||||||
# PDF
|
# PDF and DOCX export
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_pdf_title',
|
name='motions_export_title',
|
||||||
default_value='Motions',
|
default_value='Motions',
|
||||||
label='Title for PDF document (all motions)',
|
label='Title for PDF and DOCX documents (all motions)',
|
||||||
weight=370,
|
weight=370,
|
||||||
group='Motions',
|
group='Motions',
|
||||||
subgroup='PDF',
|
subgroup='Export',
|
||||||
translatable=True)
|
translatable=True)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_pdf_preamble',
|
name='motions_export_preamble',
|
||||||
default_value='',
|
default_value='',
|
||||||
label='Preamble text for PDF document (all motions)',
|
label='Preamble text for PDF and DOCX documents (all motions)',
|
||||||
weight=375,
|
weight=375,
|
||||||
group='Motions',
|
group='Motions',
|
||||||
subgroup='PDF')
|
subgroup='Export')
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_pdf_paragraph_numbering',
|
name='motions_pdf_paragraph_numbering',
|
||||||
@ -237,4 +237,4 @@ def get_config_variables():
|
|||||||
label='Show paragraph numbering (only in PDF)',
|
label='Show paragraph numbering (only in PDF)',
|
||||||
weight=380,
|
weight=380,
|
||||||
group='Motions',
|
group='Motions',
|
||||||
subgroup='PDF')
|
subgroup='Export')
|
||||||
|
@ -308,9 +308,9 @@ def all_motion_cover(pdf, motions):
|
|||||||
"""
|
"""
|
||||||
Create a coverpage for all motions.
|
Create a coverpage for all motions.
|
||||||
"""
|
"""
|
||||||
pdf.append(Paragraph(escape(config["motions_pdf_title"]), stylesheet['Heading1']))
|
pdf.append(Paragraph(escape(config["motions_export_title"]), stylesheet['Heading1']))
|
||||||
|
|
||||||
preamble = escape(config["motions_pdf_preamble"])
|
preamble = escape(config["motions_export_preamble"])
|
||||||
if preamble:
|
if preamble:
|
||||||
pdf.append(Paragraph("%s" % preamble.replace('\r\n', '<br/>'), stylesheet['Paragraph']))
|
pdf.append(Paragraph("%s" % preamble.replace('\r\n', '<br/>'), stylesheet['Paragraph']))
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
angular.module('OpenSlidesApp.motions', [
|
angular.module('OpenSlidesApp.motions', [
|
||||||
'OpenSlidesApp.users',
|
'OpenSlidesApp.users',
|
||||||
'OpenSlidesApp.motions.lineNumbering',
|
'OpenSlidesApp.motions.lineNumbering',
|
||||||
'OpenSlidesApp.motions.diff'
|
'OpenSlidesApp.motions.diff',
|
||||||
|
'OpenSlidesApp.motions.DOCX'
|
||||||
])
|
])
|
||||||
|
|
||||||
.factory('WorkflowState', [
|
.factory('WorkflowState', [
|
||||||
|
386
openslides/motions/static/js/motions/docx.js
Normal file
386
openslides/motions/static/js/motions/docx.js
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
(function () {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('OpenSlidesApp.motions.DOCX', [])
|
||||||
|
|
||||||
|
.factory('MotionDocxExport', [
|
||||||
|
'$http',
|
||||||
|
'$q',
|
||||||
|
'Config',
|
||||||
|
'gettextCatalog',
|
||||||
|
'FileSaver',
|
||||||
|
function ($http, $q, Config, gettextCatalog, FileSaver) {
|
||||||
|
|
||||||
|
var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>';
|
||||||
|
var TAGS_NO_PARAM = ['b', 'strong', 'em', 'i'];
|
||||||
|
|
||||||
|
var images;
|
||||||
|
var relationships;
|
||||||
|
var contentTypes;
|
||||||
|
|
||||||
|
// $scope.motionsFiltered, $scope.categories
|
||||||
|
|
||||||
|
var getData = function (motions, categories) {
|
||||||
|
var data = {};
|
||||||
|
data.title = Config.get('motions_export_title').value;
|
||||||
|
data.preamble = Config.get('motions_export_preamble').value;
|
||||||
|
data.date = function () {
|
||||||
|
var today = new Date();
|
||||||
|
var d = today.getDate();
|
||||||
|
var m = today.getMonth()+1; //January is 0!
|
||||||
|
var y = today.getFullYear();
|
||||||
|
if (d<10) { d='0'+d; }
|
||||||
|
if (m<10) { m='0'+m; }
|
||||||
|
return d+'.'+m+'.'+y;
|
||||||
|
}();
|
||||||
|
data.pagebreak_main = motions.length === 0 ? '' : PAGEBREAK;
|
||||||
|
data.categories_translation = gettextCatalog.getString('Categories');
|
||||||
|
data.no_categories = gettextCatalog.getString('No categories available.');
|
||||||
|
data.no_motions = gettextCatalog.getString('No motions available.');
|
||||||
|
data.categories = getCategoriesData(categories);
|
||||||
|
data.motions_list = getMotionShortData(motions);
|
||||||
|
data.motions = getMotionFullData(motions);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getCategoriesData = function (categories) {
|
||||||
|
return _.map(categories, function (category) {
|
||||||
|
return {
|
||||||
|
prefix: category.prefix,
|
||||||
|
name: category.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getMotionShortData = function (motions) {
|
||||||
|
var translation = gettextCatalog.getString('Motion');
|
||||||
|
return _.map(motions, function (motion) {
|
||||||
|
return {
|
||||||
|
motion_translation: translation,
|
||||||
|
identifier: motion.identifier,
|
||||||
|
title: motion.getTitle(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getMotionFullData = function (motions) {
|
||||||
|
var translation = gettextCatalog.getString('Motion'),
|
||||||
|
submitters_translation = gettextCatalog.getString('Submitters'),
|
||||||
|
signature_translation = gettextCatalog.getString('Signature'),
|
||||||
|
status_translation = gettextCatalog.getString('Status'),
|
||||||
|
reason_translation = gettextCatalog.getString('Reason'),
|
||||||
|
data = _.map(motions, function (motion) {
|
||||||
|
return {
|
||||||
|
motion_translation: translation,
|
||||||
|
identifier: motion.identifier,
|
||||||
|
title: motion.getTitle(),
|
||||||
|
submitters_translation: submitters_translation,
|
||||||
|
submitters: _.map(motion.submitters, function (submitter) {
|
||||||
|
return submitter.get_full_name();
|
||||||
|
}).join(', '),
|
||||||
|
signature_translation: signature_translation,
|
||||||
|
status_translation: status_translation,
|
||||||
|
status: gettextCatalog.getString(motion.state.name),
|
||||||
|
text: html2docx(motion.getText()),
|
||||||
|
reason_translation: motion.getReason().length === 0 ? '' : reason_translation,
|
||||||
|
reason: html2docx(motion.getReason()),
|
||||||
|
pagebreak: PAGEBREAK,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (data.length) {
|
||||||
|
// clear pagebreak on last element
|
||||||
|
data[data.length - 1].pagebreak = '';
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
var html2docx = function (html) {
|
||||||
|
var docx = '';
|
||||||
|
var stack = [];
|
||||||
|
|
||||||
|
var isTag = false; // Even if html starts with '<p....' it is split to '', '<', ..., so always no tag at the beginning
|
||||||
|
var hasParagraph = true;
|
||||||
|
var skipFirstParagraphClosing = true;
|
||||||
|
if (html.substring(0,3) != '<p>') {
|
||||||
|
docx += '<w:p>';
|
||||||
|
skipFirstParagraphClosing = false;
|
||||||
|
}
|
||||||
|
html = html.split(/(<|>)/g);
|
||||||
|
|
||||||
|
html.forEach(function (part) {
|
||||||
|
if (part !== '' && part != '\n' && part != '<' && part != '>') {
|
||||||
|
if (isTag) {
|
||||||
|
if (part.startsWith('p')) { /** p **/
|
||||||
|
// Special: begin new paragraph (only if its the first):
|
||||||
|
if (hasParagraph && !skipFirstParagraphClosing) {
|
||||||
|
// End, if there is one
|
||||||
|
docx += '</w:p>';
|
||||||
|
}
|
||||||
|
skipFirstParagraphClosing = false;
|
||||||
|
docx += '<w:p>';
|
||||||
|
hasParagraph = true;
|
||||||
|
} else if (part.startsWith('/p')) {
|
||||||
|
// Special: end paragraph:
|
||||||
|
docx += '</w:p>';
|
||||||
|
hasParagraph = false;
|
||||||
|
|
||||||
|
} else if (part.charAt(0) == "/") {
|
||||||
|
// remove from stack
|
||||||
|
stack.pop();
|
||||||
|
} else { // now all other tags
|
||||||
|
var tag = {};
|
||||||
|
if (_.indexOf(TAGS_NO_PARAM, part) > -1) { /** b, strong, em, i **/
|
||||||
|
stack.push({tag: part});
|
||||||
|
} else if (part.startsWith('span')) { /** span **/
|
||||||
|
tag = {tag: 'span', attrs: {}};
|
||||||
|
var rStyle = /(?:\"|\;\s?)([a-zA-z\-]+)\:\s?([a-zA-Z0-9\-\#]+)/g, matchSpan;
|
||||||
|
while ((matchSpan = rStyle.exec(part)) !== null) {
|
||||||
|
switch (matchSpan[1]) {
|
||||||
|
case 'color':
|
||||||
|
tag.attrs.color = matchSpan[2].slice(1); // cut off the #
|
||||||
|
break;
|
||||||
|
case 'background-color':
|
||||||
|
tag.attrs.backgroundColor = matchSpan[2].slice(1); // cut off the #
|
||||||
|
break;
|
||||||
|
case 'text-decoration':
|
||||||
|
if (matchSpan[2] === 'underline') {
|
||||||
|
tag.attrs.underline = true;
|
||||||
|
} else if (matchSpan[2] === 'line-through') {
|
||||||
|
tag.attrs.strike = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stack.push(tag);
|
||||||
|
} else if (part.startsWith('a')) { /** a **/
|
||||||
|
var rHref = /href="([^"]+)"/g;
|
||||||
|
var href = rHref.exec(part)[1];
|
||||||
|
tag = {tag: 'a', href: href};
|
||||||
|
stack.push(tag);
|
||||||
|
} else if (part.startsWith('img')) {
|
||||||
|
// images has to be placed instantly, so there is no use of 'tag'.
|
||||||
|
var img = {}, rImg = /(\w+)=\"([^\"]*)\"/g, matchImg;
|
||||||
|
while ((matchImg = rImg.exec(part)) !== null) {
|
||||||
|
img[matchImg[1]] = matchImg[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// With and height and source have to be given!
|
||||||
|
if (img.width && img.height && img.src) {
|
||||||
|
var rrId = relationships.length + 1;
|
||||||
|
var imgId = images.length + 1;
|
||||||
|
|
||||||
|
// set name ('pic.jpg'), title, ext ('jpg'), mime ('image/jpeg')
|
||||||
|
img.name = img.src.split('/');
|
||||||
|
img.name = _.last(img.name);
|
||||||
|
|
||||||
|
var tmp = img.name.split('.');
|
||||||
|
// set name without extension as title if there isn't a title
|
||||||
|
if (!img.title) {
|
||||||
|
img.title = tmp[0];
|
||||||
|
}
|
||||||
|
img.ext = tmp[1];
|
||||||
|
|
||||||
|
img.mime = 'image/' + img.ext;
|
||||||
|
if (img.ext == 'jpe' || img.ext == 'jpg') {
|
||||||
|
img.mime = 'image/jpeg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// x and y for the container and picture size in EMU (assuming 96dpi)!
|
||||||
|
var x = img.width * 914400 / 96;
|
||||||
|
var y = img.height * 914400 / 96;
|
||||||
|
|
||||||
|
// Own paragraph for the image
|
||||||
|
if (hasParagraph) {
|
||||||
|
docx += '</w:p>';
|
||||||
|
}
|
||||||
|
docx += '<w:p><w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extend cx="' + x +'" cy="' + y + '"/><wp:effectExtent l="0" t="0" r="0" b="0"/>' +
|
||||||
|
'<wp:docPr id="' + imgId + '" name="' + img.name + '" title="' + img.title + '" descr="' + img.title + '"/><wp:cNvGraphicFramePr>' +
|
||||||
|
'<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>' +
|
||||||
|
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">' +
|
||||||
|
'<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="' + imgId + '" name="' +
|
||||||
|
img.name + '" title="' + img.title + '" descr="' + img.title + '"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rrId' + rrId + '"/><a:stretch>' +
|
||||||
|
'<a:fillRect/></a:stretch></pic:blipFill><pic:spPr bwMode="auto"><a:xfrm><a:off x="0" y="0"/><a:ext cx="' + x + '" cy="' + y + '"/></a:xfrm>' +
|
||||||
|
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>';
|
||||||
|
|
||||||
|
// hasParagraph stays untouched, the documents paragraph state is restored here
|
||||||
|
if (hasParagraph) {
|
||||||
|
docx += '<w:p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// entries in images, relationships and contentTypes
|
||||||
|
images.push({
|
||||||
|
url: img.src,
|
||||||
|
zipPath: 'word/media/' + img.name
|
||||||
|
});
|
||||||
|
relationships.push({
|
||||||
|
Id: 'rrId' + rrId,
|
||||||
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
|
||||||
|
Target: 'media/' + img.name
|
||||||
|
});
|
||||||
|
contentTypes.push({
|
||||||
|
PartName: '/word/media/' + img.name,
|
||||||
|
ContentType: img.mime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { /** No tag **/
|
||||||
|
if (!hasParagraph) {
|
||||||
|
docx += '<w:p>';
|
||||||
|
hasParagraph = true;
|
||||||
|
}
|
||||||
|
var docx_part = '<w:r><w:rPr>';
|
||||||
|
var hyperlink = false;
|
||||||
|
stack.forEach(function (tag) {
|
||||||
|
switch (tag.tag) {
|
||||||
|
case 'b': case 'strong':
|
||||||
|
docx_part += '<w:b/><w:bCs/>';
|
||||||
|
break;
|
||||||
|
case 'em': case 'i':
|
||||||
|
docx_part += '<w:i/><w:iCs/>';
|
||||||
|
break;
|
||||||
|
case 'span':
|
||||||
|
for (var key in tag.attrs) {
|
||||||
|
switch (key) {
|
||||||
|
case 'color':
|
||||||
|
docx_part += '<w:color w:val="' + tag.attrs[key] + '"/>';
|
||||||
|
break;
|
||||||
|
case 'backgroundColor':
|
||||||
|
docx_part += '<w:shd w:fill="' + tag.attrs[key] + '"/>';
|
||||||
|
break;
|
||||||
|
case 'underline':
|
||||||
|
docx_part += '<w:u w:val="single"/>';
|
||||||
|
break;
|
||||||
|
case 'strike':
|
||||||
|
docx_part += '<w:strike/>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
var id = relationships.length + 1;
|
||||||
|
docx_part = '<w:hyperlink r:id="rrId' + id + '">' + docx_part;
|
||||||
|
docx_part += '<w:rStyle w:val="Internetlink"/>'; // necessary?
|
||||||
|
relationships.push({
|
||||||
|
Id: 'rrId' + id,
|
||||||
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
|
||||||
|
Target: tag.href,
|
||||||
|
TargetMode: 'External'
|
||||||
|
});
|
||||||
|
hyperlink = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
docx_part += '</w:rPr><w:t>' + part + '</w:t></w:r>';
|
||||||
|
if (hyperlink) {
|
||||||
|
docx_part += '</w:hyperlink>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// append to docx
|
||||||
|
docx += docx_part;
|
||||||
|
}
|
||||||
|
isTag = !isTag;
|
||||||
|
}
|
||||||
|
if (part === '' || part == '\n') {
|
||||||
|
// just if two tags following eachother: <b><span> --> ...,'>', '', '<',...
|
||||||
|
// or there is a line break between: <b>\n<span> --> ...,'>', '\n', '<',...
|
||||||
|
isTag = !isTag;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// for finishing close the last paragraph (if open)
|
||||||
|
if (hasParagraph) {
|
||||||
|
docx += '</w:p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// replacing of special symbols:
|
||||||
|
docx = docx.replace(new RegExp('\ä\;', 'g'), 'ä');
|
||||||
|
docx = docx.replace(new RegExp('\ü\;', 'g'), 'ü');
|
||||||
|
docx = docx.replace(new RegExp('\ö\;', 'g'), 'ö');
|
||||||
|
docx = docx.replace(new RegExp('\Ä\;', 'g'), 'Ä');
|
||||||
|
docx = docx.replace(new RegExp('\Ü\;', 'g'), 'Ü');
|
||||||
|
docx = docx.replace(new RegExp('\Ö\;', 'g'), 'Ö');
|
||||||
|
docx = docx.replace(new RegExp('\ß\;', 'g'), 'ß');
|
||||||
|
docx = docx.replace(new RegExp('\ \;', 'g'), ' ');
|
||||||
|
docx = docx.replace(new RegExp('\§\;', 'g'), '§');
|
||||||
|
|
||||||
|
// remove all entities except gt, lt and amp
|
||||||
|
var rEntity = /\&(?!gt|lt|amp)\w+\;/g, matchEntry, indexes = [];
|
||||||
|
while ((matchEntry = rEntity.exec(docx)) !== null) {
|
||||||
|
indexes.push({
|
||||||
|
startId: matchEntry.index,
|
||||||
|
stopId: matchEntry.index + matchEntry[0].length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (var i = indexes.length - 1; i>=0; i--) {
|
||||||
|
docx = docx.substring(0, indexes[i].startId) + docx.substring(indexes[i].stopId, docx.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return docx;
|
||||||
|
};
|
||||||
|
var updateRelationships = function (oldContent) {
|
||||||
|
var content = oldContent.split('\n');
|
||||||
|
relationships.forEach(function (rel) {
|
||||||
|
content[1] += '<Relationship';
|
||||||
|
for (var key in rel) {
|
||||||
|
content[1] += ' ' + key + '="' + rel[key] + '"';
|
||||||
|
}
|
||||||
|
content[1] += '/>';
|
||||||
|
});
|
||||||
|
return content.join('\n');
|
||||||
|
};
|
||||||
|
var updateContentTypes = function (oldContent) {
|
||||||
|
var content = oldContent.split('\n');
|
||||||
|
contentTypes.forEach(function (type) {
|
||||||
|
content[1] += '<Override';
|
||||||
|
for (var key in type) {
|
||||||
|
content[1] += ' ' + key + '="' + type[key] + '"';
|
||||||
|
}
|
||||||
|
content[1] += '/>';
|
||||||
|
});
|
||||||
|
return content.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
export: function (motions, categories) {
|
||||||
|
images = [];
|
||||||
|
relationships = [];
|
||||||
|
contentTypes = [];
|
||||||
|
$http.get('/motions/docxtemplate/').then(function (success) {
|
||||||
|
var content = window.atob(success.data);
|
||||||
|
var doc = new Docxgen(content);
|
||||||
|
|
||||||
|
doc.setData(getData(motions, categories));
|
||||||
|
doc.render();
|
||||||
|
var zip = doc.getZip();
|
||||||
|
|
||||||
|
// update relationships from 'relationships'
|
||||||
|
var rels = updateRelationships(zip.file('word/_rels/document.xml.rels').asText());
|
||||||
|
zip.file('word/_rels/document.xml.rels', rels);
|
||||||
|
|
||||||
|
// update content type from 'contentTypes'
|
||||||
|
var contentTypes = updateContentTypes(zip.file('[Content_Types].xml').asText());
|
||||||
|
zip.file('[Content_Types].xml', contentTypes);
|
||||||
|
|
||||||
|
var imgPromises = [];
|
||||||
|
images.forEach(function (img) {
|
||||||
|
imgPromises.push(
|
||||||
|
$http.get(img.url, {responseType: 'arraybuffer'}).then(function (resolvedImage) {
|
||||||
|
zip.file(img.zipPath, resolvedImage.data);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// wait for all images to be resolved
|
||||||
|
$q.all(imgPromises).then(function () {
|
||||||
|
var out = zip.generate({type: 'blob'});
|
||||||
|
FileSaver.saveAs(out, 'motions-export.docx');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
}());
|
@ -831,7 +831,8 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
|||||||
'Workflow',
|
'Workflow',
|
||||||
'User',
|
'User',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda) {
|
'MotionDocxExport',
|
||||||
|
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda, MotionDocxExport) {
|
||||||
Motion.bindAll({}, $scope, 'motions');
|
Motion.bindAll({}, $scope, 'motions');
|
||||||
Category.bindAll({}, $scope, 'categories');
|
Category.bindAll({}, $scope, 'categories');
|
||||||
Tag.bindAll({}, $scope, 'tags');
|
Tag.bindAll({}, $scope, 'tags');
|
||||||
@ -989,9 +990,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
|||||||
ngDialog.open(MotionForm.getDialog(motion));
|
ngDialog.open(MotionForm.getDialog(motion));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export the given motions as a csv file
|
// Export as a csv file
|
||||||
$scope.csv_export = function () {
|
$scope.csv_export = function () {
|
||||||
var element = document.getElementById('downloadLink');
|
var element = document.getElementById('downloadLinkCSV');
|
||||||
var csvRows = [
|
var csvRows = [
|
||||||
['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin'],
|
['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin'],
|
||||||
];
|
];
|
||||||
@ -1013,6 +1014,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
|||||||
element.download = 'motions-export.csv';
|
element.download = 'motions-export.csv';
|
||||||
element.target = '_blank';
|
element.target = '_blank';
|
||||||
};
|
};
|
||||||
|
// Export as docx file
|
||||||
|
$scope.docx_export = function () {
|
||||||
|
MotionDocxExport.export($scope.motionsFiltered, $scope.categories);
|
||||||
|
};
|
||||||
|
|
||||||
// *** delete mode functions ***
|
// *** delete mode functions ***
|
||||||
$scope.isDeleteMode = false;
|
$scope.isDeleteMode = false;
|
||||||
|
BIN
openslides/motions/static/templates/docx/motions.docx
Normal file
BIN
openslides/motions/static/templates/docx/motions.docx
Normal file
Binary file not shown.
@ -45,6 +45,7 @@
|
|||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
|
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
|
||||||
|
<!-- PDF export -->
|
||||||
<li>
|
<li>
|
||||||
<a ui-sref="motions_pdf" target="_blank">
|
<a ui-sref="motions_pdf" target="_blank">
|
||||||
<i class="fa fa-file-pdf-o fa-lg"></i>
|
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||||
@ -53,13 +54,19 @@
|
|||||||
</li>
|
</li>
|
||||||
<!--CSV export -->
|
<!--CSV export -->
|
||||||
<li>
|
<li>
|
||||||
<a href="" id="downloadLink"
|
<a href="" id="downloadLinkCSV"
|
||||||
os-perms="motions.can_manage"
|
|
||||||
ng-click="csv_export()">
|
ng-click="csv_export()">
|
||||||
<i class="fa fa-file-text-o fa-lg"></i>
|
<i class="fa fa-file-text-o fa-lg"></i>
|
||||||
<translate>CSV</translate>
|
<translate>CSV</translate>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<!--DOCX export -->
|
||||||
|
<li>
|
||||||
|
<a href="" ng-click="docx_export()">
|
||||||
|
<i class="fa fa-file-word-o fa-lg"></i>
|
||||||
|
<translate>DOCX</translate>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,10 @@ from django.conf.urls import url
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
url(r'^docxtemplate/$',
|
||||||
|
views.MotionDocxTemplateView.as_view(),
|
||||||
|
name='motions_docx_template'),
|
||||||
|
|
||||||
url(r'^pdf/$',
|
url(r'^pdf/$',
|
||||||
views.MotionPDFView.as_view(print_all_motions=True),
|
views.MotionPDFView.as_view(print_all_motions=True),
|
||||||
name='motions_pdf'),
|
name='motions_pdf'),
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
@ -16,7 +19,7 @@ from openslides.utils.rest_api import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
from openslides.utils.views import PDFView, SingleObjectMixin
|
from openslides.utils.views import APIView, PDFView, SingleObjectMixin
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
@ -462,7 +465,7 @@ class WorkflowViewSet(ModelViewSet):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Views to generate PDFs
|
# Views to generate PDFs and for the DOCX template
|
||||||
|
|
||||||
class MotionPollPDF(PDFView):
|
class MotionPollPDF(PDFView):
|
||||||
"""
|
"""
|
||||||
@ -555,3 +558,15 @@ class MotionPDFView(SingleObjectMixin, PDFView):
|
|||||||
motions_to_pdf(pdf, motions)
|
motions_to_pdf(pdf, motions)
|
||||||
else:
|
else:
|
||||||
motion_to_pdf(pdf, self.get_object())
|
motion_to_pdf(pdf, self.get_object())
|
||||||
|
|
||||||
|
|
||||||
|
class MotionDocxTemplateView(APIView):
|
||||||
|
"""
|
||||||
|
Returns the template for motions docx export
|
||||||
|
"""
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
with open(finders.find('templates/docx/motions.docx'), "rb") as file:
|
||||||
|
response = base64.b64encode(file.read())
|
||||||
|
return response
|
||||||
|
Loading…
Reference in New Issue
Block a user