Leonardo Giordani
Posted on March 17, 2020
This post was originally posted on The Digital Cat
In the first instalment of this short series, I introduced the theory behind Django class-based views and the reason why in this context classes are more powerful than pure functions. I also introduced one of the generic views Django provides out of the box, which is ListView
.
In this second post I want to talk about the second most used generic view, DetailView
, and about custom querysets and arguments. Last, I'm going to introduce unspecialised class-based views that allow you to build more complex Web pages. To fully understand DetailView
, however, you need to grasp two essential concepts, namely querysets and view parameters. So I'm sorry for the learn-by-doing readers, but this time too I'm going to start with some pure programming topics.
QuerySets or the art of extracting information
One of the most important parts of Django is the ORM (Object Relational Mapper), which allows you to access the underlying database just like a collection of Python objects. As you know, Django provides tools to simplify the construction of DB queries; they are managers (the .objects
attribute of any models, for example) and query methods (get
, filter
, and so on). Pay attention because things here are slightly more complicated than you can think at a first glance.
When you use one of the methods of a manager you get as a result a QuerySet
, which most of the time is used as a list, but is more than this. You can find the documentation about queries here and the documentation about QuerySet
here. Both are very recommended readings.
What I want to stress here is that quesysets are not evaluated until you perform an action that access the content like slicing or iterating on it. This means that we can build querysets, pass them to functions, store them, and even build them programmatically or metaprogramming them without the DB being hit. If you think at querysets as recipes you are not far from the truth: they are objects that store how you want to retrieve the data of your interest. Actually retrieving them is another part of the game. This separation between the definition of something and its execution is called lazy evaluation.
Let me give you a very trivial example to show why the lazy evaluation of querysets is important.
[...]
def get_oldest_three(queryset):
return queryset.order_by['id'][0:2]
old_books = get_oldest_three(Book.objects.all())
old_hardcover_books = \
get_oldest_three(Book.objects.filter('type=Book.HARDCOVER'))
As you can see the get_oldest_three
method is just filtering an incoming QuerySet
(which can be of any type); it simply orders the objects and gets the first three inserted in the DB. The important thing here is that we are using querysets like pure algorithms, or descriptions of a procedure. When creating the old_books
variable we are just telling the get_oldest_three
method "Hey, this is the way I extract the data I'm interested in. May you please refine it and return the actual data?"
Being such flexible objects, querysets are an important part of generic views, so keep them warm for the upcoming banquet.
Being flexible: parametric views
URLs are the API of our Web site or service. This can be more or less evident for the user that browses through the pages, but from the programmer's point of view, URLs are the entry points of a Web-based service. As such, they are not very different from the API of a library: here, static pages are just like constants, or functions that always return that same value (such as a configuration parameter), while dynamic pages are like functions that process incoming data (parameters) and return a result.
So URLs can accept parameters, and our underlying view shall do the same. You basically have two methods to convey parameters from the browser to your server using HTTP. The first method is named query string and lists parameters directly in the URL through a universal syntax. The second method is storing parameters in the HTTP request body, which is what POST requests do. We will discuss this method in a later post about forms.
The first method has one big drawback: most of the time URLs are long (and sometimes too long), and difficult to use as a real API. To soften this effect the concept of clean URL arose, and this is the way Django follows natively (though, if you want, you can also stick to the query string method).
Now, the Django official documentation on URL dispatcher tells you how you can collect parameters contained in the URL parsing it with a regular expression; what we need to discover is how class-based views receive and process them.
In the previous post we already discussed the as_view
method that shall instance the class and return the result of dispatch
(CODE).
def as_view(cls, **initkwargs):
"""
Main entry point for a request-response process.
"""
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.request = request
self.args = args
self.kwargs = kwargs
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view
Now look at what the view
wrapper function actually does with the instanced class (CODE); not surprisingly it takes the request
, args
and kwargs
passed by the URLconf and converts them into as many class attributes with the same names. Remember that URLconf is given this function itself, not the result of the call, which is the result of dispatch
.
This means that anywhere in our CBVs we can access the original call parameters simply reading request
, args
and kwargs
, where *args
and **kwargs
are the unnamed and named values extracted by the URLconf regular expression.
Getting details
Just after listing things, one of the most useful things a Web site does is giving details about objects. Obviously any e-commerce site is made for the most part by pages that list products and show product details, but also a blog is made of one or more pages with a list of posts and a page for each of them. So building a detailed view of the content of our database is worth learning.
To help us in this task Django provides DetailView
, which indeed deals, as the name suggests, with the details of what we get from the DB. While ListView
's basic behaviour is to extract the list of all objects with a given model, DetailView
extracts a single object. How does it know what object shall be extracted?
When dispatch
is called on an incoming HTTP request the only thing it does is to look at the method
attribute, which for HttpRequest
objects contains the name of the HTTP verb used (e.g. 'GET'
); then dispatch
looks for a method of the class with the lowercase name of the verb (e.g. 'GET'
becomes get
) (CODE). This handler is then called with the same parameters of dispatch
, namely the request
itself, *args
and **kwargs
(CODE).
DetailView
has no body and inherits everything from two classes, just like happened for ListView
; the first parent class is the template mixin, while the second one, BaseDetailView
, implements the get
method (CODE).
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
As you can see, this method extracts the single object that it shall represent calling get_object
, then calls get_context_data
(that we already met in the previous post) and last the familiar render_to_response
. The method get_object
is provided by BaseDetailView
's ancestor SingleObjectMixin
(CODE): the most important parts of its code, for the sake of our present topic are
def get_object(self, queryset=None):
[...]
if queryset is None:
queryset = self.get_queryset()
pk = self.kwargs.get(self.pk_url_kwarg, None)
[...]
if pk is not None:
queryset = queryset.filter(pk=pk)
[...]
try:
obj = queryset.get()
[...]
return obj
Warning: I removed many lines from the previous function to improve readability; please check the original source code for the complete implementation.
The code shows where DetailView
gets the queryset from; the get_queryset
method is provided by SingleObjectMixin
itself and basically returns queryset
if present, otherwise returns all objects of the given model (acting just like ListView
does). This queryset
is then refined by a filter
and last by a get
. Here get
is not used directly (I think) to manage the different error cases and raise the correct exceptions.
The parameter pk
used in filter
comes directly from kwargs
, so it is taken directly from the URL. Since this is a core concept of views in general I want to look at this part with some extra care.
The DetailView
class is called by an URLconf that provides a regular expression to parse the URL, for example url(r'^(?P<pk>\d+)/$',
. This regex extracts a parameter and gives it the name pk
, so kwargs
of the view will contain pk
as key and the actual number in the URL as value. For example the URL 123/
will result in {'pk': 123}
. The default behaviour of DetailView
is to look for a pk
key and use it to perform the filtering of the queryset, since pk_url_kwarg
is 'pk'
(CODE).
So if we want to change the name of the parameter we can simply define the pk_url_kwarg
of our class and provide a regex that extract the primary key with the new name. For example url(r'^(?P<key>\d+)/$',
extracts it with the name key
, so we should define pk_url_kwarg = 'key'
in our class.
From this quick exploration we learned that a class inheriting from DetailView
:
- provides a context with the
object
key initialized to a single object -
must be configured with a
model
class attribute, to know what objects to extract -
can be configured with a
queryset
class attribute, to refine the set of objects where the single object is extracted from -
must be called from a URL that includes a regexp that extracts the primary key of the searched object as
pk
-
can be configured to use a different name for the primary key through the
pk_url_kwarg
class attribute
The basic use of DetailView
is exemplified by the following code.
class BookDetail(DetailView):
model = Book
urlpatterns = patterns('',
url(r'^(?P<pk>\d+)/$',
BookDetail.as_view(),
name='detail'),
)
The view extracts a single object with the Book
model; the regex is configured with the standard pk
name.
As shown for ListView
in the previous post, any CBV uses get_context_data
to return the context dictionary to the rendering engine. So views that inherit from DetailView
can add data to the context following the same pattern. Suppose we have a function get_similar_books
that given a book, returns similar ones according to some criteria.
class BookDetail(DetailView):
model = Book
def get_context_data(self, **kwargs):
context = super(BookDetail, self).get_context_data(**kwargs)
context['similar'] = get_similar_books(self.object)
return context
urlpatterns = patterns('',
url(r'^(?P<pk>\d+)/$',
BookDetail.as_view(),
name='detail'),
)
As explained before, you can access the object being shown through object
, which in the above example is passed to a service function we implemented somewhere in our code.
Using the base views
Sometimes, when dealing with complex pages, the generic display CBVs that Django provides are not the right choice. This usually becomes evident when you start overriding method to prevent the view to perform its standard behaviour. As an instance say that you want to show detailed information of more than one object: probably DetailView will soon show its limits, having been built to show only one object.
In all those cases that cannot be easily solved by one of the generic display CBVs, your have to build your own starting from one of the base views: RedirectView
, TemplateView
, or View
(DOCS, CODE).
I'm not going to fully describe those views; I want however to briefly point out some peculiarities.
View
is by now an old friend of us; we met it when we discussed the as_view
and dispatch
methods. It is the most generic view class and can be leveraged to perform very specialized tasks such as rendering pages without templates (for example when returning JSON data).
TemplateView
is the best choice to render pages from a template, maintaining a great level of freedom when it comes to the content of the context dictionary. Chances are that this is going to be the view you will use the most after ListView
and DetailView
. Basically you just need to inherit from it and define the get_context_data
method. As you can see from the CODE TemplateView
answers to GET requests only.
RedirectView
, as the name implies, is used to redirect a request. The redirection mechanism is very simple: its get
method returns a HttpResponseRedirect
to the URL defined by the url
class attribute. The class exhibits a very interesting behaviour (CODE) when called with HTTP methods other than GET (namely HEAD, POST, OPTIONS, DELETE, PUT, or PATCH): it "converts" the method to GET simply calling get
from the respective method (head
, post
, and so on). In the next post I'll show how to leverage this simple technique to show the user a pre-filled form.
Date-based views
Django provides other class-based views that simplify dealing with objects extracted or ordered by date. As a programmer, you know that sometimes dealing with dates is awkward, to say the least; views such as YearArchiveView
or DayArchiveView
(CODE) aim to help you to tame your date-based objects; any object that contains a date (e.g. post date for articles, birth date for people, log date for messages, etc.) can be processed by these views. You can find the official documentation here.
Remember that date-based views are CBVs, so they are based on View
, just like ListView
or TemplateView
. So, apart from their specialization on date processing, they behave the same (using get_context_data
, get
, dispatch
, and so on).
Final words
In this post we covered DetailView
in deep and, more superficially, all the remaining base and data-based views. I showed you how DetailView
uses the given model and the querystring parameters to find the requested object, and how you can change its default behaviour. In the next post we will step into the rich (and strange) world of forms.
Read more posts like this on The Digital Cat
Posted on March 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.