Custom Traversers

But what about situations where you want each of your transient objects to have its own URL on your site?

The answer is that you can create grok.Traverser objects that, when the user enters a URL and Grok tries to find the object which the URL names, intercept those requests and return objects of your own design instead.

For our example application app, let’s make each Natural object live at a URL like:

http://localhost:8080/app/natural/496

There is nothing magic about the fact that this URL has three parts, by the way — the three parts being the application name "app", the word "natural", and finally the name of the integer "496". You should easily be able to figure out how to adapt the example application below either to the situation where you want all the objects to live at your application root (which would make the URLs look like /app/496), or where you want URLs to go several levels deeper (like if you wanted /app/numbers/naturals/496).

The basic rule is that for each slash-separated URL component (like "natural" or "496") that does not actually name an object in the ZODB, you have to provide a grok.Traverser. Make the grok.context of the Traverser the object that lives at the previous URL component, and give your Traverser a traverse() method that takes as its argument the next name in the URL and returns the object itself. If the name submitted to your traverser does not name an object, simply return None; this is very easy to do, since None is the default return value of a Python function that ends without a return statement.

So place the following inside your app.py file:

import grok
from transient.natural import Natural

class TransientApp(grok.Application, grok.Container):
    pass

class BaseTraverser(grok.Traverser):
    grok.context(TransientApp)
    def traverse(self, name):
        if name == 'natural':
            return NaturalDir()

class NaturalDir(object):
    pass

class NaturalTraverser(grok.Traverser):
    grok.context(NaturalDir)
    def traverse(self, name):
        if name.isdigit() and int(name) > 0:
            return Natural(int(name))

class NaturalIndex(grok.View):
    grok.context(Natural)
    grok.name('index.html')

And you will only need one template to go with this file, which you should place in app_templates/naturalindex.pt:

<html><body>
 This is the number <b tal:content="context">x</b>!<br/>
  <span tal:condition="context/is_prime">It is prime.</span>
  <span tal:condition="context/is_composite">Its prime factors are:
   <span tal:repeat="factor context/factors">
    <b tal:content="factor">f</b
    ><span tal:condition="not:repeat/factor/end">,</span>
   </span>
  </span><br>
</body></html>

Now, if you view the URL /app/natural/496 on your test server, you should see:

This is the number 496!
Its prime factors are: 2, 2, 2, 2, 31

Note that there is no view name after the URL. That’s because we chose to name our View index.html, which is the default view name in Zope 3. (With grok.Model and grok.Container objects, by contrast, the default view selected if none is named is simply index without the .html at the end.) You can always name the view explicitly, though, so you will find that you can also view the number 496 at:

http://kenaniah.ten22:8080/app/natural/496/index.html

It’s important to realize this because, if you need to add more views to a transient object, you of course will have to add them with other names — and to see the information in those other views, users (or the links they use) will have to name the views explicitly.

Two final notes:

  • In order to make this example brief, the application above does not support either the user navigating simply to /app, nor will it allow them to view /app/natural, because we have provided neither our TransientApp application object nor the NaturalDir stepping-stone with grok.View objects that could let them be displayed. You will almost always, of course, want to provide a welcoming page for the top level of your application; but it’s up to you whether you think it makes sense for users to be able to visit the intermediate /app/natural URL or not. If not, then follow the example above and simply do not provide a view, and everything else will work just fine.

  • In order to provide symmetry in the example above, neither the TransientApp object nor the NaturalDir object knows how to send users to the next objects below them. Instead, they are both provided with Traversers. It turns out, I finally admin here at the bottom of the example, that this was not necessary! Grok objects like a grok.Container or a grok.Model already have enough magic built-in that you can put a traverse() method right on the object and Grok will find it when trying to resolve a URL. This would not have helped our NaturalDir object, of course, because it’s not a Grok anything; but it means that we can technically delete the first Traverser and simply declare the first class as:

    class TransientApp(grok.Application, grok.Container):
        def traverse(self, name):
            if name == 'natural':
                return NaturalDir()
    

    The reason I did not do this in the actual example above is that showing two different ways to traverse in the same example seemed a bit excessive! I preferred instead to use a single method, twice, that is universal and works everywhere, rather than by starting off with a technique that does not work for most kinds of Python object.