commit
fac2b694d0
@ -22,6 +22,7 @@ Core:
|
|||||||
Motions:
|
Motions:
|
||||||
- Added origin field.
|
- Added origin field.
|
||||||
- Added button to sort and number all motions in a category.
|
- Added button to sort and number all motions in a category.
|
||||||
|
- Introduced pdfMake for clientside generation of PDFs.
|
||||||
|
|
||||||
Users:
|
Users:
|
||||||
- Added field is_committee and new default group Committees.
|
- Added field is_committee and new default group Committees.
|
||||||
|
@ -199,6 +199,7 @@ OpenSlides uses the following projects or parts of them:
|
|||||||
* `angular-gettext <http://angular-gettext.rocketeer.be/>`_, License: MIT
|
* `angular-gettext <http://angular-gettext.rocketeer.be/>`_, License: MIT
|
||||||
* `angular-loading-bar <https://chieffancypants.github.io/angular-loading-bar>`_, License: MIT
|
* `angular-loading-bar <https://chieffancypants.github.io/angular-loading-bar>`_, License: MIT
|
||||||
* `angular-messages <http://angularjs.org>`_, License: MIT
|
* `angular-messages <http://angularjs.org>`_, License: MIT
|
||||||
|
* `pdfmake <http://pdfmake.org>`_, License: MIT
|
||||||
* `angular-pdf <http://github.com/sayanee/angularjs-pdf>`_, License: MIT
|
* `angular-pdf <http://github.com/sayanee/angularjs-pdf>`_, License: MIT
|
||||||
* `angular-sanitize <http://angularjs.org>`_, License: MIT
|
* `angular-sanitize <http://angularjs.org>`_, License: MIT
|
||||||
* `angular-scroll-glue <https://github.com/Luegg/angularjs-scroll-glue>`_, License: MIT
|
* `angular-scroll-glue <https://github.com/Luegg/angularjs-scroll-glue>`_, License: MIT
|
||||||
|
@ -29,12 +29,16 @@
|
|||||||
"ng-dialog": "~0.5.6",
|
"ng-dialog": "~0.5.6",
|
||||||
"ng-file-upload": "~11.2.3",
|
"ng-file-upload": "~11.2.3",
|
||||||
"ngBootbox": "~0.1.3",
|
"ngBootbox": "~0.1.3",
|
||||||
|
"pdfmake": "~0.1.17",
|
||||||
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
|
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
|
||||||
"roboto-condensed": "~0.3.0",
|
"roboto-condensed": "~0.3.0",
|
||||||
"tinymce-dist": "4.3.12",
|
"tinymce-dist": "4.3.12",
|
||||||
"tinymce-i18n": "OpenSlides/tinymce-i18n#a186ad61e0aa30fdf657e88f405f966d790f0805"
|
"tinymce-i18n": "OpenSlides/tinymce-i18n#a186ad61e0aa30fdf657e88f405f966d790f0805"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
"pdfmake-dist": {
|
||||||
|
"main": "build/pdfmake.min.js"
|
||||||
|
},
|
||||||
"pdfjs-dist": {
|
"pdfjs-dist": {
|
||||||
"main": "build/pdf.combined.js"
|
"main": "build/pdf.combined.js"
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,410 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
'ui.tinymce',
|
'ui.tinymce',
|
||||||
'luegg.directives',
|
'luegg.directives',
|
||||||
])
|
])
|
||||||
|
.factory('PdfMakeDocumentProvider', function() {
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
/**
|
||||||
|
* Generates header for PDF
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
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: 'OpenSlides | Presentation and assembly system',
|
||||||
|
fontSize:10,
|
||||||
|
width: '70%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fontSize: 6,
|
||||||
|
width: '30%',
|
||||||
|
text: 'Stand: ' + 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: "Seite: " + currentPage.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: 8,
|
||||||
|
header: header,
|
||||||
|
footer: footer,
|
||||||
|
content: content,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
getDocument: getDocument
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
createInstance: createInstance
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.factory('PdfMakeConverter', function() {
|
||||||
|
/**
|
||||||
|
* 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 slice = Function.prototype.call.bind([].slice),
|
||||||
|
map = Function.prototype.call.bind([].map),
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param {object} html - html
|
||||||
|
*/
|
||||||
|
convertHTML = function(html) {
|
||||||
|
var elementStyles = {
|
||||||
|
"b": ["font-weight:bold"],
|
||||||
|
"strong": ["font-weight:bold"],
|
||||||
|
"u": ["text-decoration:underline"],
|
||||||
|
"em": ["font-style:italic"],
|
||||||
|
"i": ["font-style:italic"],
|
||||||
|
"h1": ["font-size:30"],
|
||||||
|
"h2": ["font-size:28"],
|
||||||
|
"h3": ["font-size:26"],
|
||||||
|
"h4": ["font-size:24"],
|
||||||
|
"h5": ["font-size:22"],
|
||||||
|
"h6": ["font-size:20"],
|
||||||
|
"a": ["color:blue", "text-decoration:underline"]
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Parses Children of the current paragraph
|
||||||
|
* @function
|
||||||
|
* @param {object} converted -
|
||||||
|
* @param {object} element -
|
||||||
|
* @param {object} currentParagraph -
|
||||||
|
* @param {object} styles -
|
||||||
|
*/
|
||||||
|
parseChildren = function(converted, element, currentParagraph, styles) {
|
||||||
|
var elements = [];
|
||||||
|
var children = element.childNodes;
|
||||||
|
if (children.length !== 0) {
|
||||||
|
_.forEach(children, function(child) {
|
||||||
|
currentParagraph = ParseElement(elements, child, currentParagraph, styles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (elements.length !== 0) {
|
||||||
|
_.forEach(elements, function(el) {
|
||||||
|
converted.push(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return currentParagraph;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Extracts the style from an object
|
||||||
|
* @function
|
||||||
|
* @param {object} o - the current object
|
||||||
|
* @param {object} styles - an array with styles
|
||||||
|
*/
|
||||||
|
ComputeStyle = function(o, styles) {
|
||||||
|
styles.forEach(function(singleStyle) {
|
||||||
|
var styleDefinition = singleStyle.trim().toLowerCase().split(":");
|
||||||
|
var style = styleDefinition[0];
|
||||||
|
var value = styleDefinition[1];
|
||||||
|
if (styleDefinition.length == 2) {
|
||||||
|
switch (style) {
|
||||||
|
case "padding-left":
|
||||||
|
o.margin = [parseInt(value), 0, 0, 0];
|
||||||
|
break;
|
||||||
|
case "font-size":
|
||||||
|
o.fontSize = parseInt(value);
|
||||||
|
break;
|
||||||
|
case "text-align":
|
||||||
|
switch (value) {
|
||||||
|
case "right":
|
||||||
|
case "center":
|
||||||
|
case "justify":
|
||||||
|
o.alignment = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "font-weight":
|
||||||
|
switch (value) {
|
||||||
|
case "bold":
|
||||||
|
o.bold = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "text-decoration":
|
||||||
|
switch (value) {
|
||||||
|
case "underline":
|
||||||
|
o.decoration = "underline";
|
||||||
|
break;
|
||||||
|
case "line-through":
|
||||||
|
o.decoration = "lineThrough";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "font-style":
|
||||||
|
switch (value) {
|
||||||
|
case "italic":
|
||||||
|
o.italics = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "color":
|
||||||
|
o.color = value;
|
||||||
|
break;
|
||||||
|
case "background-color":
|
||||||
|
o.background = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Parses a single HTML element
|
||||||
|
* @function
|
||||||
|
* @param {object} alreadyConverted -
|
||||||
|
* @param {object} element -
|
||||||
|
* @param {object} currentParagraph -
|
||||||
|
* @param {object} styles -
|
||||||
|
*/
|
||||||
|
ParseElement = function(alreadyConverted, element, currentParagraph, styles) {
|
||||||
|
styles = styles || [];
|
||||||
|
if (element.getAttribute) {
|
||||||
|
var nodeStyle = element.getAttribute("style");
|
||||||
|
if (nodeStyle) {
|
||||||
|
nodeStyle.split(";").forEach(function(nodeStyle) {
|
||||||
|
var tmp = nodeStyle.replace(/\s/g, '');
|
||||||
|
styles.push(tmp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var nodeName = element.nodeName.toLowerCase();
|
||||||
|
switch (nodeName) {
|
||||||
|
case "h1":
|
||||||
|
case "h2":
|
||||||
|
case "h3":
|
||||||
|
case "h4":
|
||||||
|
case "h5":
|
||||||
|
case "h6":
|
||||||
|
currentParagraph = create("text");
|
||||||
|
/* falls through */
|
||||||
|
case "a":
|
||||||
|
parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]));
|
||||||
|
alreadyConverted.push(currentParagraph);
|
||||||
|
break;
|
||||||
|
case "b":
|
||||||
|
case "strong":
|
||||||
|
case "u":
|
||||||
|
case "em":
|
||||||
|
case "i":
|
||||||
|
parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]));
|
||||||
|
break;
|
||||||
|
case "table":
|
||||||
|
var t = create("table", {
|
||||||
|
widths: [],
|
||||||
|
body: []
|
||||||
|
});
|
||||||
|
var border = element.getAttribute("border");
|
||||||
|
var isBorder = false;
|
||||||
|
if (border)
|
||||||
|
if (parseInt(border) == 1) isBorder = true;
|
||||||
|
if (!isBorder) t.layout = 'noBorders';
|
||||||
|
parseChildren(t.table.body, element, currentParagraph, styles);
|
||||||
|
var widths = element.getAttribute("widths");
|
||||||
|
if (!widths) {
|
||||||
|
if (t.table.body.length !== 0) {
|
||||||
|
if (t.table.body[0].length !== 0)
|
||||||
|
for (var k = 0; k < t.table.body[0].length; k++)
|
||||||
|
t.table.widths.push("*");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var w = widths.split(",");
|
||||||
|
for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]);
|
||||||
|
}
|
||||||
|
alreadyConverted.push(t);
|
||||||
|
break;
|
||||||
|
case "tbody":
|
||||||
|
parseChildren(alreadyConverted, element, currentParagraph, styles);
|
||||||
|
break;
|
||||||
|
case "tr":
|
||||||
|
var row = [];
|
||||||
|
parseChildren(row, element, currentParagraph, styles);
|
||||||
|
alreadyConverted.push(row);
|
||||||
|
break;
|
||||||
|
case "td":
|
||||||
|
currentParagraph = create("text");
|
||||||
|
var st = create("stack");
|
||||||
|
st.stack.push(currentParagraph);
|
||||||
|
var rspan = element.getAttribute("rowspan");
|
||||||
|
if (rspan)
|
||||||
|
st.rowSpan = parseInt(rspan);
|
||||||
|
var cspan = element.getAttribute("colspan");
|
||||||
|
if (cspan)
|
||||||
|
st.colSpan = parseInt(cspan);
|
||||||
|
parseChildren(st.stack, element, currentParagraph, styles);
|
||||||
|
alreadyConverted.push(st);
|
||||||
|
break;
|
||||||
|
case "span":
|
||||||
|
parseChildren(alreadyConverted, element, currentParagraph, styles);
|
||||||
|
break;
|
||||||
|
case "br":
|
||||||
|
currentParagraph = create("text");
|
||||||
|
alreadyConverted.push(currentParagraph);
|
||||||
|
break;
|
||||||
|
case "li":
|
||||||
|
case "div":
|
||||||
|
case "p":
|
||||||
|
currentParagraph = create("text");
|
||||||
|
var stack = create("stack");
|
||||||
|
stack.stack.push(currentParagraph);
|
||||||
|
ComputeStyle(stack, styles);
|
||||||
|
parseChildren(stack.stack, element, currentParagraph);
|
||||||
|
alreadyConverted.push(stack);
|
||||||
|
break;
|
||||||
|
case "img":
|
||||||
|
alreadyConverted.push({
|
||||||
|
image: BaseMap[element.getAttribute("src")],
|
||||||
|
width: parseInt(element.getAttribute("width")),
|
||||||
|
height: parseInt(element.getAttribute("height"))
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "ul":
|
||||||
|
var u = create("ul");
|
||||||
|
parseChildren(u.ul, element, currentParagraph, styles);
|
||||||
|
alreadyConverted.push(u);
|
||||||
|
break;
|
||||||
|
case "ol":
|
||||||
|
var o = create("ol");
|
||||||
|
parseChildren(o.ol, element, currentParagraph, styles);
|
||||||
|
alreadyConverted.push(o);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
var temporary = create("text", element.textContent.replace(/\n/g, ""));
|
||||||
|
if (styles)
|
||||||
|
ComputeStyle(temporary, styles);
|
||||||
|
currentParagraph.text.push(temporary);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return currentParagraph;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Parses HTML
|
||||||
|
* @function
|
||||||
|
* @param {string} converted -
|
||||||
|
* @param {object} htmlText -
|
||||||
|
*/
|
||||||
|
ParseHtml = function(converted, htmlText) {
|
||||||
|
var html = $(htmlText.replace(/\t/g, "").replace(/\n/g, ""));
|
||||||
|
var emptyParagraph = create("text");
|
||||||
|
slice(html).forEach(function(element) {
|
||||||
|
ParseElement(converted, element, emptyParagraph);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
content = [];
|
||||||
|
ParseHtml(content, html);
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
BaseMap = images,
|
||||||
|
/**
|
||||||
|
* Creates containerelements for pdfMake
|
||||||
|
* e.g create("text":"MyText") result in { text: "MyText" }
|
||||||
|
* or complex objects create("stack", [{text:"MyText"}, {text:"MyText2"}])
|
||||||
|
*for units / paragraphs of text
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
* @param {string} name - name of the attribute holding content
|
||||||
|
* @param {object} content - the actual content (maybe empty)
|
||||||
|
*/
|
||||||
|
create = function(name, content) {
|
||||||
|
var o = {};
|
||||||
|
content = content || [];
|
||||||
|
o[name] = content;
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
fonts.forEach(function(fontInfo) {
|
||||||
|
registerFont(fontInfo);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
convertHTML: convertHTML,
|
||||||
|
createElement: create
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
createInstance: createInstance
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
// Provider to register entries for the main menu.
|
// Provider to register entries for the main menu.
|
||||||
.provider('mainMenu', [
|
.provider('mainMenu', [
|
||||||
|
@ -19,6 +19,10 @@ urlpatterns = [
|
|||||||
views.SearchView.as_view(),
|
views.SearchView.as_view(),
|
||||||
name='core_search'),
|
name='core_search'),
|
||||||
|
|
||||||
|
url(r'^core/encode_media/$',
|
||||||
|
views.MediaEncoder.as_view(),
|
||||||
|
name="core_mediaencoding"),
|
||||||
|
|
||||||
url(r'^angular_js/(?P<openslides_app>site|projector)/$',
|
url(r'^angular_js/(?P<openslides_app>site|projector)/$',
|
||||||
views.AppsJsView.as_view(),
|
views.AppsJsView.as_view(),
|
||||||
name='core_apps_js'),
|
name='core_apps_js'),
|
||||||
@ -26,8 +30,8 @@ urlpatterns = [
|
|||||||
# View for the projectors are handelt by angular.
|
# View for the projectors are handelt by angular.
|
||||||
url(r'^projector.*$', views.ProjectorView.as_view()),
|
url(r'^projector.*$', views.ProjectorView.as_view()),
|
||||||
|
|
||||||
|
|
||||||
# Main entry point for all angular pages.
|
# Main entry point for all angular pages.
|
||||||
# Has to be the last entry in the urls.py
|
# Has to be the last entry in the urls.py
|
||||||
url(r'^.*$', views.IndexView.as_view()),
|
url(r'^.*$', views.IndexView.as_view()),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
@ -606,3 +609,143 @@ class SearchView(utils_views.APIView):
|
|||||||
return super().get_context_data(
|
return super().get_context_data(
|
||||||
elements=search(unquote(query)),
|
elements=search(unquote(query)),
|
||||||
**context)
|
**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
|
||||||
|
@ -4,6 +4,209 @@
|
|||||||
|
|
||||||
angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||||
|
|
||||||
|
.factory('MotionContentProvider', ['gettextCatalog', function(gettextCatalog) {
|
||||||
|
/**
|
||||||
|
* Provides the content as JS objects for Motions in pdfMake context
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
var createInstance = function(converter) {
|
||||||
|
/**
|
||||||
|
* Text of motion
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
*/
|
||||||
|
var textContent = function(motion, $scope) {
|
||||||
|
return converter.convertHTML(motion.getText($scope.version));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generate text of reason
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
*/
|
||||||
|
reasonContent = function(motion, $scope) {
|
||||||
|
return converter.convertHTML(motion.getReason($scope.version));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generate header text of motion
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
*/
|
||||||
|
motionHeader = function(motion, $scope) {
|
||||||
|
var header = converter.createElement("text", gettextCatalog.getString("Motion") + " " + motion.identifier + ": " + motion.getTitle($scope.version));
|
||||||
|
header.bold = true;
|
||||||
|
header.fontSize = 26;
|
||||||
|
return header;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generate text of signment
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
* @param {object} User - Current user
|
||||||
|
*/
|
||||||
|
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("columns", [label, state]);
|
||||||
|
signment.margin = [10, 20, 0, 10];
|
||||||
|
signment.lineHeight = 2.5;
|
||||||
|
return signment;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generates polls
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
*/
|
||||||
|
polls = function(motion, $scope) {
|
||||||
|
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,
|
||||||
|
yesRelative = (poll.yes) * 100 / (poll.votescast),
|
||||||
|
no = poll.no,
|
||||||
|
noRelative = (poll.no) * 100 / (poll.votescast),
|
||||||
|
abstain = poll.abstain,
|
||||||
|
abstainRelative = (poll.abstain) * 100 / (poll.votescast),
|
||||||
|
valid = poll.votesvalid,
|
||||||
|
validRelative = (poll.votesvalid) * 100 / (poll.votescast),
|
||||||
|
number = {
|
||||||
|
text: id + ".",
|
||||||
|
width: "5%"
|
||||||
|
},
|
||||||
|
headerText = {
|
||||||
|
text: "Abstimmung",
|
||||||
|
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
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
*/
|
||||||
|
titleSection = function(motion, $scope) {
|
||||||
|
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
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
*/
|
||||||
|
reason = function(motion, $scope) {
|
||||||
|
var r = converter.createElement("text", gettextCatalog.getString("Reason") + ":");
|
||||||
|
r.bold = true;
|
||||||
|
r.fontSize = 14;
|
||||||
|
r.margin = [0, 30, 0, 10];
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generates content as a pdfmake consumable
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
* @param {object} User - Current user
|
||||||
|
*/
|
||||||
|
getContent = function(motion, $scope, User) {
|
||||||
|
return [
|
||||||
|
motionHeader(motion, $scope),
|
||||||
|
signment(motion, $scope, User),
|
||||||
|
polls(motion, $scope),
|
||||||
|
titleSection(motion, $scope),
|
||||||
|
textContent(motion, $scope),
|
||||||
|
reason(motion, $scope),
|
||||||
|
reasonContent(motion, $scope)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
getContent: getContent
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
createInstance: createInstance
|
||||||
|
};
|
||||||
|
}])
|
||||||
|
.factory('SingleMotionContentProvider', function() {
|
||||||
|
/**
|
||||||
|
* Generates a content provider
|
||||||
|
* @constructor
|
||||||
|
* @param {object} motionContentProvider - Generates pdfMake structure from motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
* @param {object} User - Current User
|
||||||
|
*/
|
||||||
|
var createInstance = function(motionContentProvider, motion, $scope, User) {
|
||||||
|
/**
|
||||||
|
* Returns Content for single motion
|
||||||
|
* @function
|
||||||
|
* @param {object} motion - Current motion
|
||||||
|
* @param {object} $scope - Current $scope
|
||||||
|
* @param {object} User - Current User
|
||||||
|
*/
|
||||||
|
var getContent = function() {
|
||||||
|
return motionContentProvider.getContent(motion, $scope, User);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
getContent: getContent
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
createInstance: createInstance
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
.config([
|
.config([
|
||||||
'mainMenuProvider',
|
'mainMenuProvider',
|
||||||
'gettext',
|
'gettext',
|
||||||
@ -573,7 +776,15 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
|||||||
'User',
|
'User',
|
||||||
'Workflow',
|
'Workflow',
|
||||||
'motion',
|
'motion',
|
||||||
function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, motion) {
|
'SingleMotionContentProvider',
|
||||||
|
'MotionContentProvider',
|
||||||
|
'PdfMakeConverter',
|
||||||
|
'PdfMakeDocumentProvider',
|
||||||
|
'gettextCatalog',
|
||||||
|
function($scope, $http, ngDialog, MotionForm,
|
||||||
|
Motion, Category, Mediafile, Tag,
|
||||||
|
User, Workflow, motion,
|
||||||
|
SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, gettextCatalog) {
|
||||||
Motion.bindOne(motion.id, $scope, 'motion');
|
Motion.bindOne(motion.id, $scope, 'motion');
|
||||||
Category.bindAll({}, $scope, 'categories');
|
Category.bindAll({}, $scope, 'categories');
|
||||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||||
@ -584,6 +795,33 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
|||||||
$scope.version = motion.active_version;
|
$scope.version = motion.active_version;
|
||||||
$scope.isCollapsed = true;
|
$scope.isCollapsed = true;
|
||||||
|
|
||||||
|
$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('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) {
|
||||||
|
/**
|
||||||
|
* Converter for use with pdfMake
|
||||||
|
* @constructor
|
||||||
|
* @param {object} images - An object to resolve the BASE64 encoded images { "$src":$BASE64DATA }
|
||||||
|
* @param {object} fonts - An object representing the available custom fonts
|
||||||
|
* @param {object} pdfMake - pdfMake object for enhancement with custom fonts
|
||||||
|
*/
|
||||||
|
|
||||||
|
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),
|
||||||
|
filename = gettextCatalog.getString("Motion") + " " + id + ".pdf";
|
||||||
|
pdfMake.createPdf(documentProvider.getDocument()).download(filename);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// open edit dialog
|
// open edit dialog
|
||||||
$scope.openDialog = function (motion) {
|
$scope.openDialog = function (motion) {
|
||||||
ngDialog.open(MotionForm.getDialog(motion));
|
ngDialog.open(MotionForm.getDialog(motion));
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||||
<translate>All motions</translate>
|
<translate>All motions</translate>
|
||||||
</a>
|
</a>
|
||||||
<a ui-sref="motions_single_pdf({pk: motion.id})" target="_blank" class="btn btn-default btn-sm">
|
<a ng-click="makePDF()" class="btn btn-primary btn-sm">
|
||||||
<i class="fa fa-file-pdf-o fa-lg"></i>
|
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||||
<translate>PDF</translate>
|
<translate>PDF</translate>
|
||||||
</a>
|
</a>
|
||||||
|
Loading…
Reference in New Issue
Block a user