.. index:: z3c.table, tables 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: .. code-block:: xml 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. .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python

Welcome to ZContact

Please tell me what you would like to do:

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: .. code-block:: python 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. .. code-block:: xml 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: .. code-block:: xml Now when you restart the server, you should see something like this. (Yours might look a bit different because I added some css styling.) .. image:: 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``: .. code-block:: python class FirstNameColumn(column.Column): header = u'First Name' weight = 1 def renderCell(self, item): return '%s' % (absoluteURL(item, self.request), item.firstName) class LastNameColumn(column.Column): header = u'Last Name' weight = 2 def renderCell(self, item): return '%s' % (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``: .. code-block:: xml 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: .. code-block:: xml Now restart your server and take a look. You should see something like this: .. image:: 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 '%s' % (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 '%s' % (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: .. code-block:: xml Now you can restart your server again and click around to see the columns magically sort.