.. 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.