Skip to main content

Dynamic HTML generation and scripting of pages, content rules, portlets and emails

Project description


Easy Template (collective.easytemplate) products brings easy dynamic texts to Plone. You don’t need to create full blown product just for few dynamic pages anymore - the most simplest things can be typed straight from the visual editor.

Templating is a way to add simple programming logic to text output. This products adds or enhances templating supports on various parts of Plone site.


Plone lacks out of the box support for custom, extensible, templating support for content editors.

Use cases

Possible use cases are e.g.

  • Use unfiltered HTML on page body (<script> et. al)

  • Adding dynamic listings and tables on pages, like news listing

  • Adding dynamic email bodies, titles and receivers in content rules actions

  • Adding generated content to content rule action emails

  • Show different text to logged in and anonymous users

  • Creating a simple text portlet dynamically


The following example demostrates how text in Templated Document edit mode gets translated to generated HTML snippet in the view mode.

You write in Kupu:

Hello user!

Please select one course from below:

{{ list_folder("courses") }}

will result to the output:

Hello user!

Please select one course from below:

More information and examples.


Add to your buildout:

eggs =

zcml =

Run Add-on product installer for Easy Template product.

collective.easytemplate depends on collective.templateengines and Jinja2 template engine.

Security notice

Because collective.easytemplate allows entering unsafe HTML, like <script> on the pages by default, its creation is limited to the users with Manager role.

Running unit tests

Python eggs Cheetah, Jinja2 and Products.LinguaPlone must be installed in order to run all unit tests.



The development of this product was sponsored by London School of Marketing.

About the template engine backends

By default, Jinja 2 template engine is used. Please refer to Jinja 2 documentation for syntax. This syntax is very easy non-XML syntax resembling Django templates.

Easy syntax means that for the basic usage, no HTML knowledge is needed. You can use template logic in both the HTML source code and visual editor views.

Template engine is switchable in the application code. Besides Jinja, Django and Cheetah engines are supported by collective.templateengines backend.

Only on Jinja engine Zope security is supported. Other template engines do not provide compatible sandboxes.

Templated elements

Easy Template product makes it possible to use templates in the following places.


Use Templated Document content type to add scriptable Plone pages.

This content item has a tab Template which allows you to adjust advanced template properties:

  • Show errors: After checking this option the error messages will be available to the user regardless whether he/she is admin

  • Unfiltered template: A field for raw HTML code input. This code is not scrambled or filtered by WYSIWYG editing or HTML filtering functions.

You can debug the template code by seeing the direct template engine output by appending /testTemplate to the object url. It will return plain text view what the cooked template has eaten.

Fields and widgets

collective.easytemplate.fields.TemplatedTextField allows you to edit templated document code in Kupu and template is run in view mode. It acts as replacement for Archetypes’s TextField() for your custom content types.


Use Templated Portlet portlet to add scripts to your portlets. Templated Portlet is based on `

Enter the template code in the visual editor. Raw HTML editing is not yet supported here.

Portlets have TALES Expression field to determine whether portlets should be visible or not. This allows showing and hiding portlets conditionally.

This is useful for special cases like

  • Showing portlets by language

  • Showing portlets for specific users only

  • Showing portlers for certain time

  • etc.

Read more how to write expressions here.


Use Templated Mail Action to add scripting to your content rules based outgoing email messages.

Template expansion is available in all the fields: recipients, subject and message. You can dynamically look up the receiver email based on the context object - it doesn’t need to be a fixed address.

Below is an advanced example how to define templated outgoing email which is triggered from a workflow action.


<?xml version="1.0"?>

 <rule name="email_local_coordinator_about_local_user_approval" title="Send email to LC when new LU needs approval"
    description="Send email to local coordinators that management has approved new member to their center and local coordinator actions are needed"
    enabled="True" event="Products.CMFCore.interfaces.IActionSucceededEvent"
   <condition type="plone.conditions.WorkflowTransition">
    <property name="wf_transitions">

   <action type="collective.easytemplate.actions.Mail">
    <property name="source">{{ portal.getProperty('email_from_address') }}</property>
    <property name="message">
New local user {{ title }} needs approval.

Please approve local user at

{{ context.absolute_url() }}

    <property name="recipients">{{ context.getTheReceiverEmailAddressFromTheContextSomehow() }}</property>
    <property name="subject">New local user {{ title }} needs approval</property>



Then sample unit testing code to test this:

from zope.component import getUtility, getMultiAdapter, getSiteManager
from Products.MailHost.interfaces import IMailHost
from Products.SecureMailHost.SecureMailHost import SecureMailHost

class DummySecureMailHost(SecureMailHost):
    meta_type = 'Dummy secure Mail Host'
    def __init__(self, id): = id
        self.sent = []

        self.mto = None

    def _send(self, mfrom, mto, messageText, debug=False):
        self.mto = mto


class BaseTestCase:
    """We use this base class for all the tests in this package. If necessary,
    we can put common utility or setup code in here.

    Mix-in class, also include FunctionalTestCase or PloneTestCase.

    def afterSetUp(self):
        sm = getSiteManager(self.portal)
        self.dummyMailHost = DummySecureMailHost('dMailhost')
        sm.manage_changeProperties({'email_from_address': ''})
        sm.registerUtility(self.dummyMailHost, IMailHost)

        def test_my_email_on_workflow_transition(self):
                self.workflow = self.portal.portal_workflow
                self.portal.invokeFactory("MyContentType", "myobject")
                myobject = self.portal.myobject

                self.dummyMailHost.sent = []

                self.workflow.doActionFor(myobject, "my_workflow_transition_id_here")
                review_state = self.workflow.getInfoFor(myobject, 'review_state')
                self.assertEqual(review_state, "my_new_workflow_state")

                # Check that the email has been send
                self.assertEqual(len(self.dummyMailHost.sent), 1) # Outgoing emails increased by one

                self.assertEqual(self.dummyMailHost.mto, [""])


By default, template functions are limited to the current user priviledges - this means that output may vary depending on which user you have logged in. The user should not be able to escape Zope sandbox.

However, some tags are not totally secure (escapes viewing priviledges) and you might want to disable them on multi-user production site.

Security is not guaranteed for this product. For sites with high security requirements, please consult the author.

Security unit tests are available here.

Template authoring guide

This document describes available variables and functions in Easy Template elements. Some example template snippets are included.

The default Jinja backend exposes tags as functions. Since Jinja makes clear distinction between variables and functions, you need to add () after tags to render them.

Template context

Template context holds the top level variables you have available in your template.

Variables are defined in context/ source code file.

context variable

Plone uses subsystem called Archetypes to define content types. Content types are constructed from fields defined in the schema. All the default Plone content types (documents, folders, events, news, etc.) are Archetypes based.

Archetypes based objects are exposed “as is” to the template engine in the context variable. Other context variable functions are defined by Python classes running the object.

You can call getXXX accessor functions to query individual fields values. The exposed fields are defined in the schema source code of Archetypes object.

Examples below.

Print content title:

{{ context.Title().decode("utf-8") }}

Print document body text (HTML):

{{ context.getBody() }}

Get the URL of the current object:

{{ context.absolute_url() }}

If you have a write access to the object you can even set values in the template, though this is not very useful:

{{ context.setTitle('Moo the novel') }}

Unicode and UTF-8

Jinja, like Python 2.x software usual, assumes all strings are either ASCII or Unicode.

If you are outputting text which

  • contains international characters

  • is known to be UTF-8

you must decode the input text in your template. For Plone, the following is known to be UTF-8

  • All Archetypes text field accessors like Title(), Description() return UTF-8 bytestrings

  • portal_catalog entries like Title, Description reflect directly Archetypes values and contain UTF-8 bytestrings

  • For other strings, consult Plone source code

To output such text the decode must be performed. You can do this by directly calling decode() method of Python bytecode strings.

For function like accessors:

{{ context.Title().decode("utf-8") }}

For catalog brain data:

{{ brain.Title.decode("utf-8") }}

Otherwise you will see something like this when international characters are encountered:

Traceback (innermost last):
  Module collective.templateengines.utils, line 104, in wrapExceptions
  Module collective.templateengines.backends.jinja, line 104, in applier
  Module jinja2.environment, line 705, in render
  Module <template>, line 3, in top-level template code
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 1: ordinal not in range(128)


Traversing is a mechanism to look up objects in Zope’s object graph.

To access the other objects beside the current Template Document you can traverse in the folder hierarchy using Zope’s traversing mechanism.

Get the parent folder:

{{ context.aq_parent }}

Folder content objects can be traversed using the object id.

Get the sister page in the current folder which has URL id ‘sister’:

{{ context.aq_parent.sister }}


Portal is the root Plone object of your site. You can use it as a traversing start point to query other objects on your site. E.g.

Some of available methods are described in IPortal interface.

To access the top level news folder:

{{ }}


portal_state stores information about the current state of the system. Tells things like if the user is logged in, navigation base, portal title, active language and so on.

This object implements IPortalState interface.

Example how to separate output for anonymous and logged in users:

{% if portal_state.anonymous() %}
{% else %}
        logged in
{% endif %}


User variable holds the current user security information.

This implements Basic user interface.

The most useful feature is getting the current username via getUserName().


User membership information. This information depends on the used member backend (Plone default, LDAP, SQL, custom…).


portal_url returns the current portal root url when called.


<a href="{{ portal_url() }}">Home</a>


Tags are custom functions you are able to use in your templates. They provide an easy way to extend templates with your own Python functions. Tags are registered in file in the collective.easytemplate package.

Easy Template comes with several useful tags out of the box and they are explained below.


Dump object methods and variables for developer consumption.

Warning. This tag is not multiuser safe. You want to disable this tag on production site, since it is a read priviledge escalation.

Explore tag helps you to build scripts by exposing the variables and methods insde the objects. It prints a tabular output of available methods and variables.


object: Object to explore

Show the guts of current Templated Document object:

{{ explore(context) }}

Show what we have available in the portal root:

{{ explore(portal) }}

Show what was returned by a function which returns a list - take the first element:

{{ explore(query({"portal_type":"Folder})[0]) }}


Return site objects based on search criteria.

Query returns the list of site objects as returned by portal_catalog search. The objects are catalog brains: dictionaries containing metadata ids as key.

See ZMI portal_catalog tool for avaiable query index and returned metadata fields.

Key-value pairs are taken as the parameters and they are directly passed to the portal_catalog search.

The output is limited by the current user permissions.


  • searchParameters: Python dictionary of portal_catalog query parameters. index->query mappings. Bad index id does not seem to raise any kind of an error.

Return value:

  • List of ZCatalog brain objects. Brain objects have methods getURL, getPath and getObject and dictionary look up for catalog metadata columns.


Return the three most fresh News Item objects sorted by date:

{{ query({"portal_type":"News Item","sort_on":"Date","sort_order":"reverse","sort_limit":3,"review_state":"published"}) }}

Return items in a particular folder:

{{ query({path={"query" : "/folder", depth: 0}}) }}

For more information about possible query formats see this old document.


Render a browser:page based view. If there is no registered view for id, return a placeholder string.

Parameter name: View id, as it appears in browser/configure.zcml.

Parameter function: Optional. View instance method name to be called. If omitted, __call__() is used.

Example (render sitemap):

{{ view("sitemap_view", "createSiteMap") }}


Render a viewlet.

Parameter name: Viewlet id as it appears on portal_view_customizations ZMI page.


{{ viewlet("portal.logo") }}


This is equivalent of TAL provider expressin which is used to render viewlet and portlet managers.

To render all the left column portlets call:

{{ provider("plone.leftcolumn") }}


The function reads RSS feed. You can iterate manually through entries and format the output. This is mostly suitable when dealing with HTML source code.


  • url: URL to RSS or RSS

  • cache_timeout: Optional, default value 60. Seconds how often the HTTP GET request should be performed.


  • List of dictionaries with following keys: title, summary, url, updated and friendly_date.

Example (raw HTML edit):

{% for entry rss_feed("") %}
                <span>{{ entry.title }}

                <span>{{ entry.summary }}
{% endfor %} is used as the RSS reader backend.


List folder content and return the output as <ul> tag. Mostly suitable for simple folder output generating.

The formatting options offered here are not particular powerful. You might want to use query() tag for more powerful output formatting.


  • folder: The path to the listed folder. Same as the URI path on the site.

  • title: Render this title for the listing as a subheading

  • filters: portal_catalog query parameters to be applied for the output. See query() below for examples.

  • exclude_self: If True do not render context Templated Document in the outpput

  • extra_items: String of comma separated entries of URIs which are outside the target folder, but should be appended to the listing.

Example (create a course module listing from a course folder):

{{ list_folder("courses/marketing/cim-professional-certificate-in-marketing", title="Other modules in this course:", filters={ "portal_type" : "Module"}) }}


Render list of latest published news from the site. Uses collective.easytemplate.tags/ template.

latest_news also serves as an example how to drop a custom view into the visual editor.


  • count: How many items are rendered


{{ latest_news(3) }}


Translation catalog look up with an message id.

Translates the message to another language. The function assumes the translation is available in gettext po files.


  • message: gettext msgid to translate

  • domain: gettext domain where the message belongs, optional, defaults to “plone”

  • language: target language code, e.g. fi, optional, defaults to the currently selected language

  • default: The default value to be displayed if the msgid is missing for the selected language


  • translated string


{{ translate("missing_id", default="Foobar")  }}

{{ translate("box_more_news_link", "plone", "fi")  }}

For available default Plone msgids, see PloneTranslations product source


Get the current language for the user.

This enables conditional showing based on the language.


  • No parameters


  • The current language code as a string, e.g. “fi”


{% if current_language() == "fi" %}
{% else %}
{% endif %}

Advanced examples

News & blog table

The following snippet will create a table with two columns. The left column is filled with a summary and link to all published news on the site. The right column is filled with links to external blog entries, taken from a RSS feed. The news query is language sensitive - only news for the current active language are shown.

Both columns are limited to three entries.

The text is translated and when the default Plone translation catalogs lack suitable msgids, a custom translation catalog twinapex is used.

This example must be put into unfiltered template input box, since Kupu seems to insert unwanted &nbsp; characters into the code.


<table class="front-page two-column">
                        <td class="column-2">
                                        <a href="{{ portal_url() }}/news">
                                                {{ translate("news", "twinapex") }}
                                {% for item in query({"portal_type":"News Item", "review_state" : "published", "sort_on":"Date", "sort_order":"reverse", "sort_limit":3}) %}
                                        <div class="fp-item">
                                                <a href="{{ item.getURL() }}">{{ item.Title }}</a>
                                                        {{ item.Description }}

                                                <p class="timestamp">{{ item.Date }}</p>
                                {% endfor %}

                                <p class="more">
                                        <a class="more" href="{{ portal_url() }}/news">
                                                {{ translate("box_more_news_link", default="More news...") }}
                        <td class="column-2">
                                        <a href="{{ portal_url() }}/news">
                                                {{ translate("blog", "twinapex") }}
                                {% for item in rss_feed("")[0:3] %}
                                        <div class="fp-item">
                                                <a href="{{ item.url }}">{{ item.title }}</a>
                                                <p class="timestamp">{{ item.friendly_date }}</p>
                                {% endfor %}

                                <p class="more">
                                        <a class="more" href="">
                                                {{ translate("box_morelink", default="More...") }}

Debugging tips

If the template compilation fails you might have made copy-paste errors. Please view the template in raw HTML mode to track down the errors:

  • HTML tags inside a template expression

  • Hard line breaks inside a template expression

Registering new tags

If you want to add your template functions you must add them to collective.easytemplate.tagconfig module (note: in the future Zope configuration directives and ZCML can be used to register the tags).

All tags implement collective.templateengine.interfaces.ITag interface.

For example code, see the existing tags in collective.easytemplate.tags package.

Example how to register a custom tag (run in your product’s initialize() method):

from collective.easytemplate import tagconfig

from myproducts import MyTag


Project details

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution (106.6 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page