DTML Basics (part 3)

Twist and turn with DTML's loops.

A Twist In The Tail

If you've been following along, you now know how to incorporate decision-making constructs in your DTML code. As you saw in last week's episode, DTML comes with a fairly large family of conditional statements, which allow you to add business intelligence to your DTML scripts.

But DTML is a full-fledged programming language, and one which allows you to do a lot more than nest "if" statements within each other. One of its cooler capabilities involves using powerful loop constructs to iterate over sequences of both variables and objects. This ability to create loops is not unique to DTML - almost every programming language on the planet allows you to do this - but DTML includes some fairly interesting twists to the traditional approach.

Coincidentally, those twists just happen to be the subject of today's discussion. Keep reading - you've probably never seen this before.

Playing The Numbers

Before we get started, let's get the jargon straight - what's a sequence anyway?

As traditionally understood, a "sequence" is a series of items, usually connected to each other by a logical thread. For example,

0, 1, 2, 3, 4, 5...
p, q, r, s, t...
huey, dewey, louie...

are all valid sequences

In the Zope context, the definition of a sequence can be scoped down a little further - a sequence here is usually a set of objects. For example, if you create a folder in Zope, the objects stored within that folder can be considered a sequence. Or, if you retrieve a set of records from a database, the resultset returned can also be considered a sequence of data items.

A sequence, however, is just one part of the jigsaw. In order to access the elements that make up a sequence, you usually need a programming structure that will iterate, or loop, through the sequence, processing each element in turn. This loop can be combined with decision-making logic (remember what you learnt last time?) to perform specific actions or execute specific commands while processing the elements in a sequence.

In order to demonstrate this, let's create a sequence in Zope and write a loop to process it. Fire up Zope, log into the Zope management interface, and create an instance of the DTML Document object. Name it "SampleSequence" and fill it with the following code:

<h3><dtml-var title_or_id></h3><br>

<ul>
<dtml-in expr="0, 1, 2, 3, 4, 5">
<li> I am <dtml-var sequence-item></li>
</dtml-in>
</ul>

First, I've created a simple sequence by brute force - it contains numbers from 0 to 5. Then, the <dtml-in> tag is used to loop through the sequence, in a manner similar to the "for" loops that PHP and Perl programmers are familiar with. From the output, it's obvious that this script loops six times to display the items in the sequence.

The individual elements of the sequence can be accessed via the special "sequence-item" variable. This is true in all cases, except when the items in the sequence are object that cannot be converted to strings.

Save the code and view the output of the script - you should see something like this:

* I am 0
* I am 1
* I am 2
* I am 3
* I am 4
* I am 5

When In Rome...

Not impressed yet? Let's alter the code a little to see what else you can do with sequences.

<h3><dtml-var title_or_id></h3>
<br>
<dtml-in expr="'apple', 'banana', 'orange', 'apricot', 'grape'">
<sequence-length>
<dtml-if  sequence-start><i>Here's where it all starts</i><br></dtml-if>
My name is <dtml-var sequence-item>, my sequence position is <dtml-var sequence-number>, and my index is <dtml-var sequence-index>.<br>
<dtml-if  sequence-end><i>And here's where it ends</i><br></dtml-if>
</dtml-in>

Now take a look at the output.

Here's where it all starts
My name is apple, my sequence position is 1, and my index is 0.
My name is banana, my sequence position is 2, and my index is 1.
My name is orange, my sequence position is 3, and my index is 2.
My name is apricot, my sequence position is 4, and my index is 3.
My name is grape, my sequence position is 5, and my index is 4.
And here's where it ends

This example uses a couple of new variables to add new functionality to the example. First, the "sequence-number" variable represents the position of the current item in the sequence, while the "sequence-index" variable provides an alternative zero-based indexing mechanism that serves the same purpose. And the "sequence-start" and "sequence-end" variables provide an easy way to find out if you're at the beginning or end of the sequence - the former is true when the current item is the first element of your sequence and the latter is true when the current element is the last element of the sequence.

One more interesting variable (which I haven't demonstrated in the example above) is the "sequence-length" variable, which returns the number of items present in the sequence. There's also the "sequence-roman" variable, which can be used to display the sequence number using lower-case Roman numerals, the "sequence-Roman" for upper-case Roman numerals, and the "sequence-letter" and "sequence-Letter" variables for alphabetical indexing.

A List In Time

The example you just saw used a simple sequence. However, you can also loop over a collection of objects using the same technique. Take a look at a simple example that lists the objects in the current folder (use a DTML Method here instead of a DTML Document).

This folder contains:
<ul><dtml-in expr="objectIds()">
<li><dtml-var sequence-item></li>
</dtml-in>
</ul>

Here's the output:

This folder contains:
* ifOnly
* access
* selectDay
* SampleSequence
* WithOrWithoutYou

Over here, I've used the ObjectIds() method of the ObjectManager class to produce a list of objects, and iterated through that list using a DTML loop. For the uninitiated, the ObjectManager class is a class that contains other Zope objects; it provides methods that allow you to find out about the objects it "manages". I don't want to get into the details here - for a more technical overview, check out http://www.zope.org//Members/michel/Projects/Interfaces/ObjectManager.

Let's replace ObjectIds()with ObjectValues(), in order to make things a little more interesting.

<dtml-in expr="objectValues()">
<dtml-if  sequence-start><b>There are <dtml-var sequence-length> items in this folder.</b><br></dtml-if>
This is <dtml-var sequence-var-id>.
<br>
<dtml-var sequence-item><p>
</dtml-in>

Check the output and get ready for a big surprise.

How did this happen? Well, the ObjectValues() method returns the objects present in the folder. So, when I reference each item with the "sequence-item" variable, I'm actually accessing the object itself, and displaying the output of the DTML code that is present in the object.

Once again, the "sequence-length" variable has been used to display the number of objects in the current folder. But how was I able to display the ID of the object at the beginning of each listing?

This is another feature available with sequences: you can access all the variables associated with an object using the following syntax:

sequence-var-varname

where varname is the name of the variable whose value you want to display. So, you can access the title using "sequence-var-title" or the object ID using "sequence-var-id", just as in the example above.

And here's another variant of the example above:

<dtml-in expr="objectItems()">
This is <dtml-var sequence-key>
<br>
<dtml-var sequence-item>
<p>
</dtml-in>

The output is the same as that of the previous example. However, over here, I've used the ObjectItems() method of the Object Manager. This returns the objects in the form of a tuple (a container variable, which can contain one or more values, kinda like an associative array or hash). The ObjectItems() method returns the IDs and the objects as key-value pairs, so you can access the name of the object by using the "sequence-key" variable and the object itself using the "sequence-item" variable.

An Easy Replacement

How about a quick example to put all this in perspective? This next example allows you to replace the Zope management interface's folder listing with your own custom design, and also demonstrates how sequences can be used to do some fairly nifty things in Zope.

<table cellpadding="5" cellspacing="0" border="0">
<tr bgcolor="#CFCFCF">
<td><b>Type</b></td>
<td><b>Name</b></td>
<td><b>Size</b></td>
<td><b>Last Modified</b></td>
</tr>
<dtml-in expr="objectValues()" sort="id" >
<dtml-if sequence-even>
<tr bgcolor="#DCDCDC">
<dtml-else>
<tr>
</dtml-if>
<td><img src="<dtml-var sequence-var-icon>" border="0"></td>
<td><a href="&dtml-absolute_url;"><dtml-var sequence-var-id></td>
<td><dtml-var sequence-var-get_size> bytes</td>
<td><dtml-var sequence-var-bobobase_modification_time></td>
</tr>
</dtml-in>
</table>

And here's the output.

Isn't that neat? Just some manipulation to the sequence used in the previous example, along with some predefined variables, and you're ready to rock and roll.

<dtml-in expr="objectValues()" sort="id" >
<dtml-if sequence-even>
<tr bgcolor="#DCDCDC">
<dtml-else>
<tr>
</dtml-if>

First, I call the objectValues() method so that I have a sequence with the contents of the current folder. In this case, I've also added the "sort" attribute to the <dtml-in> tag - this allows me to specify the parameter by which sequence elements are to be sorted (the object ID).

The special "sequence-even" variable provides an easy way to implement alternating colours for the list elements - it's set to true if the value of the "sequence-index" variable is even. So the first element has a grey background and the next a white one. If you want to switch it around, you can use the "sequence-odd" variable, which works in the reverse direction.

<td><img src="<dtml-var sequence-var-icon>" border="0"></td>
<td><a href="&dtml-absolute_url;"><dtml-var sequence-var-id></td>
<td><dtml-var sequence-var-get_size> bytes</td>
<td><dtml-var sequence-var-bobobase_modification_time></td>

The next section of the code is pretty straightforward- I've used various object properties to construct each row of the list:

"icon" - the path and name of the icon that is displayed along the object in the Zope listing

"id" - the object ID

"get_size" - the size of the object in bytes

"bobobase_modification_time" - the object's last modification time

Remember the "sort" attribute of the "dtml-in" tag? I told you that you could easily alter this attribute to manipulate the sorting order of the sequence. Here's how: set the value of this attribute to "icon", "get_size" or "bobobase_modification_time" and check the output; you'll see that the items in the listing get re-sorted against the named parameter.

Bringing In The Database

Another common application of sequences involves using them in combination with database result sets to break up a large result set into smaller batches.

Consider the following example, which invokes a ZSQL Method (for the uninitiated, a ZSQL Method is a special Zope object that allows you to communicate with a database - read more about it at http://www.melonfire.com/community/columns/trog/article.php?id=116). In the following example, the GetUsers() method retrieves a list of users from a database

select * from person

as a sequence and uses a DTML loop to iterate through it and print the data.

<table cellpadding="5" cellspacing="0" border="1" width="250">
<tr bgcolor="#CFCFCF">
<td align="left"><b>User Details</b></td>
</tr>
<dtml-in GetUsers sort=FirstName>
<tr>
<td align="left"><font size="2"><b><dtml-var sequence-number>. <dtml-var Title> <dtml-var FirstName> <dtml-var Surname></b></font><br>
<font size="1"><i><dtml-var Email></i><br><dtml-var Tel><br><dtml-var Fax></font></td>
</tr>
</dtml-in>
</table>

The output displays all the records in a single page, as shown below.

This example uses the <dtml-in> tag to loop over the result set returned by the GetUsers() ZSQL Method. The rest of the code is good ol' HTML to make the page look pretty.

Depending on the number of records in the table, you can rest assured that this is a definite no-no as far as the usability and performance of the site is concerned. A better idea would be to split this result set into multiple "pages", so as to reduce overhead and also to make the site more usable. Here's the code:

<table cellpadding="5" cellspacing="0" border="1" width="250">
<tr bgcolor="#CFCFCF">
<td align="left"><b>User Details</b></td>
</tr>
<dtml-in GetUsers size="6" start=start_here sort=FirstName>

<dtml-if sequence-start>
<dtml-if previous-sequence>
<tr><td align="right"><font size="2">[ <a href="<dtml-var URL><dtml-var sequence-query>start_here=<dtml-var previous-sequence-start-number>"> Previous <dtml-var previous-sequence-size> users</a> ]</font></td></tr>
<dtml-else>
<tr><td align="right">&nbsp;</td></tr>
</dtml-if>

</dtml-if>
<dtml-if sequence-even>
<tr bgcolor="#DCDCDC">
<dtml-else>
<tr bgcolor="#CECECE">
<tr>
</dtml-if>
<td align="left"><font size="2"><b><dtml-var sequence-number>. <dtml-var Title> <dtml-var FirstName> <dtml-var Surname></b></font><br>
<font size="1"><i><dtml-var Email></i><br><dtml-var Tel><br><dtml-var Fax></font></td>
</tr>
<dtml-if sequence-end>
<dtml-if next-sequence>
<tr><td align="right"><font size="2">[ <a href="<dtml-var URL><dtml-var sequence-query>start_here=<dtml-var next-sequence-start-number>"> Next <dtml-var next-sequence-size> users</a> ]</font></td></tr>
<dtml-else>
<tr><td align="right">&nbsp;</td></tr>
</dtml-if>
</dtml-if>

</dtml-in>
</table>

Phew! That's a lot of code...but the result is worth it.

Take a close look at the output. No scroll bar. Neat little links to the next and previous "batch" of users. Just what I wanted.

Digging Deeper

Let's see how it works:

<table cellpadding="5" cellspacing="0" border="1" width="250">
<tr bgcolor="#CFCFCF">
<td align="left"><b>User Details</b></td>
</tr>
<dtml-in GetUsers size="6" start=start_here sort=FirstName>

This is fairly simple - after starting a HTML table to display the details, I've invoked the GetUsers() ZSQL Method. The difference is that, this time, I've added two attributes: the "size" attribute, which specifies the number of items in each batch (6), and the "start" attribute, which specifies the sequence number of the first item in the batch.

Remember that sequence items are numbered from 1. As a result, the first batch contains items numbered 1 to 6, the second 7 to 12, and so on. In this example, I will need to constantly change the value of the "start" attribute so that results are displayed correctly. That's why I've stored this value in a variable called "start_here" - this variable will constantly be updated, as demonstrated in the code snippet below:

<dtml-if sequence-start>
<dtml-if previous-sequence>
<tr><td align="right"><font size="2">[ <a href="<dtml-var URL><dtml-var sequence-query>start_here=<dtml-var previous-sequence-start-number>"> Previous <dtml-var previous-sequence-size> users</a> ]</font></td></tr>
<dtml-else>
<tr><td align="right">&nbsp;</td></tr>
</dtml-if>

This bit of code controls of the display of the "Previous 6 users" link that appears at the top of the listing. It first checks the value of the "sequence-start" variable to ensure that it is at the top of the current batch of records - this helps to ensure that the link only appears at the top of the listing.

Next, it checks the value of the "sequence-previous" variable. This is a special variable that is true if the current batch is not the first batch of a sequence. However, this is only true for the first iteration of the loop - for the subsequent items of the current batch, this value is again false. As a result, the link is displayed only once for the current batch.

The "URL" variable stores the URL to the DTML Method, while the "sequence-query" variable stores the query string. However, this does not contain the "start_here" variable - this variable is added dynamically via the "previous-sequence-start-number" variable (as the name of the variable suggests, this stores the starting sequence number of the previous batch.) This value is assigned to the "start_here" variable, which in turn sets the value for the "start" attribute of the <dtml-in> tag. Finally, the "previous-sequence-size" variable is used to display the number of items in the previous batch.

Time to move on.

<dtml-if sequence-even>
<tr bgcolor="#DCDCDC">
<dtml-else>
<tr bgcolor="#CECECE">
<tr>
</dtml-if>
<td align="left"><font size="2"><b><dtml-var sequence-number>. <dtml-var Title> <dtml-var FirstName> <dtml-var Surname></b></font><br>
<font size="1"><i><dtml-var Email></i><br><dtml-var Tel><br><dtml-var Fax></font></td>
</tr>

This only contains some HTML code along with program logic for manipulating the color of the background.

<dtml-if sequence-end>
<dtml-if next-sequence>
<tr><td align="right"><font size="2">[ <a href="<dtml-var URL><dtml-var sequence-query>start_here=<dtml-var next-sequence-start-number>"> Next <dtml-var next-sequence-size> users</a> ]</font></td></tr>
<dtml-else>
<tr><td align="right">&nbsp;</td></tr>
</dtml-if>
</dtml-if>

Looks familiar? It should - it's almost the same code you saw earlier...except that, this time, it's used to display a "Next 6 users" link.

The first thing to do here is to check the value of the "sequence-end" variable. This is true if the current element is the last element of the sequence and, in our the case, the current batch. This helps to ensure that the link only appears at the end of the listing.

The "sequence-next" variable is a special variable that is true if the current batch is not the last batch of a sequence. Once again, this is only true for the first iteration of the loop. Next, we have the "URL" and "sequence-query" variables that provide the same functionality again - an absolute URL to our query method.

The "next-sequence-start-number" variable comes next - this stores the starting number of the next batch. Again, this value is assigned to the "start_here" variable, and the "next-sequence-size" variable is used to display the number of items in the next batch.

That was heavy, wasn't it? But understanding it is worth the effort - this kind of code is reusable, and, once you've figured it out, you'll find it useful in more than one place. Again, Zope proves the value of object reusability, and demonstrates how easy it is to implement complicated widgets with just a few lines of code.

And that's about it for the moment. In this article, I explained how sequences work, and demonstrated how they could be processed with DTML. In the Zope world, a sequence need not consist of just variables; it can just as easily contain objects, and it comes with a number of special properties. This makes it possible to build some fairly powerful widgets with just a few lines of code - this article demonstrated a directory viewer and a pager - and also eases the task of writing simple, maintainable code.

In the next article in this series, I'll be looking at a couple of other constructs that may come in useful in your journey through DTML. Come back next week for more.

Note: All examples in this article have been tested on Linux/i586 with Zope 2.5.0. Examples are illustrative only, and are not meant for a production environment. Melonfire provides no warranties or support for the source code described in this article. YMMV!

This article was first published on29 May 2002.