Thursday, August 8, 2013

Using formsets in handling multiple identical forms in a single page

Problem:

We have a page with identical repeating forms on a single page.

For example:
  • adding resources for an article
  • adding an item in a to do list
  • creating a simple questionnaire

Answer:

Django makes this easy for you with the use of formsets.

Sample App:

Here is a simple questionnaire app which creates questions easily using the concept of formsets.



Sample Models:

 from django.db import models 
   
 class Questionnaire(models.Model):  
   name = models.CharField(max_length=100)  
   
   def __unicode__(self):  
     return self.name  
   
   
 class Question(models.Model):  
   question = models.TextField(  
     help_text="e.g. What is the meaning of life?"  
   )  
   questionnaire = models.ForeignKey(Questionnaire)  
   
   def __unicode__(self):  
     return str(self.questionnaire) + ' - ' self.question  
Here we create the models for the Questionnaire App. Question has a many-to-one relationship with Questionnaire

Sample Forms:

 from django.forms import ModelForm  
   
 from questionnaire.models import Question, Questionnaire  
   
   
 class QuestionnaireForm(ModelForm):  
  class Meta:  
   model = Questionnaire  
   
   
 class QuestionForm(ModelForm):  
  class Meta:  
   model = Question  
   exclude = ('questionnaire',)  
Here we create the forms for the Questionnaire App. We exclude the questionnaire field for the Question Form and add data for it behind the scenes.

Sample Views:

 from django.forms.formsets import formset_factory, BaseFormSet  
 from django.http import HttpResponse, HttpResponseRedirect  
 from django.shortcuts import render_to_response  
 from django.template import RequestContext  
   
 from questionnaire.forms import QuestionForm, QuestionnaireForm  
   
   
 def index(request, template='questionnaire/index.html'):  
   class RequiredFormSet(BaseFormSet):  
     def __init__(self, *args, **kwargs):  
       super(RequiredFormSet, self).__init__(*args, **kwargs)  
       for form in self.forms:  
         form.empty_permitted = False  
     
   QuestionFormSet = formset_factory(QuestionForm, formset=RequiredFormSet)  
   if request.method == 'POST':  
     questionnaire_form = QuestionnaireForm(request.POST)  
     question_formset = QuestionFormSet(request.POST, request.FILES)  
   
     if questionnaire_form.is_valid() and question_formset.is_valid():  
       questionnaire = questionnaire_form.save()  
       for question_form in question_formset.forms:  
         question = question_form.save(commit=False)  
         question.questionnaire = questionnaire  
         question.save()  
       return HttpResponseRedirect('success')  
   else:  
     questionnaire_form = QuestionnaireForm()  
     question_formset = QuestionFormSet()  
    
   context = {  
     'questionnaire_form': questionnaire_form,  
     'question_formset': question_formset  
   }  
   
   return render_to_response(template, context, RequestContext(request))  
Here we create the views for the Questionnaire App. This is where we save the data. Initially, we create a blank Questionnaire form and blank Question forms handled by the QuestionFormSet. When the forms are submitted we have a handler which validates the Questionnaire form and Question formset, and ultimately saves the data once everything is good to go.

Sample Template:

index.html
 {% extends "base.html" %}  
   
 {% block title %}Questionnaire{% endblock %}  
   
 {% block extra_js %}  
   <script type="text/javascript">  
     //ADD FORM  
     function addForm(prefix){  
       var formCount = parseInt($('#id_'+prefix+'-TOTAL_FORMS').val());  
       var id_regex = new RegExp('(' + prefix + '-\\d+-)');  
       var replacement = prefix + '-' + formCount + '-';  
         
       //DUPLICATE FORM DIV  
       $('#id_'+prefix+'-TOTAL_FORMS').val(formCount + 1);  
       var new_formset = $('.'+prefix+'-div:last').clone(false);  
       new_formset.find('.formset_errors').html('');  
       new_formset.find('input').val('');  
       new_formset.find('.delete-form').show();  
   
       //REPLACE ID AND NAME  
       new_formset.children().each(function(){  
         if (this.id && this.name){  
           this.id = this.id.replace(id_regex, replacement);  
           this.name = this.name.replace(id_regex, replacement);  
         }  
       });  
   
       //INSERT AND ASSIGN REMOVE  
       new_formset.hide().insertAfter('.'+prefix+'-div:last');  
       new_formset.fadeIn('normal');  
       new_formset.find('.delete-form').click(function(e){  
         e.preventDefault();  
         RemoveItem(this, prefix);  
       });  
     }  
   
     //DELETE FORM  
     function deleteForm(btn, prefix){  
       var formCount = parseInt($('#id_'+prefix+'-TOTAL_FORMS').val()) - 1;  
       if(formCount == 0){  
         return;  
       }  
       //DELETE FORM  
       $('#id_'+prefix+'-TOTAL_FORMS').val(formCount);  
       $(btn).parent().fadeOut('fast', function(){  
         $(this).remove();  
         //UPDATE OTHER FORMS  
         var forms = $('.'+prefix+'-div');  
         for(var i = 0;i < formCount;i++){  
           var id_regex = new RegExp('(' + prefix + '-\\d+-)');  
           var replacement = prefix + '-' + i + '-';  
           $(forms.get(i)).children().each(function() {  
             if (this.id && this.name){  
               this.id = this.id.replace(id_regex, replacement);  
               this.name = this.name.replace(id_regex, replacement);  
             }  
           });  
         }  
       });  
     }  
   
     // Register the click event handlers  
     $("#add").click(function (e) {  
       e.preventDefault();  
       return addForm("form");  
     });  
   
     $(".delete").click(function (e) {  
       e.preventDefault();  
       return deleteForm(this, "form");  
     });  
   </script>  
 {% endblock %}  
   
 {% block main %}  
   <h1>Questionnaire App</h1>  
   <h2>Questionnaire</h2>  
   <form action="" method="POST">{% csrf_token %}  
     <div>  
       {{ questionnaire_form.as_p }}  
     </div>  
     <h2>Questions</h2>  
     {{ question_formset.management_form }}  
     {% for form in question_formset.forms %}  
     <div class="item">  
      {{ form.as_p }}  
      <p style=""><a class="delete" href="#">Delete</a></p>  
     </div>  
     {% endfor %}  
     <p><a id="add" href="#">Add another question</a></p>  
     <input type="submit" value=" Submit " />  
   </form>  
 {% endblock %}  
Here we create the template which shows the Questionnaire Form and Question Formset. The javascript handles adding and deleting of Question Forms for the formset. 

So that's it. Here we have a sample app that dynamically adds questions to a questionnaire.

No comments:

Post a Comment