OpenACS Templates
Over the last few months, we've looked at the Open Architecture Community System, an open-source toolkit for creating community web sites. Last month, we even looked at how we can create a simple application package using the ArsDigita Package Manager (APM).
But at its heart, web development is all about receiving inputs in HTTP GET and POST requests and about producing pages of HTML in response to those requests. Each web development toolkit has its own set of templates, and each type of template has its own personality and quirks.
This month, we take a closer look at the OpenACS templating system, which is similar in some ways to Zope Page Templates (ZPT). The OpenACS templates are rather sophisticated in the way they collect and return data and make it possible to perform many types of automatic error checking that would otherwise be tedious or simply ignored.
Older versions of OpenACS used a templating system known as AOLserver Dynamic Pages (ADPs), similar to JSP, ASP or PHP. ADPs could contain Tcl code, thanks to the multithreaded Tcl interpreter running inside of AOLserver, the HTTP server that powers OpenACS. However, ADP had a number of problems. Each Tcl block was evaluated independently, making it difficult to have conditional code inside of a template. There was no standard way to ensure that ADPs were passed required parameters to give parameters optional values. And of course, the troubles really began when developers and designers needed to work on the same file. Moreover, much of OpenACS was written in simple .tcl files, which can be quite daunting for a nonprogrammer.
The programmers at ArsDigita, whose code lives on in the OpenACS Project, decided that a paradigm shift was in order. No longer would pages be invoked with their familiar .html and .adp suffixes; instead, they would be called without a suffix at all.
This is possible because of AOLserver's willingness to search, in order, for an appropriate page. Given the URL /foo, AOLserver will first look for /foo.tcl, then for /foo.adp and finally for /foo.html. (This configuration setting can be changed in the nsd.tcl configuration file that typically is located in /usr/local/aolserver.)
The OpenACS templating system relies on this fact to split the work between two different files. In general, the output generated by an HTTP request has to go through two different files. The .tcl file executes first, performing database queries and setting variables. Its final line is typically going to be ad_return_template, a Tcl procedure that then invokes the companion .adp page. The ADP can retrieve the variables as data sources.
Because the .tcl and .adp files are supposed to be developed by separate people, it's natural to expect them to drift apart or have compatibility issues. The ArsDigita engineers avoided this problem by setting up a “page contract”, meaning a list of HTTP parameters that the .tcl file expects to receive, and then a list of Tcl variables (known as data sources) that will be available to the .adp page in its display.
Data sources are simply Tcl variables by another name. Inside of an .adp page, you specify a data source like @this@, with @ signs around the name. If the data source has not been defined, the template will exit with a Tcl stack trace, complaining of an unknown variable.
Data sources can be placed anywhere in a file. Their values are substituted in place of those on the HTML page before the HTML is sent to the user's browser, which means that data sources can define not only text, but also image names and stylesheet attributes.
For example, here is a simple OpenACS template that displays the user's first name in a first-level headline:
<master> <h2>@first_name@</h2> <p>That's you, isn't it?</p> </master>
The master tags indicate that the page in question is not complete HTML output, but rather is meant to be inserted inside of the local master (i.e., a template pair named master.tcl/master.adp). The local master, in turn, is wrapped inside of the site's default master (normally in a template pair named default-master.tcl and default-master.adp, but this is configurable using parameters). Thus, the resulting page consists of:
default master top local master top our page local master bottom default master bottomIn this way, you can create sites that have a unified look and feel, with common headers and footers, such as menubars and contact information. The notion of masters and default masters is similar in many ways to autohandlers in the Perl-based HTML::Mason templating system.
This is all well and good, but where is @first_name@ defined? It is defined in the companion .tcl file. But it's not enough for the .tcl file to set the first_name variable. It also must mark the variable for export as a data source by naming it explicitly.
.tcl pages name their inputs and outputs in arguments to the ad_page_contract procedure, which normally executes at the top of a page. ad_page_contract is a powerful mechanism that lets us create pages that expect to receive certain inputs, which in turn promise to produce certain outputs. A call to ad_page_contract can range from extremely simple to extremely complex, depending on the needs of the .adp template page. For example, a .tcl page that sets the user's first name to a dummy value and then exports it to the .adp page could look like:
ad_page_contract { Comments and CVS information go here. } { } -properties { first_name:onevalue } set first_name "Dummy first name" ad_return_template
The call to ad_page_contract tells the templating system that the variable first_name will be exported. We then set the variable's value and finally pass those values to the template with ad_return_template. (For unfortunate historical reasons, data sources are passed in the -properties parameter, rather than something with a more informative name.)
We can pass multiple variables to the template by naming additional variables in ad_page_contract:
ad_page_contract { Comments and CVS information go here. } { } -properties { first_name:onevalue last_name:onevalue } set first_name "FirstName" set last_name "LastName" ad_return_template
As you can see, ad_page_contract makes it easy to pass individual text strings to the template. But in many cases, particularly when retrieving results from a database, we want to pass lists of values. OpenACS templates take care of this without any problem. For example, the following .tcl page retrieves the list of users on the system and places that in a “list” data source:
ad_page_contract { Comments and CVS information go here. } { users:onelist } set users [db_list get_all_users { SELECT PE.first_names || ' ' || PE.last_name as users FROM Parties PA, Persons PE WHERE PA.party_id = PE.person_id ORDER BY PE.last_name, PE.first_Names }] ad_return_template
As you can see, our SQL query returns a single column, named users. The OpenACS database API turns this into the Tcl list users, which we then export as the users' data source. But because we export it with the :onelist descriptor, an .adp page can iterate over each individual element:
<master> <list name="users"> <li> @users:item@ </list> </master>Our iterator is <list>, the contents of which execute once for each element in the list. The current element is available as @users:item@; the number of the current iteration is available as @users:rownum@, and the current iteration is available as @users.
If you want to iterate over multiple database rows, your .tcl page can export a multirow. A multirow contains all of the rows that were returned, with column names. Here's the Tcl side of things:
ad_page_contract { } { } -properties { users:multirow } db_multirow users get_info " SELECT PE.first_names || ' ' || PE.last_name as name, PA.email FROM Parties PA, Persons PE WHERE PA.party_id = PE.person_id ORDER BY PE.last_name, PE.first_names" ad_return_template
The db_multirow procedure takes three arguments: the name of an array that will be populated (and exported as a data source), the name of the query (which is used in conjunction with database-independent .xql files) and the fallback query that is used if no .xql file is found. In the .adp template, we can then do the following:
<master> <ul> <multiple name="users"> <li> <a href="mailto:@users.email@" @users.name@</a> </multiple> </ul> </master>There are two tricky things to note here. First, the iterating tag is multiple, even though the data source is exported as a multirow. Using the wrong name in the wrong place can create hard-to-understand bugs. More subtly, the element selector within a <multiple> tag is a period (@users.email@), while it's a colon (@users:item@) in a <list> tag. I find myself constantly making mistakes on this and checking previously working pages of code to ensure that I use the right syntax with the right page.
So far, we have only looked at ways in which ad_page_contract allows us to export data from the .tcl page to the .adp page. But .tcl pages can accept inputs as well, via either GET or POST requests. ad_page_contract allows us to specify which inputs we expect to receive, to assign default values as necessary and to check that the inputs are in a particular format:
ad_page_contract { } { foo } -properties { foo2:onevalue } set foo2 "$foo$foo" ad_return_template
In the above example, the .tcl page expects to receive a parameter (via either GET or POST) named foo. The parameter's value is then used in the creation of a new data source, foo2, which contains a doubled version of foo.
If someone invokes the above page without passing a foo parameter, OpenACS automatically produces an error message that looks like the following:
We had a problem processing your entry: * You must supply a value for foo Please back up using your browser, correct it, and resubmit your entry. Thank you.
We can give foo a default value if it is not passed, by making it the first element of a Tcl list and giving a default value as the second element:
ad_page_contract { } { {foo "blah"} } -properties { foo2:onevalue } set foo2 "$foo$foo" ad_return_templateSo invoking this page with a parameter of foo=abc will produce output of “abcabc”, and invoking it without any parameter will produce output of “blahblah”.
We can add one or more options to each parameter to limit the types of information that we receive. For example, we can trim any leading or trailing whitespace from an input parameter or ensure that we only receive an integer greater than zero:
ad_page_contract { } { sometext:trim anumber:naturalnum }
You can use multiple options on a parameter by separating them with commas:
ad_page_contract { } { sometext:trim,nohtml {anumber:naturalnum 50} }The above page contract says that anumber must be a natural number, with a default value of 50 if nothing is specified. sometext will be trimmed for leading and trailing whitespace but may not contain any HTML tags. There are related html and allhtml options that allow safe HTML tags and any HTML tags, respectively.
Page contracts can get even fancier. For example, you can create a date selection widget with the ad_dateentrywidget function. So you can imagine a .tcl page that looks like:
ad_page_contract { } { } -properties { datewidget:onevalue } set datewidget [ad_dateentrywidget datewidget] ad_return_template
The accompanying .adp page, which will display this date widget, then looks like:
<master> <form method="POST" action="date-2"> @datewidget@ <input type="submit" value="Send the date"> </form> </master>In other words, our HTML form will send the contents of the date widget to date-2, a .tcl page that will display its results in an .adp page. date-2.tcl could tell ad_page_contract that the incoming datewidget parameter will contain a simple array. But, we additionally can declare datewidget to be a parameter of type date, which automatically gives us four array elements:
ad_page_contract { } { datewidget:array,date } -properties { date_month:onevalue date_day:onevalue date_year:onevalue date_full:onevalue } set date_month $datewidget(month) set date_day $datewidget(day) set date_year $datewidget(year) set date_full $datewidget(date) ad_return_templateOur .adp page, date-2, can now display the date information in a variety of formats:
<master> <p>Month: @date_month@</p> <p>Day: @date_day@</p> <p>Year: @date_year@</p> <p>Full text: @date_full@</p> </master>Note that the full version of the date widget is perfectly acceptable for SQL queries. This comes in handy when entering dates or when using them in comparison queries.
We have only scratched the surface of the OpenACS templating system. ad_page_contract additionally supports verification routines that allow you to check multiple parameters or to signal errors based on information found in the database. You actually can define your own custom error messages that may appear in the case of trouble. Even outside of ad_page_contract, a .tcl page also can call the universal ad_return_complaint function, which produces error messages in nicely formatted HTML pages.
In addition, the OpenACS templating system has a complete set of form-building routines that allow a programmer to specify an HTML form using Tcl procedures. The contents of the form then can be exported to the .adp page using data sources. Not only does the form builder cut down on the amount of HTML you have to write, but it makes it easy to create a two-stage form submission process, in which users get a chance to preview their work before sending it in.
Finally, OpenACS templates include a number of additional tags, such as <if>, that allow you to include text and images conditionally, depending on the values of other data sources.
While OpenACS is often touted as a remarkable system because of its elaborate data model and advanced applications, I have found the templates to be one of the more compelling parts of OpenACS. The graphic designers with whom I work enjoy the separation between .tcl and .adp pages, and I like that I can check for errors and pass multiple values without having to remember that there is no obvious connection between them at the HTTP level.
While the learning curve for OpenACS can be quite steep, learning how the templates work is both a gratifying and interesting way to start with this system. Given the many application packages that come with OpenACS, there also are numerous examples of the templates right in the code after you download the system.
email: reuven@lerner.co.il
Reuven M. Lerner is a consultant specializing in web/database applications and open-source software. His book, Core Perl, was published in January 2002 by Prentice Hall. Reuven lives in Modi'in, Israel, with his wife and daughter.