Defying Classification

by Malcolm Tredinnick

Mon 3 Jul 2006

Django Tips: Forms With Multiple Inline Objects

Posted at 23:38 +1000 (last edited: 16 Sep 2006, 12:57)

The form creation and handling code in Django seems to strike a nice balance between "making the easy things easy and the hard things possible", as the saying goes. However, there are some areas of it that are not perfectly documented yet and sometimes you have to rummage through the source code for a few minutes to work how to achieve something that, in retrospect, is simple.

One example: how to create a form for editing multiple inline objects on a single page?

The admin interface manages this fairly nicely for relatively small numbers of objects. Surely it should be possible to do the same in our own, customised forms. It is indeed possible and not too difficult, either...

The Models

Let's assume we are working with the models from the Django tutorial. Basically, the setup from tutorial #1, plus the edit_inline stuff from tutorial #2. So our models look like this:

class Poll(models.Model):
    question = models.CharField(maxlength = 200)
    pub_date = models.DateTimeField('date published')

    class Admin:
        # ...

class Choice(models.Model):
    poll = models.ForeignKey(Poll, edit_inline = models.STACKED)
    choice = models.CharField(maxlength = 200, core = True)
    votes = models.IntegerField(core = True)

The Mission

In this example, I am going to create a form that would allow a visitor to edit the choices for a given poll, but not the question. Users will not need to be logged in (which might be one reason why the admin system is not appropriate).

The actual processing of the form data and checking for errors is straightforward and exactly as documented in the official form documentation. Since I am creating a form for updating existing objects (rather than for creating new polls), I would only have to write a view that uses a change manipulator.

Creating a URL configuration file is simple enough. We can use something like the following.

update_dict = {
    'model': Poll,
    'template_name': 'update_form.html',
}

urlpatterns = patterns('',
    (r'^update/(?P<object_id>\d+)/$',
        'django.views.generic.create_update.update_object',
        update_dict),

    # ...
)

Our template will be passed an object variable, containing the Poll object we are editing (as determined by the object_id in the above URL) and a form variable, holding the FormWrapper instance that we can use to create the form. But how can we work with the fact that there will be multiple choices for a single poll (and because of the edit_inline attribute being set in the Choice model, we will get them all in the form)?

  • Note: As with my previous Django Tips article, I have put a few links at the end to documentation about features I mention that are not at the centre of the discussion. So if the generic views or FormWrapper class are a mystery to you, see the links at the end.

Creating The Form

This is the part that causes a lot of mailing list posts — and I will admit this has not been documented too clearly (yet). Let me present the solution first and then discuss it a little bit.

<p>Today's question is: <em>{{ object.question }}</em></p>
<form method="POST" action=".">

   <hr>
   {% for the_choice in form.choice %}
     <p><label for="id_choice.{{ forloop.counter0}}.choice">
       Choice:</label> {{ the_choice.choice }}</p>
     <p><label for="id_votes.{{ forloop.counter0}}.votes">
       Votes:</label> {{ the_choice.votes }}</p>
     <hr>
   {% endfor %}

</form>

As we can see from this form creation fragment, for multiple objects, the FormWrapper class gives us something we can iterate over. In this case, each Poll class has a choice attribute, which relates to a number of Choice instances. Each Choice instance has a choice and a votes attribute that we want to provide as editable fields.

The iteration over form.choice provides us with a FormWrapper-like object inside the loop that behaves in the standard fashion. Accessing the name of a field automatically includes the right form editing HTML widget for that field. Errors are handled similarly to normal FormWrappers and so on. The only slightly tricky thing is the id values for the labels. Normally, these are of the form id_ name where name is the attribute name (so id_poll, for example). For nested (or inline) objects like this, we have to include a number indicating the position in the sequence. Fortunately, Django's for-loops provide a way to get at the offset counter, so we can construct it as indicated above.

To be honest, details like getting the label values right is not something that I worry too much (or at all) about, initially. In this case, I created the form and left the for attribute on the label empty. Then I had a look at the HTML Django produced and it was immediately clear what had to go in the label element to get the linkage right. It was 30 seconds of experimentation, versus ten minutes of worrying if I wanted to get it right the first time. My experience is that many people underestimate the value of "suck and see" as a development approach. :-)

OK, this little example is a bit simpler than you might see in the wild. For example, I don't even have a submit button and I have omitted the template fragments to display any error results. Those features are explained clearly in the forms documentation and I wanted to concentrate the pieces that were causing problems for people writing Django applications.

Documentation Links

  1. I am using the generic update view in the above code so that I can concentrate on designing the form without worrying about having to process all the data myself.

  2. If the whole FormWrapper object sounds like Greek to you, have a read of the Django form documentation. It starts off slowly and builds up through more and more complicated examples to show how all the features you will need in a form are available quite easily.

  3. If you are wondering how the Poll object knew about the Choice object and why Poll.choice works in the above code, have a look at how backward one-to-many relations work. This is one of those handy "don't repeat yourself" features in Django (and not unique to it, by any means): declaring a key on one model means the model at the other end automatically knows about the relation, too.

Topics: software/django/tips, software/django/tutorials