Monday, October 24, 2011

How to use Deform in Django

Django comes with some great modules for form handling. You can have forms generated from models, create your own form classes, build form-sets, and plenty of other goodness. However, it doesn't have everything. Some of the widgets aren't the richest, form sequences can be difficult to create, logic can often end up spread out across multiple form objects for more complex forms, and your presentation is spread through templates and form objects.

We recently had a project come up where we needed all the form handling and presentation to be easily encapsulated into one central spot. We required that sequences, and sequences of sequences be supported. Additionally, we needed to develop these forms quickly and have the same rich validation functionality of the Django form module.

We looked high and low for a solution, and found a lot of lows. Then when we had a look at what the folks over at the Pylons project were doing and came across Deform: a kick-ass Python HTML form generation library. It is easy to understand and use, it includes a wide variety of widgets, has great error handling and validation support, and you can centralize all your code in one spot.

Check out this example from the docs(http://deformdemo.repoze.org/). This all the code required to create a form representing a sequence of sequences widgets:

class NameAndTitle(colander.Schema):
    name = colander.SchemaNode(colander.String())
    title = colander.SchemaNode(colander.String())
class NamesAndTitles(colander.SequenceSchema):
    name_and_title = NameAndTitle(title='Name and Title')
class NamesAndTitlesSequences(colander.SequenceSchema):
    names_and_titles = NamesAndTitles(title='Names and Titles')
class Schema(colander.Schema):
    names_and_titles_sequence = NamesAndTitlesSequences(
        title='Sequence of Sequences of Names and Titles')
schema = Schema()
form = deform.Form(schema, buttons=('submit',))
outer = form['names_and_titles_sequence']
outer.widget = deform.widget.SequenceWidget(min_len=1)
outer['names_and_titles'].widget = deform.widget.SequenceWidget(
    min_len=1)

Now, before we continue, a little primer. Deform heavily depends on the Colander library (also part of Pylons). Colander is a system for validating and deserializing data obtained via XML, JSON, an HTML form post or any other equally simple data serialization. Deform uses it to handle validation and schemas. So make sure you have the Colander docs open whenever you have the Deform docs open. This article is about integrating Deform into Django. Please see the Deform documentation for how to use Deform.

Great huh? Looks like we found just we needed, and the components of the Pylons project are loosely coupled, so we can drop it in and start using it right away:

pip install deform

However, there seems to be one little problem: Deform does not work in Django.

The Pylons project handles parsing of POST data a little differently then Django. Deform marks up generated forms with special hidden fields to delineate where sequence data begins, ends, and what order it is in. It also reuses these fields names. The problem is Django parses submitted forms into QueryDicts, and dicts don't preserve the order and the reused hidden field names. For Deform to be able to read the data, it needs to be in the form of a an ordered list of tuples representing the name and value pairs of the submitted form elements. This is a big problem. Luckily, after some searching we were able find simple workaround using standard off the shelf python libraries. We simply use the parse_qsl() function from the urlparse module. The best part is it returns the parsed form data exactly the way Deform needs it:

from urlparse import parse_qsl
controls = parse_qsl(request.raw_post_data, keep_blank_values=True)
appstruct = form.validate(controls)

We also need to make sure we change the form enctype in our HTML. The parse_qsl() function cannot parse multipart/form-data. Sorry folks, this means no file upload.

// This will seek out a form generated by deform and square it up
$('form#deform').attr('enctype', 'application/x-www-form-urlencoded');

That's it folks, you can now use Deform in your Django projects. Here is a full example of all the code required to integrate Deform into Django.

First the Python code:

from urlparse import parse_qsl
import colander
import deform
from deform.exception import ValidationFailure
from django.shortcuts import render_to_response

def test_deform(request):
    class NameAndTitle(colander.Schema):
        name = colander.SchemaNode(colander.String())
        title = colander.SchemaNode(colander.String())
    class NamesAndTitles(colander.SequenceSchema):
        name_and_title = NameAndTitle(title='Name and Title')
    class NamesAndTitlesSequences(colander.SequenceSchema):
        names_and_titles = NamesAndTitles(title='Names and Titles')
    class Schema(colander.Schema):
        names_and_titles_sequence = NamesAndTitlesSequences(
            title='Sequence of Sequences of Names and Titles')
    schema = Schema()
    form = deform.Form(schema, buttons=('submit',))
    outer = form['names_and_titles_sequence']
    outer.widget = deform.widget.SequenceWidget(min_len=1)
    outer['names_and_titles'].widget = deform.widget.SequenceWidget(
        min_len=1)
        
    if request.method == 'GET':        
        rendered = form.render()
    if request.method == 'POST':
        # Here is our hack from above
        controls = parse_qsl(request.raw_post_data, keep_blank_values=True)
        try:
            # If all goes well, deform returns a simple python structure of
            # the data. You use this same structure to populate a form with
            # data from permanent storage
            appstruct = form.validate(controls)
        except ValidationFailure, e:
            # The exception contains a reference to the form object
            rendered = e.render()
        else:
            # See how I am populating it with the appstruct
            rendered = form.render(appstruct)
    # Deform will do you the courtesy of telling you which dependencies it
    # needs. Be sure to copy the files from the deform directory to the media
    # directory of your server
    return render_to_response('deform.html', {
        'form': rendered,
        'deform_dependencies': form.get_widget_resources()
    })

Now the template:

<html>
    <head>
        <!-- Remember those dependencies from above -->
        {% for css in deform_dependencies.css %}
            <link rel="stylesheet" href="/deform/{{ css }}" type="text/css"/>
        {% endfor %}
        {% for js in deform_dependencies.js %}
            <script type="text/javascript" src="//deform/{{ js }}"></script>
        {% endfor %}
    </head>
    <body>
        <!-- Make sure not to escape the HTML output by Deform -->
        {{ form|safe }}

        <script type="text/javascript"><!--
        // Fix Deform's default enctype
        $('form#deform').attr('enctype', 'application/x-www-form-urlencoded');
        --></script>   
    </body>
</html>

All the documentation you need can be found here:

http://docs.pylonsproject.org/projects/deform/dev/
http://deformdemo.repoze.org/
http://docs.pylonsproject.org/projects/colander/dev/

Thanks goes out to the good folks at Pylons for their great work.

No comments:

Post a Comment