Quixote.ca

2. Editing the Link Tree: Quixote's Form Framework

HomeLearn
Last Modified: 30 Aug 2006

This is part two in a series of tutorials covering the Quixote framework for writing web applications in Python.

Introduction

The simple application developed in part 1 allows browsing through a tree of links, but provides no way to edit or change them. In this tutorial, I will add the ability to add and edit categories and links, using Quixote's form framework.

Manually Processing Form Variables

Quixote passes an HTTPRequest object to all handler methods that it invokes. It has a get_form_var(var_name, default=None) method to retrieve the value of a particular form variable. If the variable has been given a value in the HTTP request, its contents are always returned as a string by get_form_var(); if no value has been supplied, the value of the default parameter will be returned.

Here's a sketch of the code to handle a login form:

def login (request):
    user_id = request.get_form_var('userid')
    password = request.get_form_var('password')
    if user_id is None:
        # Report error: "You must supply a user ID."
        ...
    elif password is None:
        # Report error: "You must supply your password."
        ...
    elif not is_password_correct(user, password):
        # Report error: "Incorrect password"
        ...
    else:
        # Login is successful
        ...

For a simple form with only one or two variables, implementing the right logic isn't very difficult and doesn't need very much code. Complicated forms can require a lot more code, though, and it can take a sizable effort to debug them. A form can be complicated for several reasons; it can have very many fields or some fields that are required and some that are optional, have field values be of different data types, or have dependencies between fields ("If the answer to question 9 is 'yes', fill in question 10. Otherwise, leave question 10 blank.")

The Form Framework

Quixote provides a framework that makes it much easier to implement the processing for complicated forms. Note that you're perfectly free to implement the processing manually, to write your own framework, or to adapt someone else's framework for use with Quixote. You don't have to use Quixote's form framework if it doesn't meet your requirements or if you simply don't like it.

The framework consists of two classes. The Form class represents a form as a whole. Fields in the form are representing by various subclasses of the Widget class.

The Form class is intended to be used as the base class for specialized subclasses; you'll probably have one subclass of Form for every form in your application. Each form object manages a list of Widget instances that represent the fields in the form. Each widget in a form has a name which will be used as the variable's name in the form's HTML, a title which will be displayed to the user, a hint which gives additional information to the user such as example inputs, and a required flag. Quixote includes a number of widgets representing text fields, numeric fields, checkboxes, radio buttons, selection lists, and file uploads. It's not difficult to write new widgets that handle new data types, generate different HTML, or run some JavaScript code.

The form object is responsible for creating its list of managed widgets, displaying them, parsing and checking their input values when the user submits a form, and finally performing an action based on their values. Usually you'll need to override four methods in form subclasses: __init__(), render(), process(), and action(). Each of these four methods is called at a particular point in the form's life cycle; the pseudocode that calls them embodies the following logic:

# Create form object; __init__ will create widgets
form = FormSubclass()

# The first time, just display the form by calling render()
if not form.form_submitted(request):
    return form.render(request, action_url)

# Check the form data for errors
values = form.process(request)

if form.error:
    # If there were errors, re-render the form using .render()
    return form.render(request, action_url)
else:
    # Call action() to do work with the values; the values can 
    # be assumed to be correct.
    return form.action(request, submit, values)

You can see the full logic, which has some additional complications, by looking at the implementation of the Form.handle() method in quixote/form/form.py.

A Simple Form Example

Let's make all this abstract talk of classes more concrete with an example. We'll add a simple form to edit a category's name to our application. The URL for editing a category's name will consist of the URL for the category plus the extra path component '/edit'. In our Quixote application code, this requires adding a new edit() method to the CategoryUI class which simply creates the appropriate form object and calls its handle() method, passing it the HTTPRequest object:

class CategoryUI:
    _q_exports = [ ..., 'edit']

    def edit (self, request):
        form = CategoryEditForm(self)
        return form.handle(request)

The above excerpt only shows the code that has actually been changed; the CategoryUI class's _q_index() and _q_lookup() methods remain the same.

Warning: please note that the code given in this tutorial doesn't restrict access to the editing pages. This means anyone on the Internet can modify objects in the application. Usually this will not be what you want in your application, so you should always be careful to implement some sort of access control checking; not having a publicly visible link to '.../edit' isn't sufficient to provide security. (An attacker can guess that adding '/edit' to a URL will get them an editing form, and they can try other possibilities such as '/change' or '/modify'; the URL of an editing page might also be sent to an outside HTTP server in the Referer: HTTP header if you go from an editing page to a page on the outside site.)

The __init__ Method

The next task is to write the CategoryEditForm class, overriding three of the four methods. First, we'll write the __init__() method. Its job is to create the two widgets we'll need, a string field for the category title and a submit button:

def __init__ (self, category):
    form.Form.__init__(self)
    self.category = category

    self.add_widget('string', name='title',
                    value = self.category.title,
                    title = "Category Title",
                    required = True)

    self.add_submit_button('update_cat', "Update")

First, the method has to call the __init__ method of the form.Form base class.

Next, a single text entry widget is created by calling self.add_widget(). The add_widget method takes a number of different parameters:

add_widget(widget_type : string | Widget,
           name : string,                  # Name of field
           value : any = None,             # Initial value of field
           title : string = None,          # Title to display
           hint : string = None,           # Explanatory hint to display
           required : boolean = 0,         # Is this field required?
           ...)

Finally, a widget for the submit button is created. In the self.add_submit_button(name, value) call, the first argument is the name of the widget and hence of the resulting form variable. The second argument is the value of the button, which will be displayed as the label on the button and will also be returned to the server as the value of the form variable 'update_cat'. (This is useful when you have multiple submit buttons that do different things; you can tell which button the user selected.) The widget will end up generating the following HTML: <input type="submit" name="update_cat" value="Update" />

The render() Method

The task of the render() method is to generate a complete HTML page that contains the form fields. You'll certainly want to override this method just so you can use your site's standard header and footer. The Form class has a render() method that outputs a form in an unexciting but readable form; we'll use it here for simplicity.:

def render [html] (self, request, action_url):
    header("Edit category: " + self.category.title)
    form.Form.render(self, request, action_url)
    footer()

The default rendering is a three-column table. Widget titles (e.g. "Category Title") are rows of their own, alternating with rows containing the HTML form elements and the hint or error message for the field.

The action() Method

The final required method is action(), which is passed the HTTPRequest, the name of the submit button selected by the user, and a dictionary containing the form variables.

The action() method is usually easy to write because it doesn't have to do any error checking. Form validation is the job of the process() method which will signal an error if there's some problem with the supplied values. The action() method can therefore assume the field values are all legal and calculate with them, insert them into a database, or do whatever the form is supposed to do.

For now we won't provide a process() method and just accept whatever the user enters, setting the title of the current category to this value. If the user leaves the text field blank, the title field will have a value of None. We then redirect the client back to the page for this category.:

def action [html] (self, request, submit, values):
    self.category.title = values['title']
    return request.redirect(request.get_url(1))

At this point, the form works. You can view it by going to http://localhost:8080/news/edit, and when you edit the text field name and submit the form, the category's title is changed.

The process() Method

If you leave the text field blank, the category's title is set to None, which is unhelpful and should be prevented. Adding a simple error check isn't difficult:

def process [html] (self, request):
    values = form.Form.process(self, request)
    if values['title'] is None:
        self.error['title'] = 'You must supply a title.'
    return values

The job of the process() method is to look at the contents of the HTTPRequest object's form variables, turn them into Python data types, and return them as a dictionary mapping widget names to the widget's value. Numeric values therefore have to be translated from strings to integers, checkboxes have to be translated to a Boolean value, and so forth. As usual, the Form class includes a default implementation that will handle the basics of parsing for you; it loops over the widgets in the form and calls each widget's parse() method. Most process() implementations will follow the above example in beginning with a call to Form.process() and then applying various checks to the contents of the resulting dictionary.

Before the process() method is called, the self.errors attribute is set to an empty dictionary. An error for a given field is signalled by inserting an error message into the dictionary with the field's name as the key. You can signal as many errors as you like, and in fact it's good practice to report as many errors as possible to avoid frustrating the user by forcing them to correct a single error at a time and re-submit the form to find out what the next error is.

Widgets

Quixote's form framework includes a number of standard widget classes that correspond to the various HTML form elements. The name in parentheses after the class name is the string to be passed as the widget_type parameter to Form.add_width().

  • StringWidget (string): For entering a single line of text.
  • TextWidget (text): For entering multi-line strings.
  • PasswordWidget (password): Like StringWidget, but obscures what's being typed.
  • FileWidget (file): For uploading files.
  • CheckboxWidget (checkbox): A single checkbox.
  • SingleSelectWidget (single_select): A select box in which the user can choose one item.
  • MultipleSelectWidget (multiple_select): A select box in which the user can choose several items.
  • RadiobuttonsWidget (radiobuttons): A group of related radio buttons.
  • SubmitButtonWidget (submit_button): A submit button.
  • HiddenWidget (hidden): A hidden form field that isn't shown to the user, but will be sent to the server when the form is submitted.

There are also widgets that are derived from these fundamental widgets and specialize them in some fashion:

  • IntWidget (int): A text widget that returns an integer value.
  • FloatWidget (float): A text widget that returns a float value.
  • OptionSelect (option_select): A fancy single-selection widget that uses JavaScript to re-submit the form whenever the value of this selection is changed. (Don't worry if it isn't clear why this is useful; an example will be presented below.)
  • ListWidget (list): Contains a list of identical sub-widgets, with an "Add" button. Useful if you want to let a user specify an arbitrary number of fields.

The purpose of most of these widgets will be obvious. OptionSelect and ListWidget are less obvious and deserve some more explanation.

Using the OptionSelect Widget

The OptionSelect widget is a selection widget that causes the form to be automatically resubmitted when you select a new value. They were invented for situations where the form changes completely based on the selection. When you're creating the form widgets in the __init__() method of your Form subclass, you can look at the OptionSelect's value and create a different set of widgets depending on its value.

As an example, here's a form where the user chooses between air, sea, and land travel.

class TravelForm (form.Form):
    def __init__ (self):
        form.Form.__init__(self)
        self.add_widget('option_select', name='transportation',
                        title = "Transportation",
                        allowed_values = ('air', 'sea', 'train'),
                        descriptions=('Air', 'Boat', 'Train'))
        sel_value = self.get_widget('transportation').get_current_option()

        if sel_value == 'air':
            self.add_widget('string', name='flight_no',
                            title = 'Flight number')
        elif sel_value == 'sea':
            self.add_widget('single_select', name='ship_name',
                            title = 'Ship name',
                            allowed_values = ("Queen Elizabeth II",
                                              "Caronia"))
        elif sel_value == 'train':
            self.add_widget('single_select', name='railway',
                            title = 'Railway',
                            allowed_values = ("Amtrak", "Via"))

        self.add_submit_button('book', "Book travel plans")

    def action (self):
        # Book trip ...

When first displayed, the transportation selection menu will default to 'air', and a text field will be displayed for entering the flight number. On selecting 'sea' or 'train', the form will be sent to the server and a new form displayed that shows a second selection menu of ship names or railways. If 'air' is reselected, the text field will be redisplayed.

Using the List Widget

The List widget allows creating a list of identical subwidgets that can be used to enter an arbitrary number of values. The element_type parameter is the name of the subwidget type that will be used. An example:

class KeywordForm (form.Form):
    def __init__ (self):
        form.Form.__init__(self)
        self.add_widget("list", "keywords",
                        element_name="Keyword",
                        element_type="string")
        self.add_submit_button("search", "Search")

    def action (self, request, submit, values):
        return "Keywords: %s" % values['keywords']

This will initially display a single text field, a submit button labeled "Search" that will submit the form, and a second button labeled "Add Keyword" that will cause the form to be redisplayed with an additional text field. When the action() method is called, the value of the keywords field will be a list of the values returned by the element widgets. In this case, the element widgets are StringWidget instances, so the field will be a list of strings.



Send comments to webmaster at quixote.ca.