elapouya / python-docx-template

Use a docx as a jinja2 template
GNU Lesser General Public License v2.1
1.92k stars 378 forks source link

Passing context variables to Templates within templates ( It has a workaround but it takes more processing time. Searching for an efficient feasible approach) #502

Open Tanishqmalu opened 12 months ago

Tanishqmalu commented 12 months ago

Describe your problem

Is there a way to add new sub documents within the main document, where we can pass context variables to the sub documents that are being used in the sub document, then render it and then add it to the main document.

More details about your problem

1. Suppose your main document("test1.docx") is like:

{{ Title }} I am from main document. {{ commandList.insert_docx_template("test2.docx") }}

2. Sub-document("test2.docx"):

{{ Name }} I am from sub document.

3. Final output(expected):

Test Report I am from main document. Subdocument report I am from sub document.

Python Code

from pathlib import Path
from docx import Document
from docxtpl import DocxTemplate

tmplPath = "test1.docx"
doc = DocxTemplate(tmplPath)
context = {
    "Title": "Test report"
}

def insert_docx_template(sub_document_path, **kwargs):
    context['name'] = "Subdocument report"                 # I want to pass such variables to the context of new sub document
    res = doc.new_subdoc(sub_document_path)
    return res 

command = {"insert_docx_template": insert_docx_template}
context["commandList"] = command

# render context into the document object
doc.render(context)
resultFilePath = 'report_test.docx'
doc.save(resultFilePath)

The _new_subdoc function in the DocxTemplate_ class takes only path as the input. Thus, the context variables cant be passed to it. (Attaching the code snippet)

    def new_subdoc(self, docpath=None):
        self.init_docx()
        return Subdoc(self, docpath)

Is there a way to pass context variables to the sub document and then render it and then add it in our main document.

Workaround

One obvious work around is to

  1. Create a new file with the context variables.
  2. Save it
  3. Merge it into the main document as a sub document
  4. Then delete the file as it wont be required any longer.
    def insert_docx_template(sub_document_path, **kwargs):
    temp = DocxTemplate(sub_document_path)
    context['name'] = "Sub_Document"
    temp.render(context)
    resultFilePath = 'report_temp.docx'
    temp.save(resultFilePath)
    res = doc.new_subdoc(resultFilePath)                      # doc = **DocxTemplate** object for the main document
    os.remove(resultFilePath)
    return res 

However, if we have large number of queries of adding subdocument, creation and deletion of the temporary subdocument files would increase the processing time for the document. I am not sure if it would be so, as i did not try testing it on large number of queries. However it is quite obvious that this method would take more time in comparison to a method(if exsists) where the variables can be simply rendered by the _newsubdoc function and can be added directly without the need of creation of a temporary file.

Tanishqmalu commented 12 months ago

@elapouya One of the workaround which i have found is to create a docx object first:

**_docx = Document(sub-Document-path)_**

Then use the _buildxml function from the Docxtemplate class and insert the xml string generated for the Sub-document into the main document.

// Build XML
xml = etree.tostring((docx._element.body), encoding='unicode', pretty_print=False)
xml = patch_xml(xml)
xml = render_xml_part(xml, docx._part, context)

Current Issue is that tables and other similar items like image are not being transferred properly. For that, one can simply copy all the functions of docxTemplate class which tries to maintain the formatting of a doc. However, that would mean redundancy in code, so can we make a super class for DocxTemplate and SubDoc, so that they can inherit these common functions. ??

jbaehr66 commented 1 month ago

A variant on your workaround is to still do the two pass render - but use a ByteIO source for the final template - still a bit ugly - but better than nothing!

# This block renders the subdocuments into the master
subdoc_context = {}
for key, template in templates.items():
    if key != 'master':
        subdoc = templates[key]
        subdoc_context[f"{key}_doc"] = subdoc

# render the master document - aka render the subdocs in
doc.render(subdoc_context, jinja_env=jinja_env)

# No save the doc into a byteio
doc_io = BytesIO()
doc.save(doc_io)

# Not well documented but you can create the template from an IO object
final_docxtplt = DocxTemplate(doc_io)

# Now render out our final_docx
final_docxtplt.render(context, jinja_env=jinja_env)