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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Could you give a example of the QuizForm and QuestionForm used.
Thanks
#
@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.
#
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.