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 adispatch()
method to call the appropriate method depending on the HTTP verb (get
,post
, etc) - They must accept an
HttpRequest
object as its first positional argument - They must return an
HttpResponse
object or raise an exception
When writing views, we have a choice: function- or class-based? Both have their advantages and disadvantages.
Function-Based Views
- 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
Class-Based Views
- can be a bit more implicit and difficult to read, especially when using Django’s generic views
- rely on the
dispatch()
method inherited fromView
to 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
An Example
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_form_class
and 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 self.object()
.
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 fromdjango.views.generic.CreateView
:
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.html
by default — you can change this by setting the class attributetemplate_name
andtemplate_name_suffix
- we need to specify
model
andform_class
attributes, because the methods you inherited fromCreateView
rely on them. - we either need to specify a
success_url
as a class attribute on the view, or else defineget_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
fields
class 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
Recipe
view which also has a list of associatedIngredient
s. Start with theDetailView
as your base. - Add mixins to build functionality. To add
Ingredient
s to ourRecipe
's detail view, we use theMultipleObjectMixin
, 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 Recipe
model.
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
and 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 get()
:
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 View
or 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.