SimpleBlog Part 4 - Widgets

In this part of the series we’re going to take a step back from SQLAlchemy and have a look at making the user experience a little more fluid. We’re going to convert all our hand-coded forms to widgets, and we’re also going to use widgets as a display mechanism.

Widget-me-do!

Widgets are a wonderful part of TurboGears. Using them in their default incarnation they offer a means of creating forms with automatic validation and error handling, but they can also be customised in almost every way possible. We’re going to start by converting our two forms using standard widgets.

I generally find the best way to create your widgets, especially if you intend to have a large number of them and/or custom widgets is to create a new module specifically for widgets.

To do this, create a new directory called widgets from your project’s root directory (where def.cfg is stored) and turn it into a python module using an __init__.py:

mkdir simpleblog/widgets
cp simpleblog/templates/__init__.py simpleblog/widgets/

These commands should work for windows users too, although I haven’t tested it.

Now open your favourite editor and create simpleblog/widgets/widgets.py:

from turbogears import widgets, validators

class CommentFields(widgets.WidgetsList):
    author_name = widgets.TextField(validator=validators.NotEmpty(), 
                        label="Name")

    author_email = widgets.TextField(validator=validators.Email(not_empty=True),
                        label="Email")

    author_url = widgets.TextField(label="URL")

    content = widgets.TextArea(validator=validators.NotEmpty(),
                        attrs=dict(rows=30, cols=70))

    post_id = widgets.HiddenField(validator=validators.Int(not_empty=True))

comment_form = widgets.TableForm(fields=CommentFields(),
                            name="w_commentform",
                            action="/comment",
                            method="post",
                            submit_text="Comment!")

Let us take a moment to see what we’ve done here. CommentFields is a WidgetsList. This is just syntactic sugar to make life a little easier, and the results are exactly the same as defining a list of widgets. So we could define the above like this:

comment_fields = [widgets.TextField("author_name", validator=validators.NotEmpty(), label="Name"),
                  widgets.TextField("author_email", validator=validators.Email(not_empty=True), label="Email"
                  ...
                  # You get the idea...  the rest have been snipped
                  )]

As you can see, using a WidgetList is a lot easier to read and write, especially when you start dealing with very large forms.

Each widget also has it’s own validator. If you have more complex validation requirements you can also create your own validator schema.

Finally we have the creation of the comment_form itself. I think the keyword arguments speak for themselves.

Note: some of these keywords are can be overridden when the widget is displayed, but not all! The fields that you are allowed to override are: action, method, form_attrs, submit_text and table_attrs. An attempt to modify anything else after instantiating will result in an error.

You will now need to add the following line to simpleblog/widgets/__init__.py:

from widgets import *

And, in your controllers.py:

from simpleblog.widgets import comment_form
from turbogears import error_handler

Now the magic happens. We need to add two to our comment method:

@expose()
@validate(form=comment_form)
@error_handler(post)
def comment(self, post_id, author_name, author_email,
            content, author_url=None, *args, **kw):
    referer = cherrypy.request.headerMap.get("Referer", "/")
    p = model.Post.get(int(post_id))
    c = model.Comment(post=p, author_name=author_name,
                        author_email=author_email,
                        content=content)
    if author_url:
        c.author_url = author_url
    c.flush()
    raise redirect(referer)

The @validate decorator takes our comment_form as an argument, using the validator schema that was automatically created when we created the form in widgets.py. The @error_handler() decorator takes our post method as it’s argument. We’ll see what this does for us once we have the form working correctly.

We now need to display the form on our post page:

@expose(template="simpleblog.templates.post")
def post(self, id):
    p = model.Post.get(int(id))

    return dict(post=p,
                comment_form=comment_form)

and index.kid needs a little change too. We can replace all of our hand written form with the widget:

<div id="commentform">
    <h3>Add your comments</h3>
    ${comment_form(value=dict(post_id=post.id))}
</div>

Now, lets restart the app and take a look at the new comment form. When you enter everything as you would for a normal comment everything is hunky-dory. If, however, you enter an invalid email address, or leave out a required field we get a nice little error message!

Not only do we not have to write any HTML forms, we get free server side validation too. In a word, awesome.

This pony has another trick

Widgets can also be used simply for display purposes. They are excellent to prevent template repetition when you have to display the same elements in multiple templates. We’re going to use it in a slightly contrived manner, but it will serve the purpose of showing how widgets can be used in this way.

Back to widgets.py for a few more lines of code:

class PostDisplayWidget(widgets.Widget):
    template = 'simpleblog.templates.post_display_widget'
    params = ['post']
post_display = PostDisplayWidget()

We’ve added a template here that doesn’t exist, so - lets create it. This template is going to be a little different to a standard kid template as it will be a ‘fragment’. Open the new, blank file, post_display_widget.kid and use the following markup:

<div xmlns:py="http://purl.org/kid/ns#" id="post_${post.id}">
    <h2><a href="/post?id=${post.id}" py:content="post.title">Post title here</a></h2>
    <div class="postmeta">
        Posted on <span class="postdate" py:content="post.post_date">01/01/01</span>.<br/>
        <span py:if="post.comment_count > 0" py:strip=''>${post.comment_count}
            <span py:if="post.comment_count == 1">comment.</span>
            <span py:if="post.comment_count > 1">comments.</span>
        </span>
        <span py:if="post.comment_count == 0" py:strip=''>No comments</span>
    </div>
    <div class="content" py:content="XML(post.html_content)">
        This is where your post's content is displayed.
    </div>
</div>

Look familiar? That’s because this code was lifted straight from index.kid, with the exception of the containing <div> tags. The span tags need to have the kid XML namespace so that this HTML fragment can be displayed properly.

Just the changes to index.kid and our controller’s index method left. We need to remove the display code we just put into our display widget and replace it with… well… the widget:

...
<div id="posts">
    <span py:for="post in posts" py:strip="True">
        ${post_display(post=post)}
    </span>
</div>
...

and the controller:

from simpleblog.widgets import comment_form, post_display
...

@expose(template="simpleblog.templates.index")
def index(self):
    posts = model.Post.select()
    return dict(posts=posts, post_display=post_display)

All we need to do now is make sure the app is still running (we made a few changes there and it’s probable that during those changes there was a syntax error or a missing import) and look at the first page.

It looks exactly like it did before! Well, I did say that the example was slightly contrived, but lets look at the possible uses for a widget used in this way.

Imagine you have an application with many pages and templates and a percentage of those templates have common page elements. You can use widgets to display these common display elements and cut out bugs if you need to change the markup slightly. Instead of having to go through every template to make the same change you just stop at one template - the widget’s - et voila; instant change wherever the widget is used.

Well, that’s the end of another SimpleBlog tutorial. SimpleBlog itself is getting closer to becoming a usable application, so watch this space!

While I have a large range of topics I’d still like to go over, my time is being devoured by my current work load. This means that the tutorials will be released on a slower cycle until after the new year.