Login

Complex Formsets

Author:
smagala
Posted:
January 19, 2009
Language:
Python
Version:
1.0
Score:
0 (after 0 ratings)

Background

Edit: This snippet doesn't make a lot of sense when Malcolm's blog is down. Read on for some history, or go here to see the new trick that Malcolm taught me.

A year ago, Malcolm Tredinnick put up an excellent post about doing complex Django forms here.

I ended up reinventing that wheel, and then some, in an attempt to create a complex formset. I'm posting my (partial) solution here, in hopes that it will be useful to others.

Edit: I should have known - just as soon as I post this, Malcolm comes back with a solution to the same problem, and with slightly cleaner code. 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.

Problem

In order to build a formset comprised of dynamic forms, you must build the forms outside the formset, add them, and then update the management form. If any data (say from request.POST) is then passed to the form, it will try to create forms inside the formset, breaking the dynamically created form.

Code

To use this code:

  • Copy BaseDynamicFormSet into your forms.py
  • Create a derived class specific to your needs (BaseQuizDynamicFormSet in this example).
  • Override __init__, and keep a reference to your object that you need to build your custom formset (quiz, in this case).
  • Call the parent __init__
  • Call your custom add forms logic
  • Call the parent _defered_init

To write your custom add_forms logic, remember these things:

  • You've got to pass any bound data to your forms, and you can find it in self.data.
  • You've got to construct your own unique prefixes by doing an enumerate, as shown in the example above. This is the same way it is usually handled by the formset.

Add a formset_factory call, and specify your derived dynamic formset as the base formset - we now have a QuizFormSet class that can instantiated in our view.

The view and template code look identical to a typical formset, and all of the dynamic code is encapsulated in your custom class.

Warning

This solution does not yet handle forms that work with files, use the ordering/delete features, or adding additional forms to the set via javascript. I don't think that any of these would be that hard, but don't assume that they'll just work out of the box.

 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
forms.py
========
    from django.forms.formsets import BaseFormSet, formset_factory, ValidationError


    class BaseDynamicFormSet(BaseFormSet):
        def __init__(self, data=None, *args, **kwargs):
            self.extra=0
            super(BaseDynamicFormSet, self).__init__(*args, **kwargs)

            if data:
                # code based on django.forms.formsets.py
                # BaseFormSet __init__
                self.data = data
                self.is_bound = True
                self.management_form.data = data
                self.management_form.is_bound = True
                if self.management_form.is_valid():
                    self._total_form_count = \
                        self.management_form.cleaned_data['TOTAL_FORMS']
                    self._initial_form_count = \
                        self.management_form.cleaned_data['INITIAL_FORMS']
                else:
                    raise ValidationError(
                        'ManagementForm data is missing or has been tampered with')

        def _defered_init(self):
            self.management_form.initial['TOTAL_FORMS'] = len(self.forms)


    class BaseQuizDynamicFormSet(BaseDynamicFormSet):
        def __init__(self, quiz, *args, **kwargs):
            super(BaseQuizDynamicFormSet, self).__init__(*args, **kwargs)
            self.add_forms(quiz)
            super(BaseQuizDynamicFormSet, self)._defered_init()

        def add_forms(self, quiz_id):
            # Malcolm's create_quiz_forms logic now goes here
            questions = Question.objects.filter(quiz__pk=quiz_id).order_by('id')
            for pos, question in enumerate(questions):
                prefix = '%s-%s' % (self.prefix, pos)
                form = QuestionForm(question, self.data, prefix=prefix)
                self.forms.append(form)
            if not self.forms:
                raise Http404('Invalid quiz id.')


    QuizFormSet = formset_factory(
        QuizForm, formset=BaseQuizDynamicFormSet)


views.py
========

    def quiz_form(request, quiz_id):
        if request.method == 'POST':
            formset = QuizFormSet(quiz_id, 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_id)

        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

matrix (on April 2, 2009):

Could you give a example of the QuizForm and QuestionForm used.

Thanks

#

smagala (on June 3, 2009):

@matrix - check out the two links in the sidebar description. I used Malcolm's code from his first link as the starting point for my code.

It is worth noting that I've switched to using code that is nearly identical to Malcolm's second post - it's a bit cleaner than mine. Malcolm has posted a full working example in that second link.

#

kramed (on November 23, 2009):

Can you please make a snippet using the new way you found? (The guys link has been dead the past few days)

#

Please login first before commenting.