Templating with ZPT
Zope Page Templates are a fresh approach to the issue of templating. The design criteria were to:
further encourage the separation of presentation and logic,
work well with XML and HTML tools (including but not limited to editors),
be more explicit about the lookup of identifiers,
be extensible in a modular way, and work well with other (non- ZPT) extensions.
A couple of design decisions support these goals. Firstly, ZPT builds on standard XML mechanisms; specifically XML and Python namespaces. [6] Secondly, Page Templates implement an attribute language, not introducing any elements of its own, which combines well with tag-based markup.
Instead of imposing an external structure on the document by the placing of dtml-in tags or other looping constructs, or the inclusion of arbitrary HTML from elsewhere, the scope of statements in the attribute language is that of the element on which the attribute is set. In other words, the HTML Document Object Model ( DOM) is the mechanism which governs the template structure. This is the Right Place for the structure to live.
The idea has caught on, and there are currently implementations of ZPT for PHP ( PHP:TAL) and Perl (Petal). It has also been implemented for Python independently of Zope as OpenTAL.
ZPT operates on three cleanly separated levels. These are:
TAL, the Template Attribute Language, which specifies eight statements, executing in a fixed order, that can occur in the tal namespace,
TALES, the TAL Expression Syntax, which specifies the syntax of attribute values in the tal namespace,
METAL, Macro Expansion TAL, which defines four statements in the metal namespace, and enables a page to be built up from multiple templates.
The structuring of a single template is always done using the strictly circumscribed TAL statement set. Building up of complex pages from multiple templates is done using METAL to reference other templates.
Of course, a wide variety of design styles, some of which conflict with some the ZPT design goals, are still possible, and sometimes necessary. As we'll see next week, this is the case with Plone.
We're going to repeat the directory example. Before we start, reorganize our templating sandbox to reflect this new branch: create a dtml folder, and move add, directory_deep, directory_flat and edit down into it. Leave acl_users, do_add and do_edit in templating--- we'll want to use them for the new version of the directory as well. Create a folder called zpt, and copy the reusable elements from dtml (the homepages). After we're done, this is more or less what we want to see (we'll probably have more homepages by now):
acl_users do_add do_edit dtml dtml/add dtml/directory_deep/homepages/anne/body dtml/directory_deep/homepages/anne/david/body dtml/directory_deep/homepages/cyril/body dtml/directory_deep/index_html dtml/directory_flat/homepages/anne/body dtml/directory_flat/homepages/cyril/body dtml/directory_flat/index_html dtml/edit zpt zpt/directory_deep/homepages/anne/body zpt/directory_deep/homepages/cyril/body zpt/directory_flat/homepages/anne/body zpt/directory_flat/homepages/cyril/body
Our existing directories should continue to work just fine --- acquisition sees to it that all the necessary bits are found, and that URLs are still correct.
As before, let's start with the simplest possible ZPT, which is plain HTML, the same as our DTML starting point:
<html>
<head><title>Directory: Jean's Homepage</title>
<style> .sidebar {float: right} </style>
</head>
<body>
<div class="sidebar">
<ul><li>My Homepage</li></ul>
</div>
<h1>Jean's Homepage</h1>
<p>We'll get to this later.</p>
</body>
</html>
Here is the dynamic version of that page:
<html>
<head><title>
<span tal:content="here/directory_title | nothing">Directory</span>:
<span tal:content="here/title">User's Homepage</span>
</title>
<style> .sidebar {float: right} </style>
</head>
<body>
<div class="sidebar">
<ul>
<li tal:repeat="homepage here/homepages/objectValues"
tal:content="homepage/title">My Homepage</li>
</ul>
</div>
<h1 tal:content="here/title">User's Homepage</h1>
<div tal:content="here/body | nothing">Homepage body text.</div>
</body>
</html>
Now you can browse to e.g. http://server:8080/templating/zpt/directory_flat/homepages/anne and see a rudimentary version of the homepage.
Can you see anything wrong with that template? Perhaps you were already wincing the moment you saw the HTML. Look at the browser's title bar. The title element may contain only character data, and the browser renders the span elements as such. We need to get rid of them. Also, as it stands, the ZPT version isn't quite as resilient as the DTML one. In the absence of any directory_title property, the DTML template falls back to Directory, while the ZPT shows a blank. Let's zoom in on that element:
<span tal:content="here/directory_title | nothing">Directory</span>
The TAL statement is content, which replaces the whole document subtree of the element on which it occurs with the output of the TALES expression that was provided as its value. In this case, the subtree is only the string "Directory", but it might as well be a slew of nested tables. It vanishes from the rendered document, to be replaced by the result of, in this case, here/directory_title | nothing. The | pipe character separates alternative expressions: there may be many, and the first one that evaluates successfully is used.
There are three expression types, and one general modifier. The expressions are: path expressions, string expressions and python expressions. The modifier is not, which can prefix any of the expressions to negate their value. We'll encounter all of the expression types soon enough, but we'll start with path expressions. Always stick with path expressions if you can, use string expressions where they fit, and use python expressions sparingly and factor them out to Python scripts when you have a chance.
There are two additional modifiers that may only be used with path expressions. They are exists (test whether a path is valid) and nocall (return the found object, rather than the result of calling it; we'll return to this one later).
A path expression looks like a relative path on the filesystem or in an URL: some/path/to/some/object. It traverses to an object in the ZODB, and returns its value or the result of calling it, if it's callable. In our initial TAL statement, the path expressions are here/directory_title and nothing.
A critical difference between ZPT and DTML is that a TALES expression can only start at one of fixed list of defined elements [7]. This greatly reduces the guesswork involved in knowing where a name comes from. The elements may be grouped as follows:
These names refer to specific objects in the ZODB, from where you may traverse to the object you want.
Predefined ZPT names
- root
the system's top-most object: the Zope root folder. In a path expression like root/something, something may refer to any of the objects in the Zope root folder, or to any part of the Zope API that is exposed at that point. (If you have DocFinderTab installed, you can explore this by visiting http://server:8080/showDocumentation.)
- here
the object to which the template is being applied. This corresponds to the this() method which is often used in DTML Methods.
- container
The folder in which the template is located. Note that this has no necessary relation to here. In our directory examples, the index_html method is applied to all of the homepage folders, but it is contained in one of the directory_ folders.
- template
the template itself.
These names refer to the state of the web request currently being processed.
REQUEST names
- request
the publishing request object. It is constructed anew for every request from a client to the server, and encapsulates all the information that is specific to that request (such as the authenticated user, form variables, any applicable cookies, and all the CGI environment variables).
- user
the authenticated user object.
- options
the keyword arguments passed to the template. These are generally available when a template is called from Methods and Scripts, rather than from the web.
These names refer to aspects of the template currently being rendered.
Template names
- attrs
a dictionary containing the initial values of the attributes of the current statement tag. For example, this:
<a title="Home" tal:content="attrs/title">Link title</a>
would render as this:
<a title="Home">Home</a>
- repeat
the repeat variables. It's a bit of a stretch to call this one a "template name", as it doesn't refer to any of the HTML on the page. It does refer to the state of the containing loop, though.
- default
special value used to specify that existing text should not be replaced. For example, with this snippet:
<a href="bill.html" tal:attributes="href here/link_to | default"> Link title</a>
the target will be "bill.html" (the default) unless link_to is available, in which case its value will replace it.
Special names
- nothing
special value used to represent a non-value.
- modules
a collection through which Python modules and packages can be accessed. Only modules which are approved by the Zope security policy can be accessed.
Meta-name
- CONTEXTS
the list of standard names (this list). This allows Page Templates a measure of introspection: if the list of names enumerated above has been changed or extended, the template may examine CONTEXTS to ascertain whether a particular one is available in the implementation under which it's executing. It also comes in handy if a Page Template has defined a variable masking one of the built-in ZPT names (in which case it can be addressed as e.g. CONTEXTS/root).
The path expression here/directory_title therefore corresponds pretty directly to <dtml-var directory_title>. To provide some default text as fallback, we'll replace the nothing branch with a string expression:
<span tal:content="here/directory_title | string:Directory"> Directory</span>
The repetition of that "Directory" string should alert you that there's some slack to be taken up here. The following is functionally equivalent, but idiomatically better:
<span tal:content="here/directory_title | default"> Directory</span>
That illustrates the ideal of Page Templates, which is to allow the HTML document itself to convey as much as possible about the intended document. Note that default acts differently when used with the content statement than when used with the attributes statement. In the first case, the attribute supplied the default value; here, it is the content of the element.
This is all well and good, but we've still got two 'span's in our title, while title may contain only text.
We'll have to combine the two parts of the title in one string, and the first part (the directory name) has a fallback. Here's how we can handle it:
<title tal:define="dir_title here/directory_title | string:Directory"
tal:content="string:${dir_title}: ${here/title}">
Directory: User's homepage</title>
First, use the TAL define statement, which adds a name to the template namespace. Its value consists of the name, a space, and then any TALES expression. define executes before content, so the name is available for use in the content statement. The string expression of the content statement contains interpolated path expressions. A path expression in a string expression is preceded by a dollar sign, and enclosed by curly braces if necessary for disambiguation.
An alternative way to achieve the above would be a string expression like this (interpolating a string expression into another string expression ... ):
tal:content="string:${here/directory_title | string:Directory}: ${here/title}"
TAL does not offer a conventional if/then/else branching structure. A certain subtree is either rendered or it is not. In our directory, we want all our sidebar items except the one currently being browsed to be linked.
In the example, the TAL repeat statement is on the li element:
<li tal:repeat="homepage here/homepages/objectValues"
tal:content="homepage/title">My Homepage</li>
Therefore, the li and anything it contains will be repeated for each item in the sequence returned by objectValues. To provide for an unlinked entry, we need to provide alternative subtrees:
<li tal:repeat="homepage here/homepages/objectValues">
<span tal:condition="python:homepage.id == here.id"
tal:replace="homepage/title_or_id">Unlinked item</span>
<a tal:condition="not:python:homepage.id == here.id"
tal:attributes="href homepage/absolute_url"
tal:content="homepage/title_or_id">Linked item</a>
</li>
If this template is viewed unrendered, e.g. while being edited by an HTML editor, both alternatives will be present:
<ul>
<li>
<span>Unlinked item</span>
<a>Linked item</a>
</li>
</ul>
It's still bothersome that the condition check is repeated: it invites typos and executes the check (which might be expensive) twice. Here's a version that does the check only once:
<li tal:repeat="homepage here/homepages/objectValues">
<span tal:define="current python:homepage.id == here.id"
tal:omit-tag="">
<span tal:condition="current"
tal:replace="homepage/title_or_id">Unlinked item</span>
<a tal:condition="not:current"
tal:attributes="href homepage/absolute_url"
tal:content="homepage/title_or_id">Linked item</a>
</span>
</li>
condition="not:current" even reads better. The span tags, however, serve no purpose beyond providing convenient hangers for the TAL statements. The outer one has even been omitted using tal:omit-tag. TAL offers another notation that avoids such noise, namely to put the tal namespace declaration on the element instead of the attributes.
To put a tag in a different namespace prefix it with namespace:; i.e. to put a tag in the tal namespace, write:
<tal:block ... />
If an element is in the tal namespace, all its attributes are also in that namespace. That is why it is not necessary to write:
<tal:datetime tal:define="..." />
The second tal: is redundant.
TAL (the Template Attribute Language) only pays attention to attributes. It ignores tags. Therefor the "block" in <tal:block ...> can be anything --- it will be ignored by the TAL parser. It's a good idea to make it something descriptive. With that in mind, our loop becomes:
<li tal:repeat="homepage here/homepages/objectValues">
<tal:choice define="current python:homepage.id == here.id">
<tal:x condition="current" replace="homepage/title_or_id">
Unlinked item</tal:x>
<a tal:condition="not:current"
tal:attributes="href homepage/absolute_url"
tal:content="homepage/title_or_id">Linked item</a>
</tal:choice>
</li>
While tal:x isn't particularly descriptive, it is neutral and it's a singleton element. In this case, the "Unlinked item" text does the job of documenting well enough.
We still need to provide the option of StructuredText rendering.
Page Templates delegate most complex functionality, such as StructuredText rendering and batching, to Python code. The modules name in the ZPT namespace gives access to authorised modules, which includes a handy collection of utility scripts.
Our revised code uses this to provide StructuredText rendering if necessary, and also provides the add and edit buttons:
<tal:choice define="has_body here/body | nothing;
is_stx here/stx | nothing">
<tal:body condition="has_body">
<tal:stx condition="is_stx"
define="pps modules/Products/PythonScripts/standard"
replace="structure python:pps.structured_text(here.body)" />
<tal:html condition="not:is_stx" replace="structure here/body" />
<form action="edit" method="POST">
<button name="edit" type="submit">Edit</button></form>
</tal:body>
<form tal:condition="not:has_body"
action="add" method="POST"> Nothing yet
<button name="add" type="submit">Add</button></form>
</tal:choice>
The name pps is defined simply to shorten the next line. This code illustrates two new aspects of ZPT:
The define statement can define multiple names, with each definition delimited by a semicolon. Two reasons for this are (i) that XML does not allow an attribute to occur multiple times on a single tag, and (ii) since XML does not guarantee consistent ordering of attributes, and TAL statements are interpreted in a predetermined order, this allows definitions to occur in a desired order.
The TAL content and replace statements escape their values by default. If the value is intended to be grafted into the rendered tree, it needs to be prefixed by the word structure.
With that, our flat directory has the same functionality in ZPT as in DTML. The add and edit forms are all but identical to their DTML counterparts, so I'll leave them to the reader. (We could have used the DTML forms unchanged.)
The nested version of the directory illustrates some more aspects of acquisition. The sidebar is simple enough. It always includes a link to its parent, unless you're browsing the root of the directory; and it limits the objects through which it iterates to folders:
<div class="sidebar">
<ul>
<li tal:condition="python:container.id!='homepages'">
<a href="..">Up</a></li>
<li tal:repeat="folder python:here.objectValues('Folder')">
<a href=""
tal:attributes="href folder/absolute_url"
tal:content="folder/title_or_id">Link to folder</a></li>
</ul>
</div>
Browsing nested folders where the leaves don't have body content of their own yet, we can see (and edit) the parent's content. Besides being misleading, this prevents us from adding new content in subfolders. Fixing this is a two-line change. From this:
<tal:choice define="has_body here/body | nothing;
is_stx here/stx | nothing">
we go to this:
<tal:body define="only_here here/aq_explicit;
has_body only_here/body | nothing;
is_stx here/stx | nothing">
The aq_explicit attribute of here returns the namespace of here alone (it still allows acquisition, but it does not happen implicitly). All acquisition-wrapped objects (i.e. everything retrieved from the ZODB) have a number of aq_ methods, such as aq_inner, aq_parent, aq_base aq_chain and aq_explicit. [8] It's easiest to illustrate what they do by example:
$ ./bin/zopectl debug Starting debugger (the name "app" is bound to the top-level Zope object) >>> i = app.templating.dtml.directory_deep.index_html >>> i <DTMLMethod instance at 40f1f7a0> >>> i.aq_explicit # The same object; but without implicit acquisition <DTMLMethod instance at 40f1f7a0> >>> i.templating # Acquired <Folder instance at 40eac230> >>> i.aq_explicit.templating # Not acquiring Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: templating >>> [o.getId() for o in app.templating.dtml.directory_deep.homepages.anne.index_html.aq_chain] ['index_html', 'anne', 'homepages', 'directory_deep', 'dtml', 'templating', 'Zope'] >>> [o.getId() for o in app.templating.dtml.directory_deep.homepages.anne.index_html.aq_inner.aq_chain] ['index_html', 'directory_deep', 'dtml', 'templating', 'Zope']
"aq_" methods
- aq_explicit
Returns a wrapper that acquires explicity (you have to call aq_acquire if you do want to acquire something).
- aq_chain
Shows the list of objects that will be searched for acquired names.
- aq_inner
Returns a wrapper based only on containment, not context. In this case app.templating.dtml.directory_deep.homepages.anne.index_html, index_html is being acquired by anne, which is its context, as reflected in the output of aq_chain. Based on containment, however, the chain is shorter. app.templating.dtml.directory_deep.homepages.anne.index_html.aq_inner is equivalent to app.templating.dtml.directory_deep.index_html.
We've mentioned METAL before, when introducing the components of Page Templates. So far, we've become familiar with TAL and TALES, but METAL is still just a name. The four METAL statements that exist are:
METAL statements
- define-macro
Define a macro to be used from other templates
- use-macro
Use a macro
- define-slot
Define a slot that may be filled by templates using this macro
- fill-slot
When using a macro, provide content for the slot
One of the default objects provided in a new Zope instance is standard_template.pt, a page template that provides a very basic whole-page macro with two slots. Let's change our directory templates to use this. As the macro is defined on the html element, that's where we should use it. Here are just the changed parts of the template:
<html metal:use-macro="here/standard_template.pt/macros/page">
<head>
<metal:head fill-slot="head">
...
</metal:head>
</head>
<body>
<div metal:fill-slot="body">
...
</div>
</body>
</html>
The omitted parts ( ...) stay the same. You'll see that the trick of using dummy tags in the metal namespace works the same as in the case of tal tags (see the section called “Conditional rendering”).
Go and view the directories; they should be identical to the standalone versions. So far, the macro doesn't really do anything for us. To use it more effectively, make a copy of standard_template.pt in /templating/zpt and edit it to something like the following:
<html metal:define-macro="page">
<head>
<metal:block define-slot="head">
<title
tal:define="directory_title here/directory_title | string:Directory"
tal:content="string:${directory_title}: ${template/title_or_id}">
The title
</title>
<style> .sidebar {float: right} .footer {clear: both}
</style>
</metal:block>
</head>
<body>
<div metal:define-slot="body">
This is where the page's body text goes.
</div>
<div class="footer">
<hr /> © 2004
</div>
</body>
</html>
In this macro, we've moved the common head part into the macro, and added a footer outside the body slot. The current directory templates can now be updated to eliminate the repeated code. In both cases, delete the whole head element. To keep them valid HTML, replace this part:
<head>
<metal:head fill-slot="head">
<title
tal:define="directory_title here/directory_title | string:Directory"
tal:content="string:${directory_title}: ${template/title_or_id}">
The title
</title>
<style> .sidebar {float: right}
</style>
</metal:head>
</head>
with an empty head element:
<head></head>
As this slot is now not being filled in the template, the default content in the macro is used.
The macro mechanism is illustrated quite well by the process of editing templates with WYSIWYG (ish) tools. For the purposes of the course, we'll use Mozilla Composer, because it's to hand, not because it works particularly well for this purpose. To get started, select File | Open Web Location from the menu, specify that the document should open in a New Composer Window, and choose our directory macro template location:
Look carefully at the path: templating/zpt/standard_template.pt/source.html--- it has source.html appended. All page templates have a source.html method that returns the un-parsed ZPT source content of the document. Here is the result of opening templating/zpt/standard_template.pt (the rendered output) in Composer:
If I edit this and save it to the server ( File | Publish), I break the template:
As you can see, everything that isn't in the HTML namespace has gone away.
Opening the source.html of the template results in a page that looks identical in the "Normal" view, but which retains the ZPT goodness:
A different way of editing the ZPT source of a page is to tell Zope to start a WebDAV server on another port (see webdav-source-server in etc/zope.conf), and use this port when editing.
Let's edit this page, changing some text inside a slot, and some that isn't:
If you reload a homepage in the directory, you'll see the change in the non-slotted text show up (selected in the screenshot). The change to the body slot doesn't show up, as it's being overridden in index_html:
Opening e.g. templating/zpt/directory_flat/index_html/source.html reveals a very bare-looking page, without styling or footer text:
In order to see a better representation of the page while editing, check the "Expand macros when editing" checkbox on the "Edit" screen. Close the Composer window, and open it anew. Once you've done that, your edited page looks like this:
with the sidebar on the right, and the footer below.
If your editor understands XML, you can now edit any of the text. Changing anything that isn't in a slot provided by the macro will have no effect --- those parts of the template see to it that it renders correctly when edited, but they come from the macro, not the template using the macro.
Unfortunately, Composer doesn't really understand XML. While trying to be helpful, it rewrites the document source, closing all the tags in non- HTML namespaces before opening any block-level HTML tags, thereby breaking the template conclusively:
To stop this happening, we could revert to using TAL only on HTML elements, creating wrapper div and span elements as necessary. This might be a necessary compromise if you needed to allow people to edit templates using WYSIWYG tools. [9]
As we won't be using Composer for editing, I suggest turning the Expand macros when editing switch back off. (To clean up the template again, just delete the bumf included from the other templates and Save; it won't be added back.)
The only difference between the deep and the flat directory templates, currently, is their handling of the sidebar. If we can factor this out, both directories can share one index_html. Move index_html to zpt, and edit the sidebar part to become:
<metal:sidebar use-macro="here/sidebar.pt/macros/sidebar"> </metal:sidebar>
In zpt/directory_flat, put a sidebar.pt template as follows:
<html>
<head> </head>
<body>
<metal:sidebar define-macro="sidebar">
<div class="sidebar">
<ul>
<li tal:repeat="folder here/homepages/objectValues">
<tal:is_link define="is_link python:folder.id != here.id">
<a href="" tal:condition="is_link"
tal:attributes="href folder/absolute_url"
tal:content="folder/title_or_id">Link to folder</a>
<tal:x condition="not:is_link" replace="folder/title_or_id" />
</tal:is_link>
</li>
</ul>
</div>
</metal:sidebar>
</body>
</html>
In zpt/directory_deep, sidebar.pt looks like this:
<html>
<head> </head>
<body>
<metal:sidebar define-macro="sidebar">
<div class="sidebar">
<ul>
<li tal:condition="python:here.id!='homepages'">
<a href="..">Up</a>
</li>
<li tal:repeat="folder python:here.objectValues('Folder')">
<a href=""
tal:attributes="href folder/absolute_url"
tal:content="folder/title_or_id">Link to folder</a>
</li>
</ul>
</div>
</metal:sidebar>
</body>
</html>
Depending on the browsing context, the correct sidebar is acquired. If we don't think we'll ever want to provide an index_html which repositions the sidebar, we can move it to the page macro as well (we can still reposition it using CSS). Remove the reference in index_html, and change standard_template.pt to this:
<html metal:define-macro="page">
<head>
<metal:block define-slot="head">
<title>The title</title>
<style> .sidebar {float: right} .footer {clear: both}
</style>
</metal:block>
</head>
<body>
<metal:sidebar use-macro="here/sidebar.pt/macros/sidebar | default" />
<div metal:define-slot="body"> This is where the page's sidebar and
body text goes. </div>
<div class="footer">
<hr> © 2004 by the author </div>
</body>
</html>
Once we've done this, we can remove all reference to the sidebar from index_html.
[6] This forms the basis of modular extensibility. Since its inception, ZPT has already been extended with support for the i18n namespace, making provision for the internationalisation of templates.
[7] The list can be extended, e.g. by defining variables in a calling template.
[8] Documentation, such as there is, may be found in the Zope Developers' Guide but watch out: the ZDG was written for Zope2.4; we're now at 2.7
[9] In the case of Plone, which composes pages from macros in many different templates, this is mostly not practical.
















