==========================
HTML Templating Mechanisms
==========================
:Version: $Revision: 1.14 $
Current Situation and Issues
============================
Syntax
------
Roundup currently uses an element-based HTML-tag-alike templating syntax::
The templates were initially parsed using recursive regular expression
parsing, and since no template tag could encapsulate itself, the parser
worked just fine. Then we got the ```` tag, which could have other
```` tags inside. This forced us to move towards a more complete
parser, using the standard python sgmllib/htmllib parser. The downside of this
switch is that constructs of the form::
">
don't parse as we'd hope. We can modify the parser to work, but that doesn't
another couple of issues that have arisen:
1. the template syntax is not well-formed, and therefore is a pain to parse
and doesn't play well with other tools, and
2. user requirements generally have to be anticipated and accounted for in
templating functions (like ``plain()`` and ``checklist()`` above), and
we are therefore artificially restrictive.
Arguments for switching templating systems:
*Pros*
- more flexibility in templating control and content
- we can be well-formed
*Cons*
- installed user base (though they'd have to edit their templates with the
next release anyway)
- current templating system is pretty trivial, and a more flexible system
is likely to be more complex
Templates
---------
We should also take this opportunity to open up the flexibility of the
templates through:
1. allowing the tracker to define a "page" template, which holds the overall
page structure, including header and footer
Possible approaches
===================
Zope's PageTemplates
--------------------
Using Zope's PageTemplates seems to be the best approach of the lot.
In my opinion, it's the peak of HTML templating technology at present. With
appropriate infrastructure, the above two examples would read:
status checklist
... which doesn't look that much more complicated... honest...
Other fun can be had when you start playing with stuff like:
Note: even if we don't switch templating as a whole, this document may be
applied to the ZRoundup frontend.
PageTemplates in a Nutshell
~~~~~~~~~~~~~~~~~~~~~~~~~~~
PageTemplates consist of three technologies:
TAL - Template Attribute Language
This is the syntax which is woven into the HTML using the ``tal:`` tag
attributes. A TAL parser pulls out the TAL commands from the attributes
runs them using some expression engine.
TALES - TAL Expression Syntax
The expression engine used in this case is TALES, which runs the expressions
that form the tag attribute values. TALES expressions come in three
flavours:
Path Expressions - eg. ``item/status/checklist``
These are object attribute / item accesses. Roughly speaking, the path
``item/status/checklist`` is broken into parts ``item``, ``status``
and ``checklist``. The ``item`` part is the root of the expression.
We then look for a ``status`` attribute on ``item``, or failing that, a
``status`` item (as in ``item['status']``). If that
fails, the path expression fails. When we get to the end, the object we're
left with is evaluated to get a string - methods are called, objects are
stringified. Path expressions may have an optional ``path:`` prefix, though
they are the default expression type, so it's not necessary.
String Expressions - eg. ``string:hello ${user/name}``
These expressions are simple string interpolations (though they can be just
plain strings with no interpolation if you want. The expression in the
``${ ... }`` is just a path expression as above.
Python Expressions - eg. ``python: 1+1``
These expressions give the full power of Python. All the "root level"
variables are available, so ``python:item.status.checklist()`` would be
equivalent to ``item/status/checklist``, assuming that ``checklist`` is
a method.
PageTemplates
The PageTemplates module glues together TAL and TALES.
Implementation
~~~~~~~~~~~~~~
I'm envisaging an infrastructure layer where each template has the following
"root level" (that is, directly accessible in the TALES namespace) variables
defined:
*klass*
The current class of item being displayed as an HTMLClass instance. Name is
mangled so it can be used in Python expressions.
*item*
The current item from the database, if we're viewing a specific item, as an
HTMLItem instance. If it doesn't exist, then we're on a new item page.
(*classname*)
this is one of two things:
1. the *item* is also available under its classname, so a *user* item
would also be available under the name *user*. This is also an HTMLItem
instance.
2. if there's no *item* then the current class is available through this
name, thus "user/name" and "user/name/menu" will still work - the latter
will pull information from the form if it can.
this is a dangerous attribute, and may cause us pain the long run (its name
may clash with other top-level variables ... it already clashed with the
proposed *user* variable). It might be safer to go with just *class* and
*item*, actually...
*form*
The current CGI form information as a mapping of form argument name to value
*request*
Includes information about the current request, including:
- the url
- the current index information (``filterspec``, ``filter`` args,
``properties``, etc) parsed out of the form.
- methods for easy filterspec link generation
- *user*, the current user item as an HTMLItem instance
*tracker*
The current tracker
*db*
The current open database
*config*
The current instance config
*modules*
python modules made available (XXX: not sure what's actually in there tho)
Accesses through a class (either through *klass* or *db.*)::
class HTMLClass:
def __getattr__(self, attr):
''' return an HTMLItem instance '''
def classhelp(self, ...)
def list(self, ...)
def filter(self):
''' Return a list of items from this class, filtered and sorted
by the current requested filterspec/filter/sort/group args
'''
Accesses through an *item*::
class HTMLItem:
def __getattr__(self, attr):
''' return an HTMLItem instance '''
def history(self, ...)
def remove(self, ...)
Note: the above could cause problems if someone wants to have properties
called "history" or "remove"...
String, Number, Date, Interval HTMLProperty
a wrapper object which may be stringified for the current plain() behaviour
and has methods emulating all the current display functions, so
``item/name/plain`` would emulate the current ``call="plain()``". Also,
``python:item.name.plain(name=value)`` would work just fine::
class HTMLProperty:
def __init__(self, instance, db, ...)
def __str__(self):
return self.plain()
class StringHTMLProperty(HTLProperty):
def plain(self, ...)
def field(self, ...)
def stext(self, ...)
def multiline(self, ...)
def email(self, ...)
class NumberHTMLProperty(HTMLProperty):
def plain(self, ...)
def field(self, ...)
class BooleanHTMLProperty(HTMLProperty):
def plain(self, ...)
def field(self, ...)
class DateHTMLProperty(HTMLProperty):
def plain(self, ...)
def field(self, ...)
def reldate(self, ...)
class IntervalHTMLProperty(HTMLProperty):
def plain(self, ...)
def field(self, ...)
def pretty(self, ...)
Link HTMLProperty
the wrapper object would include the above as well as being able to access
the class information. Stringifying the object itself would result in the
value from the item being displayed. Accessing attributes of this object
would result in the appropriate entry from the class being queried for the
property accessed (so item/assignedto/name would look up the user entry
identified by the assignedto property on item, and then the name property of
that user)::
class LinkHTMLProperty(HTMLProperty):
''' Be a HTMLItem too '''
def __getattr__(self, attr):
''' return a new HTMLProperty '''
def download(self, ...)
def checklist(self, ...)
Multilink HTMLProperty
the wrapper would also be iterable, returning a wrapper object like the Link
case for each entry in the multilink::
class MultilinkHTMLProperty(HTMLProperty):
def __len__(self):
''' length of the multilink '''
def __getitem(self, num):
''' return a new HTMLItem '''
def checklist(self, ...)
def list(self, ...)
*request*
the request object will handle::
class Request:
def __init__(self, ...)
def filterspec(self, ...)
Accesses through the *user* attribute of *request*::
class HTMLUser(HTMLItem):
def hasPermission(self, ...)
(note that the other permission check implemented by the security module may
be implemented easily in a tal:condition, so isn't needed here)
Template files
~~~~~~~~~~~~~~
Each instance will have the opportunity to supply the following templates:
page
This is the overall page look template, and includes at some point a TAL
command that includes the variable "content". This variable causes the actual
page content to be generated.
[classname].[template type]
Templates that have this form are applied to item data. There are three forms
of special template types:
[classname].index
This template is used when the URL specifies only the class, and not an item
designator. It displays a list of [classname] items from the database, and
a "filter refinement" form.
Would perform a TAL ``repeat`` command using the list supplied by
``class/filter``. This deviates from the current situation in that currently
the index template specifies a single row, and the filter part is
automatically generated.
[classname].item
This template is used when the URL specifies an item designator. It's the
default template used (when no template is explicitly given). It displays
a single item from the database using the *classname* variable (that
is, the variable of the same name as the class being displayed. If
These two special template types may be overridden by the :template CGI
variable.
Note that the "newitem" template doesn't exist any more because the item
templates may determine whether the page has an existing item to render. The
new item page would be accessed by "/tracker/url/issue?:template=item".
The old "filter" template has been subsumed by the index template.
Integrating Code
~~~~~~~~~~~~~~~~
We will install PageTemplates, TAL and ZTUtils in site-packages. If there is a
local Zope installation, it will use its own PageTemplates code (Zope modifies
the module search path to give precedence to its own module library).
We will then install the trivial MultiMapping and ComputedAttribute modules in
the Roundup package, and have some import trickery that determines whether
they are required, and if so they will be imported as if they were at the
"top level" of the module namespace.
New CGI client structure
~~~~~~~~~~~~~~~~~~~~~~~~
Handling of a request in the CGI client will take three phases:
1. Determine user, pre-set "content" to authorisation page if necessary
2. Render main page, with callback to "content"
3. Render content - if not pre-set, then determine which content to render
Use Cases
~~~~~~~~~
Meta/parent bug
Can be done with addition to the schema and then the actual parent heirarchy
may be displayed with a new template page ":dependencies" or something.
Submission wizard
Can be done using new templates ":page1", ":page2", etc and some additional
actions on the CGI Client class in the instance.