# -*- coding: utf-8 -*-
""".. _appypod:
=====================================
OpenOffice Utils via Template Backend
=====================================
This package uses appy.pod_ to generate documents using a
template in OpenDocument Format ``.odt/ods``.
Following template api, a std way to use it is as follows::
from django.template import loader
tmpl = loader.get_template('example/simple.odt')
pdf_content = tmpl.render(context={...})
Substitution in templates
==========================
Variable substituition and template logic is done as described in
``appy`` documentation. ``appy.pod`` package returns an OpenDocument
document w/o type conversion (an ``odt`` template remains an ``odt``
templates). Conversion to pdf is delegated to LibreOffice.
Conversion to PDF
==================
There are 2 main modes that we can convert to pdf using libreoffice:
1. locally, invoking a Python interpreter that has ``uno`` package
and setting a port to connect to an instance of libreoffice.
2. connetting to an external service (eg.: a Gotenborg docker).
This can be forced by setting
``POD_CONVERTER`` to the base url of a gotenberg_ server.
AppyPod Template
================
``thx.appy.backend.Template`` accepts some more parameters than default
django template, and has a ``.save_as`` method that can be very handy.
Merging files
==============
The function func:`convert_to_pdf` that uses `gotenberg` service is able to
merge files according to following rules:
* if a list of template (eg: odt) is provided, all the files are converted
in a single pdf. Pdf input files are ignored
* if a list of pdf files are provided *and* ``merge = True`` all pdf files
are converted. OpenDocument input files are ignored
API
---
.. autoclass:: AppyPodEngine
:members:
.. autoclass:: Template
:members:
.. autofunction:: convert_to_pdf
.. _appy.pod: http://appyframework.org/
.. _gotenberg: https://thecodingmachine.github.io/gotenberg/
"""
from __future__ import unicode_literals
import os
import logging
from tempfile import NamedTemporaryFile
import httpx
from django.conf import settings
from django.template import TemplateDoesNotExist
from appy.pod.renderer import Renderer
from django.template.context import make_context
from django.template.engine import Engine
from django.template.backends.django import reraise
from django.template.backends.base import BaseEngine
logger = logging.getLogger('jmb.core')
[documenti]class AppyPodEngine(BaseEngine):
app_dirname = 'templates'
def __init__(self, params):
params = params.copy()
options = params.pop('OPTIONS').copy()
options.setdefault('autoescape', True)
options.setdefault('debug', settings.DEBUG)
super().__init__(params)
# if 'loader' not in options:
# options['loader'] = [loaders.FilesystemLoader]
self.engine = Engine(self.dirs, self.app_dirs, **options)
[documenti] def from_string(self, template_code):
return Template(self.engine.from_string(template_code), self)
[documenti] def get_template(self, template_name):
try:
return Template(self.engine.get_template(template_name), self)
except TemplateDoesNotExist as exc:
reraise(exc, self)
[documenti]class Template:
def __init__(self, template, backend):
self.template = template
self.backend = backend
@property
def origin(self):
return self.template.origin
[documenti] def render(self, context=None, request=None, output_format='pdf', forceOoCall=False, external=None):
"""
False render function.
:arg context: context (default: self.context)
:arg request: the current request (optional)
:arg output_format: output file format (.odt, .ods, .pdf - default)
:return: generated file stream if created, else None
"""
result = None
output = None
output_format_orig = output_format
context = make_context_complete(self.template, context, request)
flattened_context = context.flatten()
if settings.POD_CONVERTER and output_format == 'pdf' and external is not False:
# using external service
output_format = os.path.splitext(self.template.name)[1].lstrip('.')
with NamedTemporaryFile('w', suffix='.%s' % output_format, delete=False) as f:
output = f.name
renderer = Renderer(
self.template.origin.name,
flattened_context,
output,
ooPort=settings.UNO_OOO_PORT,
overwriteExisting=True,
pythonWithUnoPath=settings.UNO_PYTHON_PATH,
forceOoCall=forceOoCall,
)
renderer.run()
if settings.POD_CONVERTER and output_format_orig == 'pdf' and external is not False:
result = convert_to_pdf(output).read()
else:
result = open(output, 'rb').read()
if output and os.path.exists(output):
os.unlink(output)
return result
[documenti] def save_as(self, output_path=None, output_format=None, context=None, forceOoCall=False, external=None):
"""
Save the template into file
:arg output_path: the output path
:return: the open handler of the generated file
.. code-block:: python
from django.template import loader
templ = loader.get_template('admin/fax/cover.odt', using='appy')
templ.save_as('/tmp/fax.pdf', context={...})
"""
assert output_format or output_path, "At least one of output_path or output_format must be provided"
output_format = output_format or os.path.splitext(output_path)[1].lstrip('.')
if not output_path:
output_path = NamedTemporaryFile(suffix='.%s' % output_format, prefix='ooo_').name
f = open(output_path, 'wb')
content = self.render(
output_format=output_format, context=context, forceOoCall=forceOoCall, external=external)
f.write(content)
f.flush()
return f
def __repr__(self):
return f'<AppyPodTemplate: {self.template.name}>'
[documenti]def convert_to_pdf(files, output_filename=None, merge=False, timeout=settings.POD_CONVERTER_TIMEOUT):
"""Convert input to pdf. If many files are handled one single merged file is returned
:arg file: filename or open file handler (can be a list/tuple)
:arg output_filename: name of the output file (optional)
:arg merge: (boolean). Suggest that we need to generate an output that is the mewrge
of input files (that ned to be ``.pdf``)
:arg timeout: timeout for the connection to the server (default settings.POD_CONVERTER_TIMEOUT)
"""
if not isinstance(files, (list, tuple)):
files = [files]
files_dict = {}
for j, file in enumerate(files):
if isinstance(file, str):
files_dict[f'file{j:02}'] = open(file, 'rb')
else:
files_dict[f'file{j:02}'] = file
if merge:
POD_CONVERTER_URL = settings.POD_CONVERTER + '/merge'
else:
POD_CONVERTER_URL = settings.POD_CONVERTER + '/convert/office'
rh = httpx.post(POD_CONVERTER_URL, files=files_dict, timeout=timeout)
if output_filename:
with open(output_filename, 'wb') as f:
f.write(rh.content)
return rh
def make_context_complete(template, context, request=None, **kwargs):
"""Return a context that also has result of all context_processors"""
context = make_context(context, request=None, **kwargs)
if request:
context_processors = template.engine.template_context_processors
for context_processor in context_processors:
context.update(context_processor(request))
return context