[<<] Industrie Toulouse

Now that a simple skin has been established, it's time to add some views to the todo list. In a style inspired by the article on how to build a todo list in Rails, we're going to present the todo list in two parts on the same page: Incomplete Items, and Completed Items. In the middle will be a form to add a new item. Looking back at the code and interfaces for the basic todo list, you'll notice that there's no real logic here. Just basic 'model' stuff. The list doesn't really care about separating complete and incomplete todo items - that's a detail specific to our browser view.

So one of the first things we need to do is write a Python class that will be a view component for an ITodoList. Its responsibilities will be:

  1. Listing completed items.
  2. Listing incomplete items.
  3. Listing all items (if desired).
  4. Returning information about the todo items that are interesting to the browser view - such as the items id.
Unlike Zope 2 objects, or even items in most RDBMS designs, Zope 3 strives for a design where if all an ITodo item cares about is its visual name and whether its done or not, it shouldn't know what its 'id' is. The 'id' is the key or name used to get the todo item out of its container. A general container in Zope 3 is not much more than a Python dictionary. So when listing objects out of the container, we want to bring its key along so that other views off of the listing can access the item.

I put my view code for this part of the project in todo/browser/__init__.py. The imports that will be used are:

from zope.app import zapi
from zope.event import notify
from zope.app.event.objectevent import ObjectCreatedEvent, ObjectModifiedEvent

from zope.app.publisher.browser import BrowserView
from zope.app.container.browser.adding import BasicAdding

from todo.interfaces import ITodo, ITodoList
from todo import Todo
zapi is an object which contains common API's for using the component architecture, as well as object traversal. The ObjectCreatedEvent and zope.event.notify will be used when we get to writing the code to handle adding a new todo object. BrowserView and BasicAdding are classes that will be subclassed. The remaining items are from the 'todo' application itself.

The TodoListView implementation looks like this:

class TodoListView(BrowserView):
    __used_for__ = ITodoList
            
    def _extractInfo(self, item):
        """ 
        Extract some useful information for use in a list view and return a
        mapping.

        :type item: tuple (name, ITodo)
        """
        id, obj = item
        info = {
            'id': id,
            'object': obj,
            'done': obj.done,
            }
        return info
        
    def listAll(self):
        return map(self._extractInfo, self.context.items())

    def listIncomplete(self):
        all = self.listAll()
        return [ todo for todo in all if not todo['done'] ]
        
    def listComplete(self):
        all = self.listAll()
        return [ todo for todo in all if todo['done'] ]
A rundown of responsibilities, which could very nicely be written into an Interface, are:
  • _extractInfo(item) takes a key and value pair and returns a Python dictionary with some keys that will be useful in the page template we'll use do display this information - the id, a shortcut to the todo item's done attribute, and the todo item itself.
  • listAll() calls 'self.context.items()' and wraps up the results in _extractInfo(). 'self.context' is the object that this browser view is operating on, which will be an ITodoList. Since ITodoList is a container, which is a Python mapping, 'items()' returns a sequence of (key, value) pair tuples.
  • listIncomplete() - Takes the results of 'listAll' and filters out the items that are not done; listComplete does the opposite.
All of this using fairly natural Python - there's no extra query language needed beyond what Python already gives, since the database engine itself is persistent Python objects. No SQL queries need to be built. Note that this is fairly basic Python. It could be written using generators, generator comprehensions (Python 2.4), or could even be modified to take advantage of Zope 3's cache components with just a couple of lines of code. However, all of these options only really become useful when optimizing for potentially large lists. In fact, a different view could be written and substituted for this one

Another view object we're going to need is something to add new Todo items with using only a single field, 'description'. The basic adding view for Zope 3 allows for objects to be added to a container without knowing too much information. We know what we want to add - a Todo. So we subclass 'BasicAdding' with the following implementation:

class TodoAdder(BasicAdding):
    """ Allows adding of ITodo items only. Highly specialized. """
    def nextURL(self):
        """See zope.app.container.interfaces.IAdding"""
        return zapi.absoluteURL(self.context, self.request)

    def addTodo(self, description=''):
        if not description:
            return self.request.response.redirect(self.nextURL())

        todo = Todo()
        todo.description = description
        notify(ObjectCreatedEvent(todo))

        self.add(todo)
        return self.request.response.redirect(self.nextURL())
'addTodo' is the main meat here, and it's pretty simple. If no 'description' is passed in, we redirect right out of the situation and don't do anything. Otherwise, we directly instantiate a todo.Todo object, set its description, and fire off an event to anything that may be listening. Then we call BasicAdding's 'add' method and redirect off to the next URL (in our case, the absolute url of 'self.context', which again will be the TodoList container object).

BasicAdding's 'add' method doesn't do much, and we probably could have stripped out the code that matters and used it here. Essentially, instead of 'self.add(todo)', we could have done something like:

        # Get an INameChooser implementation for the container
        container = self.context
        chooser = INameChooser(container)
        # The name chooser will generate a key that can be used to
        # store / retrieve the todo out of its container.
        name = chooser.chooseName('', container)

        container[name] = todo
BasicAdding.add also checks preconditions, but our current design knows that we're adding a Todo to a TodoList, which is the only precondition we have set on this container. INameChooser is an adapter which looks at the keys in a container and generates a usable one. By default, this will be a string like 'Todo' or 'Todo-1'. A future post will look at how to generate a name for an ITodo item based on the value of the 'description' attribute.

The last bit of view code we're going to need is one that operates on a Todo item itself. It will respond to checkbox clicks, and will toggle the 'done' attribute's boolean value. Its code looks like this:

class TodoToggler(BrowserView):
    __used_for__ = ITodo

    def nextURL(self):
        """ Return the next url after toggling is done.  """
        todolist = zapi.getParent(self.context)
        todolist_url = zapi.absoluteURL(todolist, self.request)
        return todolist_url

    def toggleDoneAndReturnToList(self):
        """ 
        Toggles the 'done' boolean to true on the context object (an ITodo
        instance) and returns to the parent's default view.
        """
        # Toggle the 'done' bit
        self.context.done = not self.context.done
        # Fire off an event signifying change.
        notify(ObjectModifiedEvent(self.context))

        # Redirect on to the next step.
        return self.request.response.redirect(self.nextURL())
  • 'nextURL()' in this case returns the absolute url of the ITodo's parent. We want to return to the TodoList when done.
  • 'toggleDoneAndReturnToList()' ... well, it should be pretty obvious what it does. Note that it also calls 'modified', which is basically a handy function that calls notify(ObjectModifiedEvent(self.context)).

Now what we need to do is define the template. As I was building this, the template and the lister came first, but now that it's all complete, I wanted to get the Python code out of the way to show how simple it is. I have also extracted a common Page Template macro out, which may be a more advanced concept than some readers are familiar with, so I'll cover it quickly. 'METAL' is part of Zope's Page Templates that allows us to write reusable code. The full template for the todo list will use the 'page' macro, and write content into 'slots' in that macro. Inside the todo list, the template will use the following macro to render table rows for the list:

<metal:block define-macro="todolist">
  <tr valign="top">
    <td>
      <input type="checkbox" name="ids:list" 
          tal:attributes="
            onclick string:document.location.href='${item/object/@@absolute_url}/@@toggle';
            value item/id;
            checked item/done" />
    </td>
    <td tal:content="item/object/description">todo item</td>
  </tr>
</metal:block>
This macro will be used in a loop that uses either 'listComplete' or 'listIncomplete' as defined in one of our view classes above, and operates on 'item'. Each 'item' is the result of '_extractItemInfo', which as defined returns a mapping with keys 'id', 'done', and 'object', with 'object' referring to the actual ITodo item.

Notice the @@ characters in the paths. A significant Zope 3 philosophy is that code should be separated from data. Somewhere along the way, this became muddied in Zope 2, with the ability and ease to add in page templates, DTML, and python scripts in the same places in the ZODB as one might put service type objects and data objects. In Zope 3, there are namespaces in URLs that allow access to other objects - skins, virtual hosting, resources, site configuration, and views to name a few. @@ is a shortcut to the view namespace. What 'item/object/@@absolute_url' translates into is the path 'item/object/++view++absolute_url', which means "get the view named 'absolute_url' for item.object or item['object']". The TAL statement onclick string:document.location.href='${item/object/@@absolute_url}/@@toggle'; uses the string expression syntax, returning a string value with ${item/object/@@absolute_url} expanded into the primary URL to accessing a Todo item. After that, in the string, will come '@@toggle', meaning that when the checkbox is clicked, the users web browser will go directly to that particular todo item's 'toggle' view. In a moment, we'll be defining these views in ZCML. But basically Zope will query and instantiate the 'TodoToggler' class above and visit the 'toggleDoneAndReturnToList()' method, which will flip the 'done' bit and return to the TodoList's primary view.

Now to that primary view. This is the template that will use the above macro to list todo items. It includes the 'Completed' list, the 'Incomplete' list, and an add form. I put this in the file todo/browser/todolist.pt.

<html metal:use-macro="context/@@standard_macros/view">
<body>
<div metal:fill-slot="body">
  <h2>Incomplete Items</h2>
  <table class="incomplete" border="0" cellspacing="0" cellpadding="4">
    <tal:loop repeat="item view/listIncomplete">
      <metal:macro use-macro="context/@@list_macros/todolist"/>
    </tal:loop>
  </table>

  <h3>Add Item</h3>
  <form name="addtodo" method="post"
      tal:attributes="action string:${context/@@absolute_url}/@@+/">
    <input type="text" name="description" size="35" />
    <input type="submit" value="Add Item" />
  </form>

  <h2>Complete Items</h2>
  <table class="complete" border="0" cellspacing="0" cellpadding="4">
    <tal:loop repeat="item view/listComplete">
      <metal:macro use-macro="context/@@list_macros/todolist"/>
    </tal:loop>
  </table>
</div>
</body>
</html>
In this template, you can see the use of TodoListView methods 'listComplete' and 'listIncomplete'. When we wire this template to the TodoListView class, the instance of that class will be bound to the name 'view'. Like the TodoListView class, 'context' is bound to the object that this view is being used on, which will be an ITodoList object.

And now it comes time to wire it up in ZCML. Remember that ZCML is the language used to register and wire up all of these components for use in Zope. In 'todo/browser/configure.zcml', start off with:

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

  <page
      name="list_macros"
      template="listmacros.pt"
      for="todo.interfaces.ITodoList"
      layer="todo"
      permission="zope.View"
      />
Since we're configuring browser views, we'll use the 'browser' namespace as default. The next directive, 'page', is used in this situation to make the listmacros.pt page template available to ITodoList objects under the name 'list_macros', as is seen above in the 'todolist.pt' code. The list macros are bound to the 'todo' skin layer which was created in the last post.

Now we need to turn 'todolist.pt' into a View object. It's a single page that wants to be bound to the TodoListView class. There are numerous ways of doing this - in Python code, it could be an attribute - an object that's an instance of 'ViewPageTemplateFile'. We could configure TodoListView as the main view object and configure one or more pages inside of it. Or we could configure it as a 'page' type view object using TodoListView. This last route is what we'll use, since we'll get to give the view the name 'index.html', which Zope has configured as the default view object to use (see zope/app/browzer.zcml). Of course, this default view is overridable. There's no magic name or attribute one needs to use like 'index_html' or '_q_index'. To configure the todo list view, with some security assertions and everything, and bind it to the todo skin, I added the following to the configure.zcml file:

  <page
      name="index.html"
      template="todolist.pt"
      for="todo.interfaces.ITodoList"
      class="todo.browser.TodoListView"
      layer="todo"
      permission="zope.View"
      allowed_attributes="listAll listIncomplete listComplete"
      />
This is similar to the use of 'page' defined above, but with some extra configuration attributes. Specifying the class here binds this page to the TodoListView class. Some systems might separate these out into separate view/controller objects, but in so many cases, like this one, the so-called 'controller' is so integrated with the 'view' that it just makes sense to keep them close. 'allowed_attributes' explicitly exposes those attributes to the security system. I don't know if that's really needed here, I just saw it in some examples that I lifted this configuration from.

Now we need to configure the Todo adder for this skin. This time, I've actually used a 'view' ZCML declaration:

  <view
      for="todo.interfaces.ITodoList"
      name="+"
      layer="todo"
      class="todo.browser.TodoAdder"
      permission="zope.View"
      allowed_attributes="addTodo nextURL"
      >
    <page name="index.html" attribute="addTodo"/>
  </view>
It's not much different than what has been seen so far - the interface declaration (again, allowing anything implementing ITodoList to use this view), the name (how the view is queried), the layer, the class, and the permissions. Inside the view declaration, I bound the name 'index.html' to the 'addTodo' attribute. This means that when Zope 3 traverses to todolist/@@+, it looks up the view named '+', and then looks up the default view for it. 'index.html' is the regular default view name for Zope, so that attribute (in this case, a method) gets published. Again - this keeps funny names out of Python code.

The last view that needs registering is the Toggler for todo items. It's similar to what was just done for the TodoAdder, but bound to ITodo items instead:

  <view
      for="todo.interfaces.ITodo"
      name="toggle"
      layer="todo"
      class="todo.browser.TodoToggler"
      permission="zope.View"
      allowed_attributes="toggleDoneAndReturnToList"
      >
    <page name="index.html" attribute="toggleDoneAndReturnToList"/>
  </view>
This time, 'toggleDoneAndReturnToList' is used as the default view within this view, and is what allows traversal to todolist/MyTodo/@@toggle in a URL. Finally, the configure.zcml file is closed out with a
</zope:configure>

Now, in the primary configuration file for 'todo', which is 'todo/configure.zcml', add the following line near the bottom of the file:

  <include package=".browser"/>

On using the skin and these views - nothing has been done to set up a view to add and list 'todolist' objects in this skin. In my limited experiments, I had the Rotterdam ZMI configured as my primary skin (the default), and had added a TodoList using its user interface named 'mytodo'. To view 'mytodo' under the custom skin without configuring a new default skin, or site, or anything to that nature -- basically just wanting to see the results of this work -- I traversed to a URL with the ++skin++ declaration on the URL path:

  http://localhost:8080/++skin++todo/mytodo/
The results, with some todo items, looks like this

Todo list, skinned up

Clicking on any of the checkboxes triggers the javascript that goes to the @@toggle view, which redirects back to this list after toggling the item. After toggling, the Todo item is (naturally) in the other list.

That's about all it takes to get a really really basic todo application with its own UI up and running. There's more that can be done which I hope to get to soon. But the Python code and even the template code are pretty simple, and the ZCML to wire it all up isn't all that bad.

This is still a very basic application. Zope 3 is capable of much more, and can be configured and reconfigured as needed to get larger jobs done. The main thing that I wanted to demonstrate is that Zope 3 is really quite clean and nice to work with. The only part that people might have issue with is ZCML. But I plan on addressing that issue shortly (in short - for all people ballyhoo about keeping 'content and presentation separate! business logic away from display logic!', I think a lot of people punt on configuration - especially configuration in a way that allows loose coupling and easy use/reuse - and mix configuration and registration code too freely. ZCML style configuration is one way of avoiding the troubles that can come up in that system).