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.
-
If we
pop()
special arguments out ofkwargs
dictionaries, we then can pass the remainingkwargs
along to parent methods and let them do the rest of the setup for us. See code tricks #1 and #3. -
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. -
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. -
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 theindex
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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks 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, 6 months ago
Comments
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.