PicturePile: a tutorial Woven application

  1. Custom Views
  2. Simple Input Handling
  3. Sessions
Note:

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 -noy 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:

class IPreferences(components.Interface):
    pass

class Preferences(components.Adapter):
    __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.

Index

Version: 1.3.0