Navigating To Transient Objects Tutorial
Note: Return to tutorial view.
Thanks to the magic of this database, your web apps can create Python objects that are automatically saved to disk and are available every time your application runs. In particular, every grok.Model and grok.Container object you generate can be written safely to your application's Data.fs file. But sometimes you need to create objects that do not persist in the ZODB, wonderful though it is.
Introduction
Why instantiate objects from external data sources?
If you have already read the Grok tutorial, you are familiar with the fact that behind Grok
lives a wonderful object database called the Zope Object Database, or the ZODB for short.
Thanks to the magic of this database, your web apps can create Python objects
that are automatically saved to disk and are available every time your application runs.
In particular, every grok.Model and grok.Container object you generate
can be written safely to your application's Data.fs file.
But sometimes you need to create objects
that do not persist in the ZODB,
wonderful though it is.
Sometimes these will be objects you design yourself
but have no need to save once you are done displaying them,
like summary statistics or search results.
On other occasions,
you will find yourself using libraries or packages written by other people
and will need to present the objects they return in your Grok app —
whether those objects are LDAP entries,
file system directory listings,
or rows from a relational database.
For the purpose of this tutorial,
we are going to call all such objects transient objects.
This highlights the fact that,
from the point of view of Grok,
they are going to be instantiated on-the-fly as a user visits them.
Of course,
they might (or might not) persist in some other application,
like a file system or LDAP server or relational database!
But as far as Grok can tell,
they are being created the moment before they are used
and then, very often, pass right back out of existence —
and out of your application's memory —
once Grok is finished composing the response.
To try out the examples in this tutorial,
start a Grok project named TransientApp
and edit the app.py and other files
that you are instructed to create or edit.
Choosing a method
In this tutorial,
we introduce four methods for creating an object
which you need to present on the web:
- Creating it in your View's update(), using no external data.
- Creating it in your View's update(), using URL or form data.
- Creating it in a Traverser that gets called for certain URLs.
- Creating it in a Container that gets called for certain URLs.
To choose among these methods,
the big question you need to ask yourself
is whether the object you are planning to display
is one that will live at its own particular URL or not.
There are three basic relationships we can imagine
between an object on a web page and the URL of the page itself.
The simplest case,
which is supported by the first method listed above,
is when you need to create an object
during the rendering of a page that already exists in your application.
An example would be decorating the bottom of your front page
with a random quotation
selected by instantiating a RandomQuotation class you have written,
so that each time a user reloads the page they see a different quote.
None of the quotations would thereby have URLs of their own;
there would, in fact, be no way for the user
to demand that a particular quotation be displayed;
and the user could not force the site to display again
a quote from Bertrand Russell
that they remember enjoying yesterday but have forgotten.
Such objects can simply be instantiated
in the update() method of your View,
and this technique will be our first example below.
The situation is only slightly more complex
when you need to use form parameters the user has submitted
to tailor the object you are creating.
This is very common when supporting searching of a database:
the user enters some search terms,
and the application needs to instantiate an object —
maybe a SearchResult or a DatabaseQuery —
using those user-provided search terms,
so that the page template can loop across and display the results.
The second method listed above is best for this;
since the form parameters are available in the update() method,
you are free to use them when creating the result object.
This will be the technique illustrated in our second example below.
Finally,
the really interesting case is when an object actually gets its own URL.
You are probably already familiar with several kinds of object
which have their own URLs on the Web —
such as books on Amazon.com, photographs on Flickr, and people on Facebook,
all of which live at their own URL.
Each web site has a particular scheme
which associates a URL with the object it names or identifies.
You can probably guess, for example, just by looking at them,
which object is named by each of the following three Amazon URLs:
http://www.amazon.com/Web-Component-Development-Zope-3/dp/3540223592
http://www.amazon.com/Harry-Potter-Deathly-Hallows-Book/dp/0545010225
http://www.amazon.com/J-R-R-Tolkien-Boxed-Hobbit-Rings/dp/0345340426
The Grok framework, of course,
already supports URL traversal for persistent objects in the ZODB;
if you create a Container named polygons
that contains two objects named triangle and square,
then your Grok site will already support URLs like:
http://yoursite.com/app/polygons/triangle
http://yoursite.com/app/polygons/square
But the point of this tutorial, of course,
is how you can support URL traversal
for objects which are not persistent,
which you will create on-the-fly once someone looks up their URL.
And the answer is that, to support such objects,
you will choose between the last two methods listed above:
you will either create a custom Traverser,
or actually define your own kind of Container,
that knows how to find and instantiate the object the URL is naming.
These two techniques are described last in this tutorial,
because they involve the most code.
But before starting our first example,
we need to define an object that we want to display.
We want to avoid choosing an obvious example,
like an object whose data is loaded from a database,
because then this tutorial would have to teach database programming too!
Plus, you would have to set up a database just to try the examples.
Instead,
we need an object
rich enough to support interesting attributes and navigation,
but simple enough
that we will not have to reach outside of Python to instantiate it.
Our Topic: The Natural Numbers
To make this tutorial simple, we will build a small web site that lets the user visit what some people call the natural numbers: the integers beginning with 1 and continuing with 2, 3, 4, and so forth. We will define a Natural class which knows a few simple things about each number - like which number comes before it, which comes after it, and what its prime factors are.
We should start by writing a test suite for our Natural class.
Not only is writing tests before code an excellent programming practice
that forces you to think through how your new class should behave,
but it will make this tutorial easier to understand.
When you are further down in the tutorial,
and want to remember something about the Natural class,
you may find yourself re-reading the tests instead of the code
as the fastest way to remember how the class behaves!
The reason this test will be so informative
is that it is a Python “doctest”,
which intersperses normal text with the example Python code.
Create a file in your Grok instance named src/transient/natural.txt
and give it the following contents:
A Simple Implementation of Natural Numbers
The "natural" module of this application provides a simple class for
representing any postive integer, named "Natural".
>>> from transient.natural import Natural
To instantiate it, provide a Python integer to its constructor:
>>> three = Natural(3)
>>> print 'This object is known to Python as a "%r".' % three
This object is known to Python as a "Natural(3)".
>>> print 'The number of the counting shall be %s.' % three
The number of the counting shall be 3.
You will find that there are very many natural numbers available; to
help you navigate among them all, each of them offers a "previous" and
"next" attribute to help you find the ones adjacent to it.
>>> print 'Previous: %r Next: %r' % (three.previous, three.next)
Previous: Natural(2) Next: Natural(4)
Since we define the set of "natural numbers" as beginning with 1, you
will find that the number 1 lacks a "previous" attribute:
>>> hasattr(three, 'previous')
True
>>> one = Natural(1)
>>> hasattr(one, 'previous')
False
>>> one.previous
Traceback (most recent call last):
...
AttributeError: There is no natural number less than 1.
You can also ask a number to tell you which prime factors must be
multiplied together to produce it. The number 1 will return no
factors:
>>> one.factors
[]
Prime numbers will return only themselves as factors:
>>> print Natural(2).factors, Natural(11).factors, Natural(251).factors
[Natural(2)] [Natural(11)] [Natural(251)]
Compound numbers return several factors, sorted in increasing order,
and each appearing the correct number of times:
>>> print Natural(4).factors
[Natural(2), Natural(2)]
>>> print Natural(12).factors
[Natural(2), Natural(2), Natural(3)]
>>> print Natural(2310).factors
[Natural(2), Natural(3), Natural(5), Natural(7), Natural(11)]
Each natural number can also simply return a boolean value indicating
whether it is prime, and whether it is composite.
>>> print Natural(6).is_prime, Natural(6).is_composite
False True
>>> print Natural(7).is_prime, Natural(7).is_composite
True False
Next,
we need to tell Grok about this doctest.
Create a file in your instance named src/transient/tests.py
that looks like:
import unittest
from doctest import DocFileSuite
def test_suite():
return unittest.TestSuite([ DocFileSuite('natural.txt') ])
You should now find that running ./bin/test inside of your instance
now results in a whole series of test failures.
This is wonderful and means that everything is working!
At this point Grok is able to find your doctest,
successfully execute it,
and correctly report (if you examine the first error message)
that you have not yet provided a Natural class
that could make the doctest able to succeed.
The Natural class implementation
Now we merely have to provide an implementation for our Natural class.
Create a file src/transient/natural.py under your Grok instance
and give it the contents:
class Natural(object):
"""A natural number, here defined as an integer greater than zero."""
def __init__(self, n):
self.n = abs(int(n)) or 1
def __str__(self):
return '%d' % self.n
def __repr__(self):
return 'Natural(%d)' % self.n
@property
def previous(self):
if self.n < 2:
raise AttributeError('There is no natural number less than 1.')
return Natural(self.n - 1)
@property
def next(self):
return Natural(self.n + 1)
@property
def factors(self):
if not hasattr(self, '_factors'): # compute factors only once!
n, i = self.n, 2
self._factors = []
while i <= n:
while n % i == 0: # while n is divisible by i
self._factors.append(Natural(i))
n /= i
i += 1
return self._factors
@property
def is_prime(self):
return len(self.factors) < 2
@property
def is_composite(self):
return len(self.factors) > 1
If you try running ./bin/test again after creating this file,
you should find that the entire natural.txt docfile
now runs correctly!
I hope that if you are new to Python,
you are not too confused by the code above,
which uses @property
which may not have been covered in the Python tutorial.
But I prefer to show you “real Python” like this,
that reflects how people actually use the language,
rather than artifically simple code
that hides from you the best ways to use Python.
Note that it is not necessary to understand natural.py
to enjoy the rest of this tutorial!
Everything we do from this point on
will involve building a framework to use this object on the web;
we will be doing no further development on the class itself.
So all you actually need to understand is how a Natural behaves,
which was entirely explained in the doctest.
Note that the Natural class knows nothing about Grok!
This is an important feature of the whole Zope 3 framework,
that bears frequent repeating:
objects are supposed to be simple,
and not have to know that they are being presented on the web.
You should be able to grab objects created anywhere,
from any old library of useful functions you happen to download,
and suit them up to be displayed and manipulated with a browser.
And the Natural class is exactly like that:
it has no idea that we are about to build a framework around it
that will soon be publishing it on the web.
Having Your View Directly Instantiate An Object
We now reach the first of our four techniques!
The simplest way to create a transient object for display on the web
involves a technique you may remember from the main Grok tutorial:
providing an update() method on your View
that creates the object you need
and saves it as an attribute of the View.
As a simple example,
create an app.py file with these contents:
import grok
from transient.natural import Natural
class TransientApp(grok.Application, grok.Container):
pass
class Index(grok.View):
def update(self):
self.num = Natural(126)
Do you see what will happen?
Right before Grok renders your View to answer a web request,
Grok will call its update() method,
and your View will gain an attribute named num
whose value is a new instance of the Natural class.
This attribute can then be referenced from the page template
corresponding to your view!
Let use write a small page template that accesses the new object.
Try creating an /app_templates/index.pt file that looks like:
<html><body>
<p>
Behold the number <b tal:content="view/num">x</b>!
<span tal:condition="view/num/is_prime">It is prime.</span>
<span tal:condition="view/num/is_composite">Its prime factors are:</span>
</p>
<ul tal:condition="view/num/factors">
<li tal:repeat="factor view/num/factors">
<b tal:content="factor">f</b>
</li>
</ul>
</body></html>
If you now run your instance
and view the main page of your application,
your browser should show you something like:
Behold the number 126! It has several prime factors:
* 2
* 3
* 3
* 7
You should remember,
when creating an object through an update() method,
that a new object gets created every time your page is viewed!
This is hard to see with the above example,
of course,
because no matter how many times you hit “reload” on your web browser
you will still see the same number.
So adjust your app.py file so that it now looks like this:
import grok, random
from transient.natural import Natural
class TransientApp(grok.Application, grok.Container):
pass
class Index(grok.View):
def update(self):
self.num = Natural(random.randint(1,1000))
Re-run your application and hit “reload” several times;
each time you should see a different number.
The most important thing to realize when using this method
is that this Natural object is not the object
that Grok is wrapping with the View for display!
The object actually selected by the URL in this example
is your TransientApp application object itself;
it is this application object which is the context of the View.
The Natural object we are creating is nothing more
than an incidental attribute of the View;
it neither has its own URL, nor a View of its own to display it.
Creating Objects Based on Form Input
What if we wanted the user to be able to designate which Natural object was instantiated for display on this web page?
This is a very common need
when implementing things like a database search form,
where the user's search terms need to be provided as inputs
to the object that will return the search results.
The answer is given in the main Grok tutorial:
if you can write your update() method
so that it takes keyword parameters,
they will be filled in with any form parameters the user provides.
Rewrite your app.py to look like:
import grok, random
from transient.natural import Natural
class TransientApp(grok.Application, grok.Container):
pass
class Index(grok.View):
def update(self, n=None):
self.badnum = self.num = None
if n:
try:
self.num = Natural(int(n))
except:
self.badnum = n
And make your app_templates/index.pt look like:
<html><body>
<p tal:condition="view/badnum">This does not look like a natural number:
“<b tal:content="view/badnum">string</b>”
</p>
<p tal:condition="view/num">
You asked about the number <b tal:content="view/num">x</b>!<br/>
<span tal:condition="view/num/is_prime">It is prime.</span>
<span tal:condition="view/num/is_composite">Its prime factors are:
<span tal:repeat="factor view/num/factors">
<b tal:content="factor">f</b
><span tal:condition="not:repeat/factor/end">,</span>
</span>
</span>
</p>
<form tal:attributes="action python:view.url()" method="GET">
Choose a number: <input type="text" name="n" value="" />
<input type="submit" value="Go" />
</form>
</body></html>
This time, when you restart your Grok instance
and look at your application front page,
you will see a form asking for a number:
Choose a number: __________ [Go]
Enter a positive integer and submit the form
(try to choose something with less than seven digits
to keep the search for prime factors short),
and you will see something like:
You asked about the number 48382!
Its prime factors are: 2, 17, 1423
Choose a number: __________ [Go]
And if you examine the URL to which the form has delivered you,
you will see that the number you have selected
is part of the URL's query section:
http://localhost:8080/app/index?n=48382
So none of these numbers get their own URL;
they all live on the page /app/index
and have to be selected by submitting a query to that one page.
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.
Providing Links To Other Objects
What if the object you are wrapping
can return other objects to which the user might want to navigate?
Imagine the possibilities:
a filesystem object you are presenting on the web
might be able to return the files inside of it;
a genealogical application might have person objects
that can return their spouse, children, or grandparents.
In the example we are working on here,
a Natural object can return
both the previous and the next number;
wouldn't it be nice to give the users links to them?
If in a page template
you naively ask your Grok view
for the URL of a transient object,
you will be disappointed.
Grok does know the URL of the object
to which the user has just navigated,
because, well, it's just navigated there,
so adding this near the bottom of your naturalindex.pt
should work just fine:
This page lives at: <b tal:content="python: view.url(context)">url</b><br>
But if you rewrite your template
so that it tries asking for the URL of any other object,
the result will be a minor explosion.
Try adding this to your naturalindex.pt file:
Next number: <b tal:content="python: view.url(context.next)">url</b><br>
and try reloading the page.
On the command line,
your application will return the exception:
TypeError: There isn't enough context to get URL information.
This is probably due to a bug in setting up location information.
Do you see the problem?
Because this new Natural object does not live inside of the ZopeDB,
Grok cannot guess the URL at which you intend it to live.
In order to provide this information,
it is best to call a Zope function called locate()
that marks as object as belonging inside of a particular container.
To get the chance to do this magic,
you'll have to avoid calling Natural.previous and Natural.next
directly from your page template.
Instead,
provide your view with two new properties
that will grab the previous and next attributes
off of the Natural object which is your current context,
and then perform the required modification before returning them:
class NaturalIndex(grok.View):
...
@property
def previous(self):
if getattr(self.context, 'previous', None):
n = self.context.previous
traverser = BaseTraverser(grok.getSite(), None)
parent = traverser.publishTraverse(None, 'natural')
return zope.location.location.located(n, parent, str(n))
@property
def next(self):
n = self.context.next
traverser = BaseTraverser(grok.getSite(), None)
parent = traverser.publishTraverse(None, 'natural')
return zope.location.location.located(n, parent, str(n))
This forces upon your objects
enough information that Zope can determine their URL —
it will believe that they live inside of the object
named by the URL /app/natural
(or whatever other name you use in the PublishTraverse call).
With the above in place,
you can add these links to the bottom of your naturalindex.pt
and they should work just fine:
<tal:if tal:condition="view/previous">
Previous number: <a tal:attributes="href python: view.url(view.previous)"
tal:content="view/previous">123</a><br>
</tal:if>
Next number: <a tal:attributes="href python: view.url(view.next)"
tal:content="view/next">123</a><br>
This should get easier in a future version of Grok and Zope!
Writing Your Own Container
The above approach, using Traversers, gives Grok
just enough information
to let users visit your objects,
and for you to assign URLs to them.
But there are several features of a normal grok.Container
that are missing —
there is no way for Grok to list or iterate over the objects,
for example,
nor can it ask whether a particular object lives in the container or not.
While taking full advantage of containers
is beyond the scope of this tutorial,
I ought to show you how the above would be accomplished:
import grok
from transient.natural import Natural
from zope.app.container.interfaces import IItemContainer
from zope.app.container.contained import Contained
import zope.location.location
class TransientApp(grok.Application, grok.Container):
pass
class BaseTraverser(grok.Traverser):
grok.context(TransientApp)
def traverse(self, name):
if name == 'natural':
return NaturalBox()
class NaturalBox(Contained):
grok.implements(IItemContainer)
def __getitem__(self, key):
if key.isdigit() and int(key) > 0:
n = Natural(int(key))
return zope.location.location.located(n, self, key)
else:
raise KeyError
class NaturalIndex(grok.View):
grok.context(Natural)
grok.name('index.html')
@property
def previous(self):
if getattr(self.context, 'previous'):
n = self.context.previous
parent = self.context.__parent__
return zope.location.location.located(n, parent, str(n))
@property
def next(self):
n = self.context.next
parent = self.context.__parent__
return zope.location.location.located(n, parent, str(n))
Note, first, that this is almost identical to the application
we built in the last section;
the grok.Application,
its Traverser,
and the NaturalIndex are all the same —
and you can leave alone the naturalindex.pt you wrote as well.
But instead of placing a Traverser between our Application
and the actual objects we are delivering,
we have created an actual “container”
that follows a more fundamental protocol.
There are a few differences
in even this simple example.
- A container is supposed to act like a Python dictionary,
so we have overriden the Python operation __getitem__
instead of providing a traverse() method.
This means that other code using the container
can find objects inside of it using the container[key]
Python dictionary syntax.
- A Python __getitem__ method
is required to raise the KeyError exception
when someone tries to look up a key
that does not exist in the container.
It is not sufficient to merely return None,
like it was in our Traverser above,
because, without the exception,
Python will assume that the key lookup was successful
and that None is the value that was found!
- Finally,
before returning an object from your container,
you need to call the Zope located() function
to make sure the object gets marked up
with information about where it lives on your site.
A Grok Traverser does this for you.
Again,
in most circumstances I can imagine,
you will be happier just using a Traverser
like the third example shows,
and not incurring the slight bit of extra work
necessary to offer a full-fledged container.
But,
in case you ever find yourself
wanting to use a widget or utility
that needs an actual container to process,
I wanted you to have this example available.