Login

Complex Formsets, Redux

Author:
smagala
Posted:
March 8, 2010
Language:
Python
Version:
1.1
Score:
4 (after 4 ratings)

Background

Two years ago, Malcolm Tredinnick put up an excellent post about doing dynamic Django forms. There have been several excellent write-ups on it since - Google is your friend.

One year ago, I attempted to make a dynamic Formset - see this snippet. Malcolm posted a cleaner solution two weeks later, and I liked his solution better. Some time after that happened, his site tanked.

I'm re-posting my snippet using his technique, so that everyone can see how it is done. Credit to goes to Malcolm - I'm just the messenger. If his site ever comes back up , check out his complex formset post here.

I'll use Malcolm's example code, with as few changes as possible to use a formset. The models and form don't change, and the template is almost identical.

I won't reproduce all of Malcolm's code - I'm just show the form setup. There are no models here - you'll have to use your imagination for Quiz, Question, and Answer models. He did some fancy validation as well - I'm not going to here.

Problem

Build a formset based on dynamically created forms.

Solution

The core idea in this code is found in the _construct_form method. Typically, this is where a formset makes new forms - it handles indexing them so that everything is nice and unique. We can take advantage of this by overriding the method and inserting a kwarg that will be passed on to our form class, then calling the parent _contruct_form method to let it finish doing everything else for us. This is what Malcolm, a core Django developer, knows about, and I, a random Django user, typically do not.

Code

This pattern greatly simplifies building formsets dynamically, and it really only requires a few bits of knowledge.

  1. If we pop() special arguments out of kwargs dictionaries, we then can pass the remaining kwargs along to parent methods and let them do the rest of the setup for us. See code tricks #1 and #3.

  2. If we have a form and need to add dynamic fields that we didn't declare the usual way, we can just add them to the self.fields dictionary. See code trick #2.

  3. If we need to add forms dynamically to a formset, we can use the self.extra variable to specify how many we want, based on the length of a custom queryset. See code trick #4.

  4. If we want to pass some special arguments to a form that will be part of a formset when it is constructed, we can add them to the kwargs dict in _construct_form, taking advantage of the index variable to track which object from our queryset we wanted. See code trick #5.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
forms.py
========
    from django.forms.formsets import Form, BaseFormSet, formset_factory, \
            ValidationError


    class QuestionForm(Form):
        """Form for a single question on a quiz"""
        def __init__(self, *args, **kwargs):
            # CODE TRICK #1
            # pass in a question from the formset
            # use the question to build the form
            # pop removes from dict, so we don't pass to the parent
            self.question = kwargs.pop('question')
            super(QuestionForm, self).__init__(*args, **kwargs)

            # CODE TRICK #2
            # add a non-declared field to fields
            # use an order_by clause if you care about order
            self.answers = self.question.answer_set.all(
                    ).order_by('id')
            self.fields['answers'] = forms.ModelChoiceField(
                    queryset=self.answers())


    class BaseQuizFormSet(BaseFormSet):
        def __init__(self, *args, **kwargs):
            # CODE TRICK #3 - same as #1:
            # pass in a valid quiz object from the view
            # pop removes arg, so we don't pass to the parent
            self.quiz = kwargs.pop('quiz')

            # CODE TRICK #4
            # set length of extras based on query
            # each question will fill one 'extra' slot
            # use an order_by clause if you care about order
            self.questions = self.quiz.question_set.all().order_by('id')
            self.extra = len(self.questions)
            if not self.extra:
                raise Http404('Badly configured quiz has no questions.')

            # call the parent constructor to finish __init__            
            super(BaseQuizFormSet, self).__init__(*args, **kwargs)

        def _construct_form(self, index, **kwargs):
            # CODE TRICK #5
            # know that _construct_form is where forms get added
            # we can take advantage of this fact to add our forms
            # add custom kwargs, using the index to retrieve a question
            # kwargs will be passed to our form class
            kwargs['question'] = self.questions[index]
            return super(BaseQuizFormSet, self)._construct_form(index, **kwargs)


    QuizFormSet = formset_factory(
        QuestionForm, formset=BaseQuizDynamicFormSet)


views.py
========

from django.http import Http404


    def quiz_form(request, quiz_id):
        try:
            quiz = Quiz.objects.get(pk=quiz_id)
        except Quiz.DoesNotExist:
            return Http404('Invalid quiz id.')
        if request.method == 'POST':
            formset = QuizFormSet(quiz=quiz, data=request.POST)
            answers = []
            if formset.is_valid():
                for form in formset.forms:
                    answers.append(str(int(form.is_correct())))
                return HttpResponseRedirect('%s?a=%s'
                        % (reverse('result-display',args=[quiz_id]), ''.join(answers)))
        else:
            formset = QuizFormSet(quiz=quiz)

        return render_to_response('quiz.html', locals())


template
========

Just change this:
    {% for form in forms %}
to this:
    {% for form in formset.forms %}

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 1 week ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 9 months, 4 weeks ago
  5. Help text hyperlinks by sa2812 10 months, 3 weeks ago

Comments

ActionScripted (on February 15, 2011):

This snippet has made my evening. I've been crawling the search engines, #django IRC, etc. and no one was able to help at all (god bless 'em for trying).

Thanks to anyone and everyone who contributed, especially you smagala. I used this to create a set of formsets so that we had a voting system on our site where you have categories that have questions that have choices that are either something the user chooses from a drop-down or "nominates" by using an empty text box next to the question they'd like to nominate a choice for.

It's always easy to do things in Django once you learn how, but sometimes finding the how takes a few days... :D

#

Please login first before commenting.