diff --git a/CHANGES.rst b/CHANGES.rst index 5626c55..0b512fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +0.15.0 (2021-12-20) +------------------- +- Multi-rendering with same DocxTemplate object is now possible + see tests/multi_rendering.py + 0.14.1 (2021-10-01) ------------------- - One can now use python -m docxtpl on command line @@ -22,21 +27,9 @@ 0.11.5 (2021-05-09) ------------------- - PR #351 - -0.11.4 (2021-04-06) -------------------- - It is now possible to put InlineImage in header/footer - -0.11.2 (2020-11-09) -------------------- - fix #323 - -0.11.1 (2020-10-27) -------------------- - fix #320 - -0.11.0 (2020-10-19) -------------------- - \\n, \\a, \\t and \\f are now accepted in simple context string. Thanks to chabErch@github 0.10.5 (2020-10-15) diff --git a/docxtpl/__init__.py b/docxtpl/__init__.py index 38a3f4f..bc11be4 100644 --- a/docxtpl/__init__.py +++ b/docxtpl/__init__.py @@ -4,7 +4,7 @@ Created : 2015-03-12 @author: Eric Lapouyade """ -__version__ = '0.14.2' +__version__ = '0.15.0' # flake8: noqa from .inline_image import InlineImage diff --git a/docxtpl/template.py b/docxtpl/template.py index 1054792..b615f80 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -34,15 +34,24 @@ class DocxTemplate(object): HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" - def __init__(self, docx): - self.docx = Document(docx) - self.crc_to_new_media = {} - self.crc_to_new_embedded = {} - self.zipname_to_replace = {} - self.pics_to_replace = {} + def __init__(self, template_file): + self.template_file = template_file + self.reset_replacements() + self.docx = None + self.is_rendered = False + self.is_saved = False + + def init_docx(self): + if not self.docx or self.is_rendered: + self.docx = Document(self.template_file) + self.is_rendered = False + + def render_init(self): + self.init_docx() self.pic_map = {} self.current_rendering_part = None self.docx_ids_index = 1000 + self.is_saved = False def __getattr__(self, name): return getattr(self.docx, name) @@ -53,6 +62,7 @@ class DocxTemplate(object): return etree.tostring(xml, encoding='unicode', pretty_print=False) def get_docx(self): + self.init_docx() return self.docx def get_xml(self): @@ -307,6 +317,9 @@ class DocxTemplate(object): self.docx._part._rels[relKey]._target = new_part def render(self, context, jinja_env=None, autoescape=False): + # init template working attributes + self.render_init() + if autoescape: if not jinja_env: jinja_env = Environment(autoescape=autoescape) @@ -337,6 +350,9 @@ class DocxTemplate(object): for relKey, xml in footers: self.map_headers_footers_xml(relKey, xml) + # set rendered flag + self.is_rendered = True + # using of TC tag in for cycle can cause that count of columns does not # correspond to real count of columns in row. This function is able to fix it. def fix_tables(self, xml): @@ -432,6 +448,7 @@ class DocxTemplate(object): elt.attrib['id'] = str(self.docx_ids_index) def new_subdoc(self, docpath=None): + self.init_docx() return Subdoc(self, docpath) @staticmethod @@ -540,6 +557,27 @@ class DocxTemplate(object): with open(dst_file, 'rb') as fh: self.zipname_to_replace[zipname] = fh.read() + def reset_replacements(self): + """Reset replacement dictionnaries + + This will reset data for image/embedded/zipname replacement + + This is useful when calling several times render() with different + image/embedded/zipname replacements without re-instantiating + DocxTemplate object. + In this case, the right sequence for each rendering will be : + - reset_replacements(...) + - replace_zipname(...), replace_media(...) and/or replace_embedded(...), + - render(...) + + If you instantiate DocxTemplate object before each render(), + this method is useless. + """ + self.crc_to_new_media = {} + self.crc_to_new_embedded = {} + self.zipname_to_replace = {} + self.pics_to_replace = {} + def post_processing(self, docx_file): if (self.crc_to_new_media or self.crc_to_new_embedded or @@ -662,13 +700,19 @@ class DocxTemplate(object): self.pic_map.update(part_map) def build_url_id(self, url): + self.init_docx() return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, is_external=True) def save(self, filename, *args, **kwargs): + # case where save() is called without doing rendering + # ( user wants only to replace image/embedded/zipname ) + if not self.is_saved and not self.is_rendered: + self.docx = Document(self.template_file) self.pre_processing() self.docx.save(filename, *args, **kwargs) self.post_processing(filename) + self.is_saved = True def get_undeclared_template_variables(self, jinja_env=None): xml = self.get_xml() diff --git a/tests/multi_rendering.py b/tests/multi_rendering.py new file mode 100644 index 0000000..7dec863 --- /dev/null +++ b/tests/multi_rendering.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +''' +Created : 2021-12-20 + +@author: Eric Lapouyade +''' + +from docxtpl import DocxTemplate + +tpl = DocxTemplate('templates/multi_rendering_tpl.docx') + +documents_data = [ + { + 'dest_file': 'multi_render1.docx', + 'context': { + 'title': 'Title ONE', + 'body': 'This is the body for first document' + } + }, + { + 'dest_file': 'multi_render2.docx', + 'context': { + 'title': 'Title TWO', + 'body': 'This is the body for second document' + } + }, + { + 'dest_file': 'multi_render3.docx', + 'context': { + 'title': 'Title THREE', + 'body': 'This is the body for third document' + } + }, +] + +for document_data in documents_data: + dest_file = document_data['dest_file'] + context = document_data['context'] + tpl.render(context) + tpl.save(f'output/{dest_file}') diff --git a/tests/templates/multi_rendering_tpl.docx b/tests/templates/multi_rendering_tpl.docx new file mode 100644 index 0000000..929d1e8 Binary files /dev/null and b/tests/templates/multi_rendering_tpl.docx differ