Editing multiple objects in Django with forms

February 18, 2008 at 1:45 pm | Posted in django | 6 Comments

A common problem I see when helping people in the Django IRC channel is people who are trying to create or edit multiple objects, but for some reason try to make a single form object to deal with them – this makes things quite convoluted and much harder than they have to be. My goal here is to explain a much simpler method using multiple form objects.

For this guide, I’m going to use the basic Poll and Choice models from the tutorial. I’m only showing the fields here, as that’s all that matters to the form, but they wouldn’t break anything, of course :)

from django.db import models

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField()

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

To start, we’ll need forms for each model.

from django import forms
from mysite.polls.models import Poll, Choice

class PollForm(forms.ModelForm):
    class Meta:
        model = Poll

class ChoiceForm(forms.ModelForm):
    class Meta:
        model = Choice
        exclude = ('poll',)

By using ModelForm, all the grunt work of saving/updating is handled for us, so it saves a lot of work you’d have to code yourself when trying to use a single form.

Next up is the view, but here there’s one issue we have to avoid – if you simply make 3 ChoiceForm instances, their field names will conflict – since you’ll get 3 poll fields and 3 choice fields. To get around this, we need to use the prefix arg to the forms, as you’ll see in a moment.

from mysite.polls.models import Poll, Choice
from mysite.polls.forms import PollForm, ChoiceForm
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response

def add_poll(request):
    if request.method == "POST":
        pform = PollForm(request.POST, instance=Poll())
        cforms = [ChoiceForm(request.POST, prefix=str(x), instance=Choice()) for x in range(0,3)]
        if pform.is_valid() and all([cf.is_valid() for cf in cforms]):
            new_poll = pform.save()
            for cf in cforms:
                new_choice = cf.save(commit=False)
                new_choice.poll = new_poll
                new_choice.save()
            return HttpResponseRedirect('/polls/add/')
    else:
        pform = PollForm(instance=Poll())
        cforms = [ChoiceForm(prefix=str(x), instance=Choice()) for x in range(0,3)]
    return render_to_response('add_poll.html', {'poll_form': pform, 'choice_forms': cforms})

The basic structure is identical to when you’re adding a single object, with only a few differences. The first thing I’d like to point out is the prefix arg – by setting that, we get fields named 0-choice, 1-choice, and 2-choice instead of just 3 fields named choice. This allows each ChoiceForm to pick out its own data from request.POST, so they don’t conflict.

When checking if the forms are valid, I use all(), which will only return True if every item in the list evaluates to True as well. For those who like map(), you can replace the inner list comprehension with map(lambda x: x.is_valid(), cforms).

Note: It’s come to my attention that all() is new in 2.5, and I hadn’t been aware of that previously – a quick way to define your own is here:

def all(items):
    import operator
    return reduce(operator.and_, [bool(item) for item in items])

There may be other methods, but this should work fine :)

Finally, when saving the forms, we use commit=False on the ChoiceForm instances, so that we can set the poll ForeignKey properly before saving. The commit arg tells the form to create the new Choice instance and return it, but not save it to the database.

And with that, you now have a view which can add a Poll and 3 Choice objects for it at once.

Making a view to edit the same objects is much similar, you just need to query the Poll and Choice objects and use them instead of the empty instances. You can also use the Choice IDs as the prefixes for the ChoiceForm instances. The great thing with using ModelForm is that once you change the instance arg from a new object to an existing one, it becomes an edit form instead of an add form, so you don’t have to change any internals to go from adding to editing.

Updated 11/3/08 for django 1.0

About these ads

6 Comments

  1. Thank you. That makes this unbelievable simple, but until you get your mind around it it really makes no sense. One thing I noticed it that when x=0 the prefix doesn’t show up (prefix=0), which isn’t a problem in this example, but if there is a field named the same in the base class it would be a conflict and also if you have bound information to the form, it could mess up the saving of that existing model. You can fix this easily enough by putting prefix=str(x). Thanks for posting this knowledge.

  2. Good catch on that prefix quirk, I’ve adjusted the code with that fix :)

  3. Hello,
    Thank you very much for this great recipe. This is one of the nicest and shortest recipe to handle multiple objects with newforms.
    Thank you
    –ym

  4. You really don’t like writing loops do you? I find this much more readable


    def all(items):
    result = True
    for item in items:
    result = result and item
    return result

    I like the use of list comprehensions is great though. What is explicit cast to bool for?

    Great article, I was thinking of doing this for a project of mine as well.

  5. It’s not so much that I dislike loops, I just like neat tricks like reduce ;)

    The cast to bool is just playing it safe, though it might be redundant :)

  6. A very useful article for people (like me) coming over to Django from the ‘other’ python web development framework.

    Regarding the implementation of all(), reduce() would probably give slightly better performance than a python loop although I think that reduce() will not be present in python 3000 as a built-in.

    If using a loop, the following is a slightly more efficient implementation than the one above (there is no point in continuing the iteration if an item is not True).


    def all(items):
    for item in items:
    if not item:
    return False
    return True


Sorry, the comment form is closed at this time.

Blog at WordPress.com. | The Pool Theme.
Entries and comments feeds.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: