Traversing subpaths in views

Author:Peter Bengtsson

Purpose

A URL points to a view of an object, but if the URL contains more than that, we’ll show you how to use that in the view. What we’re essentially doing is using the URL and its sub-path be positional arguments to the view. In this how-to, we’ll use an example where you want a URL that looks like this: http://localhost/app/showcalendar/2008/08/01 rather than this: http://localhost/app/showcalendar?year=2008&month=08&day=01

Prerequisities

A Grok app with a view.

Step by step

The trick is to add a method called publishTraverse(self, request, name) to the view class. In this view, not only do you pick up what the extra subpath is but you also modify it right there so that Grok doesn’t try to find the remaining things in the URL.

So, suppose you have a view called ShowCalendar that displays a nice calendar based on a date (or todays date if nothing else specified):

class ShowCalendar(grok.View):

   def update(self):
       form = self.request.form
       if 'year' in form and 'month' in form and 'day' in form:
           self.date = datetime(int(form.get('year')),
                                int(form.get('month')),
                                int(form.get('day')))
       else:
           self.date = datetime.now()

   def render(self):
       return self.date.strftime('%d %B %y')

A quick doctest explains how it works in action:

>>> from zope.testbrowser.testing import Browser
>>> browser = Browser()
>>> browser.open('http://localhost/app/showcalendar')
>>> from time import strftime
>>> strftime('%d %B %y') == browser.contents
True
>>> browser.open('http://localhost/app/showcalendar?year=2007&month=7&day=2')
>>> browser.contents
'02 July 2007'

So far so good. Now we decide we want to extend this to work based on a URL and not CGI parameters like above. The trick is to add the publishTraverse() method. Do note that the name argument is the first part of the subpath from the left and then request.getTraversalStack() is the rest in reversed order. Here’s the extended view:

class ShowCalendar(grok.View):

   def publishTraverse(self, request, name):
       self.traverse_subpath = request.getTraversalStack() + [name]
       request.setTraversalStack([])
       return self

   def update(self):
       form = self.request.form

       subpath = getattr(self, 'traverse_subpath', [])
       if subpath and len(subpath) == 3:
           self.date = datetime(int(subpath[2]), int(subpath[1]), int(subpath[0]))
       elif 'year' in form and 'month' in form and 'day' in form:
           self.date = datetime(int(form.get('year')),
                                int(form.get('month')),
                                int(form.get('day')))
       else:
           self.date = datetime.now()

   def render(self):
       return self.date.strftime('%d %B %y')

An extension of the doctest above explains how it works:

>>> browser.open('http://localhost/app/showcalendar/2009/08/01')
>>> browser.contents
'01 August 2009'

This demonstrates three important ideas:

  • The request.getTraversalStack() method
  • The request.setTraversalStack() method and notice how easy it is to use
  • In the update() method we use getattr(self, 'traverse_subpath', []) rather than self.subpath because it might not exist if the view is published without an explicit subpath on the URL.

Further information

The example above is rather simple and there might be more checks you want to add and for example raise NotFound exception which is done like this for example:

from zope.publisher.interfaces import NotFound

class ShowCalendar(grok.View):
   def publishTraverse(self, request, name):
       if name != u'2008':
           # poor example but gets the job done
           raise NotFound(self.context, name, request)
       ...

Another important thing to consider is that the above will not work with a default view which are views that are called Index unless the word index is in the URL itself. The reason for this is that the Index isn’t necessarily published unless it appears in the URL.