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 ourTransientApp
application object nor theNaturalDir
stepping-stone withgrok.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 theNaturalDir
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 agrok.Container
or agrok.Model
already have enough magic built-in that you can put atraverse()
method right on the object and Grok will find it when trying to resolve a URL. This would not have helped ourNaturalDir
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.