Django’s Views: Functional, Class-Based, Generics
Last week, we dug into Django’s middlewares and the request/response cycle. It’s helpful to imagine this cycle like the layers of an onion, with your view at the center. The user’s request passes through each layer and is modified along the way. The view processes the request and returns a response which travels back out through the response middlewares and is eventually sent to the web server through a callback function provided to the WSGI handler. This time around, let’s think a little about the center of the onion: the view.
As a reminder, Django’s views have three requirements:
- They are callable. A view can be either function or a class-based view. CBVs inherit the method
as_view()which uses a
dispatch()method to call the appropriate method depending on the HTTP verb (
- They must accept an
HttpRequestobject as its first positional argument
- They must return an
HttpResponseobject or raise an exception
When writing views, we have a choice: function- or class-based? Both have their advantages and disadvantages.
- explicit and easy to read/understand
- use conditionals to handle different HTTP methods
- not reusable; can sometimes lead to code duplication and bigger view files
- use decorators to add functionality
- good for one-off or specialized functionality
- can be a bit more implicit and difficult to read, especially when using Django’s generic views
- rely on the
dispatch()method inherited from
Viewto call different HTTP methods
- because they are classes, they are reusable through inheritance, and we can add functionality with mixins
- good for functionality which will be repeated in multiple places in an application
Here’s a simple function-based view:
It’s pretty easy to follow the flow here. We look at the request’s method, and if it’s a
POST, we process the form. Otherwise we render the template, passing the form into the context. If
is_valid() comes up false, we fall out of the
if statement and arrive at the
return, with the template rendered with the posted form data, allowing the user another chance at submitting proper data.
Here’s a class-based version of the same view:
Also pretty simple! We do have a little bit of abstraction, with the inherited
as_view() method calling
dispatch() to determine which class method should be executed, depending on the HTTP request.
as_view() allows you to over-ride class attributes in your URLconfs, so if you wanted to reuse
MyCreateView but just change the template and form used, you could do something like this:
Once you start getting into Django’s generic class-based views, you’ll have the option to also over-write the helper methods like
get_template_names, which allows you to insert additional logic at these points, rather than just over-riding a class attribute. One example is the
ModelFormMixin — the
form_valid() method is overriden and the associated model is saved with the updated value stored in
Django’s Generic Class-Based Views
Django provides a set of views and mixins, generic class-based views, which aim to solve some of the most common tasks in web development. The goal isn’t in reducing boiler plate, per se, but rather to prevent you from having to reinvent the wheel over and over. Let’s modify
MyCreateView to inherit from
Whoa. Where’d all the code go? The answer, of course, is that it’s all in
django.views.generic.CreateView. Take a look at Classy Class-Based Views. It’s a vital resource for understanding Django’s generic class-based views. If you navigate over to the entry for
CreateView, you’ll see that our little two-line view class has inherited over forty methods and class attributes!
When you inherit from
CreateView, you gain a lot of functionality and shortcuts, but you also buy into a sort of ‘convention over configuration’ style arrangement.
- template needs to be in
/<modelname>/<modelname>_form.htmlby default — you can change this by setting the class attribute
- we need to specify
form_classattributes, because the methods you inherited from
CreateViewrely on them.
- we either need to specify a
success_urlas a class attribute on the view, or else define
get_absolute_url()in the model, otherwise the view won’t know where to redirect to following a successful form submission.
- we need to either specify a
fieldsclass attribute on the view, or else define the fields in your form. In this example, I chose to do the latter. Just for reference, here’s a quick example of how that might look:
Strategies for using Django’s Generic Class-Based Views
- Start simple. What’s the main action of the view in question? Say for example you want a
Recipeview which also has a list of associated
Ingredients. Start with the
DetailViewas your base.
- Add mixins to build functionality. To add
Ingredients to our
Recipe's detail view, we use the
MultipleObjectMixin, which provides features like ordering and pagination.
- Over-ride necessary methods to get the functionality you need. Refer to http://ccbv.co.uk/ or, if your IDE (like PyCharm) provides a file structure window, to take a look at what class attributes and methods you have available to you. The inheritance structure gets complicated pretty quickly, but thankfully all the methods are named intelligently, so you don’t need to understand the exact order everything gets called in.
Let’s take this one step at a time. First, we create our class, with mixin and generic view. We set the
model attribute to our
Navigate to the appropriate URL, and you’re (naturally) presented with an error:
Looks like we’re trying to access an
object_list from inside the
get_context_data method in
django.views.generic.list. The attribute,
object_list hasn’t been defined anywhere, so we need to find a good place to set it. We could do that within
get_context_data, like this:
(Assume here that I have created a Recipe-Ingredient join table to hold other information, like quantity, and also to allow for a many-to-many relationship)
That’ll work, the parent method will have access to the
object_list attribute and as a result, we’ll have
object_list available to us in the template. There’s no wrong place to put it, really, but poking around at http://ccbv.co.uk and checking out
DetailView, I see that
self.object is assigned in
It seems like this might be a better place to define
self.object_list, if only to keep similar actions and information grouped together:
It’s really a matter of style and readability at this point. The important thing is that
get_context_data should have access to the list of associated objects so the
context object can be built to send to the template.
Because mixins and generics are by their definition inheritable, other folks have created libraries with more functionality. Take a look at django-extra-views and django-braces for more classes and mixins. Or write your own!
This seems really complicated. Should I even use CBVs at all?
There are vocal developers who advocate going all-in for Django’s generic views as well as folks who eschew class-based views entirely, preferring the composition and readability of function-based views over inheritance and extensibility provided by CBVs and generics. You’ll probably find you fall somewhere in the middle. Personally, it makes sense to me to write views which inherit from
TemplateView and to use Django’s generics when working with the standard CRUD operations. If you start with a basic CBV and later decide that you need to add more functionality, you can always add a mixin or swap out your parent view class for one of the generics.
Working through the stack trace can sometimes be tricky, and the nature of a system which spans multiple files and complex class inheritance can be confusing at times. Keep http://ccbv.co.uk open in another tab, and rely on the descriptive attribute and function naming conventions and you’ll be fine. Or just stick with function-based views! Still, unless you only ever work on code you’ve written yourself, you’re bound to encounter CBVs out in the wild. Might as well be comfortable with them.
General Advice for All Types of Views
Try to keep your views as simple as possible. Views, as their name suggest, are designed to hold presentational, not business, logic. It’s a good idea to include a
services.py module for that. This will increase readability in your code and also act as a sort of ‘internal API’, decoupling your presentation from your database. Suppose, for example, you store data in a third-party CMS but later down the line decide to host that content yourself? With this ‘services’ domain model, you only have to make changes in one location,
services.py. If you take care not to modify the calling signature in your service methods, your views can continue to call them without any change in the view code.