Code Monkey home page Code Monkey logo

secretary's Introduction

SECRETARY

Take the power of Jinja2 templates to OpenOffice and LibreOffice and create reports in your web applications.

Secretary allows you to use Open Document Text (ODT) files as templates for rendering reports or letters. Secretary is an alternative solution for creating office documents and reports in OpenDocument Text format from templates that can be visually composed using the OpenOffice.org/LibreOffice Writer word processor.

Secretary use the semantics of jinja2 templates to render ODT files. Most features in jinja can be used into your ODT templates including variable printing, filters and flow control.

Rendered documents are produced in ODT format, and can then be converted to PDF, MS Word or other supported formats using the UNO Bridge or a library like PyODConverter

Installing

pip install secretary

Rendering a Template

    from secretary import Renderer

    engine = Renderer()
    result = engine.render(template, foo=foo, bar=bar)

Secretary implements a class called Renderer. Renderer takes a single argument called environment which is a jinja Environment.

To render a template create an instance of class Renderer and call the instance's method render passing a template file and template's variables as keyword arguments. template can be a filename or a file object. render will return the rendered document in binary format.

Before rendering a template, you can configure the internal templating engine using the Renderer instance's variable environment, which is an instance of jinja2 Environment class. For example, to declare a custom filter use:

    from secretary import Renderer

    engine = Renderer()

    # Configure custom application filters
    engine.environment.filters['custom_filer'] = filter_function
    result = engine.render(template, foo=foo, bar=bar)

    output = open('rendered_document.odt', 'wb')
    output.write(result)

Composing Templates

Secretary templates are simple ODT documents. You can create them using Writer. An OpenDocument file is basically a ZIP archive containing some XML files. If you plan to use control flow or conditionals it is a good idea to familiarise yourself a little bit with the OpenDocument XML to understand better what's going on behind the scenes.

Printing Variables

Since Secretary use the same template syntax of Jinja2, to print a varible type a double curly braces enclosing the variable, like so:

    {{ foo.bar }}
    {{ foo['bar'] }}

However, mixing template instructions and normal text into the template document may become confusing and clutter the layout and most important, in most cases will produce invalid ODT documents. Secretary recommends using an alternative way of inserting fields. Insert a visual field in LibreOffice Writer from the menu Insert > Fields > Other... (or just press Ctrl+F2), then click on the Functions tab and select Input field. Click Insert. A dialog will appear where you can insert the print instructions. You can even insert simple control flow tags to dynamically change what is printed in the field.

Secretary will handle multiline variable values replacing the line breaks with a <text:line-break/> tag.

Control Flow

Most of the time secretary will handle the internal composing of XML when you insert control flow tags ({% for foo in foos %}, {% if bar %}, etc and its enclosing tags. This is done by finding the present or absence of other secretary tags within the internal XML tree.

Examples document structures

Printing multiple records in a table alt tag

Conditional paragraphs alt tag

The last example could had been simplified into a single paragraph in Writer like:

    {% if already_paid %}YOU ALREADY PAID{% else %}YOU HAVEN'T PAID{% endif %}

Printing a list of names

    {% for name in names %}
        {{ name }}
    {% endfor %}

Automatic control flow in Secretary will handle the intuitive result of the above examples and similar thereof.

Although most of the time the automatic handling of control flow in secretary may be good enough, we still provide an additional method for manual control of the flow. Use the reference property of the field to specify where where the control flow tag will be used or internally moved within the XML document:

  • paragraph: Whole paragraph containing the field will be replaced with the field content.
  • before::paragraph: Field content will be moved before the current paragraph.
  • after::paragraph: Field content will be moved after the current paragraph.
  • row: The entire table row containing the field will be replace with the field content.
  • before::row: Field content will be moved before the current table row.
  • after::row: Field content will be moved after the current table row.
  • cell: The entire table cell will be replaced with the current field content. Even though this setting is available, it is not recommended. Generated documents may not be what you expected.
  • before::cell: Same as before::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

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:

  1. Insert an image into the document as normal. This image will be replaced when rendering the final document.
  2. Change the name of the recently added image to a Jinja2 print tag (the ones with double curly braces). The variable should call the image filter, i.e.: Suppose you have a client record (passed to template as client object), and a picture of him is stored in the picture field. To print the client's picture into a document set the image name to {{ client.picture|image }}.

To change image name, right click under image, select "Picture..." from the popup menu and navigate to "Options" tab.

Media loader

To load image data, Secretary needs a media loader. The engine by default provides a file system loader which takes the variable value (specified in image name). This value can be a file object containing an image or an absolute or a relative filename to media_path passed at Renderer instance creation.

Since the default media loader is very limited. Users can provide theirs own media loader to the Renderer instance. A media loader can perform image retrieval and/or any required transformation of images. The media loader must take the image value from the template and return a tuple whose first item is a file object containing the image. Its second element must be the image mimetype.

Example declaring a media loader:

    from secretary import Renderer

    engine = Renderer()

    @engine.media_loader
    def db_images_loader(value, *args, *kwargs):
        # load from images collection the image with `value` id.
        image = db.images.findOne({'_id': value})

        return (image, the_image_mimetype)

    engine.render(template, **template_vars)

The media loader also receive any argument or keywork arguments declared in the template. i.e: If the placeholder image's name is: {{ client.image|image('keep_ratio', tiny=True)}} the media loader will receive: first the value of client.image as it first argument; the string keep_ratio as an additional argument and tiny as a keyword argument.

The loader can also access and update the internal draw:frame and draw:image nodes. The loader receives as a dictionary the attributes of these nodes through frame_attrs and image_attrs keyword arguments. Is some update is made to these dictionary secretary will update the internal nodes with the changes. This is useful when the placeholder's aspect radio and replacement image's aspect radio are different and you need to keep the aspect ratio of the original image.

Builtin Filters

Secretary includes some predefined jinja2 filters. Included filters are:

  • image(value) See Image Support section above.

  • markdown(value) Convert the value, a markdown formated string, into a ODT formated text. Example:

      {{ invoice.description|markdown }}
    
  • pad(value, length) Pad zeroes to value to the left until output value's length be equal to length. Default length if 5. Example:

      {{ invoice.number|pad(6) }}
    

Features of jinja2 not supported

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.19: Fix bug in Markdown filter on Python 3. See #47.
  • 0.2.18:
    1. Auto escaping of Secretary URL scheme was not working on Python 3.
    2. Is not longer needed to manually set as safe the output value of the markdown filter.
  • 0.2.17: Performance increase when escaping \n and \t chars. See #44.
  • 0.2.16: Fix store of mimetype in rendered ODT archive.
  • 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 '&' inside Jinja expressions.
  • 0.2.10: ---
  • 0.2.9: ---
  • 0.2.8: Fix #25. Some internal refactorings. Drop the minimal support for Jinja tags in plain text.
  • 0.2.7: Truly fix regexps used to unscape XML entities present inside Jinja tags.
  • 0.2.6: AVOID THIS RELEASE Fix regexps used to unscape XML entities present inside Jinja tags.
  • 0.2.5: Fix issues #14 and #16. Thanks to DieterBuysAI for this release.
  • 0.2.4: Fix an UnicodeEncodeError exception raised scaping tab chars.
  • 0.2.3: Fix issue #12.
  • 0.2.2: Introduce image support.
  • 0.2.1: Fix issue #8
  • 0.2.0: Backward incompatible release. Still compatible with existing templates. Introduce auto flow handling, better logging and minor bug fixes.
  • 0.1.1: New markdown filter. Introduce new flow control aliases. Bug fixes.
  • 0.1.0: Initial release.

secretary's People

Contributors

ak4nv avatar armonge avatar christopher-ramirez avatar dieterbuys avatar florianludwig avatar j123b567 avatar michalczaplinski avatar mpasternak avatar xsetra avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

secretary's Issues

Markdown and line breaks

After this manipulation I get an escaped line breaks (\n converts to \\n) and then I see \n as text in my odt file instead of new line. Also \n\n converts to \\n\\n.
I'm not sure about \t but I think it has the same issue.

Word 2013 (maybe later..) compatibility

I think that maybe a note could be added to the README.md because I thought this was a bug with secretary but I have discovered that using the latest version of Libre Office it saves the files in 1.2 format and Word claims it's corrupt so Maybe we can put a note in the README something like

Word Compatibility

To ensure your files load in Word as well as Libre/Open Office make sure you save your templates in 1.1 version mode.

Hope this helps someone else out.

more cells in the final render of table

Hi,

I do not understand how I have more cells in the construction of my table ?
capture d ecran_2018-03-15_00-26-45

result:
capture d ecran_2018-03-15_00-27-18

Can you help me ?

Thank you for your efforts, secretary is so cool !

Markdown filter ignores format from template

Hi there,

I am not sure if this might be related to #67

When using the |markdown filter, the output text in the document ignores the formatting from the odt-template file. It seems like it is using the default font and size instead.

Auto-Filter json null (Python None)

When you get a json extracted from database or other sources, it is not rare that some field are empty (aka null).
Python JSONDecoder decode them as None Python object.
But in document, I guess the majority would like to see (nothing) when its None in place of 'None' remplacing the Jinja var.

Perhaps I've missed a simple filter here, but I can't see any good reason to print None

Multiple Line Breaks

Hello,

at first ... thanks for secretary!

It's easiest if I explain my problem with an example:
For testing I put some \n in the secretary.py you provided:
{'country': 'United\nStat\nes', 'capital': 'Was\nhington', 'cities': ['miami', 'new york', 'california', 'texas', 'atlanta']},
{'country': 'Eng\nland', 'capital': 'London', 'cities': ['gales']},

The result:
image
If there is more than one \n in one item, the line-breaks are not inserted.

Perhaps you have an idea how to solve it?

Thanks!

with kind regards,
Sven

page break

is possible add a "page break" like:

{{page_break}}?

I have put the end of the "for" in new page, but for work is need to add a "blank line" up to first line and this make a page with the first line as blank.

I need ti create new page without white line :)

this don't create new page

{% for iten in datas.item %}
write some things...
--- page_break via LibreOffice ---
{% endfor %}

this work, but create a ne blank line inside new page :(

{% for iten in datas.item %}
write some things...
--- page_break via LibreOffice ---

{% endfor %}

Support for ODS?

Is support for Spreadsheets planned?

currently it per se works, but the spreadsheet suffer from the "too many cells" problem, since Calc doesn't have the necessary fields available like in Writer. A Workaround would be welcome too of course if there is some (like additional render() arguments or the like).

Explain how add page

Hello,
Is it possible to add page (with one page template )
example
Page one cover
page two general information

page 3 to n detail information
page n+1 end page
thanks for all

Markdown filter is broken

Hi, with the basic example in the README file i run the example code and got really good results except when using markdown filter. here is my code:

{{ content | markdown }}
from secretary import Renderer
engine = Renderer()
result = engine.render("template-book.odt", content='this is a **bold** text! ')
with open('rendered_document.odt', 'wb') as handler:
    handler.write(result)

and here is what I will get in the rendered ODT document:

b'<text:p text:style-name="Standard">this is a <text:span text:style-
name="markdown_bold">bold</text:span> text!</text:p>\n'

the final ODT formated text is kinda escaped in the rendered document.
please help me fix this bug :)

It looks like the secretary tool doesn't work with the latest Jinja

I have Jinja2=3.0.1 and I see "jinja2.nodes.EvalContext object at ...." in the document fields instead of the needed content.
I could get the secretary tool working after Jinja2 downgrade to 2.11.2
I use python 3.8 in my project and use the tool with OpenOffice odt document

Thank you

odp support

secretary is great to templating odt files (Libre Office Writer files).
I tried with an odp file (Libre Office Impress file), there is no error but the {{ x }} are all empty on the generated document !

Can I create links?

I read the hyperlink section in the readme but it looks like it only applies to links with variable in them? I would like to make a text link such as some text like "click me" that takes you to a link when you click it and the link destination itself is variable. Is something like this currently possible with this library?

[development] Markdown filter does not work anymore

I already said that I found an issue with the markdown filter but you did not seem to reproduce the issue... So I re-checked it more seriously and I can say I don't have any issue with the master branch, but it does not work on development.

Template

RAW CONTENTย :
{{content}}
FILTERED CONTENTย :
{{content|markdown}}
FILTERED CONTENT WITH SAFEย :
{{content|markdown|safe}}

Code

from secretary import Renderer
template = 'template.odt'
engine = Renderer()
result = engine.render(template, content="hello world")
output = open('rendered_document.odt', 'wb')
output.write(result)

Output on master branch

RAW CONTENT :
hello world
FILTERED CONTENT :
hello world
FILTERED CONTENT WITH SAFE :
hello world

Output on development branch

RAW CONTENT :
hello world
FILTERED CONTENT :

FILTERED CONTENT WITH SAFE :

Handle flat ODT files

Libreoffice handles different files, for example *.fodt files are flat XML ODT files. They're useful as all the data is in a single uncompressed xml file that you can then track using a source code manager. Currently you expect all files received to be *.odt.

image

Hello,
is it possible to add more than on image in page ?
thanks for all

solved

simple_template.odt pytest sample data

I have an issue with jinja for loop in my odt template.
I looked over the simple_template.odt file used in test_secretary.py.
The template use a jinjo loop to iterate over a countries list.
But there is nothing in the single test file about this countries data structure.

Am I missing something ?

Markdown tables to LibreOffice tables

Hello (me again!)

Current markdown filter does not convert markdown tables. I tried to modify the code myself but without success...


There are 4 main steps to achieve this if I understand correctly:

  • markdown function must be called with an additional argument: mardown(markdown_text, extras=['tables'])
  • table, tr, and td must be added to markdown_map.py, I think it looks like this:
    'table': {
		'replace_with': 'table:table',
		'attributes': {
			'table:name': 'Table' + str(randint(100000000000000000,900000000000000000)),
			'table:style-name': str(randint(100000000000000000,900000000000000000))
		}
	},
	'tr': {
		'replace_with': 'table:table-row',
		'attributes': {
			'table:style-name': str(randint(100000000000000000,900000000000000000))
		}
	},
	'td': {
		'replace_with': 'table:table-cell',
		'attributes': {
			'table:style-name': str(randint(100000000000000000,900000000000000000)),
			'office:value-type': "string"
		}
	}
  • Automatically add text:p child to td elements : the exact same thing that is already done for li tag, line 728 in secretary.py
  • One thing I don't really know how to do it precisely: count the number of columns in the table to add the <table:table-column table:style-name="TestListTable.A"/> lines in the table header.

Markdown filter and paragraph line break

Hi !
It seems that there is a problem with the mardown filter.

What I see

A line break in a markdown paragraph trigger the creation of a line break in the ODT.

fdsfdsf fdsf dsf fdsfdsf fdsfdsfdsf
fds fdsf fdsg gds gfdsg fdsg fds gfd

This generates a line break in the ODT which renders pretty badly with a justified text and a short line.

What I expect

My feeling is that a line break in a md paragraph should be rendered without line break in the ODT.
In markdown only a "double space" at the end of a line in paragraph triggers a linebreak in the result.
This avoid the text wrap in editor. You can just go at the line when you want and as soon as you do not double space you stay in the same paragraph.

Suggestion

I just discovered mistune a fast python markdown parser.
With mistune one can write a Renderer to output the parsed markdown. I started writing an ODTRenderer (it's quite easy !) but then found out that secretary has a markdown filter and stopped.

Such renderer as a markdown filter logic replacement may be easier to maintain.

What is you position about dependencies ?

Do you accept pull requests on this project ? I may be interested to do one on this subject.

Anyway Secretary is pretty cool ! Thank you for your effort on this !

BadZipFile Error

Attempting to pass an odt file to Renderer produces the following error:

2016-03-05 16:47:12,430 - DEBUG - Initing a Renderer instance
Template
 2016-03-05 16:47:12,431 - DEBUG - Initing a template rendering
 2016-03-05 16:47:12,431 - DEBUG - Unpacking template file
Traceback (most recent call last):
  File "./create_vs300.py", line 136, in <module>
    template = open('./styles/vs300.odt', 'r')
  File "./create_vs300.py", line 85, in fillOutDoc
    dict=answers_dict
  File "/home/mint/Python_Projects/virtualenvs/Document_Generator/lib/python3.4/site-packages/secretary.py", line 577, in render
    self.files = self._unpack_template(template)
  File "/home/mint/Python_Projects/virtualenvs/Document_Generator/lib/python3.4/site-packages/secretary.py", line 167, in _unpack_template
    archive = zipfile.ZipFile(template, 'r')
  File "/usr/lib/python3.4/zipfile.py", line 937, in __init__
    self._RealGetContents()
  File "/usr/lib/python3.4/zipfile.py", line 978, in _RealGetContents
    raise BadZipFile("File is not a zip file")
zipfile.BadZipFile: File is not a zip file

The .odt file I have does not appear to be corrupted. I can use it in LibreOffice just fine. Github won't let me upload an odt file for some reason, but if it's helpful, I can upload it elsewhere and post the link here.

Wrong additional line breaks in markdown

Markdown filter converts the text into pure html so there is no need to play with line breaks. Only one exception should be preformated text.

The implementation of node_to_string() and _node_to_str() is wrong, because it replaces all \n\n with new empty paragraph.

This is big problem for lists, because it adds unavoidable new lines around the list.

Lorem ipsum dolor sit amet, consectetuer adipiscing elit.

* list item 1
* list item 2

This should generate

<text:p text:style-name="Standard">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</text:p>
<text:list xml:id="list290974027745800426">
<text:list-item><text:p>list item 1</text:p></text:list-item>
<text:list-item><text:p>list item 2</text:p></text:list-item>
</text:list>

but current output is

<text:p text:style-name="Standard">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</text:p>
<text:p text:style-name="Standard"/>
<text:list xml:id="list290974027745800426"><text:line-break/>
<text:list-item><text:p>list item 1</text:p></text:list-item><text:line-break/>
<text:list-item><text:p>list item 2</text:p></text:list-item><text:line-break/>
</text:list><text:line-break/>

Main problem is unwanted <text:p text:style-name="Standard"/> before the list and <text:line-break/> after the list. Other line breaks are silently ignored.

It can be solved just by removing the replace functions .replace('\n\n', '<text:p text:style-name="Standard"/>' and .replace('\n', '<text:line-break/>') but this change breaks preformated texts.
Problem is in condition node.getAttribute('text:style-name') == 'Preformatted_20_Text' that is never true, because it is evaluated only on parent element and not on all its childs.

I would like to provide PR, but I don't know, which version is the future of the project. Development branch is broken and completely different, but seems to be the future. Or should I still post PRs against master branch?

Error in secretary.py

Line 466: self.log.error('Unescaped template was:\n{}'.format(template_string))
you are missing your parameter, needs to be
Line 466: self.log.error('Unescaped template was:\n{0}'.format(template_string))

TemplateSyntaxError at /generate/document/ unexpected '<'

Hello!
I'm trying to replicate your example in an existing app and I'm having this error, I tried many things but nothing is working, even replacing the query for a list the complete error below:

Request Method: GET
Request URL: https://xxxxxxxxxx.com/generate/document/
Django Version: 1.8.18
Exception Type: ย TemplateSyntaxError
Exception Value: unexpected '<'
Exception Location: /home/.../lib/python3.5/jinja2/parser.py in fail, line 59
Python Executable: /usr/local/bin/python3
Python Version: 3.5.5
Python Path: ['...']
Server time: Sat, 24 Feb 2018 00:25:59 +0000

There is something I could be missing?
Thank you in advance!

Update to 0.2.8 broke table template

I'm not sure what's happening here, but some table elements are obviously working fine... but others are broken.

I updated from 0.2.7 to 0.2.19 and noticed the issue, then started working my way back till it worked. Unfortunately, 0.2.7 was the last version that worked, and 0.2.8 is the one that broke it.

0.2.8: Fix #25. Some internal refactorings. Drop the minimal support for Jinja tags in plain text.

I am not using Jinja in plaintext... I'm indeed using InputFields, as you can see in the template.

I can email the template, but am not comfortable sharing it publicly.

Here's a high level view of the template and output:
template vs output

Here's a close-up of the problematic area...
template vs output

This worked fine previously, in version 0.2.7.

There's also an issue lower down, where one of the tables has an extra border that is not present in the template...

template vs output

template vs output

Am I doing something wrong? Or is this a regression?

Dynamic columns in tables

Is it possible to create tables with not predefined number of columns? For example, I need to generate report with table and columns are dates selected by user before.

Bug with markdown filter

Hello,
I am trying to use the markdown filter but it does not work well.

Writing this field in a document :
{{ description | markdown }}

I have this result in the generated .odt file :

<text:p text:style-name="Standard">This is a <text:span text:style-name="markdown_bold">text</text:span></text:p>

When I save the file into .fodt to see XML code, I get this :
<text:p text:style-name="P17">&lt;text:p text:style-name=&quot;Standard&quot;&gt;This is a &lt;text:span text:style-name=&quot;markdown_bold&quot;&gt;text&lt;/text:span&gt;&lt;/text:p&gt;<text:line-break/></text:p>

Seems that the XML code is not properly inserted into the .odt code.

edit: If I add | safe }} to the field, it does not insert anything at all.

href with template variables are escaped by libreoffice automatically

example

I always have to manually edit content.xml in the odt archive to fix this... it's a problem for all HREF type inputs, not just "document" type. I only chose the document type for the example above because it shows the escaping. ;)

Both OpenOffice and LibreOffice refuse to add a feature to allow disabling of certain auto-escape sequences, which is understandable...

So, do we think it's a good idea to try to unescape these before passing into Jinja?

Rendering may fail if autoescape is not enabled in user-provided jinja environment

To render XML, you need a jinja environment with autoescape that evaluates to True, otherwise _render_xml with fail with an expat error if you feed it strings like "m&m's":
ExpatError: ExpatError "not well-formed (invalid token)" at line 1, column 3210

You currently force autoescape when no environment is passed, which is good.
However, you don't force autoescape when the user provides its own environment.

Now, the problem is that the environment passed by the user may be shared, to produce output for templates of multiple types. For that, the user can provide (since jinja 2.4) in the Environment() constructor an autoescape a function that will return a boolean to tell if escaping is needed, based on the template filename extension. This means you can't just blindly force the autoescape to 'True', as that would break the environment for other (non-XML, non-HTML) template types.

I think the best way to fix this is:
in Renderer.render, check the self.environment.autoescape value. If the value is True, or if it's a function that evaluates to True, all is fine. If it's False, or a function that evaluates to False, then raise an error with sensible message

can you add inserting images functionality into your code

I have a draft for you reference.

#!/usr/bin/python

# * Copyright (c) 2012-2014 Christopher Ramirez [email protected]
# *
# * Licensed under the MIT license.

"""
Secretary
    This project is a document engine which make use of LibreOffice
    documents as templates and use the semantics of jinja2 to control
    variable printing and control flow.

    To render a template:
        engine = Renderer(template_file)
        result = engine.render(template_var1=...)
"""

from __future__ import unicode_literals#, print_function

import io
import re
import sys
import logging
import zipfile
import uuid
from xml.dom.minidom import parseString
import lxml.etree
from cStringIO import StringIO
from jinja2 import Environment, Undefined

ODDA_IMAGE_PREFIX = 'Pictures/odda-'          ################
JINJA_URI = 'http://jinja.pocoo.org/'


# Test python versions and normalize calls to basestring, unicode, etc.
try:
    unicode = unicode
except NameError:
    # 'unicode' is undefined, must be Python 3
    str = str
    unicode = str
    bytes = bytes
    basestring = (str, bytes)
else:
    # 'unicode' exists, must be Python 2
    str = str
    unicode = unicode
    bytes = str
    basestring = basestring


FLOW_REFERENCES = {
    'text:p':               'text:p',
    'paragraph':            'text:p',
    'before::paragraph':   'text:p',
    'after::paragraph':    'text:p',

    'table:table-row':     'table:table-row',
    'table-row':            'table:table-row',
    'row':                   'table:table-row',
    'before::table-row':   'table:table-row',
    'after::table-row':    'table:table-row',
    'before::row':          'table:table-row',
    'after::row':           'table:table-row',

    'table:table-cell':    'table:table-cell',
    'table-cell':           'table:table-cell',
    'cell':                  'table:table-cell',
    'before::table-cell':  'table:table-cell',
    'after::table-cell':   'table:table-cell',
    'before::cell':         'table:table-cell',
    'after::cell':          'table:table-cell',
}



# ---- Exceptions
class SecretaryError(Exception):
    pass

class UndefinedSilently(Undefined):
    # Silently undefined,
    # see http://stackoverflow.com/questions/6182498
    def silently_undefined(*args, **kwargs):
        return ''

    return_new = lambda *args, **kwargs: UndefinedSilently()

    __unicode__ = silently_undefined
    __str__ = silently_undefined
    __call__ = return_new
    __getattr__ = return_new

# ************************************************
#
#           SECRETARY FILTERS
#
# ************************************************

def pad_string(value, length=5):
    value = str(value)
    return value.zfill(length)


class Renderer(object):
    """
        Main engine to convert and ODT document into a jinja
        compatible template.

        Basic use example:
            engine = Renderer('template')
            result = engine.render()


        Renderer provides an enviroment variable which can be used
        to provide custom filters to the ODF render.

            engine = Renderer('template.odt')
            engine.environment.filters['custom_filer'] = filter_function
            result = engine.render()
    """

    def __init__(self, environment=None, **kwargs):
        """
        Create a Renderer instance.

        args:
            environment: Use this jinja2 enviroment. If not specified, we
                         create a new environment for this class instance.

        returns:
            None
        """
        self.log = logging.getLogger(__name__)
        self.log.debug('Initing a Renderer instance\nTemplate')

        self.images = {}      ###############

        if environment:
            self.environment = environment
        else:
            self.environment = Environment(undefined=UndefinedSilently, autoescape=True)
            # Register filters
            self.environment.filters['pad'] = pad_string
            self.environment.filters['markdown'] = self.markdown_filter

    def _unpack_template(self, template):
        # And Open/libreOffice is just a ZIP file. Here we unarchive the file
        # and return a dict with every file in the archive
        self.log.debug('Unpacking template file')

        archive_files = {}
        archive = zipfile.ZipFile(template, 'r')
        for zfile in archive.filelist:
            archive_files[zfile.filename] = archive.read(zfile.filename)

        return archive_files

        self.log.debug('Unpack completed')


    def _pack_document(self, files):
        # Store to a zip files in files
        self.log.debug('packing document')
        zip_file = io.BytesIO()

        zipdoc = zipfile.ZipFile(zip_file, 'a')
        for fname, content in files.items():
            if sys.version_info >= (2, 7):
                zipdoc.writestr(fname, content, zipfile.ZIP_DEFLATED)
            else:
                zipdoc.writestr(fname, content)

        # Save images in the "Pictures" sub-directory of the archive.
        if len(self.images):
            for identifier, data in self.images.iteritems():
                #print type(data)
                zipdoc.writestr(ODDA_IMAGE_PREFIX + identifier, data)

        self.log.debug('Document packing completed')

        return zip_file

    def _prepare_template_tags(self, xml_document):
        # Here we search for every field node present in xml_document.
        # For each field we found we do:
        # * if field is a print field ({{ field }}), we replace it with a
        #   <text:span> node.
        # 
        # * if field is a control flow ({% %}), then we find immediate node of
        #   type indicated in field's `text:description` attribute and replace
        #   the whole node and its childrens with field's content.
        # 
        #   If `text:description` attribute starts with `before::` or `after::`,
        #   then we move field content before or after the node in description.
        # 
        #   If no `text:description` is available, find the immediate common
        #   parent of this and any other field and replace its child and 
        #   original parent of field with the field content.
        # 
        #   e.g.: original
        #   <table>
        #       <table:row>
        #           <field>{% for bar in bars %}</field>
        #       </table:row>
        #       <paragraph>
        #           <field>{{ bar }}</field>
        #       </paragraph>
        #       <table:row>
        #           <field>{% endfor %}</field>
        #       </table:row>
        #   </table>
        #   
        #   After processing:
        #   <table>
        #       {% for bar in bars %}
        #       <paragraph>
        #           <text:span>{{ bar }}</text:span>
        #       </paragraph>
        #       {% endfor %}
        #   </table>

        self.log.debug('Preparing template tags')
        fields = xml_document.getElementsByTagName('text:text-input')

        # First, count secretary fields
        for field in fields:
            if not field.hasChildNodes():
                continue

            field_content = field.childNodes[0].data.strip()

            if not re.findall(r'(?is)^{[{|%].*[%|}]}$', field_content):
                # Field does not contains jinja template tags
                continue

            is_block_tag = re.findall(r'(?is)^{%[^{}]*%}$', field_content)
            self.inc_node_fields_count(field.parentNode,
                    'block' if is_block_tag else 'variable')

        # Do field replacement and moving
        for field in fields:
            if not field.hasChildNodes():
                continue

            field_content = field.childNodes[0].data.strip()

            if not re.findall(r'(?is)^{[{|%].*[%|}]}$', field_content):
                # Field does not contains jinja template tags
                continue

            is_block_tag = re.findall(r'(?is)^{%[^{}]*%}$', field_content)
            discard = field
            field_reference = field.getAttribute('text:description').strip().lower()

            if re.findall(r'\|markdown', field_content):
                # a markdown field should take the whole paragraph
                field_reference = 'text:p'

            if field_reference:
                # User especified a reference. Replace immediate parent node
                # of type indicated in reference with this field's content.
                node_type = FLOW_REFERENCES.get(field_reference, False)
                if node_type:
                    discard = self._parent_of_type(field, node_type)

                jinja_node = self.create_text_node(xml_document, field_content)

            elif is_block_tag:
                # Find the common immediate parent of this and any other field.
                while discard.parentNode.secretary_field_count <= 1:
                    discard = discard.parentNode

                if discard is not None:
                    jinja_node = self.create_text_node(xml_document,
                                                       field_content)

            else:
                jinja_node = self.create_text_span_node(xml_document,
                                                        field_content)

            parent = discard.parentNode
            if not field_reference.startswith('after::'):
                parent.insertBefore(jinja_node, discard)
            else:
                if discard.isSameNode(parent.lastChild):
                    parent.appendChild(jinja_node)
                else:
                    parent.insertBefore(jinja_node,
                                        discard.nextSibling)

            if field_reference.startswith(('after::', 'before::')):
                # Do not remove whole field container. Just remove the
                # <text:text-input> parent node if field has it.
                discard = self._parent_of_type(field, 'text:p')
                parent = discard.parentNode

            parent.removeChild(discard)

    def _unescape_entities(self, xml_text):
        # unescape XML entities gt and lt
        unescape_rules = {
            r'(?is)({[{|%].*)(&gt;)(.*[%|}]})': r'\1>\3',
            r'(?is)({[{|%].*)(&lt;)(.*[%|}]})': r'\1<\3',
            r'(?is)({[{|%].*)(<.?text:s.?>)(.*[%|}]})': r'\1 \3',
        }

        for p, r in unescape_rules.items():
            xml_text = re.sub(p, r, xml_text)

        return xml_text

    def _encode_escape_chars(self, xml_text):
        # Replace line feed and/or tabs within text span entities.
        find_pattern = r'(?is)<text:([\S]+?)>([^>]*?([\n|\t])[^<]*?)</text:\1>'
        for m in re.findall(find_pattern, xml_text):
            print(m[1])
            replacement = m[1].replace('\n', '<text:line-break/>')
            replacement = replacement.replace('\t', '<text:tab/>')
            xml_text = xml_text.replace(m[1], replacement)

        return xml_text

    def _render_xml(self, xml_document, **kwargs):
        # Prepare the xml object to be processed by jinja2
        self.log.debug('Rendering XML object')

        try:
            self._prepare_template_tags(xml_document)
            template_string = self._unescape_entities(xml_document.toxml())
            jinja_template = self.environment.from_string(template_string)
            result = jinja_template.render(**kwargs)
            result = self._encode_escape_chars(result)

            try:
                return parseString(result.encode('ascii', 'xmlcharrefreplace'))
            except:
                self.log.error('Error parsing XML result:\n%s', result, exc_info=True)
                raise

        except:
            self.log.error('Error rendering template:\n%s',
                           xml_document.toprettyxml(), exc_info=True)
            raise
        finally:
            self.log.debug('Rendering xml object finished')


    def render(self, template, **kwargs):
        """
            Render a template

            args:
                template: A template file. Could be a string or a file instance
                **kwargs: Template variables. Similar to jinja2

            returns:
                A binary stream which contains the rendered document.
        """

        self.log.debug('Initing a template rendering')
        self.files = self._unpack_template(template)

        # Keep content and styles object since many functions or
        # filters may work with then
        self.content = parseString(self.files['content.xml']) 
        self.styles = parseString(self.files['styles.xml'])
        self.manifest = parseString(self.files['META-INF/manifest.xml'])    ##############

        # Render content.xml
        self.content = self._render_xml(self.content, **kwargs)

        # Render styles.xml
        self.styles = self._render_xml(self.styles, **kwargs)

        # Render META-INF/manifest.xml
        self.manifest = self._render_xml(self.manifest, **kwargs)       ##############

        self.__prepare_namespaces()    ##############
        self.__replace_image_links()
        self.__add_images_to_manifest()

        self.log.debug('Template rendering finished')

        self.files['content.xml'] = self.content.encode('ascii', 'xmlcharrefreplace')
        self.files['styles.xml'] = self.styles.encode('ascii', 'xmlcharrefreplace')
        self.files['META-INF/manifest.xml'] = self.manifest.encode('ascii', 'xmlcharrefreplace')
        document = self._pack_document(self.files)
        return document.getvalue()


    def _parent_of_type(self, node, of_type):
        # Returns the first immediate parent of type `of_type`.
        # Returns None if nothing is found.

        if hasattr(node, 'parentNode'):
            if node.parentNode.nodeName.lower() == of_type:
                return node.parentNode
            else:
                return self._parent_of_type(node.parentNode, of_type)
        else:
            return None


    def create_text_span_node(self, xml_document, content):
        span = xml_document.createElement('text:span')
        text_node = self.create_text_node(xml_document, content)
        span.appendChild(text_node)

        return span

    def create_text_node(self, xml_document, text):
        """
        Creates a text node
        """
        return xml_document.createTextNode(text)

    def inc_node_fields_count(self, node, field_type='variable'):
        """ Increase field count of node and its parents """

        if node is None:
            return

        if not hasattr(node, 'secretary_field_count'):
            setattr(node, 'secretary_field_count', 0)

        if not hasattr(node, 'secretary_variable_count'):
            setattr(node, 'secretary_variable_count', 0)

        if not hasattr(node, 'secretary_block_count'):
            setattr(node, 'secretary_block_count', 0)

        node.secretary_field_count += 1
        if field_type == 'variable':
            node.secretary_variable_count += 1
        else:
            node.secretary_block_count += 1

        self.inc_node_fields_count(node.parentNode, field_type)


    def get_style_by_name(self, style_name):
        """
            Search in <office:automatic-styles> for style_name.
            Return None if style_name is not found. Otherwise
            return the style node
        """

        auto_styles = self.content.getElementsByTagName(
            'office:automatic-styles')[0]

        if not auto_styles.hasChildNodes():
            return None

        for style_node in auto_styles.childNodes:
            if style_node.hasAttribute('style:name') and \
               (style_node.getAttribute('style:name') == style_name):
               return style_node

        return None

    def insert_style_in_content(self, style_name, attributes=None,
        **style_properties):
        """
            Insert a new style into content.xml's <office:automatic-styles> node.
            Returns a reference to the newly created node
        """

        auto_styles = self.content.getElementsByTagName('office:automatic-styles')[0]
        style_node = self.content.createElement('style:style')

        style_node.setAttribute('style:name', style_name)
        style_node.setAttribute('style:family', 'text')
        style_node.setAttribute('style:parent-style-name', 'Standard')

        if attributes:
            for k, v in attributes.items():
                style_node.setAttribute('style:%s' % k, v)

        if style_properties:
            style_prop = self.content.createElement('style:text-properties')
            for k, v in style_properties.items():
                style_prop.setAttribute('%s' % k, v)

            style_node.appendChild(style_prop)

        return auto_styles.appendChild(style_node)

    def __prepare_namespaces(self):
        """create proper namespaces for our document
        """
        # create needed namespaces
        self.namespaces = dict(
            text="urn:text",
            draw="urn:draw",
            table="urn:table",
            office="urn:office",
            xlink="urn:xlink",
            svg="urn:svg",
            manifest="urn:manifest",
        )

        def _(s):
            return lxml.etree.parse(StringIO(s.toxml('utf-8'))).getroot().nsmap

        # copy namespaces from original docs
        self.namespaces.update(_(self.content))
        self.namespaces.update(_(self.styles))
        self.namespaces.update(_(self.manifest))


        # remove any "root" namespace as lxml.xpath do not support them
        self.namespaces.pop(None, None)

        # declare the Jinja2 namespace
        self.namespaces['py'] = JINJA_URI

        #print self.namespaces

    def __replace_image_links(self):
        """Replace links of placeholder images (the name of which starts with "odda.")
        to point to a file saved the "Pictures" directory of the archive.
        """
        if not len(self.images):
            return

        def _(s):
            image_expr = "//draw:frame[starts-with(@draw:name, 'odda.')]"

            content_tree = lxml.etree.parse(StringIO(s.toxml().encode('utf-8')))
            # Find draw:frame tags.
            draw_frames = content_tree.xpath(image_expr, namespaces=self.namespaces)
            for draw_frame in draw_frames:
                # Find the identifier of the image (py3o.[identifier]).
                image_id = draw_frame.attrib['{%s}name' % self.namespaces['draw']]
                image_id = image_id[5:]
                if image_id not in self.images:
                    raise ValueError(
                        "Can't find data for the image named 'odda.%s'; make "
                        "sure it has been added with the set_image_path or "
                        "set_image_data methods."
                        % image_id
                    )

                # Replace the xlink:href attribute of the image to point to ours.
                image = draw_frame[0]
                image.attrib['{%s}href' % self.namespaces['xlink']] = ODDA_IMAGE_PREFIX + image_id
            return lxml.etree.tostring(content_tree)

        self.manifest = _(self.manifest)
        self.content = _(self.content)
        self.styles = _(self.styles)


    def __add_images_to_manifest(self):
        """Add entries for odda images into the manifest file."""

        if not len(self.images):
            return

        def _(s):
            xpath_expr = "//manifest:manifest[1]"
            content_tree = lxml.etree.parse(StringIO(s))

            # Find manifest:manifest tags.
            manifest_e = content_tree.xpath(
                xpath_expr,
                namespaces=self.namespaces
            )
            if not manifest_e:
                return None   # TODO

            for identifier in self.images.keys():
                # Add a manifest:file-entry tag.
                lxml.etree.SubElement(
                    manifest_e[0],
                    '{%s}file-entry' % self.namespaces['manifest'],
                    attrib={
                        '{%s}full-path' % self.namespaces['manifest']: (
                            ODDA_IMAGE_PREFIX + identifier
                        ),
                        '{%s}media-type' % self.namespaces['manifest']: '',
                    }
                )
                return lxml.etree.tostring(content_tree)
        self.manifest = _(self.manifest)

    def set_image_path(self, identifier, path):
        """Set data for an image mentioned in the template.

        @param identifier: Identifier of the image; refer to the image in the
        template by setting "odda.[identifier]" as the name of that image.
        @type identifier: string

        @param path: Image path.
        @type data: string
        """

        f = file(path, 'rb')
        self.set_image_data(identifier, f.read())
        f.close()


    def set_image_data(self, identifier, data):
        """Set data for an image mentioned in the template.

        @param identifier: Identifier of the image; refer to the image in the
        template by setting "py3o.[identifier]" as the name of that image.
        @type identifier: string

        @param data: Contents of the image.
        @type data: binary
        """

        self.images[identifier] = data


    def markdown_filter(self, markdown_text):
        """
            Convert a markdown text into a ODT formated text
        """

        if not isinstance(markdown_text, basestring):
            return ''

        from xml.dom import Node
        from markdown_map import transform_map

        try:
            from markdown2 import markdown
        except ImportError:
            raise SecretaryError('Could not import markdown2 library. Install it using "pip install markdown2"')

        styles_cache = {}   # cache styles searching
        html_text = markdown(markdown_text)
        xml_object = parseString('<html>%s</html>' % html_text.encode('ascii', 'xmlcharrefreplace'))

        # Transform HTML tags as specified in transform_map
        # Some tags may require extra attributes in ODT.
        # Additional attributes are indicated in the 'attributes' property

        for tag in transform_map:
            html_nodes = xml_object.getElementsByTagName(tag)
            for html_node in html_nodes:
                odt_node = xml_object.createElement(transform_map[tag]['replace_with'])

                # Transfer child nodes
                if html_node.hasChildNodes():
                    for child_node in html_node.childNodes:
                        odt_node.appendChild(child_node.cloneNode(True))

                # Add style-attributes defined in transform_map
                if 'style_attributes' in transform_map[tag]:
                    for k, v in transform_map[tag]['style_attributes'].items():
                        odt_node.setAttribute('text:%s' % k, v)

                # Add defined attributes
                if 'attributes' in transform_map[tag]:
                    for k, v in transform_map[tag]['attributes'].items():
                        odt_node.setAttribute(k, v)

                    # copy original href attribute in <a> tag
                    if tag == 'a':
                        if html_node.hasAttribute('href'):
                            odt_node.setAttribute('xlink:href',
                                html_node.getAttribute('href'))

                # Does the node need to create an style?
                if 'style' in transform_map[tag]:
                    name = transform_map[tag]['style']['name']
                    if not name in styles_cache:
                        style_node = self.get_style_by_name(name)

                        if style_node is None:
                            # Create and cache the style node
                            style_node = self.insert_style_in_content(
                                name, transform_map[tag]['style'].get('attributes', None),
                                **transform_map[tag]['style']['properties'])
                            styles_cache[name] = style_node

                html_node.parentNode.replaceChild(odt_node, html_node)

        def node_to_string(node):
            result = node.toxml()

            # linebreaks in preformated nodes should be converted to <text:line-break/>
            if (node.__class__.__name__ != 'Text') and \
                (node.getAttribute('text:style-name') == 'Preformatted_20_Text'):
                result = result.replace('\n', '<text:line-break/>')

            # All double linebreak should be replaced with an empty paragraph
            return result.replace('\n\n', '<text:p text:style-name="Standard"/>')


        return ''.join(node_as_str for node_as_str in map(node_to_string,
                xml_object.getElementsByTagName('html')[0].childNodes))

def render_template(template, **kwargs):
    """
        Render a ODF template file
    """

    engine = Renderer(file)
    return engine.render(**kwargs)


if __name__ == "__main__":
    import os
    from datetime import datetime

    def read(fname):
        return open(os.path.join(os.path.dirname(__file__), fname)).read()

    document = {
        'datetime': datetime.now(),
        'md_sample': read('README.md')
    }

    countries = [
        {'country': 'United States', 'capital': 'Washington',
            'cities': ['miami', 'new york', 'california', 'texas', 'atlanta']},
        {'country': 'England', 'capital': 'London',
            'cities': ['gales']},
        {'country': 'Japan', 'capital': 'Tokio',
            'cities': ['hiroshima', 'nagazaki']},
        {'country': 'Nicaragua', 'capital': 'Managua',
            'cities': ['leon', 'granada', 'masaya']},
        {'country': 'Argentina',
            'capital': 'Buenos aires'},
        {'country': 'Chile', 'capital': 'Santiago'},
        {'country': 'Mexico', 'capital': 'MExico City',
            'cities': ['puebla', 'cancun']},
    ]

    render = Renderer()
    render.set_image_path('logo', 'images/new_logo.png')
    result = render.render('simple_template.odt', countries=countries, document=document)

    output = open('rendered.odt', 'wb')
    output.write(result)

    print("Template rendering finished! Check rendered.odt file.")

file type changed after render

Actually this is not a bug. But very interesting thing.

[desktop] ~/lib/secretary/samples/images $ file template.odt 
template.odt: OpenDocument Text
[desktop] ~/lib/secretary/samples/images $ file output.odt 
output.odt: Zip archive data, at least v2.0 to extract

This is a result of render.

Insert a formula in a table cell

Would bery useful to have the possibility to pass a formula as a value for a tag so that it gets converted to a table formula (if this is inside a cell). I think it should be possible by recognizing a special reference property (formula::?)

Creating tables

Do you have an example of using a table with secretary? all the methods i tried seemed to create errors or extra rows in between

Markdown cannot handle ordered lists (with numbers) ?

Hi there,

could it be that the markdown filter is somehow buggy? I set up this example repo, which shows what I am doing:

https://github.com/Tagirijus/secretary_bug

So basically I have a variable containing:

1. One
2. Two
3. Three

And I'd expect secretary to render it (with the |markdown filter) as a list with numbers. Instead I just get a list with bullets.

Am I doing somethign wrong or is this a bug? I already upgraded to the secretary version v0.2.19.

Thanks for your help! (=

Crash while catching exception in_render_xml()

Hi !

While I was modifying a piece of code to manage markdown tables (and so was messing with the xml document), I found a bug:

[...]/secretary.py", line 554, in _render_xml
    near = result.split('\n')[e.lineno -1][e.offset-200:e.offset+200]
UnboundLocalError: local variable 'result' referenced before assignment

Indeed, the try block catches exception from a big piece of code and the exception is probably raised before the assignment of result.

I suggest to check the existence of result before using it in the except ExpatError block.

HTML unescaping only replaces first ocurrance of HTML codes and does not handle & or "

The helper method _unescape_entities(xml_text) does not handle HTML codes &amp; or &quot;. Unfortunately this precludes some useful template values such as {{ date.strftime("%Y-%m-%d") }}. In addition, any expression that involves more than one instance of escaped characters would also fail due to the regular expression substitution being performed only once.

new lines in tags with attributes

The issue #8 was only resolved for line breaks in variables that occur within tags that do not contain attributes, like <text:span text:style-name="T5">.

The cause is the regex in _encode_escape_chars

Slow rendering even with a tiny amount of data

I've found out that if I try to render a simple document with a table cycling about a hundred records the rendering time is very slow.

Profiling the code revealed that almost all of the time is spent inside the function: "_encode_escape_chars"; I guess the regexp is poorly performing.

I tried monkey patching the class with:

Renderer._encode_escape_chars = lambda s, v: v

and the render was fast.

For comparison: normal render 7.5 sec, without that function: 0.3 sec

I know that this function correctly escapes new lines, tabs etc. but I think there should be a way to escape the values before begin rendered in the final xml.

If you need an example just let me know.
Btw I'm using python 3.5.2 on Ubuntu

Get list of tags in template

I need to get a list of tags in a template document, but I can't figure how to do it. Is there any public function to use? Thanks.

Explain how to conditionally display a page in the doc

I have a document that may include a page or not. This page is using a different page style.

Right now I'm trying to do:

{% if condition %} <= this is at the beginning on the optional page
Optional page content
<Page break>
{% endif %} <= this is at the beginning on the next page

It partially works as the content is conditionally inserted. However, it's not inserted in a separate page, and the custom page style is lost.

Installation Issue

Hello,
I try to install:

$ pip install secretrary
Collecting secretrary
Could not find a version that satisfies the requirement secretrary (from versions: )
No matching distribution found for secretrary

What is needed before that command?

Thank you

can not open odt file generated.

write a very simple test, can sucessfully get a odt file, but can not open it and get error hint as below,

#-*- coding:utf-8 -*-
from secretary import Render

engine = Render('simple_template.odt')

countries = 
[
    {
        'country': 'China',
        'capital': 'Beijing',
        'cities':['Suzhou', 'Wuhan', 'Hefei', 'Jiangying'],
    }
]

#Configure custom application filters
#engine.environment.filters['custom_filer'] = filter_function
result = engine.render(countries=countries)

output = open('rendered_document.odt', 'w')
output.write(result)

ERROR

The file 'rendered_document.odt' is corrupt and therefore cannot be opened. LibreOffice can try to repair the file.

The corruption could be the result of document manipulation or of structural document damage due to data transmission.

We recommend that you do not trust the content of the repaired document.
Execution of macros is disabled for this document.

Should LibreOffice repair the file?

test enviroment

python 2.7.6
windows 7

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.