excess.org

Ian Ward

Consulting
Boxkite Inc.
Software
CKAN contributor/tech lead
PyRF primary contributor
Urwid author
Speedometer author

Presentations
Contributing to Open Source
IASA E-Summit, 2014-05-16
Urwid Applications
2012-11-14
Urwid Intro
2012-01-22
Unfortunate Python
2011-12-19
Django 1.1
2009-05-16

Writing
Moving to Python 3
2011-02-17
Article Tags

Home

Ian Ward's email:
first name at this domain

wardi on OFTC, freenode and github

Locations of visitors to this page

Nontrivial Django Forms Talk Text

Non-trivial Django Forms slide
Posted on 2010-04-16.

This is the text from the Nontrivial Django Forms talk I gave last Tuesday at the April meeting of the Ottawa Python Authors Group.

This talk starts where the Django forms documentation leaves off.


Form Basics

class ToDoForm(forms.Form):
    task = forms.CharField(label=_("Task"))
    done = forms.BooleanField(label=_("Done"))

Django's forms resemble its models. Models take care of translating between database types and Python types while forms (and their widgets) take care of translating between HTML form elements and Python types. Both employ metaclass tricks to make declaring an ordered set of fields easy and clear to read. Using Dango forms and models however, is quite different.


Single-field Validation

class ToDoForm(forms.Form):
    task = forms.CharField(label=_("Task"))
    done = forms.BooleanField(label=_("Done"))

    def clean_task(self):
        if self.cleaned_data['task'] == u"nothing":
            raise forms.ValidationError(_("That's just silly"))

Forms are all about validation, and they give you a number of ways to do it. You get some validation in the field objects as they convert incoming data to simple python types. You can extend the field classes perform extra checks. You can also have a single clean() method in your form that does validation on all the cleaned_data provided by the fields.

I find it convenient to use per-field validation with clean_fieldname() methods. They are automatically called after each field converts the incoming form data to a python object in cleaned_data, and when a ValidationError is raised in one of these methods it is automatically added to the list of errors for that field in the form.


Inter-field validation

class ToDoForm(forms.Form):
    task = forms.CharField(label=_("Task:"))
    done = forms.BooleanField(label=_("Done:"))

    def clean_done(self):
        # order of fields above matters!
        task = self.cleaned_data['task']
        done = self.cleaned_data['done']
        if task == u"prove 1 = 0" and done:
            raise forms.ValidationError(_("Please re-check your work"))

Sometimes you need to check the result of another field to tell if the one you're checking is valid. That can be done with clean_fieldname(), but be careful: Forms validate each field in the order specified in the class definition, so the above code would not work if done was declared before task.


Save the User

class FormRequestUser(forms.Form):
    def __init__(self, request, *args, **varargs):
        self.user = request.user
        super(FormRequestUser, self).__init__(*args, **varargs)

In the real world form data doesn't live in isolation. Validation in my own code often depends on the privileges of the user submitting the form. This is a base class for forms that store the user from the request object for later use in validation. Note the extra request parameter in the constructor. This must be passed in the view that creates the form.


ModelForm Save the User

class ModelFormRequestUser(forms.ModelForm):
    def __init__(self, request, *args, **varargs):
        self.user = request.user
        super(ModelFormRequestUser, self).__init__(*args, **varargs)

    def save(self, commit=True):
        obj = super(ModelFormRequestUser, self).save(commit=False)
        obj.user = self.user
        if commit:
            obj.save()
            self.save_m2m() # careful with ModelForms + commit=False
        return obj

I also have forms based on models that link back to the user. This base class is similar to the one above, but it will also automatically set the user foreign key in the model being saved to the user logged in.


Custom Options for User

class PlaceForm(FormRequestUser):
    place = forms.ChoiceField(label=_("Select your workplace"),
        choices=BLANK_CHOICE_DASH, required=True)

    def __init__(self, request, *args, **varargs):
        super(PlaceForm, self).__init__(request, *args, **varargs)
        if self.user.employer:
            self.set_employer(self.user.employer)

    def set_employer(self, employer):
        place_choices = BLANK_CHOICE_DASH + [
            (place.id, place.name) for place in employer.workplaces]
        self.fields['worksite'].choices = place_choices

Once we know the user we can customize forms for them. This form populates a ChoiceField's choices based on the the user's employer workplaces. The ChoiceField built-in validation will make sure the user can only enter one of the values we specify.

We could also do anything else we want to the form: adding fields, deleting fields, changing labels etc.


Interdependent Forms

if request.POST:
    user_form = NewUserForm(request.POST, prefix='user')
    place_form = PlaceForm(request, request.POST, prefix='place')
    survey_form = SurveyForm(request, request.POST, prefix='survey')
    if user_form.is_valid():
        place_form.set_employer(user_form.cleaned_data['employer'])
        if place_form.is_valid():
            user = user_form.save()
            place_form.user = user
            survey_form.user = user
            if survey_form.is_valid():
                place_form.save()
                survey_form.save()
            else:
                user.delete() # remove record created above

Forms that know the user are great, but what about if that user doesn't exist yet? This is part of a view for signing up new users, and having them fill out their workplace and answer a survey at the same time.

PlaceForm and SurveyForm are subclasses of ModelFormRequestUser, so we pass the request object as their first parameter. In this case the user being stored is just the AnonymousUser instance, because the user account has not yet been created.

Once we clean the data in user_form by calling is_valid() we can assign the correct employer for our new user to place_form (assuming our NewUserForm includes the employer). If the workplace selection is valid we can create the user and assign the now-correct user to place_form and survey_form. Finally survey_form may be validated (assuming that depends on having a correct user) and we can save everything.

An alternative to managing this complexity in your view is to make a single form that includes everything that must be entered. In this case I decided to keep them separate because the forms are used in multiple places (including where the user is logged in) and I did not want to maintain many copies of the same forms.


Formsets in Brief

ToDoFormset = formset_factory(ToDoForm, extra=3)
if request.POST:
    todo_formset = ToDoFormset(request.POST)
    if todo_formset.is_valid():
        # .. compare against what initial data would be
        for todo_form in todo_formset.forms:
            # ... commit changes
else:
    # ... build initial_data
    todo_formset = ToDoFormset(initial=initial_data)

Formsets are part of what makes Django's admin app so cool. You can use them to display many copies of the same form, and they help manage adding, reordering and deleting the forms in the user interface.

Using formsets effectively can be a little tricky. If their documentation doesn't cover what you need you might need to read the source code for a better understanding.


Limited View Formset

class CompleteToDoForm(forms.Form):
    id = forms.IntegerField(widget=forms.HiddenInput())
    completed = forms.BooleanField(required=False)

    def set_extra_data(self, task):
        self.fields['select'].label = task

...

todo_qs = ToDo.objects.filter(done=False)
complete_formset = CompleteToDoFormset(initial=[
    {'id': todo.id, 'completed':False} for todo in todo_qs])
for todo, form in zip(todo_qs, complete_formset.forms): 
    form.set_extra_data(todo.task)

I often use formsets to create a limited editing interface across a range of models. This form and view fragment show an interface for checking off to-do items, but not unchecking items or editing their text.

A hidden id field is used to associate each form with its corresponding model. This value must be filled in as part of the formset initial data so that the necessary forms are created. The task is shown by including its text in the label of each completed checkbox. The task label will appear to the user but won't be returned as part of the form.

The tricky part about handling formsets is what you do with the posted values if the defaults have changed since they were sent to the user. In this case it's easy, we would likely just set the done flag for each id with the completed box checked and ignore ones that were deleted, it doesn't matter if new items would have been added to the form.

In a real application however, things can be complicated by the possibility of multiple users editing the same data and deciding how to handle that entirely depends on the application.


Reference

Django Forms Quick Reference

Tags: Django Ottawa Software Python OPAG