Django's Class-Based-Views, Inline Formset, and Bootstrap3

Python Django

So I'm really a big fan of Django's class-based-views (CBV) and ever since I started writing them, I keep appreciating how they make views much easier to debug.

Since most of my views are CBV based, I wanted to make this view CBV also. For those who don't know what are inline formsets, "Inline formsets is a small abstraction layer on top of model formsets. These simplify the case of working with related objects via a foreign key."

So to give you an example, I have a model calledAssignment which you can think of as a course assignment. It has information on the assignment, it's description, deadline, etc. And I have another model called AssignmentQuestion which has Assignment as a foreignKey. AssignmentQuestion is pretty much a question on the assignment. It has a field for the question it self, and an integer field for the total marks the question is worth.

class Assignment(AbstractCourse):

    description = models.TextField(help_text="Short description of the assignment")
    deadline = models.DateTimeField()
    ...

class AssignmentQuestion(AbstractAssignment):

    instructions = models.TextField(help_text="Question instructions and deliverable")
    marks = models.IntegerField(help_text="Question total marks")
    ...

Solution

Since you want to use the inline formset rendered with django-bootstrap3, you need to install it first. We'll also use django-dynamic-formset, which is just a little javascript file that will allow us to add/delete the inline forms without refreshing the page.

Ok, so lets start by making the main form for Assignment model and the inline formset for the AssignmentQuestion model. In my forms.py file, I have:


# forms.py

from django import forms
from django.forms.models import inlineformset_factory
from django.forms.widgets import SelectDateWidget
from django.utils import timezone
from django.utils.translation import ugettext as _

from .models import Assignment, AssignmentQuestion

class AssignmentForm(forms.ModelForm):
    """
    Form for creating a new assignment for a course

    """
    description = forms.CharField(max_length=512,
                                  widget=forms.Textarea(attrs={'rows': 5,
                                                               'cols': 80}
                                                        ),
                                  label=_(u'Assignment description'),
                                  required=True)
    deadline = forms.DateField(widget=SelectDateWidget(
                                    empty_label=("Choose Year",
                                                 "Choose Month",
                                                 "Choose Day"),
                                ),
                               initial=timezone.now())


AssignmentQuestionFormSet = inlineformset_factory(Assignment,  # parent form
                                                  AssignmentQuestion,  # inline-form
                                                  fields=['instructions', 'marks'], # inline-form fields
                                                  # labels for the fields
                                                  labels={
                                                        'instructions': _(u'Question and '
                                                                          u'deliverable'),
                                                        'marks': _(u'Question total '
                                                                   u'marks'),
                                                  },
                                                  # help texts for the fields
                                                  help_texts={
                                                        'instructions': None,
                                                        'marks': None,
                                                  },
                                                  # set to false because cant' delete an non-exsitant instance
                                                  can_delete=False,
                                                  # how many inline-forms are sent to the template by default
                                                  extra=1)


There's alot of arguments passed to the inlineformset_factory, I suggest looking at the django's documentation for more info.

We need to make our view send both forms to the template, so in our class-based-view, we need to override several methods. We need to override the get() because the template should recieve both the parent form and the inline formset, we need to override post() to make it validate both forms, we need to override form_valid() to process what should happen if both forms are valid, and we need to override form_invalid() to deal with the forms being invalid. In my view.py I have:


# views.py
class AssignmentCreateView(generic_view.CreateView):
    model = Assignment
    template_name = 'path_to_template_to_assignment_create_form.html'
    form_class = AssignmentForm
    object = None

    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates blank versions of the form
        and its inline formsets.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        assignment_question_form = AssignmentQuestionFormSet()
        return self.render_to_response(
                  self.get_context_data(form=form,
                                        assignment_question_form=assignment_question_form,
                                        )
                                     )

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance and its inline
        formsets with the passed POST variables and then checking them for
        validity.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        assignment_question_form = AssignmentQuestionFormSet(self.request.POST)
        if form.is_valid() and assignment_question_form.is_valid():
            return self.form_valid(form, assignment_question_form)
        else:
            return self.form_invalid(form, assignment_question_form)

    def form_valid(self, form, assignment_question_form):
        """
        Called if all forms are valid. Creates Assignment instance along with the
        associated AssignmentQuestion instances then redirects to success url
        Args:
            form: Assignment Form
            assignment_question_form: Assignment Question Form

        Returns: an HttpResponse to success url

        """
        self.object = form.save(commit=False)
        # pre-processing for Assignment instance here...
        self.object.save()

        # saving AssignmentQuestion Instances
        assignment_questions = assignment_question_form.save(commit=False)
        for aq in assignment_questions:
            #  change the AssignmentQuestion instance values here
            #  aq.some_field = some_value
            aq.save()

        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, assignment_question_form):
        """
        Called if a form is invalid. Re-renders the context data with the
        data-filled forms and errors.

        Args:
            form: Assignment Form
            assignment_question_form: Assignment Question Form
        """
        return self.render_to_response(
                 self.get_context_data(form=form,
                                       assignment_question_form=assignment_question_form
                                       )
        )


Now that the forms and the view are done, we just need to make sure our template understands that we're passing more than one form.


<!--path_to_template_to_assignment_create_form.html-->
{% load i18n %}
{% load staticfiles %}
{% load bootstrap3 %}

{% block main_content %}
<p>
    <form action="" method="post">
		{% csrf_token %}
		<fieldset>
				{{ form.non_field_errors }}
				{% for field in form %}
				<div class="row">
					<div class="col-md-3">{% bootstrap_label field.label %}</div>
					<div class="col-md-8">{% bootstrap_field field show_label=False %}</div>
				</div>
				{% endfor %}
		</fieldset>
		<fieldset>
				<legend>Assignment Questions</legend>
				{{ assignment_question_form.management_form }}
				{{ assignment_question_form.non_form_errors }}
                				{% for form in assignment_question_form %}
						<div class="inline {{ assignment_question_form.prefix }}">
						{% for field in form.visible_fields %}
								<div class="row">
										<div class="col-md-3">{% bootstrap_label field.label %}</div>
										<div class="col-md-8">{% bootstrap_field field show_label=False %}</div>
								</div>
						{% endfor %}
						</div>
				{% endfor %}
		</fieldset>
		<div class="row">
				<div class="col-md-1">
						<input type="submit" class="btn btn-groppus-primary bordered" value="{% trans "Submit Assignment" %}" />
				</div>
		</div>
		</form>

{% block bottom_scripts %}
<script src="{% static "socialnetwork/js/jquery.formset.js" %}"></script>
<script type="text/javascript">
		$(function() {
				$(".inline.{{ assignment_question_form.prefix }}").formset({
						prefix: "{{ assignment_question_form.prefix }}", // The form prefix for your django formset
						addCssClass: "btn btn-block btn-primary bordered inline-form-add", // CSS class applied to the add link
						deleteCssClass: "btn btn-block btn-primary bordered", // CSS class applied to the delete link
						addText: 'Add another question', // Text for the add link
						deleteText: 'Remove question above', // Text for the delete link
						formCssClass: 'inline-form' // CSS class applied to each form in a formset
				})
		});
</script>
{% endblock %}

The bottom_scripts part is really important. Make sure you load jquery in the page before loading jquery.formset.js, which you can download here.

And below is how it looks like in the browser (with my own css and everything).

Assignment form

Thanks for reading! and let me know if you have any feedback.

Update: Added UpdateView for reference.


class AssignmentUpdateView(CourseContextMixin, generic_view.CreateView):
    model = Assignment
    template_name = 'assignment/assignment_update_form.html'
    form_class = AssignmentForm
    object = None

    def get_object(self, queryset=None):
        self.object = super(AssignmentUpdateView, self).get_object()
        return self.object

    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates blank versions of the form
        and its inline formsets.
        """
        self.object = self.get_object()
        assignment_question_form = AssignmentQuestionFormSet(instance=self.object)
        return self.render_to_response(
                  self.get_context_data(form=AssignmentForm(instance=self.object),
                                        assignment_question_form=assignment_question_form,
                                        )
                                     )

    def get_success_url(self):
        """
        Handles getting the url when form is submitted and valid
        Returns: success url when form is valid and submited

        """
        course = self.object.course
        return reverse('assignment-detail',
                       kwargs={'course_slug': course.slug,
                               'course_abbreviation': course.abbreviation,
                               'course_number': course.number,
                               'course_year': course.year,
                               'course_term': Course.TERM_CHOICES[course.term][1],
                               'slug': self.object.slug,
                               'assignment_number': self.object.number
                               })

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance and its inline
        formsets with the passed POST variables and then checking them for
        validity.
        """
        self.object = self.get_object()
        form = AssignmentForm(data=self.request.POST, instance=self.object)
        assignment_question_form = AssignmentQuestionFormSet(data=self.request.POST,
                                                             instance=self.object)
        if form.is_valid() and assignment_question_form.is_valid():
            return self.form_valid(form, assignment_question_form)
        else:
            return self.form_invalid(form, assignment_question_form)

    def form_valid(self, form, assignment_question_form):
        """
        Called if all forms are valid. Creates Assignment instance along with the
        associated AssignmentQuestion instances then redirects to success url
        Args:
            form: Assignment Form
            assignment_question_form: Assignment Question Form

        Returns: an HttpResponse to success url

        """
        self.object = form.save()
        assignment_questions = assignment_question_form.save(commit=False)
        for aq in assignment_questions:
            aq.creator = self.profile
            aq.assignment = self.object
            aq.save()
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, assignment_question_form):
        """
        Called if a form is invalid. Re-renders the context data with the
        data-filled forms and errors.

        Args:
            form: Assignment Form
            assignment_question_form: Assignment Question Form
        """
        return self.render_to_response(
                 self.get_context_data(form=form,
                                       assignment_question_form=assignment_question_form
                                       )
        )

© 2017 Mustafa Abualsaud. All rights reserved.