I have a task of generating a DOCX file from a template and then serving it via Flask. I use python-docx-templates which is simply a wrapper around python-docx allowing for use of jinja templates.
In the end they suggest using StringIO to save file only in memory, so this is how my code looks like:
def report_doc(user_id):
# Prepare the data...
from docxtpl import DocxTemplate
doc = DocxTemplate(app.root_path+'/templates/report.docx')
doc.render({
# Pass parameters
})
from io import StringIO
file_stream = StringIO()
doc.save(file_stream)
return send_file(file_stream, as_attachment=True, attachment_filename='report_'+user_id+'.docx')
On saving it throws an error TypeError: string argument expected, got 'bytes'
. After googling it, I found this answer which says that ZipFile
expects BytesIO. However, when I substituted StringIO with BytesIO, it only returned an empty file, so it doesn't throw any error, but definitely doesn't save the file.
What exactly would work in this case? If something is entirely wrong here, how in general could this work?
Thank you!
UPD: Here's the exception with full trace to the save
function call:
File "/ms/controllers.py", line 1306, in report_doc
doc.save(file_stream)
File "/.env/lib/python3.5/site-packages/docx/document.py", line 142, in save
self._part.save(path_or_stream)
File "/.env/lib/python3.5/site-packages/docx/parts/document.py", line 129, in save
self.package.save(path_or_stream)
File "/.env/lib/python3.5/site-packages/docx/opc/package.py", line 160, in save
PackageWriter.write(pkg_file, self.rels, self.parts)
File "/.env/lib/python3.5/site-packages/docx/opc/pkgwriter.py", line 33, in write
PackageWriter._write_content_types_stream(phys_writer, parts)
File "/.env/lib/python3.5/site-packages/docx/opc/pkgwriter.py", line 45, in _write_content_types_stream
phys_writer.write(CONTENT_TYPES_URI, cti.blob)
File "/.env/lib/python3.5/site-packages/docx/opc/phys_pkg.py", line 155, in write
self._zipf.writestr(pack_uri.membername, blob)
File "/usr/lib/python3.5/zipfile.py", line 1581, in writestr
self.fp.write(zinfo.FileHeader(zip64))
TypeError: string argument expected, got 'bytes'
Using a BytesIO
instance is correct, but you need to rewind the file pointer before passing it to send_file
:
Make sure that the file pointer is positioned at the start of data to send before calling send_file().
So this should work:
import io
from docxtpl import DocxTemplate
def report_doc(user_id):
# Prepare the data...
doc = DocxTemplate(app.root_path+'/templates/report.docx')
doc.render({
# Pass parameters
})
# Create in-memory buffer
file_stream = io.BytesIO()
# Save the .docx to the buffer
doc.save(file_stream)
# Reset the buffer's file-pointer to the beginning of the file
file_stream.seek(0)
return send_file(file_stream, as_attachment=True, attachment_filename='report_'+user_id+'.docx')
(Testing on Firefox, I found the browser kept retrieving the file from cache even if I specified a different filename, so you may need to clear your browser's cache while testing, or disable caching in dev tools if your browser supports this, or adjust Flask's cache control settings).