Rendering a pod template

In order to render a pod template, the first thing to do is to create a renderer (create a appy.pod.Renderer instance). The constructor for this class looks like this:

def __init__(self, template, context, result, pythonWithUnoPath=None, ooPort=2002, stylesMapping={}, forceOoCall=False):
  '''This Python Open Document Renderer (PodRenderer) loads a document
     template (p_template) which is a OpenDocument file with some elements
     written in Python. Based on this template and some Python objects
     defined in p_context, the renderer generates an OpenDocument file
     (p_result) that instantiates the p_template and fills it with objects
     from the p_context. If p_result does not end with .odt, the Renderer
     will call OpenOffice to perform a conversion. If p_forceOoCall is True,
     even if p_result ends with .odt, OpenOffice will be called, not for
     performing a conversion, but for updating some elements like indexes
     (table of contents, etc) and sections containing links to external
     files (which is the case, for example, if you use the default function
     "document"). If the Python interpreter
     which runs the current script is not UNO-enabled, this script will
     run, in another process, a UNO-enabled Python interpreter (whose path
     is p_pythonWithUnoPath) which will call OpenOffice. In both cases, we
     will try to connect to OpenOffice in server mode on port p_ooPort.
     If you plan to make "XHTML to OpenDocument" conversions, you may specify
     a styles mapping in p_stylesMapping.'''

For the template and the result, you can specify absolute or relative paths. I guess it is better to always specify absolute paths.

The context may be either a dict, UserDict, or an instance. If it is an instance, its __dict__ attribute is used. For example, context may be the result of calling globals() or locals(). Every (key, value) pair defined in the context corresponds to a name (the key) that you can use within your template within pod statements or expressions. Those names may refer to any Python object: a function, a variable, an object, a module, etc.

Once you have the Renderer instance, simply call its run method. This method may raise a appy.pod.PodError exception.

Since pod 0.0.2, you may put a XHTML document somewhere in the context and ask pod to convert it as a chunk of OpenDocument into the resulting OpenDocument. You may want to customize the mapping between XHTML and OpenDocument styles. This can be done through the stylesMapping parameter. A detailed explanation about the "XHTML to OpenDocument" abilities of pod may be found here.

Result formats

If result ends with .odt, OpenOffice will NOT be called (unless forceOoCall is True). pod does not need OpenOffice to generate a result in ODT format, excepted in the following cases:

If result ends with:

OpenOffice will be called in order to convert a temporary ODT file rendered by pod into the desired format. This will work only if your Python interpreter knows about the Python UNO bindings. UNO is the OpenOffice API. If typing import uno at the interpreter prompt does not produce an error, your interpreter is UNO-enabled. If not, there is probably a UNO-enabled Python interpreter within your OpenOffice copy (in <OpenOfficePath>/program). In this case you can specify this path in the pythonWithUnoPath parameter of the Renderer constructor. Note that when using a UNO-disabled interpreter, there will be one additional process fork for launching a Python-enabled interpreter.

During rendering, pod uses a temp folder located at <result>.temp.

Launching OpenOffice in server mode

You launch OpenOffice in server mode by running the command:

(under Windows: ) call "[path_to_oo]\program\soffice" "-accept=socket,host=localhost,port=2002;urp;"& (put this command in .bat file, for example)

Under Windows you may also need to define this environment variable (with OpenOffice 3.x) (here it is done in Python):

os.environ['URE_BOOTSTRAP']='file:///C:/Program%20Files/OpenOffice.org%203/program/fundamental.ini'

(under Linux: ) soffice "-accept=socket,host=localhost,port=2002;urp;"

Of course, use any port number you prefer.

Unfortunately, OpenOffice, even when launched in server mode, needs a Windowing system. So if your server runs Linux you will need to run a X server on it. If you want to avoid windows being shown on your server, you may launch a VNC server and define the variable DISPLAY=:1. If you run Ubuntu server and if you install package "openoffice.org-headless" you will not need a X server. On other Linux flavours you may also investigate the use of xvfb.

Rendering a pod template with Django

pod can be called from any Python program. Here is an example of integration of pod with Django for producing a PDF through the web. In the first code excerpt below, in a Django view I launch a pod Renderer and I give him a mix of Python objects and functions coming from various places (the Django session, other Python modules, etc).

01 gotTheLock = Lock.acquire(10)
02 if gotTheLock:
03    template = '%s/pages/resultEpmDetails.odt' % os.path.dirname(faitesletest.__file__)
04    params = self.getParameters()
05    # Add 'time' package to renderer context
06    import time
07    params['time'] = time
08    params['i18n'] = i21c(self.session['language'])
09    params['floatToString'] = Converter.floatToString
10    tmpFolder = os.path.join(os.path.dirname(faitesletest.__file__), 'temp')
11    resultFile = os.path.join(tmpFolder, '%s_%f.%s' % (self.session.session_key, time.time(), self.docFormat))
12    try:
13        renderer = appy.pod.renderer.Renderer(template, params, resultFile, coOpenOfficePath)
14        renderer.run()
15        Lock.release()
16    except PodError, pe:
17        Lock.release()
18        raise pe
19    docFile = open(resultFile, 'rb')
20    self.session['doc'] = docFile.read()
21    self.session['docFormat'] = self.docFormat
22    docFile.close()
23    os.remove(resultFile)
24 else:
25    raise ViaActionError('docError')

When I wrote this, I was told of some unstabilities of OpenOffice in server mode (! this info is probably outdated, it was written in 2007). So I decided to send to OpenOffice one request at a time through a Locking system (lines 1 and 2). My site did not need to produce a lot of PDFs, but it may not be what you need for your site.

Line 11, I use the Django session id and current time to produce in a temp folder a unique name for dumping the PDF on disk. Let's assume that self.docFormat is pdf. Then, line 20, I read the content of this file into the Django session. I can then remove the temporary file (line 23). Finally, the chunk of code below is executed and a HttpResponse instance is returned.

01 from django.http import HttpResponse
02 ...
03    # I am within a Django view
04    res = HttpResponse(mimetype=self.getDocMimeType()) # Returns "application/pdf" for a PDF and "application/vnd.oasis.opendocument.text" for an ODT
05    res['Content-Disposition'] = 'attachment; filename=%s.%s' % (
06        self.getDocName(), self.extensions[self.getDocMimeType()])
07    res.write(self.getDocContent())
08    # Clean session
09    self.session['doc'] = None
10    self.session['docFormat'] = None
11    return res

Line 7, self.getDocContent() returns the content of the PDF file, that we stored in self.session['doc'] (line 20 of previous code chunk). Line 5, by specifying attachment we force the browser to show a popup that asks the user if he wants to store the file on disk or open it with a given program. You can also specify inline. In this case the browser will try to open the file within its own window (potentially with the help of a plugin).

Rendering a pod template with Plone

The following code excerpt shows how to render the "ODT view" of a Plone object. Here, we start from an Archetypes class that was generated with ArchGenXML. It is a class named Avis that is part of a Zope/Plone product named Products.Avis. While "Avis" content type instances can be created, deleted, edited, etc, as any Plone object, it can also be rendered as ODT (and in any other format supported by pod/OpenOffice). As shown below we have added a method generateOdt to the Avis class, that calls a private method that does the job.

01 import appy.pod.renderer
02 import Products.Avis
03 ...
04    # We are in the Avis class
05    security.declarePublic('generateOdt')
06    def generateOdt(self, RESPONSE):
07        '''Generates the ODT version of this advice.'''
08        return self._generate(RESPONSE, 'odt')
09
10    # Manually created methods
11
12    def _generate(self, response, fileType):
13        '''Generates a document that represents this advice.
14           The document format is specified by p_fileType.'''
15        # First, generate the ODT in a temp file
16        tempFileName = '/tmp/%s.%f.%s' % (self._at_uid, time.time(), fileType)
17        renderer = appy.pod.renderer.Renderer('%s/Avis.odt' % os.path.dirname(Products.Avis.__file__), {'avis': self}, tempFileName)
18        renderer.run()
19        # Tell the browser that the resulting page contains ODT
20        response.setHeader('Content-type', 'application/%s' % fileType)
21        response.setHeader('Content-disposition', 'inline;filename="%s.%s"' % (self.id, fileType))
22        # Returns the doc and removes the temp file
23        f = open(tempFileName, 'rb')
24        doc = f.read()
25        f.close()
26        os.remove(tempFileName)
27        return doc

First, I plan to create the ODT file on disk, in a temp folder. Line 16, I use a combination of the attribute self._at_uid of the Zope/Plone object and the current time to produce a unique name for the temp file. Then (lines 17 and 18) I create and call a pod renderer. I give him a pod template which is located in the Products folder (Avis subfolder) of my Zope instance. Lines 20 and 21, I manipulate the Zope object that represents the HTTP response to tell Zope that the resulting page will be an ODT document. Finally, the method returns the content of the temp file (line 27) that is deleted (line 26). In order to trigger the ODT generation from the "view" page of the Avis content type, I have overridden the header macro in Products/Avis/skins/Avis/avis_view.pt:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US" i18n:domain="plone">
<body>
    <div metal:define-macro="header">
    <div class="documentActions"><a href="" tal:attributes="href python:here['id'] + '/generateOdt'">Generate ODT version</a></div>
    <h1 tal:content="here/title">Advice title</h1>
    <div class="discreet">
        If you want to edit this advice, click on the "edit" tab above.
    </div>
    <p></p>
    </div>
</body>
</html>

Hum... I know there are cleanest ways to achieve this result :-D