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.

A tested document

This is a tested document. The following instructions are used for initialization:

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

Which means that code examples in this document use the lino_book.projects.lydia demo project.


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

$ pip install appy

On Python 3 the installation is less trivial:

pip install svn+

The appypod build methods require a running LibreOffice server (see Running a LibreOffice server). 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.


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"),default=False



Template used to print a table in landscape orientation.


Template used to print a table in portrait orientation.


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)
============================= ================================ ================================= ==========
 Designation                   Designation (de)                 Designation (fr)                  ISO code
----------------------------- -------------------------------- --------------------------------- ----------
 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
============================= ================================ ================================= ==========

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 ..."countries.Countries""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)
>>> print('\n'.join(e.tag for e in children))
>>> columns = children[0]
>>> header_rows = children[1]
>>> rows = children[2]

The rows

>>> len(rows)
>>> len(rows) == ar.get_total_count()
>>> cells = list(rows[0])
>>> print('\n'.join(e.tag for e in cells))

The columns

>>> print('\n'.join(e.tag for e in columns))
>>> print('\n'.join(e.tag for e in header_rows))

The appy_params setting


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