The PicturePile tutorial illustrates a simple Woven web application. However, the Woven framework should not be used for new projects. The newer Nevow framework, available as part of the Quotient project, is a simpler framework with consistent semantics and better testing and is strongly recommended over Woven.
The tutorial is maintained only for users with an existing Woven codebase.
To illustrate the basic design of a Woven app, we're going to walk through building a simple image gallery. Given a directory of images, it will display a listing of that directory; when a subdirectory or image is clicked on, it will be displayed.
To begin, we write an HTML template for the directory index, and save it as directory-listing.html:
<html> <head> <title model="title" view="Text">Directory listing</title> </head> <body> <h1 model="title" view="Text"></h1> <ul model="directory" view="List"> <li pattern="listItem"><a view="Anchor" /></li> <li pattern="emptyList">This directory is empty.</li> </ul> </body> </html>
The main things that distinguish a Woven template from standard XHTML are the
model
, view
, and pattern
attributes on
tags. Predictably, model
and view
specify which model
and view will be chosen to fill the corresponding node. The pattern
attribute is used with views that have multiple parts, such as List. This
example uses two patterns List
provides; listItem
marks the node that will be used as the template
for each item in the list, and emptyList
marks the node displayed
when the list has no items.
Next, we create a Page
that will display the directory listing, filling the template
above (after a few imports):
import os from twisted.application import service, internet from twisted.web.woven import page from twisted.web import server from twisted.web import microdom from twisted.web import static class DirectoryListing(page.Page): templateFile = "directory-listing.xhtml" templateDirectory = os.path.split(os.path.abspath(__file__))[0] def initialize(self, *args, **kwargs): self.directory = kwargs['directory'] def wmfactory_title(self, request): """Model factory for the title. This method will be called to create the model to use when 'model="title"' is found in the template. """ return self.directory def wmfactory_directory(self, request): """Model factory for the directory. This method will be called to create the model to use when 'model="directory"' is found in the template. """ files = os.listdir(self.directory) for i in xrange(len(files)): if os.path.isdir(os.path.join(self.directory,files[i])): files[i] = files[i] + '/' return files def getDynamicChild(self, name, request): # Protect against malicious URLs like '..' if static.isDangerous(name): return static.dangerousPathError # Return a DirectoryListing or an ImageDisplay resource, depending on # whether the path corresponds to a directory or to a file path = os.path.join(self.directory,name) if os.path.exists(path): if os.path.isdir(path): return DirectoryListing(directory=path) else: return ImageDisplay(image=path)
Due to the somewhat complex inheritance hierarchy in Woven's internals, a lot
of processing is done in the __init__
method for Page
. Therefore, a separate
initialize
method is provided so that one can easily access keyword
args without having to disturb the internal setup; it is called with the same
args that Page.__init__
receives.
The templateFile
attribute tells the Page what file to load the
template from; in this case, we will store the templates in the same directory
as the Python module. The wmfactory
(short for Woven Model Factory)
methods return objects to be used as models; In this case,
wmfactory_title
will return a string, the directory's name, and
wmfactory_directory
will return a list of strings, the directory's
content.
Upon rendering, Woven will scan the template's DOM tree for nodes to fill;
when it encounters one, it gets the model (in this case by calling methods on
the Page prefixed with wmfactory_
), then creates a view for that
model; this page uses standard widgets for its models and so contains no custom
view code. The view fills the DOM node with the appropriate data. Here, the view
for title
is Text
, and so will merely insert the
string. The view for directory
is List
, and so each element of the list
will be formatted within the '<ul>'. Since the view for list items is
Anchor, each item in the list will be formatted as an <a>
tag.
So, for a directory Images
containing foo.jpeg
,
baz.png
, and a directory MoreImages
, the rendered page will look
like this:
<html> <head> <title>/Users/ashort/Pictures</title> </head> <body> <h1>/Users/ashort/Pictures</h1> <ul> <li> <a href="foo.jpeg">foo.jpeg</a> </li> <li> <a href="baz.png">baz.png</a> </li> <li> <a href="MoreImages/">MoreImages/</a> </li> </ul> </body> </html>
As you can see, the nodes marked with model
and
view
are replaced with the data from their models, as formatted by
their views. In particular, the List view repeated the node marked with the
listItem
pattern for each item in the list.
For displaying the actual images, we use this template, which we save as image-display.html:
<html> <head> <title model="image" view="Text">Filename</title> </head> <body> <img src="preview" /> </body> </html>And here is the definition of
ImageDisplay
:
from twisted.web import static class ImageDisplay(page.Page): templateFile="image-display.xhtml" def initialize(self, *args, **kwargs): self.image = kwargs['image'] def wmfactory_image(self, request): return self.image def wchild_preview(self, request): return static.File(self.image)
Instead of using getDynamicChild
, this class uses a
wchild_
method to return the image data when the
preview
child is requested. getDynamicChild
is only
called if there are no wchild_
methods available to handle the
requested URL.
Finally, we create a webserver set to start with a directory listing, and
connect it to a port. We will tell this Site to serve a DirectoryListing of a
directory named Pictures
in our home directory:
rootDirectory = os.path.expanduser("~/Pictures") site = server.Site(DirectoryListing(directory=rootDirectory)) application = service.Application("ImagePool") parent = service.IServiceCollection(application) internet.TCPServer(8088, site).setServiceParent(parent)
And then start the server by running the following command-line:
twistd -ny picturepile.py
.
Custom Views
Now, let's add thumbnails to our directory listing. We begin by
changing the view for the links to thumbnail
:
<html> <head> <title model="title" view="Text">Directory listing</title> </head> <body> <h1 model="title" view="Text"></h1> <ul model="directory" view="List"> <li pattern="listItem"><a view="thumbnail" /></li> <li pattern="emptyList">This directory is empty.</li> </ul> </body> </html>
Woven doesn't include a standard thumbnail
widget, so we'll have
to write the code for this view ourselves. (Standard widgets are named with
initial capital letters; by convention, custom views are named like methods,
with initial lowercase letters.)
The simplest way to do it is with a wvupdate_
(short for Woven
View Update) method on our DirectoryListing class:
def wvupdate_thumbnail(self, request, node, data): a = microdom.lmx(node) a['href'] = data if os.path.isdir(os.path.join(self.directory,data)): a.text(data) else: a.img(src=(data+'/preview'),width='200',height='200').text(data)
When the thumbnail
view is requested, this method is called with
the HTTP request, the DOM node marked with this view, and the data from the
associated model (in this case, the name of the image or directory). With this
approach, we can now modify the DOM as necessary. First, we wrap the node in
lmx
, a class provided by
Twisted's DOM implementation that provides convenient syntax for modifying DOM
nodes; attributes can be treated as dictionary keys, and the text
and add
methods provide for adding text to the node and adding
children, respectively. If this item is a directory, a textual link is
displayed; else, it produces an IMG
tag of fixed size.
Simple Input Handling
Limiting thumbnails to a single size is rather inflexible; our app would be nicer if one could adjust it. Let's add a list of thumbnail sizes to the directory listing. Again, we start with the template:
<html> <head> <title model="title" view="Text">Directory listing</title> </head> <body> <h1 model="title" view="Text"></h1> <form action=""> Thumbnail size: <select name="thumbnailSize" onChange="submit()" view="adjuster"> <option value="400">400x400</option> <option value="200">200x200</option> <option value="100">100x100</option> <option value="50">50x50</option> </select> </form> <ul model="directory" view="List"> <li pattern="listItem"><a view="thumbnail" /></li> <li pattern="emptyList">This directory is empty.</li> </ul> </body> </html>
This time, we add a form with a list of thumbnail sizes named
thumbnailSize
: we want the form to reflect the selected option, so
we place an adjuster
view on the select
tag that looks
for the right option
tag and puts selected=1
on it
(the default size being 200):
def wvupdate_adjuster(self, request, widget, data): size = request.args.get('thumbnailSize',('200',))[0] domhelpers.locateNodes(widget.node.childNodes, 'value', size)[0].setAttribute('selected', '1')
request.args
is a dictionary, mapping argument names to lists of
values (since multiple HTTP arguments are possible). In this case, we only care
about the first argument named thumbnailSize
.
domhelpers.locateNodes
is a helper function which, given a list of
DOM nodes, a key, and a value, will search each tree and return all nodes that
have the requested key-value pair.
Next, we modify the thumbnail
view to look at the arguments from
the HTTP request and use that as the size for the images:
def wvupdate_thumbnail(self, request, node, data): size = request.args.get('thumbnailSize',('200',))[0] a = microdom.lmx(node) a['href'] = data if os.path.isdir(os.path.join(self.directory,data)): a.text(data) else: a.img(src=(data+'/preview'),width=size,height=size).text(data)
Sessions
A disadvantage to the approach taken in the previous section is that subdirectories do receive the same thumbnail sizing as their parents; also, reloading the page sets it back to the default size of 200x200. To remedy this, we need a way to store data that lasts longer than a single page render. Fortunately, twisted.web provides this in the form of a Session object. Since only one Session exists per user for all applications on the server, the Session object is Componentized, and each application adds adapters to contain their own state and behaviour, as explained in the Components documentation. So, we start with an interface, and a class that implements it, and registration of our class upon Session:
from zope.interface import Interface, implements class IPreferences(Interface): pass class Preferences: implements(IPreferences) components.registerAdapter(Preferences, server.Session, IPreferences)
We're just going to store data on this class, so no methods are defined.
Next, we change our view methods, wvupdate_thumbnail
and
wvupdate_adjuster
, to retrieve their size data from the Preferences
object stored on the Session, instead of the HTTP request:
def wvupdate_thumbnail(self, request, node, data): prefs = request.getSession(IPreferences) size = getattr(prefs, 'size','200') a = microdom.lmx(node) a['href'] = data if os.path.isdir(os.path.join(self.directory,data)): a.text(data) else: a.img(src=(data+'/preview'),width=size,height=size).text(data) def wvupdate_adjuster(self, request, widget, data): prefs = request.getSession(IPreferences) size = getattr(prefs, 'size','200') domhelpers.locateNodes(widget.node.childNodes, 'value', size)[0].setAttribute('selected', '1')
Controllers
Now we turn to the question of how the data gets into the session in the
first place. While it is possible to to place it there from within the
wvupdate_
methods, since they both have access to the HTTP request,
it is desirable at times to separate out input handling, which is what
controllers are for. So, we add a wcfactory_
(short for Woven
Controller Factory) method to DirectoryListing:
def wcfactory_adjuster(self, request, node, model): return ImageSizer(model, name='thumbnailSize')
ImageSizer is a controller. It checks the input for validity (in this case,
since it subclasses Anything
, it merely ensures the input is
non-empty) and calls handleValid
if the check succeeds; in this
case, we retrieve the Preferences component from the session, and store the size
received from the form upon it:
class ImageSizer(input.Anything): def handleValid(self, request, data): prefs = request.getSession(IPreferences) prefs.size = data
Finally, we must modify the template to use our new controller. Since we are
concerned with the input from the <select>
element of the
form, we place the controller upon it:
<html> <head> <title model="title" view="Text">Directory listing</title> </head> <body> <h1 model="title" view="Text"></h1> <form action=""> Thumbnail size: <select name="thumbnailSize" onChange="submit()" view="adjuster" controller="adjuster"> <option value="400">400x400</option> <option value="200">200x200</option> <option value="100">100x100</option> <option value="50">50x50</option> </select> </form> <ul model="directory" view="List"> <li pattern="listItem"><a view="thumbnail" /></li> <li pattern="emptyList">This directory is empty.</li> </ul> </body> </html>
Now, the selected size will be remembered across subdirectories and page reloads.