Merge branch 'master' of github.com:christopher-ramirez/secretary

This commit is contained in:
Christopher 2017-08-08 20:46:47 -06:00
commit 3bb25820a0
7 changed files with 106 additions and 19 deletions

5
.gitignore vendored
View file

@ -3,6 +3,11 @@
*.*~ *.*~
*.egg *.egg
*.egg-info *.egg-info
*.odt
*.txt
.tox
.cache
.vscode
dist dist
build build
eggs eggs

View file

@ -1,7 +1,10 @@
install: pip install -U tox
language: python language: python
python: python:
- "2.6" - 2.6
- "2.7" - 2.7
- "3.3" - 3.3
install: "python setup.py develop" - 3.4
script: "python setup.py test" - 3.5
- 3.6
script: tox -e py${TRAVIS_PYTHON_VERSION//./}

View file

@ -1,5 +1,13 @@
# SECRETARY # SECRETARY
<a href="https://pypi.python.org/pypi/secretary">
<img src="https://img.shields.io/pypi/v/secretary.svg">
</a>
<a href="https://travis-ci.org/christopher-ramirez/secretary">
<img src="https://img.shields.io/travis/christopher-ramirez/secretary.svg">
</a>
#### Take the power of Jinja2 templates to OpenOffice and LibreOffice and create reports in your web applications. #### Take the power of Jinja2 templates to OpenOffice and LibreOffice and create reports in your web applications.
@ -93,6 +101,10 @@ Although most of the time the automatic handling of control flow in secretary ma
* `after::cell`: Same as `after::row` but for a table cell. * `after::cell`: Same as `after::row` but for a table cell.
> Field content is the control flow tag you insert with the Writer *input field* > Field content is the control flow tag you insert with the Writer *input field*
### Hyperlink Support
LibreOffice by default escapes every URL in links, pictures or any other element supporting hyperlink functionallity. This can be a problem if you need to generate dynamic links because your template logic is URL encoded and impossible to be handled by the Jinja engine. Secretary solves this problem by reserving the `secretary` URI scheme. If you need to create dynamic links in your documents, prepend every link with the `secretary:` scheme.
So for example if you have the following dynamic link: `https://mysite/products/{{ product.id }}`, prepend it with the **`secretary:`** scheme, leaving the final link as `secretary:https://mysite/products/{{ product.id }}`.
### Image Support ### Image Support
Secretary allows you to use placeholder images in templates that will be replaced when rendering the final document. To create a placeholder image on your template: Secretary allows you to use placeholder images in templates that will be replaced when rendering the final document. To create a placeholder image on your template:
@ -147,6 +159,9 @@ Pad zeroes to `value` to the left until output value's length be equal to `lengt
Secretary supports most of the jinja2 control structure/flow tags. But please avoid using the following tags since they are not supported: `block`, `extends`, `macro`, `call`, `include` and `import`. Secretary supports most of the jinja2 control structure/flow tags. But please avoid using the following tags since they are not supported: `block`, `extends`, `macro`, `call`, `include` and `import`.
### Version History ### Version History
* **0.2.15**: Fix bug reported in #39 escaping Line-Feed and Tab chars inside `text:` elements.
* **0.2.14**: Implement dynamic links escaping and fix #33.
* **0.2.13**: Fix reported bug in markdown filter outputing emply lists.
* **0.2.11**: Fix bug when unescaping `&quot;`, `&apos;`, `&lt;`, `&gt;` and '&amp;' inside Jinja expressions. * **0.2.11**: Fix bug when unescaping `&quot;`, `&apos;`, `&lt;`, `&gt;` and '&amp;' inside Jinja expressions.
* **0.2.10**: --- * **0.2.10**: ---
* **0.2.9**: --- * **0.2.9**: ---

View file

@ -177,11 +177,18 @@ class Renderer(object):
self.log.debug('packing document') self.log.debug('packing document')
zip_file = io.BytesIO() zip_file = io.BytesIO()
zipdoc = zipfile.ZipFile(zip_file, 'a') mimetype = files['mimetype']
del files['mimetype']
zipdoc = zipfile.ZipFile(zip_file, 'a', zipfile.ZIP_DEFLATED)
# Store mimetype without without compression using a ZipInfo object
# for compatibility with Py2.6 which doesn't have compress_type
# parameter in ZipFile.writestr function
mime_zipinfo = zipfile.ZipInfo('mimetype')
zipdoc.writestr(mime_zipinfo, mimetype)
for fname, content in files.items(): for fname, content in files.items():
if sys.version_info >= (2, 7):
zipdoc.writestr(fname, content, zipfile.ZIP_DEFLATED)
else:
zipdoc.writestr(fname, content) zipdoc.writestr(fname, content)
self.log.debug('Document packing completed') self.log.debug('Document packing completed')
@ -397,8 +404,9 @@ class Renderer(object):
def _unescape_entities(self, xml_text): def _unescape_entities(self, xml_text):
""" """
Unescape '&amp;', '&lt;', '&quot;' and '&gt;' within jinja instructions. Unescape links and '&amp;', '&lt;', '&quot;' and '&gt;' within jinja
The regexs rules used here are compiled in _compile_escape_expressions. instructions. The regexs rules used here are compiled in
_compile_escape_expressions.
""" """
for regexp, replacement in self.escape_map.items(): for regexp, replacement in self.escape_map.items():
while True: while True:
@ -406,6 +414,23 @@ class Renderer(object):
if not substitutions: if not substitutions:
break break
return self._unescape_links(xml_text)
def _unescape_links(self, xml_text):
"""Fix Libreoffice auto escaping of xlink:href attribute values.
This unescaping is only done on 'secretary' scheme URLs."""
import urllib
robj = re.compile(r'(?is)(xlink:href=\")secretary:(.*?)(\")')
def replacement(match):
return ''.join([match.group(1), urllib.unquote(match.group(2)),
match.group(3)])
while True:
xml_text, rep = robj.subn(replacement, xml_text)
if not rep:
break
return xml_text return xml_text
@staticmethod @staticmethod
@ -413,7 +438,11 @@ class Renderer(object):
""" """
Replace line feed and/or tabs within text:span entities. Replace line feed and/or tabs within text:span entities.
""" """
<<<<<<< HEAD
find_pattern = r'(?is)<text:([\S]+?)>([^>]*?([\n|\t|\r|\x0b|\x0c])[^<]*?)</text:\1>' find_pattern = r'(?is)<text:([\S]+?)>([^>]*?([\n|\t|\r|\x0b|\x0c])[^<]*?)</text:\1>'
=======
find_pattern = r'(?is)<text:([\S]+?).*?>([^>]*?([\n\t])[^<]*?)</text:\1>'
>>>>>>> f74046fb09401facbece39056dd53dafe3f814e9
for m in re.findall(find_pattern, xml_text): for m in re.findall(find_pattern, xml_text):
replacement = m[1].replace('\n', '<text:line-break/>') replacement = m[1].replace('\n', '<text:line-break/>')
replacement = replacement.replace('\t', '<text:tab/>') replacement = replacement.replace('\t', '<text:tab/>')
@ -554,7 +583,10 @@ class Renderer(object):
return final_xml return final_xml
except ExpatError as e: except ExpatError as e:
if not 'result' in locals():
result = xml_source
near = result.split('\n')[e.lineno -1][e.offset-200:e.offset+200] near = result.split('\n')[e.lineno -1][e.offset-200:e.offset+200]
raise ExpatError('ExpatError "%s" at line %d, column %d\nNear of: "[...]%s[...]"' % \ raise ExpatError('ExpatError "%s" at line %d, column %d\nNear of: "[...]%s[...]"' % \
(ErrorString(e.code), e.lineno, e.offset, near)) (ErrorString(e.code), e.lineno, e.offset, near))
except: except:
@ -722,8 +754,20 @@ class Renderer(object):
# Transfer child nodes # Transfer child nodes
if html_node.hasChildNodes(): if html_node.hasChildNodes():
# We can't directly insert text into a text:list-item element.
# The content of the item most be wrapped inside a container
# like text:p. When there's not a double linebreak separating
# list elements, markdown2 creates <li> elements without wraping
# their contents inside a container. Here we automatically create
# the container if one was not created by markdown2.
if (tag=='li' and html_node.childNodes[0].localName != 'p'):
container = xml_object.createElement('text:p')
odt_node.appendChild(container)
else:
container = odt_node
for child_node in html_node.childNodes: for child_node in html_node.childNodes:
odt_node.appendChild(child_node.cloneNode(True)) container.appendChild(child_node.cloneNode(True))
# Add style-attributes defined in transform_map # Add style-attributes defined in transform_map
if 'style_attributes' in transform_map[tag]: if 'style_attributes' in transform_map[tag]:

View file

@ -26,7 +26,7 @@ class PyTest(TestCommand):
setup( setup(
name='secretary', name='secretary',
version='0.2.11', version='0.2.15',
url='https://github.com/christopher-ramirez/secretary', url='https://github.com/christopher-ramirez/secretary',
license='MIT', license='MIT',
author='Christopher Ramírez', author='Christopher Ramírez',

View file

@ -18,6 +18,7 @@ def test_pad_string():
assert pad_string('TEST', 4) == 'TEST' assert pad_string('TEST', 4) == 'TEST'
assert pad_string(1) == '00001' assert pad_string(1) == '00001'
class RenderTestCase(TestCase): class RenderTestCase(TestCase):
def setUp(self): def setUp(self):
root = os.path.dirname(__file__) root = os.path.dirname(__file__)
@ -63,7 +64,6 @@ class RenderTestCase(TestCase):
for test, expect in test_samples.items(): for test, expect in test_samples.items():
assert self.engine._encode_escape_chars(test) == expect assert self.engine._encode_escape_chars(test) == expect
def _test_is_jinja_tag(self): def _test_is_jinja_tag(self):
assert self._is_jinja_tag('{{ foo }}')==True assert self._is_jinja_tag('{{ foo }}')==True
assert self._is_jinja_tag('{ foo }')==False assert self._is_jinja_tag('{ foo }')==False
@ -79,3 +79,23 @@ class RenderTestCase(TestCase):
def test_create_text_span_node(self): def test_create_text_span_node(self):
assert self.engine.create_text_span_node(self.document, 'text').toxml() == '<text:span>text</text:span>' assert self.engine.create_text_span_node(self.document, 'text').toxml() == '<text:span>text</text:span>'
class EncodeLFAndFWithinTextNamespace(TestCase):
"""Test encoding of line feed and tab chars within text: namespace"""
def test_encode_linefeed_char(self):
xml = '<text:span>This\nLF</text:span>'
espected = '<text:span>This<text:line-break/>LF</text:span>'
assert (Renderer._encode_escape_chars(xml) == espected)
def test_encode_tab_char(self):
xml = '<text:span>This\tTab</text:span>'
espected = '<text:span>This<text:tab/>Tab</text:span>'
assert (Renderer._encode_escape_chars(xml) == espected)
def test_escape_elem_with_attributes(self):
"""A bug in _encode_escape_chars was preventing it from escaping
LF and tabs inside text elements with tag attributes. See:
https://github.com/christopher-ramirez/secretary/issues/39"""
xml = '<text:span attr="value">This\nLF</text:span>'
espected = '<text:span attr="value">This<text:line-break/>LF</text:span>'
assert (Renderer._encode_escape_chars(xml) == espected)

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py26, py27, py3, py33, py34 envlist = py26, py27, py3, py33, py34, py35, py36
[testenv] [testenv]
deps = pytest deps = pytest