FunFormKit Quick Start

Version 0.4.1, written for Webware for Python 0.8

Contents

Creating a Form Definition

In this guide we'll use the example of a registration page. The code for this example is in FunFormKit/Examples/Register.py

The FormDefinition describes your form. You give it the URL of the servlet that handles the results of the form, a list of fields, and occasionally some other information.

The FormDefinition is entirely static. Once you've created a definition, you cannot change its fields or any other attributes. This is necessary because the servlet will be accessed a number of times to generate, process, and potentially regenerate the form. If the form definition changes from one access to the next, the results will be unpredictable. Unpredictable programs are buggy programs. There are ways to make the contents of the form dynamic, however, which is addressed later.

The FormDefinition and the Fields are all objects. They are created like normal objects. For instance, here's a form that asks for a username and password:

from FunFormKit.Form import *
from FunFormKit.Field import *
from FunFormKit import Validator

formDef = FormDefinition(
    "Register",
    [TextField("firstName", maxLength=20, validators=[Validator.NotEmpty()]),
     TextField("middleInitial", description="MI",
               size=1, maxLength=1),
     TextField("lastName", maxLength=50, size=20, validators=[Validator.NotEmpty()]),
     TextField("email", size=20, validators=[Validator.NotEmpty(), Validator.Email()]),
     TextareaField("address", rows=4, cols=40),
     PasswordVerifyField("password", size=20, validators=[Validator.NotEmpty()]),
     SelectField("gender", 
                 selections=[("male", "Male"), ("female", "Female")], 
                 nullInput="Gender"),
     TextField("age", size=2, maxLength=2, validators=[Validator.AsInt()]),
     SubmitButton("submit", methodToInvoke="register"),
     ])

FormDefinition requires two arguments: the URL of the servlet that handles the form, and a list of fields. There are other optional arguments, see The Form Definition in the User Guide.

The Fields are objects as well. The Field module contains all of the fields that come with FunFormKit, and it is fairly safe to do from FunFormKit.Field import * -- almost all the exported simples are field classes that end in Field. See Field Definitions in the User Guide for more, or Special Fields and Normal Fields for descriptions of all the available fields.

There are some default arguments all Fields. They all take a name (e.g. "username", "password", "submit"). They also all take two optional keyword arguments: description and validators.

description is useful in form generation, where it is used by the automatic layout to describe the field (though if you use mixedCase style names, FunFormKit will split the name into words). In a submit button, it is the text on the button.

validators is a list of ValidatorConverters. These are located in the Validator module. Validation and conversion occur at the same time -- this is useful since conversion usually implies some sort of validation. For instance, if you want an integer (using Validator.AsInt), the converter has to signal an error if the user entered ten instead of 10.

Create Your Servlet

The next step is creating a servlet to handle the form. We'll be using the same form definition as given above.

The basic template for a servlet is like this:

# [imports and the form definition is assigned to the
#  formDef variable]

# The layout doesn't need to be global, but we'll reuse it
# later so we'll put it here.  It's explained after the
# renderForm method.
layout = [("firstName*", "middleInitial", "lastName*"),
          "email*",
          "address",
          ":password",
          ("gender", "age*"),
          "submit",
         ]

class Register(ExamplePage, FormServlet):
    def __init__(self):
        ExamplePage.__init__(self)
        FormServlet.__init__(self, formDef)

In __init__, we invoke FormServlet.__init__ with the form definition that this servlet handles, or a list of such form definitions. As always, we must also call __init__ for each of the base classes.

# continued...
    def writeContent(self):
        # .processForm() is inherited from FormServlet
        formProcessed, data = self.processForm()
        if formProcessed:
            self.renderForm()
        else:
            self.write('Thanks for signing up!')

In writeContent we process the form and potentially render the form. .processForm() handles any form data that was submitted, potentially collecting errors, or if no form has yet been submitted it simply does nothing. It returns a tuple, (formProcessed, data). formProcessed is a boolean, and it tells you if a form was successfully submitted. I'll explain data later. If no form was submitted, we should render the form -- here we put the rendering in its own method.

# continued...
    def renderForm(self):
        renderableForm = self.renderableForm()
        self.write(renderableForm.htFormLayout(layout, spacing=2))

In renderForm we attain a RenderableForm, via the .renderableForm() method. The RenderableForm is essentially a wrapper around the FormDefinition, with certain ephemeral values attached -- error messages, previously entered values, default values, and per-field options. Some of these values -- the default values and per-field options -- can optionally be passed to .renderableForm() to control the form.

We use the layout we defined to pass to the RenderableForm.htFormLayout method. The layout is a list, where each item in the list is a row in the form. If you want multiple fields on one row, you use a sublist with the names of those fields. Another renderer is RenderableForm.htFormTable, which needs no arguments, and displays the form in a simpler table, one field per row.

We using spacing=2 to space the fields on the line. You might notice that the password field was indicated with ":password" -- since the password field was compound, it will already include it's only labels ("Password" and "Verify"), and the : suppresses the extra label that htFormLayout would put on it. Lastly, we use a * at the end of a name to add a red star, which implies to the user that the field is required.

# continued...
    def register(self, fields):
        # ... do something to register user in database here ...
        # fields is a dictionary, filled from the form, field
        # names as keys.
        self.session().setValue('userInfo', fields)

The register method is called because we passed the argument methodToInvoke="register" to the SubmitButton. You can put multiple submit buttons in your form, each with a different method. Alternately, you can give no methodToInvoke, and the fields will be passed back in data from .processForm() All your processing would happen in writeContent() in that case.

fields is simple a dictionary, with field names for the keys and the user input for the values.

More Complex Forms

Actually, in this section we will use the same form as before, except that we will allow a user to edit their record instead of just entering new information. (This example is located in FunFormKit/Examples/EditUser.py)

To do this we will use the same form definition, changing only the servlet code:

from FunFormKit.Form import *
from WebKit.Examples.ExamplePage import ExamplePage
from Register import formDef, layout

class EditUser(ExamplePage, FormServlet):

    def __init__(self):
        ExamplePage.__init__(self)
        FormServlet.__init__(self, formDef)

    def writeContent(self):
        formProcessed, data = self.processForm()
        if formProcessed:
            self.write('You user information has been saved!')
        else:
            fields = self.session().value('userInfo', None)
            if not fields:
                self.write('You must Register first!')
            else:
                self.renderForm(fields)

    def renderForm(self, fields):
        # Normally the form definition will point to Register.
        # We set this special form option to change that:
        optionSet = {'form': {'handlerServletURL': 'EditUser'}}
        rf = self.renderableForm(defaults=fields, optionSet=optionSet)
        self.write(rf.htFormLayout(layout))

    def register(self, fields):
        self.session().setValue('userInfo', fields)

You'll notice we are able to reuse most of the form we used in Register, and that this servlet looks very similar. The biggest change is in renderForm, where we use options to change the form action, and where we set up default values. The defaults argument to renderableForm sets the defaults for the form -- it is simply a dictionary, with field names for the keys, and the default value for the value.

Another common thing to do when setting up your renderable form is to make a dynamic select box. We don't have a dynamic select box in our example, but a select box looks like this:

formDef = FormDefinition(...
    [SelectField("section")]...)

We didn't indicate the selections, so we'll have to do that when we render the form:

def renderForm(self):
    optionSet = {'section': {'selections': [('a', 'Section A'),
                                            ('b', 'Section B')]}}
    rf = self.renderableField(optionSet=optionSet)
    self.write(rf.htFormTable())

Hopefully that is self-explanatory.

Creating Your Own Validator/Converters

While a number of validators are included already, you may very well want to make your own. I will go further -- you should create your own, as it is important all data validation occurs as a validator (otherwise your users won't get a chance to correct their mistakes).

Consider the simple case where you want to validate that a username exists -- perhaps for a form that sends inter-user messages, or for a login page (this code can be seen in FunFormKit/Examples/Login.py)

We'll imagine you have a function usernameExists(username) which returns True or False. To create the validator:

from FunFormKit.Validator import ValidatorConverter

class UsernameExists(ValidatorConverter):
    def validate(self, value):
        if usernameExists(value):
            return None
        else:
            return "That username does not exist"

As you can see, the validator returns None when the value is valid and an error message when it isn't. This is a little backwards -- None is a false value after all, yet it means that the validation succeded. But success is more boring than failure.

For intruction purposes, let's look up the user ID during this process. It's not nearly as important to do conversion as validation -- it's perfectly fine to do the conversion later -- but often the two go together. In this case, imagine a function usernameToUserID(username). We'll say that usernameToUserID returns None when the username does not exist, and we'll avoid using usernameExists altogether:

from FunFormKit.Validator import ValidatorConverter, InvalidField

class UsernameToUserID(ValidatorConverter):
    def convert(self, username):
        userID = usernameToUserID(username)
        if userID is None:
            raise InvalidField, "That username does not exist"
        else:
            return userID

As you see, when there's an error when converting the value we raise an exception (Validator.InvalidField) with an argument of the error message. We don't have to put both steps together -- we can chain the validator (UsernameExists) and the converter (UsernameToUserID) by using validators=[UsernameExists(), UsernameToUserID()] in our field definition. But sometimes it's more efficient to put steps together.

Now, we want to know if the username matches the password. Imagine we have a function correctPassword(userID, password):

from FunFormKit import FormValidator

class ValidatePassword(FormValidator):
    def validate(self, fieldDict):
        if correctPassword(fieldDict["username"], fieldDict["password"]):
            return None
        else:
            return {"password": "Password is incorrect"}

The fieldDict is the dictionary of processed values for each field, so you'll get the user ID not the username (assuming we've used the UsernameToUserID validator/convert). We return None on success, or a string or dictionary on failure. If it's a string, the error shows up at the top of the form. If it's a dictionary, it gives errors for individual fields (the password field in this case).

Now we need to create a form definition that includes the code we just created:

from FunFormKit.Form import FormDefinition
from FunFormKit import Field

formDef = FormDefinition(
    'Login',
    [Field.TextField('username', maxLength=10,
                     validators=[UsernameToUserID()]),
     Field.PasswordField('password', size=10, maxLength=20),
     Field.SubmitButton('submit', description='Login', 
                        methodToInvoke='login'),
    ],
    formValidators=[ValidatePassword()])

For More Information

The User Guide presents a lot more information on all the fields, validators, and form validators that are included with FunFormKit. It also contains more detailed descriptions of usage and all available options.