Complex Formsets, Redux

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 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.

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 the index variable to track which object from our queryset we wanted. See code trick #5.

forms.py========fromdjango.forms.formsetsimportForm,BaseFormSet,formset_factory, \
ValidationErrorclassQuestionForm(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 parentself.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 orderself.answers=self.question.answer_set.all().order_by('id')self.fields['answers']=forms.ModelChoiceField(queryset=self.answers())classBaseQuizFormSet(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 parentself.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 orderself.questions=self.quiz.question_set.all().order_by('id')self.extra=len(self.questions)ifnotself.extra:raiseHttp404('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 classkwargs['question']=self.questions[index]returnsuper(BaseQuizFormSet,self)._construct_form(index,**kwargs)QuizFormSet=formset_factory(QuestionForm,formset=BaseQuizDynamicFormSet)views.py========fromdjango.httpimportHttp404defquiz_form(request,quiz_id):try:quiz=Quiz.objects.get(pk=quiz_id)exceptQuiz.DoesNotExist:returnHttp404('Invalid quiz id.')ifrequest.method=='POST':formset=QuizFormSet(quiz=quiz,data=request.POST)answers=[]ifformset.is_valid():forforminformset.forms:answers.append(str(int(form.is_correct())))returnHttpResponseRedirect('%s?a=%s'%(reverse('result-display',args=[quiz_id]),''.join(answers)))else:formset=QuizFormSet(quiz=quiz)returnrender_to_response('quiz.html',locals())template========Justchangethis:{%forforminforms%}tothis:{%forforminformset.forms%}

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