GenshiのTutorial6(Adding a Submission FormとAdding Form Validation)途中。

嫁さまの体調が悪くて、ちょっとストップしてましたが、だんだん落ち着いてきたので再開。
今日はとりあえず、準備だけ。。。

Adding a Submission Form

In the previous step, we've already added a link to a submission form to the template, but we haven't implemented the logic to handle requests to that link yet.

To do that, we need to add a method to the Root class in geddit/controller.py:

    @cherrypy.expose
    def submit(self, cancel=False, **data):
        if cherrypy.request.method == 'POST':
            if cancel:
                raise cherrypy.HTTPRedirect('/')
            # TODO: validate the input data!
            link = Link(**data)
            self.data[link.id] = link
            raise cherrypy.HTTPRedirect('/')

        tmpl = loader.load('submit.html')
        stream = tmpl.generate()
        return stream.render('html', doctype='html')

Note: we explicitly check for the HTTP request method here. And only if it's a “POST” request we actually go and look into the submitted data and add it to the database. That's because “GET” requests in HTTP are supposed to be idempotent, that is, they should not have side effects. If we didn't make this check, we'd also be accepting requests that would change the database via “GET” or “HEAD”, thereby violating the rules.

And of course we'll need to add a template to display the submission form. In geddit/templates, create a file named submit.html, with the following content:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/">
  <head>
    <title>Geddit: Submit new link</title>
  </head>
  <body class="submit">
    <div id="header">
      <h1>Submit new link</h1>
    </div>

    <form action="" method="post">
      <table summary=""><tbody><tr>
        <th><label for="username">Your name:</label></th>
        <td><input type="text" id="username" name="username" /></td>
      </tr><tr>
        <th><label for="url">Link URL:</label></th>
        <td><input type="text" id="url" name="url" /></td>
      </tr><tr>
        <th><label for="title">Title:</label></th>
        <td><input type="text" name="title" /></td>
      </tr><tr>
        <td></td>
        <td>
          <input type="submit" value="Submit" />
          <input type="submit" name="cancel" value="Cancel" />
        </td>
      </tr></tbody></table>
    </form>

    <div id="footer">
      <hr />
      <p class="legalese">© 2007 Edgewall Software</p>
    </div>
  </body>
</html>

Now, if you click on the “Submit new link” link on the start page, you should see the submission form. Filling out the form and clicking "Submit" will post a new link and take you to the start page. Clicking on the “Cancel” button, will take you back to the start page, but not add a link.

Please note though that we're not performing any kind of validation on the input, and that's of course a bad thing. So let's add validation next.

Adding Form Validation

We'll use FormEncode to do the validation, but we'll keep it all fairly basic. Let's declare our form in a separate file, namely geddit/form.py, which will have the following content:

from formencode import Schema, validators


class LinkForm(Schema):
    username = validators.UnicodeString(not_empty=True)
    url = validators.URL(not_empty=True, add_http=True, check_exists=False)
    title = validators.UnicodeString(not_empty=True)


class CommentForm(Schema):
    username = validators.UnicodeString(not_empty=True)
    content = validators.UnicodeString(not_empty=True)

Now let's use those in the Root.submit() method. First add the form classes, as well as the Invalid exception type used by FormEncode, to the imports at the top of geddit/controller.py, which should then look something like this:

import cherrypy
from formencode import Invalid
from genshi.template import TemplateLoader

from geddit.form import LinkForm, CommentForm
from geddit.model import Link, Comment

Then, update the submit() method to match the following:

    @cherrypy.expose
    def submit(self, cancel=False, **data):
        if cherrypy.request.method == 'POST':
            if cancel:
                raise cherrypy.HTTPRedirect('/')
            form = LinkForm()
            try:
                data = form.to_python(data)
                link = Link(**data)
                self.data[link.id] = link
                raise cherrypy.HTTPRedirect('/')
            except Invalid, e:
                errors = e.unpack_errors()
        else:
            errors = {}

        tmpl = loader.load('submit.html')
        stream = tmpl.generate(errors=errors)
        return stream.render('html', doctype='html')

As you can tell, we now only add the submitted link to our database when validation is successful: all fields need to be filled out, and the url field needs to contain a valid URL. If the submission is valid, we proceed as before. If it is not valid, we render the submission form template again, passing it the dictionary of validation errors. Let's modify the submit.html template so that it displays those error messages:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/">
  <head>
    <title>Geddit: Submit new link</title>
  </head>
  <body class="submit">
    <div id="header">
      <h1>Submit new link</h1>
    </div>

    <form action="" method="post">
      <table summary=""><tbody><tr>
        <th><label for="username">Your name:</label></th>
        <td>
          <input type="text" id="username" name="username" />
          <span py:if="'username' in errors" class="error">${errors.username}</span>
        </td>
      </tr><tr>
        <th><label for="url">Link URL:</label></th>
        <td>
          <input type="text" id="url" name="url" />
          <span py:if="'url' in errors" class="error">${errors.url}</span>
        </td>
      </tr><tr>
        <th><label for="title">Title:</label></th>
        <td>
          <input type="text" name="title" />
          <span py:if="'title' in errors" class="error">${errors.title}</span>
        </td>
      </tr><tr>
        <td></td>
        <td>
          <input type="submit" value="Submit" />
          <input type="submit" name="cancel" value="Cancel" />
        </td>
      </tr></tbody></table>
    </form>

    <div id="footer">
      <hr />
      <p class="legalese">© 2007 Edgewall Software</p>
    </div>
  </body>
</html>

So now, if you submit the form without entering a title, and having entered an invalid URL, you'd see something like the following:

tutorial02
tutorial02 posted by (C)dai_yamashita

But there's a problem here: Note how the input values have vanished from the form! We'd have to repopulate the form manually from the data submitted so far. We could do that by adding the required value="" attributes to the text fields in the template, but Genshi provides a more elegant way: the HTMLFormFiller stream filter. Given a dictionary of values, it can automatically populate HTML forms in the template output stream.

To enable this functionality, first you'll need to add the following import to the geddit/controller.py file:

from genshi.filters import HTMLFormFiller

Next, update the bottom lines of the Root.submit() method implementation so that they look as follows:

        tmpl = loader.load('submit.html')
        stream = tmpl.generate(errors=errors) | HTMLFormFiller(data=data)
        return stream.render('html', doctype='html')

Now, all entered values are preserved when validation errors occur. Note that the form is populated as the template is being generated, there is no reparsing and reserialization of the output.