Comprehending Class-Based Views in Django - Creating a CBV

In this article, we will look at how to create a class-based view by first implementing a minimal CBV and then expanding on it to highlight how CBVs can be designed for reuse. The first article of this series, looked at how a class-based view is initiated and how the View class is used as a base to setup the view and initially route the logic. Since View cannot be used as a standalone CBV itself, we need to define additional logic to handle the request properly and provide the correct response back to the user.

Prerequisites

  • Have read the previous article - The View Base Class
  • Basic understanding of class inheritance in Python

A Minimal CBV

Let's look at the minimal implementation of a class-based view. We will create a CBV in a views.py file by first importing the View base class. Our view, BasicView, will be a class that inherits from View.

# app-level views.py
from django.views.generic import View

class BasicView(View):
    pass

This view does nothing, so we need to add functionality for it to handle at least one type of HTTP request. Let's add a method to handle a GET request.

from django.shortcuts import render # New
from django.views.generic import View

class BasicView(View):
    def get(self, request, *args, **kwargs):
        return render(request, "basic.html")

We've defined a new method on the class, get(), which only returns a call to the newly imported render function. At this point, we have made a fully functioning view that will render a template, basic.html, to the user. If we only needed to render a static page that had all the necessary information in the template already, then this view would be complete.

While this view is minimal with very little logic, it demonstrates the basic components necessary for a working CBV. A CBV must have at least one method that matches the expected HTTP verb from the request. In this example, we would only be expecting a GET request which results in the get() method being called. If you don't remember how get() is being called, take a look at the previous article, specifically the dispatch() method. The view also needs to return an HTTP response. Our example view used the render shortcut to return a template in the response, but could have used HttpResponse or one of it's sub-classes.

Let's expand our view to handle both GET and POST requests.

from django.shortcuts import render
from django.views.generic import View

class BasicView(View):
    def get(self, request, *args, **kwargs):
        return render(request, "basic.html")

    def post(self, request, *args, **kwargs):
        # Do something with POST data
        return render(request, "basic_post.html")

Now when a POST request is made, the post() method will be called which will render a different template after any processing of the POST data. We can add logic to handle any other type of request by adding the appropriately named HTTP request method to the view.

So far we have only used one method for each request type. The get() or post() method is called and that method directly returns a response. This isn't necessary though, we could structure the view so that get() and post() return by calling another method which then returns a response.

from django.shortcuts import render
from django.views.generic import View

class BasicView(View):
    def get(self, request, *args, **kwargs):
        return self.return_response(request)

    def post(self, request, *args, **kwargs):
        # Do something with POST data
        return self.return_response(request)

    def return_response(self, request):
        return render(request, "basic.html")

Here both get() and post() return by calling return_response() which will return the final response with the template. We could have also added other intermediary method calls along the way between the initial call to get() or post() and return_response() if we wanted to separate out different logic. This highlights an important part of how class-based views are created: the basic composition of CBVs is methods calling other methods until a method returns a response.

Make a CBV Reusable

To understand how CBVs can be composed with multiple methods and be reused for multiple purposes, let's build a slightly more complex view. We'll create a basic version of Django's built in generic class-based ListView (we'll cover generic class-based views in a later article). This view gets a list of objects from the database and returns them in the context dictionary to be displayed in a given template. Let's assume we have model, Country, that stores information about a country, and another model, City, that contains information about a city.

A CBV that would provide a list of all Country objects could look like this:

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

class BasicListView(View):
    def get(self, request, *args, **kwargs):
        countries = Country.objects.all()
        context = {"country_list": countries}
        return render(request, "list.html", context)

This view handles only a GET request by retrieving all Country objects from the database, storing them in context, and then returning a template with the context dictionary. We've created the view in such a way that ties it very closely to the Country model. Country is explicitly stated in Country.objects.all() to get all object instances, we've stored that queryset in a variable called countries, and in the context dictionary we named the key to the queryset country_list. All of this means that the view isn't reusable if we wanted to use it to display of list of other information, like a list of City objects.

Using class attributes

We can make the view more generic by moving explicit references to the model out of the get() method and doing some renaming.

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

class BasicListView(View):
    model = Country

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

We defined a class attribute, model, and assigned Country to it. Now in the in get() method, we can access this attribute through self.model. By renaming the queryset and the context dictionary key to something more generic, we have made the get() method suitable for any model that we might want to display a list of objects of.

At this point, we refactored the view to make it more generic, but we are still only using it to display a list of Country objects. Why go through all this work then?

One of the key features of class-based views is their reusability. Since classes can inherit from other classes, like we do with BasicListView by inheriting from the View class, we can reuse the functionality of one class in other classes. Let's make our view even more generic

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

class BasicListView(View):
    model = None # Remove connection to specific model

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

All we've done is set the model attribute to None. Now the class isn't functional on its own anymore since it has no model to get a list of objects from. But we can think of this view as an extended base class that other views can use. Since we've already defined the get() logic in this view, any CBV that inherits from it only needs to define a model to function correctly.

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

We created CountryListView and CityListView which both inherit from BasicListView (which in turn inherits from View) and only contain one line in their definition to indicate which model to use. When these views are requested by the user, the correct model will now be used in the get() method that comes from BasicListView.

Since CountryListView and CityListView only need to define the model being used, there is actually another way that we can get the same functionality these views provide without defining them individually. If we look back to the View class we know that we can use keyword arguments in the .as_view() method call in the URLconf. These arguments are then used to set class attributes on the view. Since model is a class attribute and is the only specific logic that CountryListView and CityListView define, we don't even need to define these two views and can instead use BasicListView with model as a keyword argument.

# urls.py
from django.urls import path
from .views import BasicListView
from .models import Country, City

urlpatterns = [
    path("country-list/", BasicListView.as_view(model=Country), name="country_list"),
    path("city-list/", BasicListView.as_view(model=City), name="city_list")
]

With model passed as a keyword argument, we now have the same functionality without defining specific views for each model we want to display as a list. This is a valid way to use a CBV and take advantage of their reusability and flexibility, but code readability should also be considered before using .as_view() and keyword arguments to create new views. If we have to revisit our code later, will we remember that we used keyword arguments instead of explicitly defining the view? And maybe more importantly, what if we want to add more specific logic to one of the views that isn't available in the base class?

Adding more methods

The second question leads us to look at how CBVs can be created in such a way that allows us to add specific logic without rewriting the entire view. Let's look back at BasicListView.

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

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)

Aside from being able to define new views that inherit from it and defining the model to use, we can't do much else besides just show a list of all objects. If we wanted to only show a list of a subset of objects or perform some transformation to all objects before adding them to context, we would have to overwrite the get() method, which means that we are basically creating a whole new view.

To prevent us from having to rewrite a view entirely just to add some functionality, we can break down the view logic into separate methods. We then only need to overwrite the methods that we want to change. Here is how we could change BasicListView to allow us to change what data is added to context.

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

class BasicListView(View):
    model = None
    template_name = "list.html" # Moved the template to an attribute

    def get(self, request, *args, **kwargs):
        objects = self.get_object_list() # Objects now come from another method  
        context = {"object_list": objects} 
        return render(request, self.template_name, context)

    # Method to get objects
    def get_object_list(self):
        objects = self.model.objects.all()
        return objects

We've done a couple of things here. First, to make the view even more generic, we removed the template from being explicitly named in the render function. This means we can more easily overwrite it if we want to use a different template. Then we created a new method get_object_list() and moved logic to it that was previously in get() to retrieve the list of objects. These objects are returned and then assigned to context and used in render, just like before.

With can use this new implementation of BasicListView in multiple ways. We can change the model and have a different list of all model instances available in the template, or we can define a completely different queryset and make whatever list of objects we choose to pull from the database available.

For example, if we wanted to have a view to list all countries, but a separate view to only list cities that end with "town", our views could look like this:

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

class BasicListView(View):
    model = None
    template_name = "list.html"

    def get(self, request, *args, **kwargs):
        objects = self.get_object_list()
        context = {"object_list": objects} 
        return render(request, self.template_name, context)

    def get_object_list(self):
        objects = self.model.objects.all()
        return objects

class CountryListView(BasicListView):
    model = Country

class CityListView(BasicListView):
    model = City
    
    def get_object_list(self):
        objects = self.model.objects.filter(name__endswith="town")
        return objects

In CityListView to get a specific list of objects, all we had to do was overwrite get_object_list() to make the specific changes we wanted instead of having to completely overwrite all of get() and duplicate much of the code.

We could have implemented BasicListView differently by creating an attribute that defines the queryset to get the objects instead of defining a separate method. Then we would only need to define the attribute instead of overwriting the method.

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

class BasicListView(View):
    model = None
    template_name = "list.html"
    objects = None  # Objects in now an attribute

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

class CountryListView(BasicListView):
    model = Country
    objects = model.objects.all()

class CityListView(BasicListView):
    model = City
    objects = model.objects.filter(name__endswith="town")

This comes with the tradeoff that we wouldn't be able to do any extra processing of the object list before adding it to the context dictionary and we now need to define objects for each sub-class.

Alternatively, we could break down the view into more methods to allow more specific customization.

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

class BasicListView(View):
    model = None
    template_name = "list.html"

    def get(self, request, *args, **kwargs):
        objects = self.get_object_list()
        context = self.get_context_data(objects)
        return render(request, self.template_name, context)

    def get_object_list(self):
        objects = self.model.objects.all()
        return objects

    def get_context_data(self, objects):
        return {"object_list": objects}

class CountryListView(BasicListView):
    model = Country

class CityListView(BasicListView):
    model = City
    
    def get_object_list(self):
        objects = self.model.objects.filter(name__endswith="town")
        return objects

    def get_context_data(self, objects):
        context = super().get_context_data(objects)
        context.update({"extra_context": "Some extra data"})
        return context

We added a method get_context_data() which takes the list of objects and returns them in a dictionary. This method doesn't initially seem to do anything important that would require it to be separate. But by breaking it out, we are able to include additional logic to add more data to the context dictionary if we wanted to. In CityListView we have used the hook that get_context_data() provides to add data to context. We first use super() to call the original version of get_context_data() defined in BasicListView. This returns the original data of context. Then we can update context to include more data before returning it back to get().

At this point we have started to make our initial version of BasicListView more complex. There are many more attributes or methods that could be added to make the view even more reusable and to add different functionality, such as pagination. Luckily, Django comes with several built in generic class-based views that implement this logic for us. These views have attributes and methods that can be overwritten to customize the view like we did in the example views in this article. Additionally, mixins can be used to share logic between views even when those views perform different functions. We'll look at generic class-based views and mixins in future articles in this series.

Summary

Class-Based Views perform operations based on the request type and can be as simple or as complex as necessary depending on the need for reusability.

  • CBVs need to inherit from the View base class
  • The basic composition of CBVs is methods calling other methods until a method returns a response
  • CBVs need to have methods defined to handle the expected HTTP request method used to access the view
  • CBVs can be made more reusable by defining class attributes or by breaking down the view logic into separate methods that can be overwritten

Helpful Resources

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