Comprehending Class-Based Views in Django - Generic Views

Using class-based views can feel like a lot of extra work when you only need the view to do something simple, like provide a list of items from the database or the details of one specific item. In the last article, we looked briefly at how we could make a CBV reusable by pulling out model-specific information into class attributes and by breaking down the view logic into different methods. Luckily, we don't always need to do this ourselves. Django comes with a set of built-in class-based views that are ready to go for a variety of tasks.

Prerequisites

Class-Based Generic Views

Class-based generic views(CBGVs) are a built-in feature of Django that reduce the lines of code we need to write to create views that perform common tasks. Generic views exist for a wide range of patterns we might want to implement including listing a group of objects, displaying the details about a single object, handling forms, updating or deleting objects, redirecting, and more. We have actually been using a generic view already in this series as the View base class is a generic view. We know that View isn't a complete view on it's own, but it is generic since it takes care of some logic for us so we don't have to repeat it ourselves in every CBV we write.

Before we go any further, I want to point out that as you work with generic class-based views, you will likely need to keep some form of reference documentation close by since these views have a lot of attributes and methods to keep track of. Some good references to know:

Using a Generic View

The best way to see the utility of class-based generic views is to use one. In our previous example, we built out views to display a list of countries and a list of cities. With generic views, we can quickly create views that perform the same function with fewer lines of code.

Previously, to create two views that display all Country and City objects we had

from django.shortcuts import render
from django.views.generic import View
from .models import Country, City

class BasicListView(View):
    model = None

    def get(self, request, *args, **kwargs):
        objects = self.model.objects.all()
        context = {"object_list": objects}
        return render(request, "list.html", context)

# View to display a list of countries
class CountryListView(BasicListView):
    model = Country

# View to display a list of cities
class CityListView(BasicListView):
    model = City

With generic views, our code would be

from django.views.generic import ListView
from .models import Country, City

# View to display a list of countries
class CountryListView(ListView):
    model = Country
    template_name = "list.html"

# View to display a list of cities
class CityListView(ListView):
    model = City
    template_name = "list.html"

Right away we can see some big differences. First, when we define the class, we are no longer inheriting from the View base class. Instead, we import the generic ListView and have our classes inherit from it. Second, we don't define any methods. All we do is set two class attributes, model and template_name. That's it.

Let's look at another example. Suppose we want a view that displays the details about a specific country or city. To do this, we can use the generic DetailView.

from django.views.generic import DetailView
from .models import Country, City

# View to display details of a country
class CountryDetailView(DetailView):
    model = Country
    template_name = "detail.html"

# View to display details of a city
class CityDetailView(DetailView):
    model = City
    template_name = "detail.html"

These views return a template with a context dictionary containing the specific object we want to display. Again, all we did was create a class that inherits from the generic view and set the values of a few attributes.

It is important to note that it just so happens that using ListView and DetailView requires setting the same two attributes. This is not the case for all generic views, so don't expect this to work in all cases. Also, when using DetailView, we made an assumption that the view is being reached by a URL that includes the primary key of the object we want to see the details of, but this could be configured differently if we wanted it to. Finally, I will mention that specifying a value for template_name isn't actually necessary at all, but not doing so would require us to understand what template Django would look for automatically to use with the view. Understandably, all of these notes and caveats can make using generic views confusing at first.

Now we've seen the basic concept of how to use a class-based generic view: inherit from the generic view that has the logic we want and then set the value of some class attributes. But we don't really know what is going on or what any of the underlying logic is. The code with CBGVs is substantially shorter but how do we know what is happening? We don't write or see a get() method and don't explicitly render anything, so what is really going on? To better understand that, we need to look at what we aren't doing - writing methods.

Behind the Scenes

In the examples above, we aren't writing any methods or explicitly returning anything. There is no explicit get() method or final return statement. This seems to go against what we saw previously about the basic composition of a CBV being methods calling other methods until a method returns a response. But even though we didn't define any methods ourselves, that doesn't mean there aren't any. All the methods in generic views are initially "hidden" and only become visible when we explicitly want them to.

If we want to understand what logic is being executed in a generic view, we have to dig a little deeper into how these views are composed. You'll notice that in our list example, our views are inheriting from ListView and not explicity from View like they did in previous articles. We know that View plays an important role in setting up and kicking off the events needed to properly handle the logic of the view. So View, or at least it's same logic, must be present somewhere in ListView. If we search for the definition of the ListView class in the Django source code, we see this

class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
        """
        Render some list of objects, set by `self.model` or `self.queryset`.
        `self.queryset` can actually be any iterable of items, not just a queryset.
        """

It turns out that ListView itself doesn't contain any code at all. Instead, it inherits from two other classes: MultipleObjectTemplateResponseMixin and BaseListView. This is an important observation. Similar to how our views inherited from ListView, ListView also inherits from other classes. This is true for all generic views and is how different generic views are made - by mixing different classes together to get the desired functionality. We can dig deeper by looking at the classes that ListView inherits from. Let's look at BaseListView.

class BaseListView(MultipleObjectMixin, View):
    """A base view for displaying a list of objects."""
    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()

        if not allow_empty:
            # When pagination is enabled and object_list is a queryset,
            # it's better to do a cheap query than to load the unpaginated
            # queryset in memory.
            if self.get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'):
                is_empty = not self.object_list.exists()
            else:
                is_empty = not self.object_list
            if is_empty:
                raise Http404(_('Empty list and “%(class_name)s.allow_empty” is False.') % {
                    'class_name': self.__class__.__name__,
                })
        context = self.get_context_data()
        return self.render_to_response(context)

BaseListView inherits from two views with one being the View class. Now we know that we do have the logic View provides and it wasn't rewritten somewhere else in the view, it was inherited directly from View itself. We also see that BaseListView has some actual logic code in it, specifically the get() method. get() does contain calls to other methods that aren't defined in BaseListView though. This means that these methods exist somewhere else in the series of classes that ListView is composed of. We could look into all the classes in the class hierarchy of ListView to find all the methods and see what they do, but many of the references mentioned above already do that for us. The CBV diagrams reference provides a way to see the logic flow for a generic view starting at the top instead of having to start at the bottom and work to the top.

In addition to methods, there are many attributes that we don't see unless we need or want to set them to values different than the default. Luckily, many times we don't really need to see what is happening in the background, but it can be important to understand what attributes the class has since changing them can be a quick way to modify the view without having to add any custom logic. In the case of ListView, there are 16 different attributes that exist in the view. We used two, model and template_name, in the example, but could have specified others, like a specific name for the context object or if we wanted to have pagination of the returned results. Looking at your reference documentation of choice is a must in these situations as it will make it easier to understand what attributes are available.

Even though we don't see all the attributes and methods of a generic view, they do exist and our basic idea of a class-based view being a chain of method calls that eventually return a response holds true. Fully understanding the logic requires us to look at the source code or dig through documentation, a task that can feel overwhelming when just starting out. Fortunately, if we want to use a generic view with slight modification, there is a middle ground that exists between using a generic view blindly and understanding every single line of code in it.

Modifying a Generic View

Class-based generic views can automatically do a lot of the heavy lifting for us when it comes to creating views. However, there will be times when they don't quite do everything we need, no matter what attributes we set. When there is custom logic that we need to add to a generic view, we have to insert that logic into the existing framework of the view. We can't just add our own logic to the view when we define it because a generic view already has a complete logic flow and it isn't looking for any methods we create on our own outside of that flow. This means in order to change the view's logic, we have to modify the methods that already exist.

Modifying a generic view's existing methods can be simple or complex depending on what and where logic needs to be added in the overall flow. The simplest way is to take a method that already exists and completely overwrite it.

Overwriting an existing method

Let's say that we wanted to display a specific number of countries ranked by population, such as the top 10, 20 or 50 most populous countries.

We could do this by subclassing ListView and using the queryset attribute to select the specific number of Country objects ordered by population. To show the top 10 countries ranked by population, the view could look like this

rom django.views.generic import ListView
from .models import Country

# View to display top 10 countries ranked by population
class CountryListPopulation10View(ListView):
    model = Country
    template_name = "list.html"
    queryset = self.model.objects.order_by('-population')[0:10]

This way of doing things creates lots of repetition. To create views to list a different number of countries, we would need to create separate views that are almost identical but have a different number than 10. This is a far from ideal situation.

A better way is to copy our CountryListView and modify it to dynamically create the list for us using only keyword arguments that come from the URL. This only requires having one view that can handle any different number of length lists we want to display.

We will override the get_queryset() method to achieve this. get_queryset() is a method in ListView that comes from the MultipleObjectMixin and contains the following code

def get_queryset(self):
    """
    Return the list of items for this view.
    The return value must be an iterable and may be an instance of
    `QuerySet` in which case `QuerySet` specific behavior will be enabled.
    """
    if self.queryset is not None:
        queryset = self.queryset
        if isinstance(queryset, QuerySet):
            queryset = queryset.all()
    elif self.model is not None:
        queryset = self.model._default_manager.all()
    else:
        raise ImproperlyConfigured(
            "%(cls)s is missing a QuerySet. Define "
            "%(cls)s.model, %(cls)s.queryset, or override "
            "%(cls)s.get_queryset()." % {
                'cls': self.__class__.__name__
            }
        )
    ordering = self.get_ordering()
    if ordering:
        if isinstance(ordering, str):
            ordering = (ordering,)
        queryset = queryset.order_by(*ordering)
    return queryset

To make sure that our version of get_queryset() will work with the rest of the preexisting logic in the view, we need to make sure it returns an iterable, as stated in the docstring.

Our version only requires one line of code that gets all the objects of the Country model, orders them by population, and then takes the top number of instances as determined by the keyword argument in the URL, which is accessible through self.kwargs.

def get_queryset(self):
      return self.model.objects.order_by('-population')[0:self.kwargs['num_of_countries']]

For this to work, we need to make sure we have properly setup the corresponding URLConf to be something like this

path('country-population-ranking/<int:num_of_countries>', CountryListPopulationView.as_view(), name='country-population')

where we define the keyword argument num_of_countries to be an integer that will be used in get_queryset() to determine how many countries to display.

Putting our version of get_queryset() together with the existing code we have from CountryListView gives us our final view.

from django.views.generic import ListView
from .models import Country

class CountryListPopulationView(ListView):
    model = Country
    template_name = "list.html"

    def get_queryset(self):
        return self.model.objects.order_by('-population')[0:self.kwargs['num_of_countries']]

By overriding get_queryset() we were able to essentially swap out the version of the method built into ListView with our own version. Sometimes we don't want to completely replace a method though, we just want to modify it or add some extra logic to it.

Adding to an existing method

In addition to listing a certain number of countries by population we may also want to include the average population of all countries in the list. We want to add this item to the context dictionary so it will be available in the template. In this situation, we don't want to change any of the existing logic in the view, we just want to add some of our own. We can do this by taking advantage of class inheritance and Python's super() function.

If we look at the code for ListView, we will find that the context dictionary gets built in the get_context_data() method. This method resides in the MultipleObjectMixin. The whole mixin is too long to show here, but the get_context_data() method contains the following code

def get_context_data(self, *, object_list=None, **kwargs):
    """Get the context for this view."""
    queryset = object_list if object_list is not None else self.object_list
    page_size = self.get_paginate_by(queryset)
    context_object_name = self.get_context_object_name(queryset)
    if page_size:
        paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
        context = {
            'paginator': paginator,
            'page_obj': page,
            'is_paginated': is_paginated,
            'object_list': queryset
        }
    else:
        context = {
            'paginator': None,
            'page_obj': None,
            'is_paginated': False,
            'object_list': queryset
        }
    if context_object_name is not None:
        context[context_object_name] = queryset
    context.update(kwargs)
    return super().get_context_data(**context)

This method calls other methods to build the context dictionary. If we look at the return statement, we see that it isn't returning the context dictionary directly, but is instead returning by calling super().get_context_data(**context).

MultipleObjectMixin is a subclass of ContextMixin which also includes a get_context_data() method.

class ContextMixin:
    """
    A default context mixin that passes the keyword arguments received by
    get_context_data() as the template context.
    """
    extra_context = None
    
    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs

By using the super() function in the return statement of get_context_data() in MultipleObjectMixin, the view is able to also call get_context_data() from the ContextMixin. The method in ContextMixin takes the context dictionary and adds to it before finally returning the dictionary itself.

We can follow the same approach to add the average population to the context dictionary. We want to maintain all the existing functionality so we need to make sure that get_context_data() is called as it normally would. We can then take the resulting dictionary and add a key-value pair for the average population.

Our version of get_context_data() will look like this:

def get_context_data(self):
    # Execute existing logic and store the resulting context dictionary in kwargs
    kwargs = super().get_context_data()
    # Calculate the average population of the countries in the queryset
    average_population = kwargs["object_list"].aggregate(avg_pop=Avg("population"))
    # Update the context dictionary
    kwargs.update(average_population)
    # Return the context dictionary back to the original function call in the main view logic
    return kwargs

We first call super().get_context_data() which performs all the preexisting logic of get_context_data() in ListView. We know the end result of those methods is the context dictionary, kwargs, so we assign that name to it. Next, we utilize Django's aggregatefeature to calculate the average population of the queryset. This will return a dictionary with a key of avg_pop and a value of the average of the populations. We need to import the Avg function from django.db.models for this to work, which we'll do when we incorporate it with the rest of the view code. We then add the average population dictionary to kwargs using Python's update() method. Finally, we return the complete context dictionary back to the calling logic in the main body of the view.

Adding this code to what we have already written gets us the complete version of CountryListPopulationView.

from django.views.generic import ListView
from django.db.models import Avg # New import for average population aggregation
from .models import Country

class CountryListPopulationView(ListView):
    model = Country
    template_name = "list.html"

    def get_queryset(self):
        return self.model.objects.order_by('-population')[0:self.kwargs['num_of_countries']]

    def get_context_data(self):
        kwargs = super().get_context_data()
        average_population = kwargs["object_list"].aggregate(avg_pop=Avg("population"))
        kwargs.update(average_population)
        return kwargs

This view now provides a list of the desired number of countries ordered by population as well as makes available the average population of those countries.

Modifying a class-based generic view can feel daunting at first and can get quite complex depending on how much modification is needed, but the two methods shown here can help with customizing a CBGV without having to rewrite the entire view from scratch. No matter what you do though, you will likely have to look at the source code or another resource to understand what methods to modify to achieve the desired result. Don't be afraid to look at the source code or use another resource. While writing these examples, I looked at multiple resources to make sure I was changing the right part of the view. With generic views, make understanding them the goal, not remembering every detail.

Summary

Class-based generic views do a lot of the heavy lifting automatically when it comes to creating common view patterns.

  • Generic views are used by subclassing the view that contains the logic we want
  • Certain attributes need to be defined for the view to work correctly
  • Optional attributes can be changed to modify the behavior of the view
  • Custom logic can be added to the view by overwriting existing methods or adding to an existing method

Helpful Resources

Questions, comments, still trying to figure it out? - Let me know!