Working with forms

While form handling is conceptually simple - user submits a form, which is processed on the server - there are potentially many nuances that make things more or less complicated. Instead of accounting for every possible workflow involving forms, bottle-streamline provides simple classes that provide the base for workflow-specific subclasses.

Form CBRH basics

The streamline.forms.SimpleFormRoute is, as its name suggest, a simple variant of form-handling CBRHs but it has all the fundamental pieces that you need to know in order to work with other variants. Let’s take a look at an example of how to works:

import bottle
from streamline import FormRoute

class Simple(FormRoute):
    def show_form(self):
        return 'Imagine this is a form'

    def form_valid(self):
        return 'OK'

    def form_invalid(self):
        self.response.status = 400
        return 'WRONG'

Simple.route('/simple')

The show_form() method is normally used to render the form or otherwise make the form controls available to the user. In this case, we’re simply returning a dummy string for simplicity. It’s more or less an alias for get() and there is nothing wrong with overloading the get() method instead.

The form_valid() and form_invalid() methods are called depending on whether the form is valid or not. Simple class has no validation code, so it will always validate.

Let’s add some validation code:

try:
    unicode = unicode
except NameError:
    unicode = str

required = lambda v: v and v.strip()
numeric = lambda v: unicode(v).strip().isnumeric() if v else True

The first lambda verfies that any data is entered and that the data is not just a series of whitespace characters. Second lambda makes the value optional, but, if specified, required it to be an integer. To make the two lambdas do anything, we first need to add them to the validator dictionary:

Simple.form_factory.validators['string'] = required
Simple.form_factory.validators['number'] = numeric

Now the Simple class is fully equipped to perform validation. Submitting data to an app running

Note

The form CBRHs have a form_factory property, which is a function or a class that returns objects that implement the streamline.forms.FormAdaptor API. The class itself can be used as a stand-alone rudimentary support code for data validation.

Let’s test the form using curl:

$ curl --data "string=test" localhost:8080/simple
OK
$ curl --data "number=12" localhost:8080/simple
WRONG

It appears to be working.

Within the form_valid() and form_invalid() methods, the form object (as returned by the factory function) is available as the form property. Let’s use this to show a bit more information on sucessful submission:

def form_valid(self):
    return 'OK: {} {}'.format(self.form.data.get('string', ''),
                              self.form.data.get('number', ''))

Let’s test this:

$ curl --data "string=test" localhost:8080/simple
OK: test
$ curl --data "string=test&number=2" localhost:8080/simple
OK: test 2

Using form handling with template rendering

Form handling with template rendering is a cross between the FormRoute and the two template classes, TemplateRoute and XHRPartialRoute. The two classes are TemplateFormRoute and XHRPartialFormRoute.

We won’t go into the details of how they work because they are simply a mix of fetures provided by the template CBRHs and the features outlined in the previous section.

Customizing form validation

Form validation can be customized in a few different ways. Most straightforward way is to use a different (and proper) form or validation library and write an adaptor for it. Another possibility is to override the validate_form() method.

Here is an example of a custom form adaptor:

from streamline.forms import FormAdaptor


class MyAdaptor(FormAdaptor):
    def __init__(self, data):
        self.messages = {}
        super(MyAdaptor, self).__init__(data)

    def is_valid(self):
        try:
            my_cool_validation_library(self.data)
        except ValidationError as e:
            for error in e.errors:
                self.messages[error.field_name] = error.message
        return not self.messages:


class Simple(FormRoute):
    form_factory = MyAdaptor
    ...

The custom adaptor saves error messages in the messages property on the adaptor object so that it can be accessed in the form_invalid() method later.

Now let’s take a look at the second option of overloading the validation function:

class Simple(FormRoute):
    def __init__(self, *args, **kwargs):
        super(Simple, self).__init__(*args, **kwargs)
        self.errors = {}

    def validate_form(self, _):
        # We will completely ignore the form that is passed in, and instead
        # use ``request.forms``
        data = self.request.forms
        try:
            my_cool_validation_library(data)
        except ValidationError as e:
            for error in e.errors:
                self.errors[error.field_name] = error.message
        return not self.errors

It works similar to the first example, except we have chosen to ignore the form object that is passed in as an argument to validate_form(). The form object passed to this method is a form object returned by the factory function so we could have actually used the form.data instead of request.forms (they are identical).

Performing a redirect on sucessful submission

The most common way of performing a redirect is to use the bottle’s own redirect() function. This function is made available as a method on the CBRH instances. Here is an example:

class Simple(FormRoute):
    ...
    def form_valid(self):
        return self.redirect('/see-other')