diff --git a/__init__.py b/__init__.py
index f6150d0..b0afc21 100644
--- a/__init__.py
+++ b/__init__.py
@@ -24,10 +24,309 @@
"""
Secretary
-Take the power of Django or Jinja2 templates to OpenOffice and LibreOffice.
+Take the power of Jinja2 templates to OpenOffice and LibreOffice.
-Basic usage:
- from secretary.renders import BaseRender
-
- to be added...
+This file implements BaseRender. BaseRender prepares a XML which describes
+ODT document content to be processed by jinja2 or Django template system.
"""
+
+import re
+import os
+import sys
+import logging
+import zipfile
+import StringIO
+import xml.dom.minidom
+from os.path import isfile
+from jinja2 import Environment, Undefined
+
+
+PARAGRAPH_TAG = '{% control_paragraph %}'
+TABLEROW_TAG = '{% control_tablerow %}'
+TABLECELL_TAG = '{% control_tablecell %}'
+
+ODF_PARAGRAPH_NODE = 'text:p'
+ODF_TABLEROW_NODE = 'table:table-row'
+ODF_TABLECELL_NODE = 'table:table-cell'
+
+
+class UndefinedSilently(Undefined):
+ # Silently undefined,
+ # see http://stackoverflow.com/questions/6182498/jinja2-how-to-make-it-fail-silently-like-djangotemplate
+ def silently_undefined(*args, **kwargs):
+ return u''
+
+ 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 Render():
+ """
+ Prapares a XML string or file to be processed by a templating system.
+
+ Use example:
+ render = BaseRender('content/xml', var1, var2.. varN)
+ render.render
+ """
+
+ _template = None
+ _environment = None
+ _working_template = None
+ _unpacked_template = None
+ _packed_template = None
+ _content_file = None
+ _style_file = None
+ _mimetype = ''
+
+
+ @property
+ def enviroment(self):
+ return self._environment
+ @enviroment.setter
+ def enviroment(self, value):
+ self._environment = value
+
+ @property
+ def template(self):
+ return self._template
+ @template.setter
+ def template(self, value):
+ self._template = value
+
+
+ # def __init__(self, xml_doc, template_args):
+ def __init__(self, template, **kwargs):
+ """
+ Builds a ODFRender instance
+ TODO: document
+ """
+
+ self.template = template
+ self._environment = Environment(undefined=UndefinedSilently)
+ self._environment.filters['pad'] = pad_string
+
+
+ def unpack_template(self):
+ """
+ Loads the template into a ZIP file, allowing to make
+ CRUD operations into the ZIP archive.
+ """
+
+ if os.path.exists(self.template):
+ f = open(self.template, 'r')
+ self._unpacked_template = zipfile.ZipFile(f, "r" )
+ else:
+ self._unpacked_template = zipfile.ZipFile(self._template, "r" )
+
+ # go through the files in source
+ for zi in self._unpacked_template.filelist:
+ archive_file = self._unpacked_template.read( zi.filename )
+
+ if zi.filename == 'content.xml':
+ self._content_file = archive_file
+ elif zi.filename == 'styles.xml':
+ self._style_file = archive_file
+ elif zi.filename == 'mimetype':
+ self._mimetype = archive_file
+
+ def pack_document(self):
+ """
+ Make an archive from _unpacked_template
+ """
+ self.rendered = StringIO.StringIO()
+ self._packed_template = zipfile.ZipFile(self.rendered, 'a')
+
+ for zip_file in self._unpacked_template.filelist:
+ out = self._unpacked_template.read( zip_file.filename )
+
+ if zip_file.filename == 'mimetype':
+ # mimetype is stored within the ODF
+ mimetype = self._mimetype
+
+ if zip_file.filename == 'content.xml':
+ out = self._content_file
+
+ if zip_file.filename == 'styles.xml':
+ out = self._style_file
+
+ if sys.version_info >= (2, 7):
+ self._packed_template.writestr(zip_file.filename, out, zipfile.ZIP_DEFLATED)
+ else:
+ self._packed_template.writestr(zip_file.filename, out)
+
+ self._packed_template.close()
+ self._unpacked_template.close()
+
+
+ def render(self, **kwargs):
+ """
+ Unpack and render the internal template
+ """
+
+ self.unpack_template()
+ self._template_vars = kwargs
+
+ # Load content.xml and style.xml file
+ self.content = xml.dom.minidom.parseString(self._content_file)
+ body = self.content.getElementsByTagName('office:body')
+ self.content_body = body and body[0]
+
+ self.styles = xml.dom.minidom.parseString(self._style_file)
+ body = self.styles.getElementsByTagName('office:master-styles')
+ self.headers = body and body[0]
+
+ # Render content.xml
+ self.prepare_template_tags(self.content_body)
+ template = self._environment.from_string(self.content.toxml())
+ result = template.render(**kwargs)
+ result = result.replace('\n', '')
+ self.content = xml.dom.minidom.parseString(result.encode('ascii', 'xmlcharrefreplace'))
+ self.content_body = self.content.getElementsByTagName('office:body')
+
+ # Render style.xml
+ self.prepare_template_tags(self.styles)
+ template = self._environment.from_string(self.styles.toxml())
+ result = template.render(**kwargs)
+ result = result.replace('\n', '')
+ self.styles = xml.dom.minidom.parseString(result.encode('ascii', 'xmlcharrefreplace'))
+ self.headers = None
+
+ # Save rendered content and headers
+ self._content_file = self.content.toxml().encode('ascii', 'xmlcharrefreplace')
+
+ self._style_file = (self.styles.toxml().encode('ascii', 'xmlcharrefreplace'))
+
+ self.pack_document()
+ return self.rendered
+
+
+ def node_parents(self, node, parent_type):
+ """
+ Returns the first node's parent with name of parent_type
+ If parent "text:p" is not found, returns None.
+ """
+
+ if hasattr(node, 'parentNode'):
+ if node.parentNode.nodeName.lower() == parent_type:
+ return node.parentNode
+ else:
+ return self.node_parents(node.parentNode, parent_type)
+ else:
+ return None
+
+
+
+ def render_with_engine(self):
+ """
+ Once the XML have been prepared, this routine is called
+ to do the actual rendering.
+ """
+
+
+
+ template = environment.from_string(self.xml_document.toxml())
+ rendered = template.render(**self.template_vars)
+
+ # Replace all \n in field values with a ODT line break
+ rendered = rendered.replace('\n', '')
+
+ return rendered
+
+
+ def create_text_span_node(self, xml_document, content):
+ span = self.content.createElement('text:span')
+ text_node = self.create_text_node(self.content, content)
+ span.appendChild(text_node)
+
+ return span
+
+ def create_text_node(self, xml_document, text):
+ """
+ Creates a text node
+ """
+ return self.content.createTextNode(text)
+
+
+ def prepare_template_tags(self, xml_document):
+ """
+ Search every field node in the inner template and
+ replace them with a field. Flow tags are
+ replaced with a blank node and moved into the ancestor
+ tag defined in description field attribute.
+ """
+ fields = xml_document.getElementsByTagName('text:text-input')
+
+ for field in fields:
+ if field.hasChildNodes():
+ field_content = field.childNodes[0].data.replace('\n', '')
+
+ jinja_tags = re.findall(r'(\{.*?\}*})', field_content)
+ if not jinja_tags:
+ # Field does not contains jinja template tags
+ continue
+
+ field_description = field.getAttribute('text:description')
+
+ if not field_description:
+ new_node = self.create_text_span_node(xml_document, field_content)
+ else:
+ if field_description in \
+ ['text:p', 'table:table-row', 'table:table-cell']:
+ field = self.node_parents(field, field_description)
+
+ new_node = self.create_text_node(xml_document, field_content)
+
+ parent = field.parentNode
+ parent.insertBefore(new_node, field)
+ parent.removeChild(field)
+
+
+def render_template(template, **kwargs):
+ """
+ Render a ODF template file
+ """
+
+ engine = Render(file)
+ return engine.render(**kwargs)
+
+
+if __name__ == "__main__":
+ from sys import argv
+ from datetime import datetime
+
+ document = {
+ 'datetime': datetime.now()
+ }
+
+ 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': [u'león', 'granada', 'masaya']},
+ {'country': 'Argentina', 'capital': 'Buenos aires'},
+ {'country': 'Chile', 'capital': 'Santiago'},
+ {'country': 'Mexico', 'capital': 'MExico City', 'cities': ['puebla', 'cancun']},
+ ]
+
+
+ render = Render('simple_template.odt')
+ result = render.render(countries=countries, document=document)
+
+ output = open('rendered.odt', 'w')
+ output.write(result.getvalue())
+
+ print "Template rendering finished! Check rendered.odt file."
\ No newline at end of file
diff --git a/renders.py b/renders.py
deleted file mode 100644
index 57d6350..0000000
--- a/renders.py
+++ /dev/null
@@ -1,296 +0,0 @@
-#!/usr/bin/python
-# -*- encoding: utf-8 -*-
-
- # * Copyright (c) 2012 Christopher Ramírez blindedbythedark [at} gmail (dot] com.
- # * All rights reserved.
- # *
- # * Permission is hereby granted, free of charge, to any person obtaining a
- # * copy of this software and associated documentation files (the "Software"),
- # * to deal in the Software without restriction, including without limitation
- # * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- # * and/or sell copies of the Software, and to permit persons to whom the
- # * Software is furnished to do so, subject to the following conditions:
- # *
- # * The above copyright notice and this permission notice shall be included in
- # * all copies or substantial portions of the Software.
- # *
- # * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- # * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- # * DEALINGS IN THE SOFTWARE.
-
-"""
-Secretary
-Take the power of Jinja2 templates to OpenOffice and LibreOffice.
-
-This file implements BaseRender. BaseRender prepares a XML which describes
-ODT document content to be processed by jinja2 or Django template system.
-"""
-
-import re
-import os
-import sys
-import logging
-import zipfile
-import StringIO
-import xml.dom.minidom
-from os.path import isfile
-from jinja2 import Environment, Undefined
-
-
-PARAGRAPH_TAG = '{% control_paragraph %}'
-TABLEROW_TAG = '{% control_tablerow %}'
-TABLECELL_TAG = '{% control_tablecell %}'
-
-OOO_PARAGRAPH_NODE = 'text:p'
-OOO_TABLEROW_NODE = 'table:table-row'
-OOO_TABLECELL_NODE = 'table:table-cell'
-
-
-# Silently undefined,
-# see http://stackoverflow.com/questions/6182498/jinja2-how-to-make-it-fail-silently-like-djangotemplate
-def silently_undefined(*args, **kwargs):
- return u''
-
-return_new = lambda *args, **kwargs: UndefinedSilently()
-
-class UndefinedSilently(Undefined):
- __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 BaseRender():
- """
- Prapares a XML string or file to be processed by a templating system.
-
- Use example:
- render = BaseRender('content/xml', var1, var2.. varN)
- render.render
- """
-
- def __init__(self, xml_doc, template_args):
- self.template_vars = template_args
- self.xml_document = xml.dom.minidom.parseString(xml_doc)
- self.debug = template_args.get('debug', False)
-
- body = self.xml_document.getElementsByTagName('office:body') or \
- self.xml_document.getElementsByTagName('office:master-styles')
-
- self.content_body = body and body[0]
-
- # ------------------------------------------------------------------------@
-
-
- def get_parent_of(self, node, parent_type):
- """
- Returns the first node's parent with name of parent_type
- If parent "text:p" is not found, returns None.
- """
-
- if hasattr(node, 'parentNode'):
- if node.parentNode.nodeName.lower() == parent_type:
- return node.parentNode
- else:
- return self.get_parent_of(node.parentNode, parent_type)
- else:
- return None
-
- # ------------------------------------------------------------------------@
-
-
- def render_with_engine(self):
- """
- Once the XML have been prepared, this routine is called
- to do the actual rendering.
- """
-
- environment = Environment(undefined=UndefinedSilently)
- environment.filters['pad'] = pad_string
-
- template = environment.from_string(self.xml_document.toxml())
- rendered = template.render(**self.template_vars)
-
- # Replace all \n in field values with a ODT line break
- rendered = rendered.replace('\n', '')
-
- # if self.debug:
- # # Return a indented XML
- # return xml.dom.minidom.parseString(
- # rendered.encode('ascii', 'xmlcharrefreplace')).toprettyxml()
-
-
- return rendered
-
-
- # -----------------------------------------------------------------------
-
- def create_text_span_node(self, content):
- span = self.xml_document.createElement('text:span')
- text_node = self.create_text_node(content)
- span.appendChild(text_node)
-
- return span
-
- def create_text_node(self, text):
- """
- Creates a text node
- """
- return self.xml_document.createTextNode(text)
-
-
- def prepare_document(self):
- """
- Search in every field node in the document and
- replace it with a field
- """
- fields = self.content_body.getElementsByTagName('text:text-input')
-
- for field in fields:
- if field.hasChildNodes():
- field_content = field.childNodes[0].data
-
- jinja_tags = re.findall(r'(\{.*?\}*})', field_content)
- if not jinja_tags:
- # Field does not contains jinja template tags
- continue
-
- field_description = field.getAttribute('text:description')
-
- if not field_description:
- new_node = self.create_text_span_node(field_content)
- else:
- if field_description in \
- ['text:p', 'table:table-row', 'table:table-cell']:
- field = self.get_parent_of(field, field_description)
-
- new_node = self.create_text_node(field_content)
-
- parent = field.parentNode
- parent.insertBefore(new_node, field)
- parent.removeChild(field)
-
-
- def render(self):
- """
- render prepares the XML and the call render_with_engine
- to parse template engine tags
- """
-
- self.prepare_document()
- return self.render_with_engine()
-
- # -----------------------------------------------------------------------
-
-
-def render_template(template, **kwargs):
- """
- Renders *template* file using *kwargs* variables.
- Returns the ODF file generated.
- """
- input_template = StringIO.StringIO(template)
- input = zipfile.ZipFile(input_template, "r" )
- rendered = StringIO.StringIO()
- output = zipfile.ZipFile(rendered, 'a')
-
- # go through the files in source
- for zi in input.filelist:
- out = input.read( zi.filename )
-
- if zi.filename in ('content.xml', 'styles.xml'):
- render = BaseRender(out, kwargs)
- out = render.render().encode('ascii', 'xmlcharrefreplace')
-
- elif zi.filename == 'mimetype':
- # mimetype is stored within the ODF
- mimetype = out
-
- if sys.version_info >= (2, 7):
- output.writestr(zi.filename, out, zipfile.ZIP_DEFLATED)
- else:
- output.writestr(zi.filename, out)
-
- # close and finish
- input.close()
- output.close()
-
- return rendered.getvalue()
-
-
-def render_template_file(file, **kwargs):
- """
- Render a ODF template file
- """
-
- template_string = StringIO.StringIO()
- template_string.write(file)
-
- return render_template(template_string, **kwargs)
-
-
-if __name__ == "__main__":
- from sys import argv
- from datetime import datetime
-
- document = {
- 'datetime': datetime.now()
- }
-
- 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': [u'león', 'granada', 'masaya']},
- {'country': 'Argentina', 'capital': 'Buenos aires'},
- {'country': 'Chile', 'capital': 'Santiago'},
- {'country': 'Mexico', 'capital': 'MExico City', 'cities': ['puebla', 'cancun']},
- ]
-
- # ODF is just a zipfile
- input = zipfile.ZipFile( 'simple_template.odt', "r" )
-
- if len(argv) > 1:
- if isfile(argv[1]):
- input = zipfile.ZipFile(argv[1])
-
-
- text = open('rendered.odt', 'wb')
- output = zipfile.ZipFile( text, "w" )
-
- # go through the files in input
- for zi in input.filelist:
- out = input.read( zi.filename )
-
- if zi.filename == 'content.xml':
- render = BaseRender(out, document=document, countries=countries)
- out = render.render().encode('ascii', 'xmlcharrefreplace')
-
- elif zi.filename == 'mimetype':
- # mimetype is stored within the ODF
- mimetype = out
-
- output.writestr( zi.filename, out,
- zipfile.ZIP_DEFLATED )
-
- # close and finish
- input.close()
- output.close()
-
- print "Template rendering finished! Check rendered.odt file."
-
-
-
diff --git a/simple_template.odt b/simple_template.odt
index a1f84a2..d2c759c 100644
Binary files a/simple_template.odt and b/simple_template.odt differ