Tables With z3c.table

Right now, the front page of our ZContact application is not very optimal for a large number of contacts. Suppose you entered in 3000 contacts. The list of contacts on the front page would get very very long. Besides taking a long time to load, it’s impossible to sort by first name or by most recently modified. We’re going to make this all much easier using a table with sortable columns, and built in batching. z3c.table provides all this functionality in a highly pluggable, extendable, customizable manner. Let’s get started.

As usual, the first thing to do is add z3c.table as a dependency in the setup.py file. Next go through the buildout process again to download the new dependency:

$ ./bin/buildout -N

Make sure to include z3c.table‘s zcml configuration in the zcontact/configure.zcml file:

<include package="z3c.table" />

Creating a Table

Now we can open up zcontact/browser/contact.py and begin to construct our table. First, import z3c.table into the file; we specifically want the table and column modules.

from z3c.table import table, column

Since this table is going to appear on the Front Page, we’ll be editing the FrontPage class. Right now, it should be pretty spartan, looking like this:

class FrontPage(BrowserPagelet):
    """Pagelet for the front page."""

We want the FrontPage class to extend the Table class so we get all the functionality of tables in our view. Here is what it should look like:

class FrontPage(BrowserPagelet, table.Table):
    """Pagelet for the front page."""

    update = table.Table.update

Note that the inheritance order matters! Both the BrowserPagelet class and the Table class have update and render methods. We want to keep using BrowserPagelet‘s render method because it automatically looks up the frontpage.pt template we have registered in ZCML. However, we want to use Table‘s update method because it populates the table’s cells with all the right data. I should also note here that both base classes have the same __init__ method more or less, taking context and request as parameters. The Table class uses the context as a container object whose values it iterates over to generate the table rows.

Now we need to modify zcontact/browser/frontpage.pt to have it render the table where our list used to be. Chage it to look like this:

<h3>Welcome to ZContact</h3>
<p>Please tell me what you would like to do:</p>
<ul>
  <li><a href="@@addContact.html">Add a Contact</a></li>
  <li>Look at contacts: <div tal:replace="structure view/renderTable" /></li>
</ul>

The Table class provides the renderTable method to spit out the html table code. We call this method with the structure keyword to make sure the output isn’t escaped.

If you restart your server and check out the front page again, you should find something missing! We haven’t created any columns yet.

Creating Columns

Let’s start by creating two columns for a contact’s first and last name. We define table columns as their own objects implementing the z3c.table.interfaces.IColumn interface. Most of the work is done for us by base classes that we can extend. One such base class is the GetAttrColumn class. This base class populates column cells with the value of a certain attribute. Very quickly we can define our two columns:

class FirstNameColumn(column.GetAttrColumn):
    header = u'First Name'
    attrName = 'firstName'
    weight = 1

class LastNameColumn(column.GetAttrColumn):
    header = u'Last Name'
    attrName = 'lastName'
    weight = 2

The GetAttrColumn class uses the value of attrName to do the attribute lookup. The header is what gets displayed in the columns header. The weight is used to specify the order of columns in a table. In this case, the first name column would come to the left of the last name column.

To add these columns to our table, we must register them as named multiadapters. When our table gets rendered, the columns are looked up based on the context, request, and table. This makes the table automatically pluggable. Add the following tags to the zcontact/browser/configure.zcml file.

<zope:adapter
    name="firstName"
    for="zope.app.folder.interfaces.IRootFolder
         zcontact.layer.IZContactBrowserLayer
         z3c.table.interfaces.ITable"
    provides="z3c.table.interfaces.IColumn"
    factory=".contact.FirstNameColumn"
    />

<zope:adapter
    name="lastName"
    for="zope.app.folder.interfaces.IRootFolder
         zcontact.layer.IZContactBrowserLayer
         z3c.table.interfaces.ITable"
    provides="z3c.table.interfaces.IColumn"
    factory=".contact.LastNameColumn"
    />

Here we are specifying that these columns should be registered for contexts providing IRootFolder, requests providing IZContactBrowserLayer, and tables providing ITable. This means we could potentially have tables appear with different columns for different skins. There might be an administrative skin that presents more information. The possibilities are truly endless.

This is the first time we are using the zope namespace in zcontact/browser/configure.zcml so we have to declare it in the configure tag. It should look like this:

<configure xmlns="http://namespaces.zope.org/browser"
           xmlns:zope="http://namespaces.zope.org/zope"
           xmlns:z3c="http://namespaces.zope.org/z3c">

Now when you restart the server, you should see something like this. (Yours might look a bit different because I added some css styling.)

_images/TableContents1.png

Customizing Table Cell Rendering

Before we were using a table, we had a list of contacts which were actually links to the contact’s display form. We want to bring these links back by customizing the way our table cells are rendered. You could do it more simply using the z3c.table.column.LinkColumn class as a subclass, but we will write our own for demonstration purposes. All you have to do is override the renderCell method.

This is incredibly straight forward, so here’s the code that I’ve modified in zcontact/browser/contact.py:

class FirstNameColumn(column.Column):
    header = u'First Name'
    weight = 1

    def renderCell(self, item):
        return '<a href="%s">%s</a>' % (absoluteURL(item, self.request),
                                        item.firstName)

class LastNameColumn(column.Column):
    header = u'Last Name'
    weight = 2

    def renderCell(self, item):
        return '<a href="%s">%s</a>' % (absoluteURL(item, self.request),
                                        item.lastName)

Note that now I don’t even bother inheriting from column.GetAttrColumn, since it is not providing us with any necessary functionality.

Pluggable Tables - That was easy!

Now we want to go above and beyond and start displaying some really useful information in our contact list table, like the date a contact was added, and the last date that it was modified. Fortunately for us, someone out there has alrady written generic table columns that present this type of meta data. Since the table machinery utilizes Zope’s component architecture, all we have to do is register the additional table columns in zcml and we are done. I’ve added the following two zcml declarations to zcontact/browser/configure.zcml:

<zope:adapter
    name="created"
    for="zope.app.folder.interfaces.IRootFolder
         zcontact.layer.IZContactBrowserLayer
         z3c.table.interfaces.ITable"
    provides="z3c.table.interfaces.IColumn"
    factory="z3c.table.column.CreatedColumn"
    />

<zope:adapter
    name="modified"
    for="zope.app.folder.interfaces.IRootFolder
         zcontact.layer.IZContactBrowserLayer
         z3c.table.interfaces.ITable"
    provides="z3c.table.interfaces.IColumn"
    factory="z3c.table.column.ModifiedColumn"
    />

These columns use Zope’s Dubline Core metadata system to retrieve creation and modification dates. Dublin Core only works for objects that can be annotated; that is objects that can be adapted to IAnnotations. To make our contact objects automatically keep track of this metadata, we must declare that the Contact class implements IAttributeAnnotatable. We can do this with the zcml implements directive, which would look like this:

<class class="zcontact.contact.Contact">
  <implements interface="zope.annotation.interfaces.IAttributeAnnotatable" />
  <require
      interface=".interfaces.IContact"
      permission="zope.View" />
  <require
      set_schema=".interfaces.IContact"
      permission="zope.ManageContent" />
</class>

Now restart your server and take a look. You should see something like this:

_images/TableContents2.png

Sorting Tables By Column

What would a table be without column based sorting? All we have to do for our columns to be sortable is provide a getSorkKey method in our column classes. I’ve added such methods down below:

class FirstNameColumn(column.Column):

header = u’First Name’ weight = 1

def getSortKey(self, item):
return item.firstName
def renderCell(self, item):
return ‘<a href=”%s”>%s</a>’ % (absoluteURL(item, self.request),
item.firstName)
class LastNameColumn(column.Column):

header = u’Last Name’ weight = 2

def getSortKey(self, item):
return item.lastName
def renderCell(self, item):
return ‘<a href=”%s”>%s</a>’ % (absoluteURL(item, self.request),
item.lastName)

Now if you restart your server and go back to the start pages, the columns can be sorted based on variables from the request. The request takes two variables, table-sortOrder and table-sortOn. The names of these variables actually depend on the name of your table. For example, try sorting by last name in ascending order by going to the following url:

http://localhost:8080/++skin++ZContact/@@index.html?table-sortOrder=ascending&table-sortOn=table-lastName-1

Now that sorting is setup, we just need to expose the sort options to the user interface. One possible way would be to make the heading of each column a link that points to the correct URL. There exists a SortingColumnHeader class that does exactly this. All we have to do is register is an adapter. The adapter takes as its arguments the context, request, table, and column, and provides the IColumnHeader class. The zcml registration in zcontact/browser/configure.zcml looks like this:

<zope:adapter
    for="zope.app.folder.interfaces.IRootFolder
         zcontact.layer.IZContactBrowserLayer
         z3c.table.interfaces.ITable
         z3c.table.interfaces.IColumn"
    provides="z3c.table.interfaces.IColumnHeader"
    factory="z3c.table.header.SortingColumnHeader"
    />

Now you can restart your server again and click around to see the columns magically sort.