Sunday, November 11th, 2012

Mapping out Django's class-based views

If you have ever played with Django's class-based views, you will know how tangled the view code can be when it comes to customizing the view. In recent projects when working with class-based views, I tend to look at the Django source code directly to determine the best place to put specific code snippets. A commenter on my previous class-based views article didn't understand why I couldn't just modify the object, rather than creating the object from an uncommitted form, then modifying the object, then finally saving it to the database. Class-based views tend to bring with it some complexities, since there is a specific order of operations, much like in math.

This article here will explain this order of operations when it comes with class-based views, and how you should go about overriding specific callables in the class. Lets start with my most used class-based view DetailView. This is how a request is handled when a DetailView class is being used:

View.as_view.view(request, *args, **kwargs)
This is the main entrance point for the view code, as_view() is the class constructor, and assigns self as the generated class with the keyword arguments sent in from the urls.py. It returns the output of the next function in this list.
View.dispatch(request, *args, **kwargs)
This function sets up the class instance variables self.request, self.args, and self.kwargs. This function checks to see if the request.method is in the list of available methods for this particular view, and calls it's function.
BaseDetailView.get(request, *args, **kwargs)
The HTTP method function called from View.dispatch. This function assigns the instance variable of self.object by calling get_object(). It also obtains the context by calling get_context_data(object=self.object). Finally this function returns the result from render_to_response(context).
SingleObjectMixin.get_object(queryset=None)
This function has the task of obtaining a model instance by looking at the various resources available, such as variables from the URL. It's query can be limited by sending it a different queryset. If queryset is None, which is normally the case, then it will call get_queryset() to obtain the queryset to use from the current class. It then checks for either the pk or slug from the self.kwargs dict, which is normally set from the URL. It will then apply a filter on the queryset using both the PK and SLUG, if they are available. It will use both. Then it does a queryset.get() and returns the object found back to BaseDetailView.get().
SingleObjectMixin.get_queryset()
This provides the initial queryset for the DetailView. It basically checks to see if your subclass has a queryset model instance variable and uses that, otherwise provides an appropriate error message about being misconfigurated. It is very handy to override this to limit what users can and cannot access.
SingleObjectMixin.get_context_data(**kwargs)
Here is where a context data Python dictionary is generated for the view. First it takes in all the kwargs and uses it as the initial dictionary. Since the get() function provides the context with object=self.object, it generates an initial dictionary with an object key, similar to how function-based generic views worked. This is also where it adds the context for the model instances object name, such todo or entry. It then returns the dictionary back to get().
TemplateResponseMixin.render_to_response(context, **response_kwargs)
Almost all class-based views use this Mixin to render the final response. This function calls response_class(request=self.request, template=self.get_template_names(), context=context, **response_kwargs). It then returns the result from this back to get(). You can override the response_class in your subclass if you choose, the default is TemplateResponse.
TemplateResponseMixin.get_template_names()
This functions checks for and uses the template_name variable which should be in the class instance. It returns a list, with one element being the template_name.

There you have an entire class-based view, right from the request, down to where it renders the template for the end-user's browser. A great use of response_class would be to return different document types, such as JSON, XML, or even PDF generated data. You just need to create your own response class and make sure it accepts what the TemplateResponseMixin sends to it as variables.

Okay, now that we got a fairly simple class-based view out of the way, yes this is a more simpler one. Lets tread into CreateView, which uses two HTTP methods and therefore branches conditionally.

View.as_view.view(request, *args, **kwargs)
Does the same thing as the previous view mentioned, since both view types subclass View.
View.dispatch(request, *args, **kwargs)
Same as before, since we haven't left the View class yet, this will dispatch between get() and post() this time around.
BaseCreateView.get(request, *args, **kwargs)
This method obviously works much differently than the one included with DetailView. Instead, during the get request, it sets self.object to None so that the rest of the class knows that we are going to be creating a new object. It then returns the super of this, which we continue...
ProcessFormView.get(request, *args, **kwargs)
This function first gets the form_class by calling get_form_class(). It then takes the returned form_class and hands it over to get_form(form_class). The context is generated the same way it is in the previous view mentioned, get_context_data(form=form). Finally, the context is handed over to render_to_response(context), which is eventually sent to the browser.
ModelFormMixin.get_form_class()
This function is used to get the class which is used to render the form in the browser. This should return a valid ModelForm with the Model your attempting to create an instance of. By default it obtains the class from the class instance variable form_class. If this is not set, then it attempts to use the provided Model as the form to display in the browser.
FormMixin.get_form(form_class)
This function uses the form_class and creates an instance of it using the result from get_form_kwargs() as the keyword variables to instantiate the form class.
FormMixin.get_form_kwargs()
You can use this to customize the form instance, but by default it does plenty of what you'll need. It uses the result from calling get_initial() to provide the form with some initial data, and place the data and files into the form instance if the HTTP method is either POST or PUT.
FormMixin.get_initial()
This does a copy of the class instance's variable of initial. You can override this function to provide some Python logic to dynamically generate a dictionary for the initial data.
ModelFormMixin.get_context_data(**kwargs)
This function does the exact same thing as the get_context_data() for SingleObjectMixin.
TemplateResponseMixin.render_to_response(context, **response_kwargs)
This View also uses the same function as the previous view.
SingleObjectTemplateResponseMixin.get_template_names()
Unlike the DetailView, this function does a little more, which includes using a suffix such as _form for the template name.
BaseCreateView.post(request, *args, **kwargs)
Here is where we start the POST method, this is only called when the user actually submits the form. Firstly, it also sets self.object to None like the get() method does. Then it calls super and returns the result to the browser.
ProcessFormView.post(request, *args, **kwargs)
For the most part, this is very similar to get(), it obtains a form_class, and then a form to use. Once the form is obtained, this is where things change. The function then checks the result of form.is_valid(), very standard Django stuff from normal views. If the form is valid, it calls and returns the result of form_valid(form). If the form is not valid, it calls and returns the result of form_invalid(form).
ModelFormMixin.form_valid(form)
If the form is valid, this function is called and it sets the variable self.object to the return of form.save(), again very standard Django form handling stuff. Once the object is returned, it does a super.
FormMixin.form_valid(form)
This function does one thing only, and that is return an HttpResponseRedirect object, which is the result of get_success_url().
ModelFormMixin.get_success_url()
The task of this function is to return a valid URL to direct the user to after the form is valid. First it tries to use self.success_url if it is available. If it is not available, then it attempts to use self.object.get_absolute_url(), and if this fails, returns a misconfigured error. You can override this in your class to generate the URL dynamically using Python code.
FormMixin.form_invalid(form)
This is only called if the form was not valid, it basically returns the form back to the template and provides appropriate validation errors. An idea would be to override this to set other variables on self.object and attempt to re-validate. An example is setting the request.user on an object's field. I personally override form_valid() and it makes more sense to me.

As you can see from these two very different view classes, the Django class-based views system is very modular and expandable. It comes with many base classes, and plenty of mixins to make using class-based views work for almost any scenario. To transition a customized function-based view over to a class-based view is pretty easy as you may think:

def function_view(request):
    # Do something fancy here.
    if request.method == 'POST':
        # Do something with the POST data.
    elif request.method == 'GET':
        # Do something with GET.
    else:
        raise Http404

# Transition to a class-based solution:
class ClassView(TemplateResponseMixin, View):
    template_name = "custom_view.html"
    def get_context_data(self, **kwargs):
        # This allows this View to be easily subclassed in the future to interchange context data.
        context = kwargs
        return context
    def dispatch(self, request, *args, **kwargs):
        # Do something fancy here.
        return super(ClassView, self).dispatch(request, *args, **kwargs)
    def get(self, request, *args, **kwargs):
        # Do something with GET.
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)
    def post(self, request, *args, **kwargs):
        # Do something with the POST data.
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

Made correction from commenter Visa, thanks!

Most POST data in Django will be through forms, so to limit all your work, just subclass the Form class-based views to minimize the work overhead. The above can definitely apply to custom GET requests where you are returning calculated scientific data. If you need more than one view to calculate different datasets, using classes will be much easier, since you just need to create a base class and subclass this for each calculation/dataset. Class-based views allow you to make your view code much more modular and to be more DRY when developing custom views.

Although at first class-based views might seem like more work, they will pay off in very large projects which use the same logic in multiple views. You can place a large chunk of your business logic into a custom Mixin, and don't need to worry about if you need to pass around a request variable or other data. Since the business logic will be part of your class when the Mixin is applied, it will have immediate access to the request and kwargs, and other data. This can lower the overall work and better organize your code and logic.

Comment #1: Posted 1 year, 11 months ago by Visa

In your last example, wouldn't it be possible to use the dispatch() method instead of creating your custom all_methods() method? I would also inherit the class from TemplateView.

Something like this: (https://gist.github.com/4065214)

from django.views.generic import TemplateView

class ClassView(TemplateView):
template_name = "custom_view.html"

def get_context_data(self, **kwargs):
# This allows this View to be easily subclassed in the future to interchange context data.
context = kwargs
return context

def dispatch(self, request, *args, **kwargs):
# Do something fancy here.
return super(ClassView, self).dispatch(request, *args, **kwargs)

def get(self, request, *args, **kwargs):
# Do something with GET.
context = self.get_context_data(**kwargs)
return self.render_to_response(context)

def post(self, request, *args, **kwargs):
# Do something with the POST data.
context = self.get_context_data(**kwargs)
return self.render_to_response(context)

Comment #2: Posted 1 year, 11 months ago by Kevin Veroneau

Hello Visa -- You are correct, your method is actually better than the one I made. I will correct the post shortly.

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

This Month

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2014 Kevin Veroneau