Plugging in new template languages

Replace the default template language.

Introduction

Grok, like the Zope 3 framework on which it is built, uses Zope Page Templates as its default templating language. While you can, of course, use whatever templating language you want in Grok by calling it manually, you can also “plug in” a template language such that both inline templates and templates stored in files are automatically linked with your Views - just like inline grok.PageTemplates and files with the .pt extension are by default.

Existing template extensions

There are already some template language extensions available for Grok. See

Note, that in grok projects you have to include these extensions before you grok your own package, i.e. in configure.zcml a line like:

<include package='megrok.genshi' />

must appear before everything else to make the extensions work properly.

A simple template engine

For demonstration purposes we create a very plain template engine that returns any text passed formatted uppercase:

class UpperCaseTemplate(object):
    """A template engine that renders to uppercase."""

    def __init__(self, sourcetext):
        self._text = sourcetext

    def render(self, **namespace):
        return self._text.upper()

As you can see this is plain Python. In real world usage you will normally use a thirdparty-package like the ones mentioned above which provide ‘real’ template functionality.

Inline templates

“Inline” templates are templates that you create right in your Python code - for example, by instantiating the default grok.PageTemplate class with a literal string value as its argument. Such templates are automatically associated with nearby View classes: if you create a View named Mammoth and next to it instantiate a template named mammoth, then Grok will use them together.

To enable such automatic association for a new templating language, you need to write a subclass of grokcore.view.components.GrokTemplate. You will need to override three methods. The setFromFilename and setFromString methods should each load the template from disk or a given string, depending on method. Your render method should run the template with the dictionary of values returned by self.getNamespace() and return the resulting string.

Here is an example of a minimal page template integration, assuming that your template engine is available as UpperCaseTemplate:

import os
from grokcore.view.components import GrokTemplate

class MyPageTemplate(GrokTemplate):

    def setFromString(self, string):
        self._template = UpperCaseTemplate(string)

    def setFromFilename(self, filename, _prefix=None):
        file = open(os.path.join(_prefix, filename))
        self._template = UpperCaseTemplate(file.read())

    def render(self, view):
        return self._template.render(**self.getNamespace(view))

With this class finished you can create an inline template, like this:

class AView(grok.View):
    pass

aview = MyPageTemplate('<html><body>Some text</body></html>')

The result of the rendered inline template will appear when you request the @@aview and look like this:

<HTML><BODY>SOME TEXT</BODY></HTML>

You can also create a filebased template, inline. In this case you only have to pass the filename as filename parameter:

class AView(grok.View):
    pass

aview = MyPageTemplate(filename='lasceaux.html')

Here the complete contents of the local file lasceaux.html will appear uppercase when you browse the view.

Templates in the templates/ directory

The most common use case, however, is to place templates for a file foo.py in the corresponding foo_templates directory. Grok, of course, already recognizes that files with a .pt extension each contain a Zope Page Template. To tell Grok about a new file extension, simply register a global utility that generates a MyPageTemplate when passed a filename. That utility needs to implement the ITemplateFileFactory interface which is found in grokcore.view.interfaces.

Let’s register a .uct (for ‘upper case template’) filename extension:

from grokcore.view.interfaces import ITemplateFileFactory

class MyPageTemplateFactory(grok.GlobalUtility):

    grok.implements(ITemplateFileFactory)
    grok.name('uct')

    def __call__(self, filename, _prefix=None):
        return MyPageTemplate(filename=filename, _prefix=_prefix)

When your module gets grokked, Grok will discover the MyPageTemplateFactory class, register it as a global utility for templates with the .uct extension, and you can start creating .uct files in the template directory for your class.

That’s all you basically need! Have fun!

Advanced usage: namespaces

Often you do not only want text to be turned uppercase but produce different output based on other attributes of the context object, the name of the view or whatever. In other words: templates are often used in a certain environment or namespace.

A namespace is actually only a dict containing environment parameters like the context object used, the view and similar often Zope-related things. Your template engine might expect those (or other) parameters to process templates correctly or provide useful stuff.

Here is another (silly) template plugin, which replaces all occurences of <TIMESTAMP> with a timestamp passed via namespace. The timestamp is expected to be the usual 9-tuple:

import time
class DatedTemplate(object):
    """A template engine that replaces <TIMESTAMP>s."""

    def __init__(self, sourcetext):
        self._text = sourcetext

    def render(self, **namespace):
        timestamp = time.strftime('%c', namespace['timestamp'])
        return self._text.replace('<TIMESTAMP>', timestamp)

Our template extension now has to provide this timestamp in the namespace. The handling happens in the render() method only:

import os
from grokcore.view.components import GrokTemplate

class MyTimestampedTemplate(GrokTemplate):

    def setFromString(self, string):
        self._template = DatedTemplate(string)

    def setFromFilename(self, filename, _prefix=None):
        file = open(os.path.join(_prefix, filename))
        self._template = DatedTemplate(file.read())

    def render(self, view):
        namespace = self.getNamespace(view)
        namespace.update(dict(timestamp= time.gmtime()))
        return self._template.render(**namespace)

Of course you can modify the values in namespace during rendering.

Any view that uses this template type like this:

class ADatedView(grok.View):
    pass

adatedview = MyTimestampedTemplate('The datetime is: <TIMESTAMP>')

will now get something like this:

The dateime is: Wed Sep 23 23:23:23 2023

depending on the datetime of access. There is nearly no limit for more useful scenarios.