Django Class-Based Views: The Basics
Recently updated on
Class-based views are my favorite feature of Django, so there is an obvious bias when discussing function views and class-based views. I’ll throw my two cents in here and move on. I prefer the structure and predictability that class-based views provide. I recognize that there are many instances when a class-based view might be unnecessary.
Throughout this post, we will look at some comparative examples between function-based and class-based views. We will go over patterns that are useful, as well as anti-patterns that you should try to avoid along with the reason that you should avoid them. While you do not need to use class-based views in your own code, you’ll find that it will be useful to understand how they work in case you come across them in a package provided by a third party.
We are going to start at the bottom. The most basic class-based view is, well, View. View is the foundation for every other view. View has two important methods, as_view and dispatch. We’ll talk briefly about these now and refer back to this discussion in later parts of this series. It isn’t overly important to completely understand what as_view or dispatch do, but having an understanding of their role in the request-response cycle is useful.
as_view is the entry point for views. Whenever you see a class-based view inside a urls.py url definition, you will usually see MyView.as_view(). as_view does all the heavy lifting of converting your Class into a callable that the url system understands.
dispatch translates the request method (GET, POST, HEAD, etc.) to the method on the view that matches it. So it maps GET requests to self.get, POST requests to self.post, HEAD requests to self.head, and so on. dispatch checks for an attribute on the class named http_method_names. Note that View does not define a method for get, post, head, etc. It is the responsibility of views that inherit from View to define these functions. Additionally, many of the generic views provided by Django will override http_method_names. Dispatch will almost always return some form of HTTP response.
Now that we’ve gone over the bare minimum for View, we are going to go over one of the most common class-based generic views, TemplateView. Additionally, we will discuss some of the Mixins that these classes use.
Let’s look at a View class that inherits from View. TemplateView is used for simple views that only render a template and optionally set some context data used when the template is rendered. TemplateView is a mixture of View and two mixins: TemplateResponseMixin and ContextMixin. These two mixins do quite a bit of work and are inherited by many other views.
ContextMixin is pretty straightforward. ContextMixin defines single method get_context_data. This returns a dictionary. You are probably used to creating this dictionary manually in your function views. It is important to become familiar with get_context_data as it is one of the methods I most frequently override. As you use class-based views more and more, you’ll begin to notice that many other views override this method as well. FormView, for instance, will add the form to the context. A common thing to remember when overriding this method is to not forget to make the super call to collect all this context. If you do, you’ll start wondering why your form isn’t appearing on your page or why your list page doesn’t have any results. I know I have missed this more than once.
TemplateResponse has two methods: render_to_response and get_template_names. get_template_names returns a list of template names to render in the order in which they should be tried. render_to_response more or less replaces that line at the bottom of all of your function based views:
return render(request, ‘my_template.html’, context)
If we were to flatten TemplateView and the Mixins that comprise it, we would see a class that looks like this. We are going to ignore some methods which are not important to our understanding of this view:
class TemplateView(object): template_name = "my_template.html" # This is the preferred place to set the name of the template to render def as_view(self): # returns a callable used by urls def dispatch(self, request, *args, **kwargs): # calls class method matching request type # returns an HTTP response def get(self, request, *args, **kwargs): # gets context from self.get_context_data # calls self.render_to_response def get_template_names(self): # returns a list of template names to attempt to render def render_to_response(self, context, **reponse_kwargs): # renders template response rendered with passed in context Now, let’s look at a very basic TemplateView written as a functional view and a class-based view. def my_view(request): context = { "message": "Hello!" } return render(request, "my_template.html", context) class MyView(TemplateView): template_name = "my_template.html" def get_context_data(self, **kwargs): context = super(MyView, self).get_context_data(**kwargs) context["message"] = "Hello!" return context
The definition of MyView is longer and more complex than the equivalent function! There are many circumstances when defining a class-based view will be quite a bit longer and more complex than the functional equivalent. However, as the complexity of the view increases we will still be able to assume that all of the context used to render our view will be added via get_context_data.
We could also have written MyView like this:
class MyView(TemplateView): template_name = "my_template.html" def get(self, request, *args, **kwargs): context = {"message": "Hello!"} return self.render_to_response(context)
While this definition works, I trongly recommend not overriding the functions that map http method types. There will be scenarios where we will need to override get, post, head, put, etc., but for the most part adopting this pattern will lead to trouble down the road. This pattern has a couple of problems. The first problem is that we’ve broken our call chain. We no longer call get_context_data. We also do not make a super call to do whatever is defined on TemplateView (in this instance, we are doing the exact thing the super call would do, but if we inherited from a different view this would not be the case).
Let’s look at an example of how we might get into trouble using the above pattern.
class MyBaseTemplateView(TemplateView): template_name = "app/my_template_a.html" def get_context_data(self, **kwargs): context = super(MyBaseTemplateView, self).get_context_data(**kwargs) context["critical_information"] = "Some piece of critical information!" return context class MyTemplateView(TemplateView): def get(self, request, *args, **kwargs): context = {"random_thing": "This is some random text!"} return self.render_to_response(request, context)
Will our “critical_information” be present in the context when rendering the template for MyTemplateView? No. We have broken this by our seemingly innocuous override of get.
There are some great sources of information about class-based views. The django documentation on class-based views has improved quite a bit. You can view the offical documentation here (https://docs.djangoproject.com/en/1.8/topics/class-based-views/). For fun you can have a look back to the original of this documentation here (https://web.archive.org/web/20140901051303/https://docs.djangoproject.com/en/1.3/topics/class-based-views/). Additionally, I find ccbv.co.uk (http://ccbv.co.uk/) to be an invaluable resource. It provides a flattened perspective of all of the default view types. It is a great tool for learning how different mixins and views work together to form the complex views used in many applications.
Questions? Comments? Leave a message below or message me on twitter @penguin_dan.