Monday, August 12, 2013

Django Class Based Views and Inline Form Sets

Django 1.5.1, Python 2.7.5

Ok, so you moved on to working with class based views, got a usual form going on, and everything went smooth, but now you want to use more than one form on the same page and, even better, one of the forms is actually a form set. Cool! Here is a real example of how I made it work on one of the projects I was working on.

For the sake of the example, a few words of what I have and what I want to achieve.
I want to let my users manage theirs sponsors. Simple in itself and a basic need. My users want to be able to define a sponsor and it's sponsorship dates. From this I have 2 models: Sponsor and Sponsorship, connected with one to many field, aka ForeignKey.
Let's say we defined the models, and take  a look at forms.py :

from django.forms import ModelForm
from django.forms.models import inlineformset_factory

from models import Sponsor, Sponsorship


class SponsorForm(ModelForm):

    class Meta:
        model = Sponsor

 

class SponsorshipForm(ModelForm):
     class Meta:
        model = Sponsorship

SponsorShipsFormSet = inlineformset_factory(Sponsor, Sponsorship,
                                            form=SponsorshipForm, extra=2)


Notice that I defined the formset in forms.py and not inside the view.

Ok, so I have the models, I have the forms, now I need a view to work it all out.
views.py:



class CreateSponsor(SponsorMixin, CreateView):
    form_class = SponsorForm
    template_name = 'sponsor_form.html'

    def get_context_data(self, **kwargs):
        data = super(CreateSponsor, self).get_context_data(**kwargs)
        if self.request.POST:
            data['sponsorships'] = SponsorShipsFormSet(self.request.POST)
        else:
            data['sponsorships'] = SponsorShipsFormSet()
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        sponsorships = context['sponsorships']
        with transaction.commit_on_success():
            form.instance.created_by = self.request.user
            form.instance.updated_by = self.request.user
            self.object = form.save()

        if sponsorships.is_valid():
           sponsorships.instance = self.object
           sponsorships.save()

        return super(CreateSponsor, self).form_valid(form)

    def get_success_url(self):
        return reverse('sponsors')


Let's go over what is going on here. My main form is SponsorForm and the view will take care of that pretty much by itself. Notice get_context_data(), on get it will create an unbound SponsorShipsFormSet and on post instantiate it with the data in self.request.POST. But if you remember to instantiate inline formset you also need, on get and on post, to define an instance, which is the main model instance. You don't see it here because it's a create new instance view, there is no instance yet to which i want to bind the formset.
Next is form_valid method. I get the context of the view, and extract from it the formset, which now holds the data the user entered. I make sure that i first save the main form - SponsorForm, that will create an instance i need for the inline formset. Notice that i call is_valid() method on sponsorships, that's because the class doesn't take care of it for me (this is SponsorForm class, for it, it happens automatically). All that is left to do is to bind the formset to this new instance and we are done ;)

Here is a gist for better readability.

1 comment:

  1. Thanks for your very interesting post! But what about case when you need to produce formset based on results of queryset?
    Also I mentioned that there is a way to use CBV based on overriding such methods as get_form without analysing request method. Can you explain such an approach? Thanks in advance!

    ReplyDelete