From 448756f17a6b07230cb1a24940988940fc6e17c2 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Tue, 13 Sep 2016 11:54:30 +0200 Subject: [PATCH] Docx export with docxtemplater --- bower.json | 2 + openslides/core/static/js/core/site.js | 1 + openslides/motions/config_variables.py | 16 +- openslides/motions/pdf.py | 4 +- openslides/motions/static/js/motions/base.js | 3 +- openslides/motions/static/js/motions/docx.js | 386 ++++++++++++++++++ openslides/motions/static/js/motions/site.js | 11 +- .../static/templates/docx/motions.docx | Bin 0 -> 7927 bytes .../static/templates/motions/motion-list.html | 11 +- openslides/motions/urls.py | 4 + openslides/motions/views.py | 19 +- 11 files changed, 439 insertions(+), 18 deletions(-) create mode 100644 openslides/motions/static/js/motions/docx.js create mode 100644 openslides/motions/static/templates/docx/motions.docx diff --git a/bower.json b/bower.json index 911996f26..2fd28f98c 100644 --- a/bower.json +++ b/bower.json @@ -9,6 +9,7 @@ "angular-bootstrap-colorpicker": "~3.0.25", "angular-chosen-localytics": "~1.5.0", "angular-csv-import": "~0.0.36", + "angular-file-saver": "~1.1.2", "angular-formly": "~8.4.0", "angular-formly-templates-bootstrap": "~6.2.0", "angular-gettext": "~2.3.7", @@ -21,6 +22,7 @@ "angular-ui-tinymce": "~0.0.17", "angular-ui-tree": "~2.22.0", "bootstrap-css-only": "~3.3.6", + "docxtemplater": "~2.1.5", "font-awesome-bower": "~4.5.0", "jquery.cookie": "~1.4.1", "js-data": "~2.9.0", diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index d768045b9..ab615b680 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -13,6 +13,7 @@ angular.module('OpenSlidesApp.core.site', [ 'localytics.directives', 'ngBootbox', 'ngDialog', + 'ngFileSaver', 'ngMessages', 'ngCsvImport', 'ui.tinymce', diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py index 1b1dfe849..bc7590f67 100644 --- a/openslides/motions/config_variables.py +++ b/openslides/motions/config_variables.py @@ -211,24 +211,24 @@ def get_config_variables(): subgroup='Voting and ballot papers', validators=(MinValueValidator(1),)) - # PDF + # PDF and DOCX export yield ConfigVariable( - name='motions_pdf_title', + name='motions_export_title', default_value='Motions', - label='Title for PDF document (all motions)', + label='Title for PDF and DOCX documents (all motions)', weight=370, group='Motions', - subgroup='PDF', + subgroup='Export', translatable=True) yield ConfigVariable( - name='motions_pdf_preamble', + name='motions_export_preamble', default_value='', - label='Preamble text for PDF document (all motions)', + label='Preamble text for PDF and DOCX documents (all motions)', weight=375, group='Motions', - subgroup='PDF') + subgroup='Export') yield ConfigVariable( name='motions_pdf_paragraph_numbering', @@ -237,4 +237,4 @@ def get_config_variables(): label='Show paragraph numbering (only in PDF)', weight=380, group='Motions', - subgroup='PDF') + subgroup='Export') diff --git a/openslides/motions/pdf.py b/openslides/motions/pdf.py index 744509e64..fbbf224c3 100644 --- a/openslides/motions/pdf.py +++ b/openslides/motions/pdf.py @@ -308,9 +308,9 @@ def all_motion_cover(pdf, 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: pdf.append(Paragraph("%s" % preamble.replace('\r\n', '
'), stylesheet['Paragraph'])) diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index 46c7134e9..a93f73be6 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -5,7 +5,8 @@ angular.module('OpenSlidesApp.motions', [ 'OpenSlidesApp.users', 'OpenSlidesApp.motions.lineNumbering', - 'OpenSlidesApp.motions.diff' + 'OpenSlidesApp.motions.diff', + 'OpenSlidesApp.motions.DOCX' ]) .factory('WorkflowState', [ diff --git a/openslides/motions/static/js/motions/docx.js b/openslides/motions/static/js/motions/docx.js new file mode 100644 index 000000000..d9f7d4da7 --- /dev/null +++ b/openslides/motions/static/js/motions/docx.js @@ -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 = ''; + 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 ')/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 += ''; + } + skipFirstParagraphClosing = false; + docx += ''; + hasParagraph = true; + } else if (part.startsWith('/p')) { + // Special: end paragraph: + docx += ''; + 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 += ''; + } + docx += '' + + '' + + '' + + '' + + '' + + '' + + ''; + + // hasParagraph stays untouched, the documents paragraph state is restored here + if (hasParagraph) { + docx += ''; + } + + // 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 += ''; + hasParagraph = true; + } + var docx_part = ''; + var hyperlink = false; + stack.forEach(function (tag) { + switch (tag.tag) { + case 'b': case 'strong': + docx_part += ''; + break; + case 'em': case 'i': + docx_part += ''; + break; + case 'span': + for (var key in tag.attrs) { + switch (key) { + case 'color': + docx_part += ''; + break; + case 'backgroundColor': + docx_part += ''; + break; + case 'underline': + docx_part += ''; + break; + case 'strike': + docx_part += ''; + break; + } + } + break; + case 'a': + var id = relationships.length + 1; + docx_part = '' + docx_part; + docx_part += ''; // 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 += '' + part + ''; + if (hyperlink) { + docx_part += ''; + } + + // append to docx + docx += docx_part; + } + isTag = !isTag; + } + if (part === '' || part == '\n') { + // just if two tags following eachother: --> ...,'>', '', '<',... + // or there is a line break between: \n --> ...,'>', '\n', '<',... + isTag = !isTag; + } + }); + + // for finishing close the last paragraph (if open) + if (hasParagraph) { + docx += ''; + } + + // 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] += 'U4la(XmAUX!6k&?f#6OcxI-Yo zgY3WmvisiK>T~*ZpSsoZ)vc;~>r;_OL;}L0qN2iGm~hF$JrIn$YeQ#qI|v)={kc3r zQ65ngGi3i7Uwm)g?pfHUp)+bRF!d=)P}L~h?lkK+TfG5evL zT&2v&NYSoRYf&fm(PnlV_gNt}N~3YMW-Z~kuX1U6Or;A!dB(W1v0*(5J0;v4PQ@8i z7RqXU&rPUni{hQ;8_rYxICFD2CMIn@>9!!$U&;L9jy{peyBeYt0Sz9{AX2H)^mF zIhfZqO?r}}B9DT)IRZ~JefPgY2yk#J|1TnBcO%@u&Sq?Xz+whAb+I>hfU>&V+x-Wk z!H7|5a%3ShsgpHxlQtFy{4s?pW-pdQKz$64}91~)(a zuk#+mco1r6kwc`WoB7ILi`VP=is~>QQIgU2JbSyXZ*&-qh9E$FKIoxBZbOW_J<^@n z>&e0x)4HxynrIhaIR@>z&PM?%XWJQK99#(?YU-1vr9<- z(4~d{#vd-+Zn&xiSrJU)xy13ys%5Rnc&^Q{vY#~&Ak5Z>P7HQ-1={{0ROBE)~g3+GR~Am&i0wSy((9w|xX0i~`N zm>-Yvvn|{O>z_z$$okui9WkrsH;oIG7VRs2&b(WA;=1pR|E1f?|9KQoD_exc)$ADUJRQ7kA#=ngfW z91!KO9WgZJV(DLm+S;}TxOSdJJU66Sdabf6iDxiHrP!}}A#W&xC2|i$f4+nK=X)UF z{|yM-KY_3SJ3uvzP3+9?k?2WO1b4FEMbs6(>K;vS7xzmbH`;C;hjsA0;`W%r&*GJ& z7{4}^7h=qb4QSQ6xPrb0ZA!i4kISbXk;~e03X{=hzGbvy928g`=vmzrvRvy($}Q`N z0>@uKow@eLK9eXj>+vPI5?8{&YE;F=s)$&2fls@cwz)5=Tx>^mw0MUIWPqi%bW*a; z^xQICkIm_sAdHiFUQ5$ThS)sNY13j zZ)zg!dVAwt!{+4nf{Ok90_1&o(F-q3Hz0TB?kz;6cF8}CN zO>XBFXNj}PPC>ete2GPH0^`KYg7Ag`I}xpcQmW(Z?1k!v6u*4(S5B_0k}pDO=FA1B z<#ujIB*WsxoCND3>OpH?P@2@6Sv^TvplB^(Q#KbKd zV=@~7sJjX`zn1hAGL@kQ&UvMpB@`|65o2<@o)cA*3 z^S$={7AcM_s`>aUCDeIq&se4wet1lb+I~FVyVRS5mCo9ESJWHOd%7%7Rey0Y7S{tv zevbe~+FZPH5qNK6RNJ#Xg^@5mc_O9PaxyHNr3$uhKXUB1aOSVJR!R7F^f{{mjF(ku z{cMSwD>_lpZ%&~wLB77B;Zkt#9msPK_-%CGfH8kqPiaAGwu zEMt_q3tr9%p2@+8r$O=7oV1v8rcOWP64VIB&rz@m?Kn}Bj6)VfRVWEc}P8ZR7RtK zo;)0*QPiG1#P&QyVB{}&q-I3uS8`a?nP%JKZvVEv3&|H_&wZgpNIK*FfVZvV| z+@HHK;jy`p09;6*FMtd*cZSGyErO` zc-YAm&IKYaHrM}BbYW6;V=gDAF4GGwh5-D6Kwtu@pff7$8EL?bG{}ll)QWOB*9YmX zPf*|)PS6?7m?wOr(d$31|2F6&iUc4=8U&&c1yL+7xnnN7M+L5O1g&yl$MgYW`tB6s zAd1yL6m>@!tk=S5eSTx(R+w*nq5{u2g3dUwXZip$eRuB^x1wCl^+9;+6A*ZY7IcP& zJwp$ep$A#zys*kydZ-vvqI7)!7(0d@5JL|FWxoJrFDpBz<3*zCn+2(Ec3E+GrCBJZi|U)r%;=K!!nGVCY{xcY^XkVh zlST5rw*5r98Sgq69mnYlB^ea+t0)P>AH1^Dh) z8g16b(&otP@i_O^Z+DL?>{O3`bOne>ccY@_LVm2bxxveB;KuePo>Y6r5lmfA>AQ*? zUJe%GTlGY(%MpMw-Vt%gzU*1AX>arUD9qpWNr3tpb&?tK0%zo6=Iw!J7GJWc5wm>- z`W$hZ?o3t6RAP#Hep%@}+DrS2t#tKCKS!`J`W&d}nD$5XQ#md)+j>qGCwA@u+H^g} znRnXEWG{c+|7GUOwa8%JI$JfV^XRXENi*=PKzmy{GDb$Fc8i_ z`N6*ZL&;RzFqX&UO%aiT5?A~=Wt-rzjE%F=S6CTFlG4UX!Y@5@jf>cVHpIcs1d87Z^FZgEeLvJ+vj#|4+RYe z&Z37*{)`AixGjWU)2{|BceD8xb_E#KM?!jM>F&xRBHPrV7|4RwX-7>Kg&gpdjTD(R z&+DvUHAEEW1s#&e9Qwfz)6KbjU!eL_J58vYE13A5dkyxo^C`?c5<|okjQ|mJKd*cwl;6t z-*C&{tyi`8FTvG+c9;v%{g%>ntRpZ;Fipl;FCvIZGXlR;M5C>UsY8;NVY+5_IsX2H ze@?mMwf!&AC+JR@g~!ZU;@rT_euF`RqGREwn%|Ea6p|E{y#iZGgDu3v3Wa4F%SsS~QXl$wuK z24u;mRVHZ{zZRYvzIa}Ww8AC$jwg$)w@ztzC_t>HcCxqmgF^i$3}oK8Z2-Ow>;tRS z-aFB6mGUlw!KfK%9_;bmX=WR?ao%jwyIz`IO-OIN>{AcrdEjUL}zKMpcmi)3j(RnR2DEhvlSyIIJnWsmv3u-+rH_5FDKQ;`r7 zB2c=(=8@X9;a6!%*qc;Z0(lLriA1um8b@oeC&ZaoTT07(ZNVaeK7fxV1lQ4fOOtR? z%LLI&2C-rFWh^}e)8#H@J~PIRJ2|%)_vz^?*EQ4_2@Z}K^WW0be|J|8_o2JGy6Uq6 z0AG=|&Q;++2o1eE$&6B(tGiXGHE5%nSSQ84K4mR_Er`3zuhOjF3Dutj!5a6Ac$7m+ zPOE--dGhtyJdbP^+6n@f0CcvG<(nw51~BfvdE<8a!)i%D41pvdNtGLFDbu(=bmJj- zD*gx(Pz+5%V)EvNLX>>q@g!b@sEM4PYSYF(p%IRx#*BwGx*}0FhbN`&d$^8p3;3BaNXzkz!1Q%6c07N_so#kbM=CnhJb3VyLGm`me*rjTOx870DXWGW|ifCdW^-KWvb zIj(vF1cIRxo@c(S9!|vlIuV&ABJe*(u1hVmnzJ2nh+m^8*vN56E;_SduO&1!SZump zvFM}5QOo1c%0)8;$WS}T1(P`d50#_j-p(R-yKiRaQ!d@k5yLZ`6t5xTKdbNAH2Qg- z&;H3b*ndtoF2~|cT4l9{X~}0bT^QTfC>DSWkSivhNWLN{ zeWUm3*iP#0+2o6zd5-THpVKq6+I>lcAJ zkVUG3e02MSLTIJ#^b-zj$kgTJk*%~lK5|QS%>JzUgzkIl$EYc|hqGQxi=HwZI$%1? zJ-elnr{nJKtIGxIOQ{HHkh)uJVc*Tl!;9Y4@;>?XeH5JG$nc#$(=`DCgP=5_Dde4o z0$x3nmbx*+W!WE8WwD*E+qjr5=x5i${>hu3`FgKvpQd~D!Y^`x5KQP~Pnft)6q9b= z8%2}yIyHxnUs2MQ;tfAdS{P0^CyWU`i_YJlUEf4uMh1G+tZV-amCj~q3q3I-LVlkV zO}aSi=!1?_@FN{KJq~6NBU3Fd;gnvXig=MN_ib4iJCwBzb+@Fzf%R0ap1-BeazoDf z`4~gdCC`QWb=K>Iiktn%&4D#tgt?FB>%F#==2s>>Vph#cQm3E}Iw{QKVoEK|01nNM zmSBSpOEpfl(e-oz_4fSc#6N4#3zb(i9G`+6+1?Ozj3I;VK))ZhHx&q|W zl4VR+ivn?9Q#bC@8CqniR5)R^ijas*_x-v>hnys~%4Dg7&#}OdTLW3Oi}xw3t^zI; zdY7_pxc)6={kIR|;9_rL?tIr0`n{%lHSdtYez)&dmuLmUCjtnPH4+QuW_x(8=Me!W zxU{!VW@&d>&jGWfJ0@&Z=hGNF_%@hmBlQ%D`C_ycWJ7K-yZc-M!u0(;>sTH6SS!@V z+;BrpSW;n9c4;Fo;ac?!vwj-Un#sJOvH&745uiEp`?*wSjYcs8w0voJknwBn;U?7z zxdw1YN#a#9Yk{i4JyD4&rP8f64N+|BL)5fE5wueQ4ZAwD3JeTI#_*p&S;>&hc-j?( zD(!5^s4v=N^f*kR`bgkfN1US+Nn%kAf&u3s@e=n)mxPFUJJRt>(tr*^%x6A$aFTSugh8Fq*#;f@mA*g(+Q~!;$cql#!+Ve#qGAx+Rz){E7*s& zSXa%cl`)~~?qktXSkmf{GzzO@!Ck@OjgFfq78_#Yx`i5y3_NQ89J+UbBd=#VPZVL~yEgt7|b)a|AF#Njy z#sQ>=jYi)%kY(CAqw@l_+G~J94F&wj%C;KW(%=QMWnW_Dta%Ckkhge^w_0DcoTxFi zeRth@=sJGTZx&*0$Qg(c&6ggcd;~kaN+3=$kqU+(g6pt>4J1CF{4#(bZbr5^xsWqc zE0O4;CoLHEc-oVb;@fCY<>LF?wxXZ!7H2L_Qt-O%(lf9F|*64K9j zi%B23=W_La7JZKh-jWNRyzLof&?R}LJ#(DHe&J5U9n=&X+H}hP{;VzIC;5F4^=nZd zBj0sYVKo01MEBd0yOOWs40eRD89O@Ohfr4hphG7+AZXtw;?s&8*%%VQIqEV$J1*jD z-ARa^m#iLLgE=V@c(FjndI+|=Q`V|eIej_j){o{rGO1yfVnGW}Q$AK$^2zI(Q$K-L z!Z>NO%s)c!NgPj`sV0b9eQ03Hf!%sF z3Z8YgIRt$JRmwlT8D7SrdxG(Xu(a&s9-;77q&S5o&q)Oy4k+Kx+&+KPW**THI7JEc za;z9vlX#A{xXJdovW>V-!O}YZdtx+i-|-EEe{nd{jJv9j&pR**nsPe5c33RPGe}In zI;F`C0J(yZXLTwO*f^!Y_+B=_gq$m}2b_H{DbKZCsNG^_To}VX;c7{Ips@kBK_da) zfbH57Rm}76zgxDuH^h?+cl|-+f9>_(ude=!vMJd4cgHqbO<(!0!S8df#q6`#44-G) z5YKQ#8y-#+4X%epZB6pn8z1cN^75*99PDO$DGguSaL&F7{(xvNL8lHxraF326M>Od zv}s;{0FJk0PR=Aq4g!+5=jym7vrB3hEX!)4*WnvIx1_}~W|Abe#qXG^!b znw^D?1zK||2^dI&N54Kc6=^mI=0Gc={pg{5PYJMe1>}~P(LfT9lO$2rn^z4yqB&WG zH@G;EH`}oFI-_CmY!xqc*Mj7AWO)@b^oQ`TW2##4eK7Dpa@VuEbJN_vx(V;jO}gTD zUG+O2#!$n<(Hx?G9}lpkJCcDNAbEZr@kuwS(cB*(H-n5TznNDSZ>vlac|jGcJJV)4 zCmjUG6Yz*Po_ZLz(OsiH)KECc3s4c%t(N7mAlCko)bKt+h>hY%mp3EkbI%Z>1-EUr ziDJ}E3?vDQ$Upel$hyF_BTS~!xCa(jX@dOA^Lgv?*fc8dcMg{->z@*hx%g%foRxfb z6^^aFd3{kzLj4_YVGB#u@|-wTf((J<1Tb4^*rYH_z;oWJ8E{EAbQsYi-{ z&wT^73L>(GaESW#LR`w}pkpP2DD?30mzx+$gD`X+Q9_gH&I4?Hms{a(eGI>9jH?9$ zZ$vTCRLo%!;Q5hAHq02g8K0(Z_q58kHt0qFvSjlX1Q(ctvJsEBV}d-Trsn^-ze_yq z@|Wo5)L5|LT=4j{@{h`H)&6sl!5@P69@yf~E)%`;fa&i>Iy?dp?k}S7Az^k;6#iWv z(1t4Ve>Z%{9NqJ2e+lm0tNx8$`{$D$QZM(>`j?2^O_|?MddNHd)BGV#aKA43OXTlT z@Ne_Ku{Hm+f7nC1ClmgXg}ZgoAFux}weU~lhdaFceEyff-=X>2_(9X@pVki-TYvxa z+`BLNZT)ah_)qhPW#;dHt`7W<`QO%O|FnOYK>z;nse})b=6_1lKdm3e_1{1KhUkIy npDFjB<_{<3eenM!1tkAxDo~L}esn(w diff --git a/openslides/motions/urls.py b/openslides/motions/urls.py index 9b37d60d8..d2359a1a2 100644 --- a/openslides/motions/urls.py +++ b/openslides/motions/urls.py @@ -3,6 +3,10 @@ from django.conf.urls import url from . import views urlpatterns = [ + url(r'^docxtemplate/$', + views.MotionDocxTemplateView.as_view(), + name='motions_docx_template'), + url(r'^pdf/$', views.MotionPDFView.as_view(print_all_motions=True), name='motions_pdf'), diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 356241676..7678ee99d 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -1,3 +1,6 @@ +import base64 + +from django.contrib.staticfiles import finders from django.db import IntegrityError, transaction from django.http import Http404 from django.utils.text import slugify @@ -16,7 +19,7 @@ from openslides.utils.rest_api import ( ValidationError, detail_route, ) -from openslides.utils.views import PDFView, SingleObjectMixin +from openslides.utils.views import APIView, PDFView, SingleObjectMixin from .access_permissions import ( CategoryAccessPermissions, @@ -462,7 +465,7 @@ class WorkflowViewSet(ModelViewSet): return result -# Views to generate PDFs +# Views to generate PDFs and for the DOCX template class MotionPollPDF(PDFView): """ @@ -555,3 +558,15 @@ class MotionPDFView(SingleObjectMixin, PDFView): motions_to_pdf(pdf, motions) else: 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