Personal tools
You are here: Home Documentation Tutorials Grok Poller Tutorial

Grok Poller Tutorial

Note: Return to tutorial view.

This tutorial shows how to implement a simple polling application using Grok.

Introduction

The core audience of this tutorial is programmers that already are already familiar with Python and Web development, but might not know anything about Zope.
Author: Väinö Järvelä
Date: 2009-06-30
Copyright: 2009 by Väinö Järvelä, Code from zope.app.form documentation Zope Corporation and Contributors
License:ZPL 2.1

This tutorial shows how to implement a simple polling application using Grok. Grok is a Web application framework based on Zope Toolkit [1]; for a proper introduction please read more about it on the Grok Web site [2].

[1]Zope Web site <http://www.zope.org/> and Zope Toolkit documentation <http://docs.zope.org/zopetoolkit/>
[2]Grok Web site <http://grok.zope.org/>

The core audience of this tutorial is programmers that already are already familiar with Python and Web development, but might not know anything about Zope. I will try to explain the basics of some Zope concepts -- interfaces and adapters for example, but most of the details are left out; but I will try to provide links to other articles for more info. The tutorial should be read in a linear order, as I will try to build on concepts sequentially.

The application that is implemented in this tutorial is similar to Django's introductory tutorial [3]. Except that Django uses it's excellent administration panel which allows them to skip many details, such as form generation, which has to be implemented manually in Grok.

I am not an experienced Zope or Grok developer, and this tutorial is also made for my own learning purposes; thus there might be errors and I might not use best practices. Please contact me for any corrections or improvements.

[3]Django Tutorial Web site <http://docs.djangoproject.com/en/dev/intro/>

Poller Idea

The poller application, that we will implement in this tutorial, is a simple Web application, which let's the users create polls with multiple voting options.

When a user votes, he can then see the voting results of that poll. The following list is a list of tasks, or user stories as they are called in agile terminology; which specifies the functionality:

  • The users can list all polls
  • The users can add new polls
  • The users can vote on poll options
  • The users can see poll results

With these simple user stories, we can start implementing the application.

Grok Setup

Follow the Grok Tutorial instructions on installing Grok an setting up a project.

The Grok tutorial [1] has a great section on Grok setup, and I do not want to duplicate it here, please read "Getting started with Grok" section and come back here. After you have created your project using grokproject, the name of the project used in this tutorial is Poller.

NOTE: When adding the application in the Grok Admin UI, we call it "poller" which is the application name used later in this tutorial.

One small addition to the Grok tutorial setup section, is a small tip on debugging Grok applications: Grok supports Web Server Gateway Interface (WSGI) [2] and includes a nice setup for using z3c.evalexception [3], which is a WSGI middleware package providing interactive AJAX debugger and a post-mortem pdb support. To run a Grok instance with debugging enabled, use the following command:

$ bin/paster serve parts/etc/debug.ini

This command should be run whenever you make changes to Python code, template code is updated automatically. Now that Grok and the application has been set up, we'll start coding in the next section.

[1]Grok Tutorial <http://grok.zope.org/doc/current/tutorial.html>
[2]WSGI Web site <http://www.wsgi.org/>
[3]z3c.evalexception Web site <http://pypi.python.org/pypi/z3c.evalexception/>

Models

Creating the application data model.

The base class for models in Grok applications is aptly called the Model, subclasses of it can persist in the Zope Object Database [1] (ZODB), and also adds some containment attributes to it, more on containment later. The models are used for handling the "display-independent logic of the application" [2] in Grok.

[1]ZODB on Wikipedia <http://en.wikipedia.org/wiki/Zope_Object_Database>
[2]Grok Tutorial: Models <http://grok.zope.org/doc/current/tutorial.html#models>

For this application, we will implement two separate models:

Poll
which contains a poll question and the actual options. We will save a label and a description for every separate option. The options are implemented as their own objects.
PollOption
saves the label and description mentioned above. We could save them as a tuple sequence, for example, in the Poll object; but this design is a bit easier to handle as we don't have named tuples in Python 2.x series. It also supports our tutorial in the form generation part.

To implement these objects, we will create a new module named poll.py inside src/poller/ directory; where all files and directories created in this tutorial must be made.

Let's start with the PollOption implementation as it's the simpler object of the two:

import grok

class PollOption(grok.Model):
    label = u''
    description = u''

As you can see, the PollOption class is a subclass of grok.Model, which makes it persistent, as mentioned above -- additionally, this object defines the label and the description as class attributes.

The Poll object contains the question and the options, but we also wish to implement some methods for interfacing with the options and voting. Add the following to poll.py:

class Poll(grok.Model):
    question = u''

    def __init__(self):
        self._options = ()
        self._responses = {}

    def get_response(self, option):
        return self._responses[option]

    def choose(self, option):
        self._responses[option] += 1
        self._p_changed = True

    def get_options(self):
        return self._options

    def set_options(self, options):
        self._options = options
        self._responses = {}
        for option in self._options:
            self._responses[option.label] = 0

    options = property(get_options, set_options)

The code for this class is a copy [3] from zope.app.form package documentation [4]. It implements the options attribute as a Python property -- when the options attribute is set, it creates a "private" dictionary, which maps the options to votes. get_response() and choose() methods acts as the interface for the dictionary.

[3]The class includes a single modification from the original, it implements the __init__() method to initialize _options and _responses to work with empty polls.
[4]zope.app.form documentation <http://pypi.python.org/pypi/zope.app.form>

This is all basic Python code, except for:

self._p_changed = True

which tells the ZODB that a mutable attribute has changed in this object, which it doesn't know otherwise. ZODB saves all object attribute reference modifications, which means that it works for all immutable attributes automatically. Basically you can't use it directly with mutable objects such as Python list or dict, because modifying them won't change the attribute reference, and thus ZODB won't save them. ZODB provides some utilities for working with these basic types, we'll use one called PersistentDict here. By using PersistentDict in place of Python dict, ZODB automatically tracks changes to that dictionary; otherwise it works like dict. In the following code listing, we have removed the manual self._p_changed usage, and replaced the dict instantiation with PersistentDict:

from persistent.dict import PersistentDict

class Poll(grok.Model):
    question = u''

    def __init__(self):
        self._options = ()
        self._responses = {}

    def get_response(self, option):
        return self._responses[option]

    def choose(self, option):
        self._responses[option] += 1

    def get_options(self):
        return self._options

    def set_options(self, options):
        self._options = options
        self._responses = PersistentDict()
        for option in self._options:
            self._responses[option.label] = 0

    options = property(get_options, set_options)

The class could have been a bit simpler, but this implementation works great in the future for explaining some Grok concepts. For more information about ZODB, read Andrew Kuchling's ZODB/ZEO Programming Guide [5].

[5]<http://wiki.zope.org/ZODB/Documentation/guide/index.html>

The First View

We will now implement the first view of the poller application; which will list all the polls for the users, and provide a link to add new polls.

The first view is implemented in the app module, which is the template module created by grokproject. The following code is the template view created for us:

class Index(grok.View):
    pass # see app_templates/index.pt

This is the simplest view possible, which works by using some implicit conventions built in Grok views. Grok views automatically finds out some configuration parameters for the views by the contents of the modules the views are in. In this case, the Index view knows that its context is the Poller model which is the only model implemented in that module, the name of this view is index which is the lower case name of the class, and it's template is read from app_templates/index.pt file -- more specifically <module_name>_templates/<viewclass_name>.pt.

All views in Grok has a context, which ties that view to a specific class or interface (interfaces are explained later in this tutorial). The context is the object for which this view is "attached" to. By default, the URLs in Grok are generated by traversing the object database, as if it was a file system; so when we instantiated the Poller application in the admin UI as poller, the URL for it became http://localhost:8080/poller. All views whose context is Poller, can now be accessed through that URL; for example to access the Index view in this case, open http://localhost:8080/poller/index (index is also the default view name so the previous URL also works). The traversal system will be visitited in a bit more detail later.

You can also declare the view configuration explicitly, overriding the implicit conventions. The following is the same view, with the same configuration, but declared explicitly:

class Index(grok.View):
    grok.context(Poller)
    grok.name('index')
    grok.templatedir('app_templates')
    grok.template('index.pt')

grok.context() etc. are called Grok directives, (a full list of them can be found at Grok reference documentation [1]); in short, the directives is the way to configure Grok objects as an alternative to Zope Configuration Markup Language (ZCML) [2], which is not covered in this tutorial at all.

[1]Grok reference on directives <http://grok.zope.org/doc/current/reference/directives.html>
[2]"The Zope Configuration Markup Language is an XML-based language for configuring Zope software and components" -- <http://wiki.zope.org/zope3/ZCML>

The view context is usually a grok.Model or a grok.Container class. We implemented two models in the previous section, and the grok template implements a simple container with the name of our application (the Poller class in app.py). The containers in Grok are also persistent objects like the models -- their simple purpose is to act as a container for models (the containers actually are models too.) grok.Container implements a Python dict like interface for adding models to them, and to show this in action, we will use Grok debug console to add a simple Poll object to our instance of the Poller.

To launch the debug console, run bin/poller-debug in a terminal. This starts the Python interactive interpreter, but with Grok and ZODB initialized and running; thus providing a simple access to the database and to simulate requests to the server. The default database in Grok has a root object, which is a container itself, and it's where the admin UI instantiated our application. To access the Poller instance, type:

>>> poller = root['poller']
>>> poller
<poller.app.Poller object at 0x234c5f0>

as you can see, the containers uses the dictionary interface, and it returns Python objects directly from the database. Next we will create a Poll instance manually and add it to the poller container:

>>> from poller.poll import Poll, PollOption
>>> poll = Poll()
>>> poll.question = u'How do you do?'

create a couple of options and set them to the poll:

>>> option1 = PollOption()
>>> option1.label = u"I'm okay"
>>> option2 = PollOption()
>>> option2.label = u"I feel bad"
>>> poll.options = [option1, option2]

finally add the new poll to the container:

>>> poller['how_do_you_do'] = poll

commit the changes to the ZODB (not necessary in Grok application code):

>>> import transaction
>>> transaction.commit()

Zope Page Templates

How to create display templates using the templating language TAL.

Now we have a poll in the database for testing, and we can see it in our view after we implement it. As its template language Grok uses Template Attribute Language (TAL), also referred to as Zope Page Templates (ZPT). It's an XML based language, but we won't be diving too deep in it in this tutorial.

Open app_templates/index.pt and write the following in it:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:tal="http://xml.zope.org/namespaces/tal">
  <body>
    <h1>Polls</h1>

    <ul>
      <li tal:repeat="poll context/values">
        <a href="" tal:attributes="href python: view.url(poll)"
                   tal:content="poll/question">
          A poll
        </a>
      </li>
    </ul>

    <a href="" tal:attributes="href python: view.url('add')">Add a poll</a>
  </body>
</html>

The code is kept simple in this example; in real life the XHTML code would use CSS styling etc. As you can see the template language uses XML attributes in the namespace tal to dynamically modify the elements they are placed in. The list items are repeated by using tal:repeat attribute, or more specifically with repeat TAL command. The repeat command equals to Python's for-loop, where the loop contents is the li element:

# self is a view instance
for poll in self.context.values:
    ...

context/values part is a TAL Expression Syntax (TALES) [1] expression, which is an expression system used in TAL. Grok defines the following special names for the templates [2]:

[1]TALES Web site <http://wiki.zope.org/ZPT/TALES>
[2]Taken from Grok overview Web site <http://grok.zope.org/doc/current/grok_overview.html#templates>
view
The view that this template is associated with
context
The model that is being viewed
request
The current request object
static
To make URLs to static content made available by this module

thus context/values calls values() method of the model being viewed. The foo/bar expressions are called path expressions, which allows you to access attributes and methods of objects. Methods are called automatically, so you don't use parenthesis for them, as context/values shows.

Tip

If you want to make sure that no methods are called, you can use nocall: modifier before the path: tal:repeat="item nocall:context/itemsvar"

The next TAL command in the template is attributes, which is used to replace attribute contents, in this case, it replaces the contents of href with the result of python: view.url(poll) expression. TALES expressions that is prefixed with python: runs the following snippet as Python. The Python environment works in the same namespace as TAL, so we can use view name in the Python code to access the view instance. Grok views has url() method [3] which is used to return an absolute URL to that exact view for the current context. The first attributes command returns an URL to the current Poll object in the loop, and the second returns a specific view called "add", which we haven't implemented yet; hence is the topic of the next section.

[3]Grok view reference <http://grok.zope.org/doc/current/reference/components.html#views>

content command replaces the content of the element with the TALES expression, so in this case "A poll" text will be replaced with poll/question or as in Python: poll.question.

Now you should be able to view all polls at http://localhost:8080/poller/, or at least the one we created in the debug console.

Adding Objects

To add polls, we want to provide a Web form for the users.

The view made in the last section should have a link to a new view, which displays an add form for the polls. We could code the form by hand, and fetch the results manually in the view by implementing a update() method for it; which is a view method that handles HTTP GET, POST and form parameters of the request before rendering the view, this method will be used at the Poll View section.

Grok has built-in form support, so you don't have to code them by hand, and it even includes validation and fully customizable widget support. The forms work like views, they have a context and a name, which is used to determine an URL for it. The following code (in app.py) implements a simple form for adding polls to our poll container, but it doesn't adding support poll options yet, just the question:

from zope import schema
from poller.poll import Poll

class Add(grok.AddForm):
  # Note that form_fields is a tuple
  form_fields = (
      grok.Fields(schema.TextLine(__name__='question',
                                  title=u'Question',
                                  required=True)),
      )
  # Title/header for the form template
  label = u'New poll'

  @grok.action('Add poll')
  def handle_add(self, **data):
      poll = Poll()
      self.applyData(poll, **data)
      name = poll.question
      self.context[name] = poll
      return self.redirect(self.url(self.context))

grok.AddForm is a grok.Form specialization, which handles creating new objects through a form, there is also grok.EditForm which is used for editing existing objects. At first we define form_fields class attribute, which is tells the form generator what fields to create. The form system uses Zope schema package to define these fields. The schema package includes all of the basic Python types and more, and you can use schema fields' built-in validation attributes such as min_length or max_length or you can implement your own invariants to the fields. The form system validates the fields based on that info, which allows you to create complex forms easily.

To define form buttons you use grok.action decorator on a method; that method is called when that button is pressed. The parameter for the decorator is the name of the button. In handle_add we will instantiate a new Poll object, and apply the form data to it using applyData method of the form. handle_action method accepts the form data as keyword arguments, and we use **data to catch them all in a single dictionary, which is then unpacked for the applyData method. Now we have a new poll with the input from the form applied to it, next we save it to the container as we did in the debug console; then we redirect back to the container because we don't have a view for the actual polls yet.

You can now test the form, and create new polls, even though without the poll options; which is the next step, but first we will delve a bit in to Zope interfaces.

Interfaces

In the previous section, we defined a schema for the ``Poll`` class manually, which is not a good solution, as we might need multiple forms for them, and thus will need to repeat a lot of code.

We can fix this by defining interfaces for our models. Grok uses Zope interface framework [1] for this. In short, the interfaces define what attributes and methods a class implementing the interface should have, but it's not limited to classes, you can also define interfaces for functions or even modules too. The interface system is used extensively by the Zope component architecture [2], which too large to cover completely in here, instead we will just use the interface system so that you can see how it works, but for more information you should read other documents about it.

[1]Zope Interface <http://pypi.python.org/pypi/zope.interface>
[2]Zope Component <http://pypi.python.org/pypi/zope.component>

To define the interfaces for the PollOption model, add the following code to a new module called interfaces.py:

from zope.interface import Interface, Attribute

# By convention, interfaces are prefixes with an "I"
class IPollOption(Interface):
    """An option for ``Poll``"""
    label = Attribute(u'Label of a poll option')
    description = Attribute(u'Description of a poll option')

this is a simple interface definition, which defines an interface called IPollOption, which contains two attributes. The attributes can be of any type, and the string as the parameter is the documentation for that attribute. This is all there is to defining interfaces. Next we will define the interface for the Poll model, or more precisely an interface which the Poll model is an implementation of, as there could be multiple implementations of a single interface:

class IPoll(Interface):
    """A poll"""
    question = Attribute(u'Question')
    options = Attribute(u'Options')

    def get_response(option):
        """Get the response for an option"""

    def choose(option):
        """User chooses an option"""

simple as that. There a few things to note in this interface:

  • The methods do not define self. This is because it's an implementation detail, this interface could be a Python module, where these "methods" are actually module functions.
  • The methods don't have pass in them. Without the docstrings, this would be a SyntaxError in Python, but the docstrings is enough for Python, it's a common misconception that docstringed methods are "empty."

These interfaces would work for our models, but there is still something missing, the actual schema definition we typed in the form. The attributes do not define a type; thus the form system doesn't know their types nor any invariants applied to them. The schema fields we used in the forms are actually interface attributes, they are just specialized attributes; again, you should read more about them in another tutorial, but the actual interface definitions we need is below:

from zope.interface import Interface
from zope.schema import Object, Tuple, TextLine

class IPollOption(Interface):
    """An option for ``Poll``"""
    label = TextLine(title=u'Label', min_length=1)
    description = TextLine(title=u'Description', min_length=1)


class IPoll(Interface):
    """A poll"""
    question = TextLine(title=u'Question')

    # The tuple default must be defined (as with all sequences) because
    # of a bug in zope.app.formlib which is used in Grok form system.
    # (grok 1.0a4)
    options = Tuple(title=u'Options',
                    value_type=Object(IPollOption, title=u'Poll Option'),
                    default=tuple())

    def get_response(option):
        """Get the response for an option"""

    def choose(option):
        """User chooses an option"""

this version actually defines what is the type of the different attributes, and their invariants. Sequence fields such as Tuple can even define the type of the values contained in them, in this case we define that options is a tuple which contains only objects which implement IPollOption interface.

Now we must modify our models to inform the interface system that they implement these interfaces, this is done using grok.Implements directive in the models, and can be used in any classes, not just models. The following is the PollOption model code again, you should be able to change Poll by yourself:

from poller.interfaces import IPollOption

class PollOption(grok.Model):
    grok.implements(IPollOption)

    label = u''
    description = u''

Now we are ready to change the add form implementation to use the defined schemas, by replacing form_fields in app.py Add class with:

from poller.interfaces import IPoll
...
class Add(grok.AddForm):
    form_fields = grok.AutoFields(IPoll)
    ...

But if you try to display the form, it will raise an exception, because it doesn't know how to display IPollOption as a field. If the tuple value type would have been a "simple" schema field, like TextLine, then the form would work automatically, but in this case, we must define a custom widget for the object; for more information, see the documentation of zope.app.form which explains this problem in detail. Fortunately this is just a couple of lines of boilerplate code, but it breaks the DRY [3] concept a bit as the customization has to be done to all forms that use IPoll interface.

[3]DRY <http://en.wikipedia.org/wiki/Don't_repeat_yourself>

Add the following lines to poll.py module:

from zope.app.form import CustomWidgetFactory
from zope.app.form.browser import SequenceWidget, ObjectWidget

poll_objectwidget = CustomWidgetFactory(ObjectWidget, PollOption)
poll_seqwidget = CustomWidgetFactory(SequenceWidget,
                                     subwidget=poll_objectwidget)

This is what zope.app.form documentation says about this:

"Note the creation of the widget via a CustomWidgetFactory. So, whenever the options_widget is used, a new SequenceWidget(subwidget=CustomWidgetFactory(ObjectWidget, PollOption)) is created. The subwidget argument indicates that each item in the sequence should be represented by the indicated widget instead of their default. If the contents of the sequence were just Text fields, then the default would be just fine - the only odd cases are Sequence and Object Widgets because they need additional arguments when they're created.

Each item in the sequence will be represented by a CustomWidgetFactory(ObjectWidget, PollOption) - thus a new ObjectWidget(context, request, PollOption) is created for each one."

Next we must tell the form about the custom widgets by adding this line below the form_fields definition, after importing poller.poll.poll_seqwidget:

form_fields['options'].custom_widget = poll_seqwidget

this simply sets the custom widget for the form field for options. Now the form works fine, and is actually quite cool as it dynamically generates subforms for the poll options. Now the form works, but there is still a small detail that should be fixed.

Naming Content

In the `Adding Objects`_, we made a form that creates new polls and adds them to a container. But we overlooked a problem in the way that Grok object traversal works.
When a user calls an URL in a Grok application, ``http://localhost:8080/poller/question`` for example, Grok will try to traverse the URL "path" item by item. So at first Grok asks from a root container if it contains an item calles "poller", if it does, then the traversal moves forward and asks if the "poller" contains "question" and so on. The traversal system can be customized, but this is the default behaviour. The problem in our is the following lines from our add form class: .. code-block:: python name = poll.question self.context[name] = poll the poll name in the container is the question input by the user, but there is no validation, in this case it's a string, and it can contain any characters a Python string can contain; which poses a problem as the same name is used to traverse the URL's. How do you traverse a poll which question is "How/do/you/do?", the traverser would split this to objects separated by the slashes. The solution is to filter the names, which is easy to do in Python string manipulation. But you shouldn't do it manually, as you would have to repeat the code everywhere that works with container names. Instead you can use a ``NameChooser``, which will convert and validate names for containers. But to use it, we must delve into adapters for a bit.

Adapters

This section tries to be a short introduction to adapters as they are used in Zope component architecture; thus Grok, and as an example, we will use the ``NameChooser`` mentioned in the previous section, and implement our own version of it.

Adapters are a way to add functionality to existing objects, without actually changing the objects, this is called "adapting an object." The following is the complete interface definition of INameChooser from zope.app.container:

class INameChooser(Interface):

    def checkName(name, object):
        """Check whether an object name is valid.

        Raises a user error if the name is not valid.
        """

    def chooseName(name, object):
        """Choose a unique valid name for the object.

        The given name and object may be taken into account when
        choosing the name.

        chooseName is expected to always choose a valid name (that would pass
        the checkName test) and never raise an error.

        """

We want to add those two methods to our container, so it can validate and choose a name for the polls, but we don't want to clutter the implementation with all the different interfaces we need or might need in the future, so we "outsource" the implementation to an adapter. To test adapting our container to INameChooser we can run the following code in the debug console:

>>> from zope.app.container.interfaces import INameChooser
>>> poller = root['poller']
>>> namechooser = INameChooser(poller)
>>> namechooser
<zope.app.container.contained.NameChooser object at 0x140cad0>

as you can see, namechooser is now an instance of an object which implements INameChooser interface. This means that the actual object is now different, but works in the context of our container. For this to work, an adapter must be registered for our container interface, in this case it was already done for us by default in Grok.

The adaptation is not as magical as it seems. The following is a simple mock name chooser adapter, which allows us to customize name choosing for our container:

from zope.app.container.interfaces import INameChooser

class MockNameChooser(object):
    def __init__(self, context):
        self.context = context

    def checkName(self, name, object):
        # Only allow unique names
        return not name in self.context

    def chooseName(self, name, object):
        # Don't modify the name
        return name

So if we now wish to adapt Poller to our MockNameChooser, we just run the following code:

namechooser = MockNameChooser(poller)

it just assigns poller as self.context in our namechooser instance, so that the adapter can implement INameChooser interface in our objects context. This is the basic adaptation pattern that doesn't need frameworks around it. But in the debug console above, we didn't use the adapter class name when we adapted the poller to a name chooser, we actually used the interface for adaptation, and this is where the Zope component framework comes in.

If we adapt the poller to INameChooser interface, it still returns Zope's own NameChooser instance, not the adapter we just implemented. This is because the component framework doesn't know that MockNameChooser exists. To actually register the adapter, we must use a couple of Grok directives, and we can also use grok.Adapter as a subclass to shorten the code a bit:

class MockNameChooser(grok.Adapter):
    grok.context(Poller)
    grok.provides(INameChooser)

    def checkName(self, name, object):
        # Only allow unique names
        return not name in self.context

    def chooseName(self, name, object):
        # Don't modify the name
        return name

grok.Adapter is just a simple class, that implements the __init__ method according to Zope adapter convention; that the class accepts a single parameter which is the context of the adapter, aptly named self.context:

class Adapter(object):
    def __init__(self, context):
        self.context = context

grok.context works in the same way as with views [1], it tells the component framework that the context for this adapter is Poller. grok.provides is a new directive for us, which informs the component framework that this adapter provides INameChooser framework (for Poller context.)

[1]The views are actually adapters too; for more information about it, read the Grok documentation.

Now that the adapter is registered, whenever someone tries to adapt the poller to the name chooser interface, it gets our implementation. This is a simple, but effective pattern to create extendable frameworks and applications in Python.

For more information about adapters, read "Grok Developer's Notes" section on adapters [2] and watch Brandon Craig Rhodes' "Introduction to Zope Component Architecture" presentation [3] from NOLA Symposium '08.

[2]<http://grok.zope.org/doc/current/grok_overview.html#adapters>
[3]<http://plone.tv/media/1442083381/view>

Implementing the Name Chooser

We now have a simple name chooser adapter, but it doesn't really do anything useful, and it doesn't fix the problems we had with the URL's.

So scratch the MockNameChooser code completely and let's rewrite it by basing it on the default name chooser implementation (in app.py):

from zope.app.container.contained import NameChooser
from zope.app.container.interfaces import INameChooser

class PollerNameChooser(grok.Adapter, NameChooser):
    grok.context(Poller)
    grok.provides(INameChooser)

    def chooseName(self, name, obj):
        # At first choose a name by using Zopes default NameChooser
        name = super(PollerNameChooser, self).chooseName(name, obj)

        # Lower case the result and replace spaces with underscores
        name = name.replace(u' ', u'_').lower()

        # Check the name for errors just to be careful
        self.checkName(name, obj)
        return name

Zope's name chooser does a couple of things for the names:

  • Checks for duplicates and adds a counter if found
  • Converts slashes to dashes to work with URLs
  • Converts to unicode
  • Strips '+' and '@' characters from the beginning, as those are special to Zope

to this behaviour, we added lower casing and space replacing to make the URLs a bit more neat. You could also register this adapter for all containers by defining the context to grok.Container class.

Now to use it in our add form, replace handle_add method with the following code:

@grok.action('Add poll')
def handle_add(self, **data):
    poll = Poll()
    self.applyData(poll, **data)
    name = INameChooser(self.context).chooseName(poll.question, poll)
    self.context[name] = poll
    return self.redirect(self.url(self.context))

to adhere to the DRY pattern, we should still modify this code a bit by extracting the name chooser pattern as a method on the container, which is the first time we touch the template container. Add the following method to the Poller class:

def add_poll(self, poll):
    name = INameChooser(self).chooseName(poll.question, poll)
    self[name] = poll
    return name

now we can use this method to add polls to this container without thinking about the naming, all that is left is to update handle_add() method to use this new method for adding the new polls to the container.

Poll View

Now it's time to implement a view for the poll items which allows the user to vote on options.

As before, we must implement a new view class, but this time it will be a bit different. We will implement the form by hand for learning purposes, and we will also support some basic error message handling in the view. There are two new concepts that will be introduced in this view, the update() method, and view attributes; add the following code to the poll.py module:

class Index(grok.View):
    grok.context(Poll)

    def update(self, option=None):
        self.error = None
        if option:
            try:
                self.context.choose(option)
                return self.redirect(self.url(self.context, 'results'))
            except KeyError:
                self.error = u'Invalid option, please choose again.'
        elif self.request.form:
            self.error = u'Choose an option'

before a view is rendered, Grok calls the update() method of the view class, and provides any HTTP parameters as method arguments. In this view, the form will send the voting selection as option, thus we added that as an attribute for the method. We can also leave it out and access the form through self.request.form, which we will use to handle empty selections in this example.

We had to specify the context manually, as otherwise Grok wouldn't know which model this view is meant for, Poll or PollOption; the auto-detection is only possible if there is just a single model in the same module. Grok refuses to guess the models, as that would result in an unexpected behaviour.

In the update() method, we define attribute self.error which can be accessed through the view in the template, this will contain a error which will be shown to the user on the form; then we simply check if option is set and call the voting method of our poll, if it doesn't throw an exception the vote succeeded and we redirect to the results view, which we will implement after this view. The elif clause checks if the form was submitted (the dictionary is not empty because the form button sends a value), but the option was not provided, in this case we use the error message to tell the user that no option was provided.

To implement the template, write the following code in poll_templates/index.pt:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:tal="http://xml.zope.org/namespaces/tal">
  <body>
    <h1 tal:content="context/question">Poll question</h1>

    <p tal:condition="view/error" tal:content="view/error">
      Possible errors go here
    </p>

    <form action=""
          tal:attributes="action python: view.url(context)"
          method="post">
      <tal:block repeat="option context/options">
        <tal:block define="index string:choice${repeat/option/index}">
          <input type="radio" name="option" id="" value=""
                 tal:attributes="id index; value option/label" />
          <label for="" tal:attributes="for index">
            <span tal:replace="option/label">Option content</span>
            -- <i tal:content="option/description">Description</i>
          </label>
          <br />
        </tal:block>
      </tal:block>
      <input type="submit" name="submit" value="Vote" />
    </form>

    <a href="" tal:attributes="href python: view.url(context.__parent__)">
      Back to polls
    </a>
  </body>
</html>

Most of the code here should be familiar now, but there are some parts which needs explanation. First new TAL command is condition, if the expression in it evaluates to boolean true, then this element is rendered to the output. In the same element, we also use the error variable we defined in the view class. So in plain english, if error attribute in the view evaluates to true, then the <p> element is rendered with the content of that variable.

tal:block is a complex XML element defined by the template system, which can be used to use TAL commands without rendering any actual elements. Here we use it to run a loop of options, and the loop consists of multiple elements, not just a single looping one as with previous templates. Then we use define command to make a shortcut for numbering our options by repeat index. When using repeat it automatically defines a path for the loop, the path format is repeat/<loop name>/ under which there are different variables of the loop, such as the index; see the TALES specification for the full variable listing.

replace command works almost like a combination of tal:block and content command, it replaces the whole element with the result of the TALES expression. In Python the following template code:

<span tal:replace="option/label">Option content</span>
-- <i tal:content="option/description">Description</i>

equals to:

'%s -- <i>%s</i>' % (option.label, option.description)

ife we didn't want to italize the description, we could have used TALES string expression as follows:

<span tal:content="string:${option/label} -- ${option/description}">
  Option label and description here
</span>

The last new thing is in the "Back to polls" link's Python code, the context.__parent__ part. Every model in Grok containes these two automatic attributes, __parent__ and __name__. The former is the container for this model, and the latter is the name of this model in that container. So this expression returns a link to the poller container.

Poll Results View

Now all that is left for our simple poller application is the results view for the poll, which doesn't show us anything new, we just use the "string:" TALES expression just to show how it works.

The view class in poll.py:

class Results(grok.View):
    grok.context(Poll)

and the template poll_templates/results.pt:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:tal="http://xml.zope.org/namespaces/tal">
  <body>
    <h1 tal:content="context/question">Poll question</h1>

    <ul>
      <li tal:repeat="option context/options">
        <span tal:define="votes python:context.get_response(option.label)"
              tal:replace="string:${option/label} has ${votes} votes.">
        </span>
      </li>
    </ul>

    <a href="" tal:attributes="href python: view.url(context.__parent__)">
      Back to polls
    </a>
  </body>
</html>

This completes the last user story, and the tutorial application is completed.

Conclusion

In this tutorial you learned about multiple concepts regarding Grok Web application development, such as interfaces, schemas, adapters and page templates with models, containers, views and form generation.

But mostly this tutorial just scratches the surface for everyone of these topics -- many of them would need their own stand-alone tutorials to be complete. I wish that this has given you the basic info for building simple Web applications in Grok, and some little insight to using Zope 3 toolkit.

Thanks for running through the tutorial goes to Jyrki Puttonen.