.. contents:: :depth: 2
Introduction
============
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.
Motivation
----------
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
Example
-------
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:
* `Math <http://example.example>`_
* `Marketing <http://example.example>`_
* `Chemistry <http://example.example>`_
`More information and examples <http://opensourcehacker.com/2009/07/30/putting-views-like-sitemap-into-plone-content-tree-using-easy-template-add-on/>`_.
Installation
------------
Add to your buildout::
eggs =
collective.easytemplate
zcml =
collective.easytemplate
Run Add-on product installer for *Easy Template* product.
collective.easytemplate depends on `collective.templateengines <http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=collective.templateengines>`_
and `Jinja2 <http://pypi.python.org/pypi/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.
Authors
-------
* `Mikko Ohtamaa, opensourcehacker.com <http://opensourcehacker.com>`_
* Hans-Peter Locher [mr_savage]
* [jensens]
Sponsorship
------------
The development of this product was sponsored by `London School of Marketing <http://londonschoolofmarketing.com>`_.
About the template engine backends
==================================
By default, Jinja 2 template engine is used.
Please refer to `Jinja 2 documentation <http://jinja.pocoo.org/2/documentation/templates>`_
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 <http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=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.
Document
--------
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.
Portlets
--------
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 <http://plonemanual.twinapex.fi/content/expressions.html>`_.
Email
-----
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.
profiles/defaul/contentrules.xml::
<?xml version="1.0"?>
<contentrules>
<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"
stop-after="False">
<conditions>
<condition type="plone.conditions.WorkflowTransition">
<property name="wf_transitions">
<element>my_workflow_transition_id_here</element>
</property>
</condition>
</conditions>
<actions>
<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>
<property name="recipients">{{ context.getTheReceiverEmailAddressFromTheContextSomehow() }}</property>
<property name="subject">New local user {{ title }} needs approval</property>
</action>
</actions>
</rule>
<assignment
location="/"
name="email_local_coordinator_about_local_user_approval"
enabled="True"
bubbles="True"
/>
</contentrules>
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):
self.id = id
self.sent = []
self.mto = None
def _send(self, mfrom, mto, messageText, debug=False):
self.sent.append(messageText)
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):
...
self.loginAsPortalOwner()
sm = getSiteManager(self.portal)
sm.unregisterUtility(provided=IMailHost)
self.dummyMailHost = DummySecureMailHost('dMailhost')
sm.manage_changeProperties({'email_from_address': 'moo@isthemasteofuniverse.com'})
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, ["receiver@dummy.host"])
...
Security
========
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 <https://svn.plone.org/svn/collective/collective.easytemplate/trunk/collective/easytemplate/tests/test_jinja_zope_security.py>`_.
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/plone.py <https://dev.plone.org/collective/browser/collective.templateengines/trunk/collective/templateengines/context/plone.py>`_
source code file.
context variable
----------------
Plone uses `subsystem called Archetypes <http://plone.org/products/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)
.. note ::
As always, there is an exception to the rule. Content rule emails use UTF-8
internally and in them variables must not be decoded.
Traversing
----------
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
------
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.news }}
portal_state
------------
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 <https://svn.plone.org/svn/plone/plone.app.layout/trunk/plone/app/layout/globals/interfaces.py>`_ interface.
Example how to separate output for anonymous and logged in users::
{% if portal_state.anonymous() %}
anon
{% else %}
logged in
{% endif %}
user
----
User variable holds the current user security information.
This implements `Basic user <http://api.plone.org/Plone/3.0/public/products/PluggableAuthService/AccessControl.User.BasicUser-class.html>`_ interface.
The most useful feature is getting the current username via getUserName().
member
------
User membership information. This information depends on the used
member backend (Plone default, LDAP, SQL, custom...).
portal_url
----------
portal_url returns the current portal root url when called.
Example::
<a href="{{ portal_url() }}">Home</a>
Tags
====
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 tagconfig.py file in the collective.easytemplate package.
Easy Template comes with several useful tags out of the box and they are explained below.
explore
-------
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.
**Parameters**:
*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]) }}
query
-----
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.
**Parameters**:
- *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.
**Examples**
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 <http://www.zope.org/Documentation/Books/ZopeBook/2_6Edition/SearchingZCatalog.stx>`_.
view
----
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") }}
viewlet
-------
Render a viewlet.
Parameter *name*: Viewlet id as it appears on portal_view_customizations ZMI page.
Example::
{{ viewlet("portal.logo") }}
provider
--------
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") }}
rss_feed
--------
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.
**Parameters**
- *url*: URL to RSS or RSS
- *cache_timeout*: Optional, default value 60. Seconds how often the HTTP GET request should be performed.
**Return**
- List of dictionaries with following keys: *title*, *summary*, *url*, *updated* and *friendly_date*.
Example (raw HTML edit)::
{% for entry rss_feed("http://blog.redinnovation.com/feed/") %}
<p>
<b>Title:</b>
<span>{{ entry.title }}
</p>
<p>
<b>Summary:</b>
<span>{{ entry.summary }}
</p>
{% endfor %}
plone.app.portlets.rss.RSSFeed is used as the RSS reader backend.
list_folder
-----------
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.
**Parameters**
- *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"}) }}
latest_news
-----------
Render list of latest published news from the site. Uses *collective.easytemplate.tags/latest_news.pt* template.
latest_news also serves as an example how to drop a custom view into the visual editor.
**Parameters**
- *count*: How many items are rendered
**Example**::
{{ latest_news(3) }}
translate
---------
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.
**Parameters**
- *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
**Return**
- translated string
Examples::
{{ translate("missing_id", default="Foobar") }}
{{ translate("box_more_news_link", "plone", "fi") }}
For available default Plone msgids, see `PloneTranslations product source <https://dev.plone.org/collective/browser/PloneTranslations/trunk/i18n>`_
current_language
----------------
Get the current language for the user.
This enables conditional showing based on the language.
**Parameters**
- No parameters
**Return**
- The current language code as a string, e.g. "fi"
Example::
{% if current_language() == "fi" %}
Paivaa
{% else %}
Hello
{% 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 characters into the code.
Example::
<table class="front-page two-column">
<tbody>
<tr>
<td class="column-2">
<h2>
<a href="{{ portal_url() }}/news">
{{ translate("news", "twinapex") }}
</a>
</h2>
{% 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>
<p>
{{ item.Description }}
</p>
<p class="timestamp">{{ item.Date }}</p>
</div>
{% endfor %}
<p class="more">
<a class="more" href="{{ portal_url() }}/news">
{{ translate("box_more_news_link", default="More news...") }}
</a>
</p>
</td>
<td class="column-2">
<h2>
<a href="{{ portal_url() }}/news">
{{ translate("blog", "twinapex") }}
</a>
</h2>
{% for item in rss_feed("http://blog.redinnovation.com/feed/")[0:3] %}
<div class="fp-item">
<a href="{{ item.url }}">{{ item.title }}</a>
<p class="timestamp">{{ item.friendly_date }}</p>
</div>
{% endfor %}
<p class="more">
<a class="more" href="http://blog.twinapex.fi">
{{ translate("box_morelink", default="More...") }}
</a>
</p>
</td>
</tr>
</tbody>
</table>
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
tagconfig.tags.append(MyTag())