Merge branch 'master' of github.com:christopher-ramirez/secretary
This commit is contained in:
commit
3bb25820a0
7 changed files with 106 additions and 19 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -3,6 +3,11 @@
|
|||
*.*~
|
||||
*.egg
|
||||
*.egg-info
|
||||
*.odt
|
||||
*.txt
|
||||
.tox
|
||||
.cache
|
||||
.vscode
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
13
.travis.yml
13
.travis.yml
|
|
@ -1,7 +1,10 @@
|
|||
install: pip install -U tox
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
install: "python setup.py develop"
|
||||
script: "python setup.py test"
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
script: tox -e py${TRAVIS_PYTHON_VERSION//./}
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -1,5 +1,13 @@
|
|||
# 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.
|
||||
|
||||
|
||||
|
|
@ -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.
|
||||
> 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
|
||||
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`.
|
||||
|
||||
### 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 `"`, `'`, `<`, `>` and '&' inside Jinja expressions.
|
||||
* **0.2.10**: ---
|
||||
* **0.2.9**: ---
|
||||
|
|
|
|||
60
secretary.py
60
secretary.py
|
|
@ -177,12 +177,19 @@ class Renderer(object):
|
|||
self.log.debug('packing document')
|
||||
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():
|
||||
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')
|
||||
|
||||
|
|
@ -397,8 +404,9 @@ class Renderer(object):
|
|||
|
||||
def _unescape_entities(self, xml_text):
|
||||
"""
|
||||
Unescape '&', '<', '"' and '>' within jinja instructions.
|
||||
The regexs rules used here are compiled in _compile_escape_expressions.
|
||||
Unescape links and '&', '<', '"' and '>' within jinja
|
||||
instructions. The regexs rules used here are compiled in
|
||||
_compile_escape_expressions.
|
||||
"""
|
||||
for regexp, replacement in self.escape_map.items():
|
||||
while True:
|
||||
|
|
@ -406,6 +414,23 @@ class Renderer(object):
|
|||
if not substitutions:
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -413,7 +438,11 @@ class Renderer(object):
|
|||
"""
|
||||
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])[^<]*?)</text:\1>'
|
||||
>>>>>>> f74046fb09401facbece39056dd53dafe3f814e9
|
||||
for m in re.findall(find_pattern, xml_text):
|
||||
replacement = m[1].replace('\n', '<text:line-break/>')
|
||||
replacement = replacement.replace('\t', '<text:tab/>')
|
||||
|
|
@ -554,7 +583,10 @@ class Renderer(object):
|
|||
|
||||
return final_xml
|
||||
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]
|
||||
|
||||
raise ExpatError('ExpatError "%s" at line %d, column %d\nNear of: "[...]%s[...]"' % \
|
||||
(ErrorString(e.code), e.lineno, e.offset, near))
|
||||
except:
|
||||
|
|
@ -722,8 +754,20 @@ class Renderer(object):
|
|||
|
||||
# Transfer child nodes
|
||||
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:
|
||||
odt_node.appendChild(child_node.cloneNode(True))
|
||||
container.appendChild(child_node.cloneNode(True))
|
||||
|
||||
# Add style-attributes defined in transform_map
|
||||
if 'style_attributes' in transform_map[tag]:
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -26,7 +26,7 @@ class PyTest(TestCommand):
|
|||
|
||||
setup(
|
||||
name='secretary',
|
||||
version='0.2.11',
|
||||
version='0.2.15',
|
||||
url='https://github.com/christopher-ramirez/secretary',
|
||||
license='MIT',
|
||||
author='Christopher Ramírez',
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ def test_pad_string():
|
|||
assert pad_string('TEST', 4) == 'TEST'
|
||||
assert pad_string(1) == '00001'
|
||||
|
||||
|
||||
class RenderTestCase(TestCase):
|
||||
def setUp(self):
|
||||
root = os.path.dirname(__file__)
|
||||
|
|
@ -63,7 +64,6 @@ class RenderTestCase(TestCase):
|
|||
for test, expect in test_samples.items():
|
||||
assert self.engine._encode_escape_chars(test) == expect
|
||||
|
||||
|
||||
def _test_is_jinja_tag(self):
|
||||
assert self._is_jinja_tag('{{ foo }}')==True
|
||||
assert self._is_jinja_tag('{ foo }')==False
|
||||
|
|
@ -79,3 +79,23 @@ class RenderTestCase(TestCase):
|
|||
def test_create_text_span_node(self):
|
||||
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)
|
||||
|
|
|
|||
4
tox.ini
4
tox.ini
|
|
@ -1,6 +1,6 @@
|
|||
[tox]
|
||||
envlist = py26, py27, py3, py33, py34
|
||||
envlist = py26, py27, py3, py33, py34, py35, py36
|
||||
|
||||
[testenv]
|
||||
deps = pytest
|
||||
commands= py.test
|
||||
commands = py.test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue