Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

appypod : Generate printable documents from odt templates

The lino_xl.lib.appypod plugin adds a series of build methods for generating printable documents using LibreOffice and the appy.pod package. It also adds certain generic actions for printing tables using these methods.

See also the user documentation in Using Appy POD templates.

Lines starting with >>> in this document are code snippets that get tested as part of our development workflow.

>>> from lino import startup
>>> startup('lino_book.projects.lydia.settings.doctests')
>>> from lino.api.doctest import *

Dependencies

A site that uses the lino_xl.lib.appypod plugin must also have appy installed:

$ pip install appy

This is done automatically by install.

Rendering a printable document using an appypod build method requires a running LibreOffice server (see More about the LibreOffice service). While this might refrain you from using them, they has several advantages compared to the built-in methods WeasyBuildMethod and the (now deprecated) PisaBuildMethod:

  • They can be used to produce editable files (.rtf or .odt) from the same .odt template.

  • Features like automatic hyphenation, sophisticated fonts and layouts are beyond the scope of pisa or weasyprint.

  • Templates are .odt files (not .html), meaning that end users dare to edit them more easily.

Build methods

class lino_xl.lib.appypod.AppyBuildMethod

Base class for Build Methods that use .odt templates designed for appy.pod.

class lino_xl.lib.appypod.AppyOdtBuildMethod

Generates .odt files from .odt templates.

This method doesn’t require OpenOffice nor the Python UNO bridge installed (except in some cases like updating fields).

class lino_xl.lib.appypod.AppyPdfBuildMethod

Generates .pdf files from .odt templates.

class lino_xl.lib.appypod.AppyRtfBuildMethod

Generates .rtf files from .odt templates.

class lino_xl.lib.appypod.AppyDocBuildMethod

Generates .doc files from .odt templates.

Actions

The lino_xl.lib.appypod plugin adds two actions PrintTableAction and PortraitPrintTableAction to every table in your application.

If lino_xl.lib.contacts (or a child thereof) is installed, it also adds a PrintLabelsAction.

class lino_xl.lib.appypod.PrintTableAction

Show this table as a pdf document.

class lino_xl.lib.appypod.PortraitPrintTableAction
class lino_xl.lib.appypod.PrintLabelsAction

Add this action to your table, which is expected to execute on a model which implements Addressable.

get_recipients(self, ar)

Return an iterator over the recipients for which we want to print labels.

This is here so you can override it. For example:

class MyLabelsAction(PrintLabelsAction)
    # silently ignore all recipients with empty 'street' field
    def get_recipients(self,ar):
        for obj in ar:
            if obj.street:
                yield obj

You might want to subclass this action and add a parameters panel so that users can explicitly say whether they want labels for invalid addresses or not:

class MyTable(dd.Table):
    parameters = dict(
        only_valid_recipients=models.BooleanField(
            _("only valid recipients"),default=False
        )

Templates

Table.odt

Template used to print a table in landscape orientation.

Table-portrait.odt

Template used to print a table in portrait orientation.

appypod/Labels.odt

Template used to print address labels.

The Appy renderer

class lino_xl.lib.appypod.AppyRenderer

The extended appy.pod.renderer used by Lino.

A subclass of appy.pod.renderer.Renderer (not of lino.core.renderer.Renderer.

restify_func(self, text)
insert_jinja(self, template)
insert_html(self, html)

Insert a chunk of HTML (not XHTML) provided as a string or as an etree element.

This is the function that gets called when a template contains a do text from html(...) statement.

insert_story(self, story)
insert_table(self, ar)

This is the function that gets called when a template contains a do text from table(...) statement.

story2odt(self, story, *args, **kwargs)

Yield a sequence of ODT chunks (as utf8 encoded strings).

How tables are rendered using appypod

We chose a simple Lino table request and then have a look how such a request is being rendered into a pdf document using appypod.

>>> from lxml import etree
>>> from unipath import Path
>>> import tempfile

Here is a simple Lino table request:

>>> ar = rt.login('robin').spawn(countries.Countries)
>>> ar.show()
============================= ================================ ================================= ==========
 Designation                   Designation (de)                 Designation (fr)                  ISO code
----------------------------- -------------------------------- --------------------------------- ----------
 Bangladesh                    Bangladesh                       Bangladesh                        BD
 Belgium                       Belgien                          Belgique                          BE
 Congo (Democratic Republic)   Kongo (Demokratische Republik)   Congo (République democratique)   CD
 Estonia                       Estland                          Estonie                           EE
 France                        Frankreich                       France                            FR
 Germany                       Deutschland                      Allemagne                         DE
 Maroc                         Marokko                          Maroc                             MA
 Netherlands                   Niederlande                      Pays-Bas                          NL
 Russia                        Russland                         Russie                            RU
 United States                 United States                    United States                     US
============================= ================================ ================================= ==========

This code is produced by the insert_table method, which dynamically creates a style for every column and respects the widths it gets from the request’s get_field_info, which returns col.width or col.preferred_width for each column.

To get an AppyRenderer for this test case, we must give a template file and a target file. As template we will use Table.odt. The target file must be in a temporary directory because and every test run will create a temporary directory next to the target.

>>> from lino_xl.lib.appypod.appy_renderer import AppyRenderer
>>> ctx = {}
>>> template = rt.find_config_file('Table.odt')
>>> target = Path(tempfile.gettempdir()).child("out.odt")
>>> rnd = AppyRenderer(ar, template, ctx, target)

If you open the Table.odt, you can see that it is mostly empty, except for headers and footers and a comment which says:

do text
from table(ar)

Background information about this syntax in the appy.pod docs.

This command uses the table() function to insert a chunk of ODF XML.

>>> odf = rnd.insert_table(ar)
>>> print(odf)  
<table:table ... table:...name="countries.Countries" ...name="countries.Countries">...

Let’s parse that long string so that we can see what it contains.

>>> root = etree.fromstring(odf)

The root element is of course our table

>>> root  
<Element {urn:oasis:names:tc:opendocument:xmlns:table:1.0}table at ...>

Every ODF table has three children:

>>> children = list(root)
>>> len(children)
3
>>> print('\n'.join(e.tag for e in children))
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-columns
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-header-rows
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-rows
>>> columns = children[0]
>>> header_rows = children[1]
>>> rows = children[2]

The rows

>>> len(rows)
10
>>> len(rows) == ar.get_total_count()
True
>>> cells = list(rows[0])
>>> print('\n'.join(e.tag for e in cells))
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-cell
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-cell
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-cell
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-cell

The columns

>>> print('\n'.join(e.tag for e in columns))
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-column
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-column
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-column
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-column
>>> print('\n'.join(e.tag for e in header_rows))
{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-row

The appy_params setting

pythonWithUnoPath

This setting was needed on sites where Lino ran under Python 2 while python-uno needed Python 3. To resolve that conflict, appy.pod has this configuration option which caused it to run its UNO call in a subprocess with Python 3.

If you have Python 3 installed under /usr/bin/python3, then you don’t need to read on. Otherwise you must set your appy_params to point to your python3 executable, e.g. by specifying in your settings.py:

SITE.appy_params.update(pythonWithUnoPath='/usr/bin/python3')

Using Appy POD templates

When a printable document is generated using a subclass of AppyBuildMethod, then you provide a document as template in .odt format, which you can edit using LibreOffice Writer.

This template document contains special instructions defined by the appy.pod library.

The Appy renderer installs additional functions to be used in do text|section|table from statements (described here).

lino_xl.lib.appypod.context.jinja(template_name)

Render the template named template_name using Jinja. The template is supposed to produce HTML markup.

If template_name contains no dot, then the default filename extension .body.html is added.

lino_xl.lib.appypod.context.restify(s)

Render a string s which contains reStructuredText markup.

The string is first passed to lino.utils.restify.restify() to convert it to XHTML, then to appy.pod’s built-in xhtml() function. Without this, users would have to write each time something like:

do text
from xhtml(restify(self.body).encode('utf-8'))
lino_xl.lib.appypod.context.html(html)

Render a string that is in HTML (not XHTML).

lino_xl.lib.appypod.context.ehtml(e)

Render an ElementTree node (generated using etgen.html) into this document. This is done by passing it to lino.utils.html2odf.

table(ar, column_names=None)`

Render an lino.core.tables.TableRequest as a table. Example:

do text
from table(ar.spawn('users.UsersOverview'))