ZOPE 2 TEMPLATING EXPLAINED AS BEST AS CAN BE HOPED
Templates live in layers which, due to Zope magic, are available anywhere in the object tree. As is the case with most templating languages, Zope templates are context-agnostic, meaning that they may be used as views on any object. When the name of a template is called against a particular context, the skins tool (/zport/portal_skins in Zenoss) will supply the appropriate template object, determined by the priority of the layers -- given two templates with the same name, that in the higher priority layer will prevail. This allows Zope products to override the templates of other products to provide different functionality. It can also result in total confusion as to the source of a template, as this process is in no way transparent.
Templates may be created in the ZODB, or they may live on the filesystem; the latter is preferable for all but the most ad hoc situations. Typically, a Zope product that provides templates will register a 'skins' directory, which will include one or more layers. When the product is initialized, the layers it provides will be added to the skins tool under whatever skin is specified. Zenoss has a single skin, so only the order of the layers determines template inheritance.
The Zenoss UI comprises several layers, mostly for the purposes of organization. The ZenModel and ZenEvents products each have a layer (named 'zenmodel' and 'zenevents,' respectively), the ZenUtils product has one (inexplicably located at ZenUtils/js), and the ZenWidgets product has two ('zentablemanager' and 'zenui'). zenmodel and zenevents generally contain templates applicable to classes provided by their respective products. The zenui layer contains most of the dialog templates, nearly all of the CSS, Javascript and image files (including the YUI library) and other templates that don't necessarily belong to a single product. The zentablemanager layer provides resources related to ZenTableManager. The ZenUtils/js layer provides the MochiKit library and a few Javascript utilities. Both the zentablemanager layer and the ZenUtils/js layer are legacies and shouldn't be modified. All new templates should go in one of the other three, and all static browser resources should go in zenui.
Zope page templates are a combination of METAL, TAL and TALES, each of which is summarized more succinctly than one familiar with them might expect here:
http://www.owlfish.com/software/simpleTAL/tal-guide.html
In short: METAL allows templates to define macros (which are essentially subtemplates that may be called by other templates) and slots (which may be filled by other templates). For example, one wishing to have a title on all pages might create the following base.pt:
<html metal:define-macro="base_template">
<head>
<title>
Zenoss:
<tal:block metal:define-slot="subtitle">
Default Subtitle
</tal:block>
</title>
</head>
<body>
<tal:block metal:define-slot="content">
Default Content
</tal:block>
</body>
</html>
Then on a template that might be used to view an object, one could:
<tal:block metal:use-macro="here/base/macros/base_template">
<tal:block metal:fill-slot="subtitle">
My Subtitle
</tal:block>
<tal:block metal:fill-slot="content">
My Content
</tal:block>
</tal:block>
This allows for relatively complex abstraction.
Zenoss has a base template providing several basic page types that include global CSS and javascript resources, the basic page structure, and optionally the tab pane. This template is located at ZenModel/skins/zenmodel/templates.pt. Typically, when creating a new template, find another like it and copy the templates.pt macro reference used there.
TAL comprises a set of attributes for page elements allowing for iteration loops, dynamic attribute mutation, and other dynamic content. The above resource will summarize these more fully.
TALES allows access to the template's namespace. Some useful properties available on all templates:
here: the context object
container: the folder containing the context object
template: the template object
root: the portal object (zport)
user: the current authenticated user object
request: the current HttpRequest object
portal_url: the base url of the portal ('http://localhost:8080/zport')
TALES accepts paths (e.g. here/id) which it resolves into object properties. It will attempt to resolve the final path element as a key index, a key name, an attribute, or a callable. For example, if 'mydict' is a dictionary on the context, 'here/mydict/mykey' will return mydict[mykey]. If 'getSomething' is a method on the context, 'here/getSomething' will return the result of that method. However, if here.getSomething() returns a dictionary, one cannot do 'here/getSomething/mykey'.
The path resolution is fairly limited -- for example, one cannot pass arguments to methods. In case something more complex is needed, one can use 'python:' followed by arbitrary Python code. For example, 'python:here.mydict[mykey]' will return the same thing as 'here/mydict/mykey', while 'python:here.getSomething(template.id)' is not possible using a path. The previous paragraph's impossible 'here/getSomething/mykey' can be resolved this way: 'python:here.getSomething()[mykey]'.
Finally, if one wishes to generate a string, one may prepend the argument with 'string:'. Everything after that will be treated as a string, unless contained within ${}, in which case it will be evaluated as a TALES path. For example: <span tal:content='string:The name of this template is ${template/id}'/>
A few final points:
-
ZPT ignores everything inside a <script> tag, although it does not ignore TAL defined on the tag itself. This can make dynamic Javascript problematic. One way around this, however, is like this:
<script tal:content="string: var templateId = '${template/id}'; "></script>This is obviously unwieldy, especially in the case of several levels of nested quotes, but it at least allows Javascript access to the template's namespace.
-
Slots on macros are /not/ inherited unless specifically defined. For example, if one has a template base.pt:
<tal:block metal:define-macro="base"> My Base Template <span metal:define-slot="content">Default Content</span> </tal:block>from which one wishes to create a more specific base template, plaintext.pt:
<tal:block metal:define-macro="plaintext"> <style>body{font-family:Courier,monospace}</style> <tal:block metal:use-macro="here/base/macros/base"/> </tal:block>templates calling here/plaintext/macros/plaintext will not be able to fill here/base/macros/base's 'content' slot. One must chain the slots, defining a plaintext content slot inside the fill of base's content slot:
<tal:block metal:define-macro="plaintext"> <style>body{font-family:Courier,monospace}</style> <tal:block metal:use-macro="here/base/macros/base"> <tal:block metal:fill-slot="content"> <tal:block metal:define-slot="content"> </tal:block> </tal:block> </tal:block> </tal:block> -
Thanks to Zope's magical acquisition, templates can be treated as methods on objects. If an object may be viewed at /zport/dmd/object/mytemplate, then calling:
object.mytemplate()in a Python file will return the HTML that template generates. In this case, however, there's no request object, so templates that ask for one will throw an error. This is both a blessing and a curse; many man-hours have been wasted searching for methods that do not exist.
-
Generally, unless a specific tag is required, use <tal:block> for purely logical structures, as it will produce no side effects (whereas using <div> could easily do so).