Albatross Forms Guide

Albatross Forms provides support for developing Albatross applications which gather data from the user, validate it, and then return the user’s data to the app. Much of this work is mechanical and it is tedious and error prone writing the same code on different pages in the application.

Using Forms lets the developer organise the presentation of related data on a web page programmatically. The Forms support handles the basic layout, type conversions and validation as the user interacts with the form. By using centrally defined data types, presentation, validation and error reporting can be done consistently and modified easily. It has been our experience that development is much faster and lots of code can be removed from the Albatross templates where it’s hard to read, difficult to test and gets mucked up by web designers using WYSIWYG design tools.

Albatross Forms is designed to use CSS to change the layout of the forms when they are displayed rather than encoding the HTML into the form tags. This consolidates the web site’s presentation and makes it easier to change the presentation globally.

Concepts

There are three main concepts that sit behind the Albatross Forms implementation:

  • Field

    A data input field. It can format its output and validate its input. It contains a copy of the value so that the user can edit it without needing to maintain a separate copy in the application.

  • Fieldset

    Groups together a list of Fields and renders them in a table. It is conceptually related the HTML fieldset tag which groups related input fields together. Fieldsets are intended to only hold data fields. If you try to insert a Button into a Fieldset it’s not likely to work, in part because Fieldset expects that each Field member will respond to certain methods, and in part because notionally is that buttons apply actions to all the fields in the fieldset.

  • Form

    Manages the all of the fields in the form. Most interactions in the application are with Form instances: they coordinate loading values from model objects (typically attributes of classes) into the Fields, organise rendering and updating the values from the browser, validation, and storing the values back into the model objects.

In practice, the developer will assemble a Form instance containing one or more Field instances and place this Form into ctx.locals (optionally fetching field values from an associated data or Model class via the load() method).

The developer then refers to this Form via a new <alx-form> tag in the page template (note that the <alx-form> must still be contained within an <al-form> tag in the page template). When the template is executed, the Form will be rendered to an HTML table containing appropriate inputs (including any values associated with the Fields).

When the user subsequently submits their responses, the developer will call the Form instance merge() method from the page_process() method and the user values will be merged back to the associated data storage class (or Model).

Getting started

You need to have a version of Albatross which has the Albatross Forms support included (or have installed it by hand yourself). You can quickly test whether it is present by running:

>>> from albatross.ext import form

If it’s missing, you’ll see an import error.

Registering the <alx-form> tag

In each Albatross application, there is a point at which an App subclass is instantiated (usually in app.py or app.cgi or wherever the main entry point of your application is). This instance needs to be told about the new <alx-form> tag. This is done with code that looks something like this:

import albatross
from albatross.ext import form
...
if __name__ == '__main__':
    app = albatross.SimpleApp()
    app.register_tagclasses(*form.tag_classes)
    app.run(Request())

Alternatively, if you subclass one of the Albatross application classes, you can register the new tags in your subclass’s constructor method (__init__):

import albatross
from albatross.ext import form
...
class Application(albatross.SimpleApp):

    def __init__(self, *args):
        albatross.SimpleApp.__init__(self, *args)
        self.register_tagclasses(*form.tag_classes)

A simple example

Here is a simple example of how we could use Albatross Forms to collect a username and password from the user.

We need to define a model class to hold the data:

import pwd, crypt

class User:

    def __init__(self, username, password):
        self.username = username
        self.password = password

    def is_password_valid(self):
        try:
            pw = pwd.getpwnam(self.username)
        except KeyError:
            return False
        return (crypt.crypt(self.password, pw.pw_passwd) == pw.pw_passwd)

Next, we need to define a form to display the fields:

from albatross.ext.form import *

class LoginForm(FieldsetForm):

    def __init__(self, user):
        fields = (
            TextField('Username', 'username'),
            PasswordField('Password', 'password'),
        )
        fieldsets = (Fieldset(fields), )
        buttons = Buttons((
            Button('Login', 'login'),
        ))
        FieldsetForm.__init__(self, 'User login', fieldsets, buttons=buttons)
        self.load(user)

We need to create an instance of the Login model and maintain that so that any captured data is retained. In our login.py, we use:

def page_enter(ctx):
    if not ctx.has_value('user'):
        ctx.locals.user = User('', '')
        ctx.add_session_vars('user')
        ctx.locals.login_form = LoginForm(ctx.locals.user)
        ctx.add_session_vars('login_form')
    ctx.locals.login_error = ''

In login.html, to display the form to the user we use:

<al-form method="post">
    <alx-form name="login_form" errors />
    <al-expr expr="login_error" />
</al-form>

When the user presses the “Login” button, it will come back to our page_process method in login.py. We check if the username and password are correct and punt them into the application proper (via the “search” page) or tell them they’ve got it wrong:

def page_process(ctx):
    if ctx.req_equals('login'):
        # nothing to validate
        ctx.locals.login_form.merge(ctx.locals.user)
        if ctx.locals.user.is_password_valid():
            ctx.redirect('search')
        else:
            ctx.locals.login_error = 'Login incorrect'

Flow of Control

The flow of control through Albatross Forms is tied in with Albatross’s flow of control.

A common mistake is to reinitialise the form from the model part way through the user’s interaction with the page (ie, before they’ve saved it). It winds up losing any changes that the user has made on the form. The lesson here is that the form should only be loaded from the model once when the user starts interacting with it; don’t reload it on each page refresh.

  1. Constructor

    Create the form itself.

  2. Load values from model

    This is often done in the form subclass constructor method (__init__).

  3. Display the form

    Render the form to the web page, using <alx-form name=”model_form” errors> in your Albatross template for the page.

  4. Validate

    Check that the data that the user entered is correct. The call to validate will raise a FormValidationError exception.

  5. Merge

    Update the data class (Model) with the data fields collected from the form.

Field types

Albatross Forms defines a number of standard fields. You can also add your own, subclassing the standard fields to add validation or type-casting. The standard fields are:

TextField

A normal text input. Corresponds to the <input type="text"> tag.

PasswordField

Same as a text field but it doesn’t display the characters as the user enters them. Corresponds to the <input type="password"> tag.

StaticField

A TextField with static content.

TextArea

Corresponds to the <textarea> tag.

IntegerField

An integral value, get_value() will return an int type, non-integer values will result in a FieldValidationError on validate().

FloatField

A floating point value, get_value() will return a float type, non-floating point values will result in a FieldValidationError on validate().

Checkbox

Renders as <input type=”checkbox”>, get_value() will return a bool type.

SelectField

Returns one of the values listed in the (value, display_value) list passed to the constructor. If the value can be converted to an int, it will be; otherwise it will be returned as a str.

RadioField

Returns one of the values listed in the (value, display_value) list passed to the constructor. If the value can be converted to an int, it will be; otherwise it will be returned as a str.

FileField (not implemented yet)

Internal storage within a field

The interaction of the field with the browser is complicated because the browser requires and returns string values, which results in the loss of type information for the objects that the fields wrap.

To manage this, the field class notes when the field is rendered to the browser and converts the value to a string using the object’s get_display_value() method. When the values are posted back to the form by the browser, they are returned as strings. If you need to access the value stored in a field, use the get_value() method which tracks the type of the stored representation and does a conversion (using get_merge_value()) when appropriate.

A more complex example

Here is a rather more complex example that uses a number of different input types.

Here’s the model:

class User:
    def __init__(self):
        self.name = ''
        self.an_int = 0
        self.a_float = 0.0
        self.country = 0
        self.password = ''
        self.active = False

The form that describes how we want it laid out:

# Slightly abridged list of all the countries in the world.
country_names = (
    'Australia',     'Belgium',      'Cuba',
    'Greenland',     'Madagascar',   'Netherlands',
    'Switzerland',   'Uzbekistan',   'Zimbabwe'
)
country_menu = [e for e in enumerate(country_names)]
fields = (
    TextField('Name', 'name', required=True),
    IntegerField('Integer', 'an_int'),
    FloatField('Float', 'a_float'),
    SelectField('Country', country_menu, 'country'),
    PasswordField('Password', 'password',
                  required=True),
    Checkbox('Active', 'active'),
)
buttons = Buttons((
    Button('Save', 'save'),
    Button('Cancel', 'cancel'),
))
fieldsets = (Fieldset(fields), )
ctx.locals.test_form = FieldsetForm('User details',
                                    fieldsets,
                                    buttons=buttons)

Render the form on the page:

<al-form method="POST">
   <div class="alxform">
     <alx-form name="test_form" errors />
   </div>
 </al-form>

To render the form as static report:

<div class="alxform">
  <alx-form name="test_form" static />
</div>

In the forms.py file, the code looks like:

def page_enter(ctx):
    if not ctx.has_value('test_form'):
        ctx.locals.description = User()
        ctx.add_session_vars('description')
        ctx.locals.test_form = test_form # see above

def page_display(ctx):
    ctx.run_template('forms.html')

def page_process(ctx):
    if ctx.req_equals('save'):
        try:
            ctx.locals.test_form.validate()
        except FormValidationError, e:
            ctx.locals.test_form.set_disabled(False)
            return
        ctx.locals.test_form.set_disabled(True)
        ctx.locals.test_form.merge(ctx.locals.description)
    elif ctx.req_equals('reset'):
        ctx.locals.test_form.clear()
        ctx.locals.test_form.set_disabled(False)
    elif ctx.req_equals('cancel'):
        if ctx.locals.test_form.disabled:
            ctx.locals.test_form.set_disabled(False)
        else:
            ctx.locals.test_form.clear()
            ctx.redirect('main')

Customising Fields

Here is an example of how we created a MoneyField that knows how to validate a currency value. Both parser and formatter are modules that we wrote to convert between formats. Our modules deal in dollars and cents but that’s hidden from the application code.

Note that the parser needs to be able to parse the output of the formatter: the field will be initialised with the formatter’s output when it is rendered. It is reasonable to expect the parser to accept “$5.50” if that is the format that the application is presenting to the user.

import parser, formatter

class MoneyField(FloatField):

    def validate(self, form, s):
        s = s.strip()
        if not self.required and not s:
            return
        try:
            parser.money(s)
        except ValueError, e:
            raise FieldValidationError('Invalid value "%s" for money' % s)

    def get_merge_value(self, s):
        s = s.strip()
        if not self.required and not s:
            return
        return parser.money(s)

    def get_display_value(self, ctx, form):
        return formatter.money(self.get_value())

Attaching buttons to a form

It’s common to have buttons at the bottom of a form even if they just say “Save” and “Cancel”. This is supported in Albatross Forms by adding an optional keyword arg when creating the Form object, for example:

from albatross.ext.form import *

class LoginForm(FieldsetForm):

    def __init__(self, user):
        fields = (
            TextField('Username', 'username'),
            PasswordField('Password', 'password'),
        )
        fieldsets = (Fieldset(fields), )
        buttons = Buttons((
            Button('Login', 'login'),
        ))
        FieldsetForm.__init__(self, 'User login', fieldsets, buttons=buttons)

        self.load(user)

The Buttons class takes a list of Button instances in its constructor and displays buttons in the bottom right hand corner of the form display.

You check for the buttons being clicked by the user in the usual Albatross way in page_process:

def page_process(ctx):
    if ctx.req_equals('save'):
        ...

Table support

Rendering tables using Albatross Forms is relatively straightforward, using them for input is no harder.

Table support revolves around two classes: IteratorTable and IteratorTableRow.

IteratorTable acts as a field in a form in which the table is rendered. The first argument to the IteratorTable constructor is the name of the attribute in the class in which the the table is stored. This is necessary so that Albatross can navigate through the form’s fields to update the values from the user’s browser. It’s a little arcane but it isn’t too bad.

IteratorTableRow should be subclassed within the application to render each row in turn.

The IteratorTable class steps through the list of objects that it’s passed and calls the IteratorTableRow subclass that’s specified with each object in turn. Each of these is responsible for rendering a single row.

When the IteratorTable is rendered, it will display the header columns (if specified) and then ask each row to render itself.

Here’s an example which should render a list of name, address and phone numbers in a table. First we define the model object:

class Entry:

    def __init__(self, name, address, phone):
        self.name = name
        self.address = address
        self.phone = phone

Now we’ll define the components of the form to render a list of Entry instances:

class EntryTableRow(IteratorTableRow):

    def __init__(self, entry):
        cols = (
            Col((TextField('Name', 'name'), )),
            Col((TextField('Address', 'address'), )),
            Col((TextField('Phone', 'phone'), )),
        )
        IteratorTableRow.__init__(self, cols)

        self.load(entry)


class EntryTableForm(Form):

    def __init__(self, entries):
        headers = Row((
            HeaderCol((Label('Name'), )),
            HeaderCol((Label('Address'), )),
            HeaderCol((Label('Phone'), )),
        ))
        self.table = IteratorTable('table', headers, EntryTableRow, entries,
                                html_attrs={'width': '100%'})
        Form.__init__(self, 'Address book', (self.table, ))

To create the form:

def page_enter(ctx):
    entries = [
        Entry('Ben Golding',
              'Object Craft, 123/100 Elizabeth St, Melbourne Vic 3000',
              '+61 3 9654-9099'),
        Entry('Dave Cole',
              'Object Craft, 123/100 Elizabeth St, Melbourne Vic 3000',
              '+61 3 9654-9099'),
    ]
    if not ctx.has_value('entry_table_form'):
        ctx.locals.entry_table_form = EntryTableForm(entries)
        ctx.add_session_vars('entry_table_form')

Rendering the list just requires:

<al-form method="post">
    <div class="alxform">
        <alx-form name="entry_table_form" static />
    </div>
</al-form>

To support editing the fields, you would change how it renders using:

<al-form method="post">
    <div class="alxform">
        <alx-form name="entry_table_form" errors />
    </div>
</al-form>

When the user has made changes, your page_process method can pick up the changes using:

def page_process(ctx):
    if ctx.req_equals('save'):
        ctx.locals.entry_table_form.merge(ctx.locals.entries)
        save(ctx.locals.entries)

Paginating a table

To add pagination to a table, you need to specify a pagesize and the rest of the work is done for you.

<al-form method="post">
    <div class="alxform">
        <alx-form name="entry_table_form" errors pagesize="15" />
    </div>
</al-form>

Currently, the simple paginator only emits “Prev” and “Next” links at the bottom of the table. It’s straightforward to change the style of those links by subclassing the IteratorTable class: pass in an instance of your own custom subclass of PageSelectionDisplayBase as the page_selection_display argument of IteratorTable‘s constructor to render the page links differently.

The pagesize argument can also be specified when creating an IteratorTable instance. The argument specified overrides any that are used in the <alx-form> tag.

Adding rows to a table

The developer is responsible for keeping the form’s idea of the number of rows in the table in sync with the rows in the model. :

def page_process(ctx):
    ...
    if ctx.req_equals('add_entry'):
        entry = Entry('', '', '')
        ctx.locals.entries.append(entry)
        ctx.locals.entry_table_form.table.append(entry)

Note that the IteratorTable.append() method will call the row class with the model data that’s specified in the constructor.

Adding heterogenous rows to a table

If you were displaying a series of rows each of which was a product, at the end of the table it would be great to display a total entry for all of the included lines. In this example, we use append_row() to append a pre-formatted row (ie, an IteratorTableRow subclass instance) to the table.

Note that while this works when rendering a form, I don’t think it will work if the form is used for input. :

class ProductTableRow(IteratorTableRow):

    def __init__(self, product):
        cols = (
            Col((TextField('Code', 'code'), )),
            Col((TextField('Name', 'name'), )),
            Col((FloatField('Cost', 'cost'), ),
                 html_attrs={'class': 'number-right'}),
        )
        IteratorTableRow.__init__(self, cols)
        self.load(product)


class ProductTotalRow(IteratorTableRow):

    def __init__(self, products):
        cols = (
            Col((Label(''), )),
            Col((Label('Total'), )),
            Col((FloatField('Total', value=products.total_amount(), static=True), ),
                 html_attrs={'class': 'number-right'}),
        )
        IteratorTableRow.__init__(self, cols)


class ProductTableForm(FieldsetForm):

    def __init__(self, products):
        headers = Row((
            HeaderCol((Label('Product code'), )),
            HeaderCol((Label('Product name'), )),
            HeaderCol((Label('Cost'), ),
                      html_attrs={'class': 'number-right'}),
        ))
        self.table = IteratorTable('table', headers, ProductTableRow, products,
                                html_attrs={'width': '100%'})
        self.table.append_row(ProductTotalRow(products))
        buttons = Buttons((
            Button('save', 'Save'),
            Button('cancel', 'Cancel'),
        ))
        FieldsetForm.__init__(self, 'Product table', (self.table, ),
                              buttons=buttons)

Deleting rows from a table

When deleting rows from a table, I normally put a check box next to each of the rows and include a “delete selected” button so that the user can delete multiple rows at once.

In the table row constructor, I poke an is_selected value into the model object as a placeholder for the selected check box. I feel like this is impolite but it works very effectively. :

class EntryTableRow(IteratorTableRow):

    def __init__(self, entry):
        entry.is_selected = False         # placeholder
        cols = (
            Col((Checkbox('Selected', 'is_selected'), )),
            Col((TextField('Name', 'name'), )),
            Col((TextField('Address', 'address'), )),
            Col((TextField('Phone', 'phone'), )),
        )
        IteratorTableRow.__init__(self, cols)

        self.load(entry)

When processing the request, I step through each list element in the model list in in sync with each child in the table form and delete both of them when the checkbox is selected.

def page_process(ctx):
    if ctx.req_equals('delete_selected'):
        for entry, entry_form_child in zip(ctx.locals.entries,
                                           ctx.locals.entry_table_form.table.children):
            is_selected_field = entry_form_child.get_field('Selected')
            if is_selected_field.get_value():
                ctx.locals.entries.remove(entry)
                ctx.locals.entry_table_form.table.remove(entry_form_child)

Querying fields before merge

Sometimes it can be really nice (read: improve the user experience) to be able to query some fields without merging the value from the form back into an object. An example is when a user changes a input select field in a form which has an attached value. In an app we have, a user has a select list of product numbers. We make the app update the product’s name on the page when the product changes.

Note that we are treading on thin ice here: if the user has not entered a valid value for the field, when get_value() tries to convert it to the appropriate type, it will raise an exception that will need to be dealt with. It is (generally) safe to reference values from a TextField or a SelectField though. :

class ProductFieldsetForm(FieldsetForm):

    products_menu = product_factory.all_products_menu()

    def __init__(self, product):
        self.product_id = product.product_id
        elements = (
            Fieldset((
                SelectField('Product code', self.products_menu, 'product_id',
                            html_attrs={'onchange': 'javascript:product_form.submit()'}),
                StaticField('Product', product.name),
            )),
        )
        FieldsetForm.__init__(self, 'Product selection', elements)
        self.load(product)

    def update(self):
        product_code_field = self.get_field('Product code')
        if self.product_id != product_code_field.get_value():
            self.product_id = product_code_field.get_value()
            product = product_factory.product_with_product_id(self.product_id)
            product_field = self.get_field('Product')
            product_field.set_value(product.name)

The interesting part here is where we query the form for a specific field using its get_field() method.

We use the little bit of Javascript to make a change to the select list force an update to the page. That means having to name the form when we render the page:

<al-form method="post" name="product_form">
    <alx-form name="product_form_form" />
</al-form>

In the app itself, we use:

def page_enter(ctx):
    if not ctx.has_value('product_form'):
        ctx.locals.product_form = ProductForm(ctx.locals.product)
        ctx.add_session_vars('product_form')

def page_process(ctx):
    ctx.locals.product_form.update()      # update product name if product changed
    if ctx.req_equals('save'):
        ...