From 589262664f30890e58e8cabbec0c80321f4ccdf2 Mon Sep 17 00:00:00 2001 From: Eric Lapouyade Date: Mon, 20 Dec 2021 12:07:12 +0100 Subject: [PATCH] fix #392 --- CHANGES.rst | 17 ++----- docxtpl/__init__.py | 2 +- docxtpl/template.py | 56 ++++++++++++++++++++--- tests/multi_rendering.py | 40 ++++++++++++++++ tests/templates/multi_rendering_tpl.docx | Bin 0 -> 9259 bytes 5 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 tests/multi_rendering.py create mode 100644 tests/templates/multi_rendering_tpl.docx 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 0000000000000000000000000000000000000000..929d1e88a88cba2ad70e7ea4be2d1c6012e3cefa GIT binary patch literal 9259 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 Z91>(CU>{+HfIxoyH9z(f1)`^;{|6R-g`fZc literal 0 HcmV?d00001