Function-based views are typically easy for beginners to understand. Just create a function that receives a request object and returns a response object. But class-based views (CBV) tend to cause more confusion. Most of the functionality lives in out-of-sight base views and mixins, so using a CBV usually means overwriting a handful of class attributes and methods.

When I started using CBVs, I had no mental model how they worked. The result was lots of googling and trying to make sense of Django source code. Over time I learned how views fit together, but the journey there was tedious and at times confusing.

What This Tutorial Is

This tutorial aims to conceptually connect function-based views with class-based views. More specifically, we’ll build views that provide a JSON-only, REST API endpoint for a /very/ simple Django application. By starting with something familiar, we can refactor the code into custom class-based views. The result is a base view class with mixins and generics, similar in design to the classes used by Django and Django-Rest-Framework.

What This Tutorial Isn’t

We aren’t building a perfect reproduction of Django’s class based views. Our views don’t need nearly as much functionality, and we don’t even address topics like content negotiation, headers, or permissions (anyone can edit anything). But I see that as an asset. By understanding the core pieces of CBVs, you’ll understand that Django’s extra functionality comes from adding more layers of code to this simple architecture.

Getting Started

We’ll build our class based views in three main steps.

  1. Built out functionality with function based views
  2. Modify each function into a standalone class-based view
  3. Pull out shared functionality into parent classes and mixins

 

To start, create a new Django project named project and an app named transactions. Make sure to include this app in your settings.py file.

To build the function-based views, we need to update project/urls.py, transactions/models.py, transactions/forms.py, and transactions/views.py.

Start with the models. These are purposefully simple. I only want fields that can be easily converted to JSON, since the focus of this tutorial is the views, not models or data formatting. For this reason, the models don’t relate in any way. This is just your standard web app that only models items for sale and user-to-user text messages. If you need a reason, pretend it’s a Twilio-MVP that also has a swag store.

transactions/models.py

from django.db import models

class Item(models.Model):
    name = models.CharField(max_length=256)
    price = models.IntegerField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


class Message(models.Model):
    to_phone_number = models.CharField(max_length=256)
    from_phone_number = models.CharField(max_length=256)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

Now make and run your database migrations.

Time to update project/urls.py. Our paths have two patterns. items/ and messages/ let us look up existing items/messages by their primary keys. The _detail_view methods can handle detail, update, partial update, and delete operations. The items/ and messages/ paths, along with the corresponding view methods, cover list and create operations.

project/urls.py

from django.urls import path

from transactions import views

urlpatterns = [
    path("items/", views.item_detail_view),
    path("items/", views.item_list_create_view),
    path("messages/", views.message_detail_view),
    path("messages/", views.message_list_create_view),
]

Create the file transactions/forms.py, and define JsonMixin, ItemForm, and MessageForm. JsonMixin is included because forms don’t come with the ability to output all fields to JSON. Django has methods that convert objects to JSON, but it’s difficult output both editable and non-editable fields. Because of JsonMixin, the view methods can call form.to_json and get properly formatted JSON objects in response.

transactions/forms.py

from datetime import datetime
from django.forms import ModelForm
from transactions.models import Item, Message

class JsonMixin:
    def format_datetime(self, fields):
        for field, value in fields.items():
            if isinstance(value, datetime):
                iso, _ = value.isoformat(timespec="milliseconds").split("+")
                fields[field] = iso + "Z"
        return fields

    def to_json(self):
        serialized = {
            field.name: getattr(self.instance, field.name)
            for field in self._meta.model._meta.get_fields()
        }

        return self.format_datetime(serialized)

class ItemForm(ModelForm, JsonMixin):
    class Meta:
        model = Item
        fields = "__all__"  # Don't do this. For real apps, define fields explicitly.


class MessageForm(ModelForm, JsonMixin):
    class Meta:
        model = Message
        fields = "__all__"

Briefly, lets look at how .to_json() and .format_datetime() operate. .to_json()takes a model instance and looks up the model class that created the instance (Item or Message). It then calls .get_fields() on the model class to get the relevant field classes. Using field.name, it looks up the name of the field (name, price, etc.) and gets the attribute with that name from the model instance. The field name and value are then placed in a dictionary together.

This works well for fields where the underlying datatype is a primitive (IntegerField or CharField, for example), because the value is easily printable. Unfortunately, DateTimeField returns a datetime.datetime object, so we need to convert it to an ISO-8601 formatted string, which format_datetime handles.

To be clear, the functionality in JsonMixin is a bit hacky and only handles simple field types. It’s enough functionality for this tutorial, but for real applications, use Django Rest Framework, which provides serializers to handle converting object data to JSON.

Ok, that’s enough setup for us to move on to the views. This is about 80 lines of code, so we’ll cover it in pieces.

First, add the import statements.

transactions/views.py

import json

from django.http import HttpResponse, HttpResponseNotAllowed, JsonResponse
from django.shortcuts import get_object_or_404

from transactions.forms import ItemForm, MessageForm
from transactions.models import Item, Message

Now, let’s take a look at the item_detail_view() function. It starts out by looking up the Item object and returning a 404 response if none exists. Next, it determines functionality based on the HTTP method. Let’s look at each in turn:

  1. GET – Detail View – Uses ItemForm.to_json() method to convert the Item to JSON.
  2. PUT – Update View – Since PUT is used for whole-object updating, edited_item_data must include all editable fields. The form handles validation and object updating.
  3. PATCH – Partial Update View – PATCH allows you to update specific fields, but ItemForm expects input data to include all editable fields. To accommodate these conflicting rules, we first use .to_json() to get the current item data. We then combine this with the newly uploaded values before passing the resulting dictionary to ItemForm.
  4. DELETE – Delete View – Pretty self explanatory.
  5. Default Case – If another HTTP type is used (such as “HEAD” or “POST”), we return HttpResponseNotAllowed.

 

You may notice that this code uses both JsonResponse and HttpResponse, without specifying content_type='application/json' for HttpResponse. This is acceptable because HttpResponse is never used to send actual content. If we included a response message, we’d need to specify the encoding.

transactions/views.py

def item_detail_view(request, id):

    item = get_object_or_404(Item, id=id)

    if request.method == "GET":
        return JsonResponse(status=200, data=ItemForm(instance=item).to_json())
    elif request.method == "PUT":
        edited_item_data = json.loads(request.body)
        form = ItemForm(edited_item_data, instance=item)
        if form.is_valid():    
            form.save()
              return HttpResponse(status=200)
        else:
            return HttpResponse(status=400)
    elif request.method == "PATCH":
        current_item_data = ItemForm(instance=item).to_json()
        edited_item_data = json.loads(request.body)
        updated_item_data = {**current_item_data, **edited_item_data}
        form = ItemForm(updated_item_data, instance=item)
        if form.is_valid():
            form.save()
              return HttpResponse(status=200)
        else:
            return HttpResponse(status=400)
    elif request.method == "DELETE":
        item.delete()
          return HttpResponse(status=200)

    return HttpResponseNotAllowed()

Next, add item_list_create_view(). This is similar to item_detail_view(), with a few pieces worth mentioning.

  1. When an item is successfully created via POST, we return the item content as JSON.
  2. The .to_json() method doesn’t handle encoding multiple item instances. In this simple case, it works to use a list comprehension to encode the items individually before placing them in a list. Since JsonResponse expects the data value to be a dictionary, rather than a list, we have to specify safe=False. This weirdness, like most web development weirdness, is because of a JavaScript quirk.

transactions/views.py

def item_list_create_view(request):
    if request.method == "POST":
        form = ItemForm(request.POST)
        if form.is_valid():
            form.save()
            return JsonResponse(status=201, data=form.to_json())
        else:
            return JsonResponse(status=400)
    elif request.method == "GET":
        items = [ItemForm(instance=item).to_json() for item in Item.objects.all()]
        return JsonResponse(status=200, data=items, safe=False)
    return HttpResponseNotAllowed()

That was a lot. Thankfully, message_detail_view() and message_list_create_view() are exactly the same, only shorter, since message_detail_viewdoesn’t support updating messages, so there’s no need for PUT or PATCH support.

transactions/views.py

def message_detail_view(request, id):

    message = get_object_or_404(Message, id=id)

    if request.method == "GET":
        return JsonResponse(status=200, data=MessageForm(instance=message).to_json())
    elif request.method == "DELETE":
        message.delete()
        return HttpResponse(status=200)
    return HttpResponseNotAllowed()


def message_list_create_view(request):
    if request.method == "POST":
        form = MessageForm(request.POST)
        if form.is_valid():
            form.save()
            return JsonResponse(status=201, data=form.to_json())
        else:
            return JsonResponse(status=400)
    elif request.method == "GET":
        messages = [
            MessageForm(instance=message).to_json() for message in Message.objects.all()
        ]
        return JsonResponse(status=200, data=messages, safe=False)
    return HttpResponseNotAllowed()

While it might be annoying to rewrite almost identical code as before (or, like I did, aggressively copy and paste), it’s a useful illustration of why class-based views are powerful. There’s an enormous amount of identical, or nearly identical, boiler plate code in function-based views.

Now for the fun part. It’s time to migrate our function-based views into class-based views. There won’t be any inheritance yet, so there’s still plenty of duplication, but it lays the groundwork for aggressive refactoring.

Class Based Views – Kinda

For the first step of refactoring, we’ll take the following approach. If the size of this list looks intimidating, I encourage you to take a look at the code first. I suspect you’ll see that the actual code isn’t all that scary; it’s only when written out in semi-formal steps that it looks daunting.

  1. Create a corresponding class for each view function. For example, alongside item_detail_view, we’ll now have the ItemDetailView class.
  2. For every HTTP method supported by a view, the new view class will have a corresponding method. Since item_detail_view supported GET, PUT, PATCH, and DELETE HTTP methods, ItemDetailView will have .get(), .put(), .patch(), and delete() methods.
  3. The original view method now has three responsibilities:
    • Instantiate the corresponding view class.
    • Call the class method to match the HTTP method of the incoming request.
    • Return an error response if a request uses an unsupported HTTP method.
  4. request and id values (for detail views) will now be passed to a class’s .__init__() method and stored as instance attributes. Other methods will reference self.request and self.id when looking up these values.
  5. Detail views will get an .get_object() method that either returns the relevant object or returns a 404 response.

 

Looking at the following code, you should see that it’s quite similar to original function-based approach. We’ve primarily just broken different pieces of functionality into their own methods, and wrapped those methods in a class.

transactions/views.py

def item_detail_view(request, id):
    allowed_methods = ["GET", "PUT", "PATCH", "DELETE"]
    view = ItemDetailView(request, id)

    if request.method not in allowed_methods:
        return HttpResponseNotAllowed(allowed_methods)

    method = getattr(view, request.method.lower())
    return method()


class ItemDetailView:
    def __init__(self, request, id):
        self.request = request
        self.id = id

    def get_object(self):
        return get_object_or_404(Item, id=self.id)

    def get(self):
        return JsonResponse(
            status=200, data=ItemForm(instance=self.get_object()).to_json()
        )

    def put(self):
        edited_item_data = json.loads(self.request.body)
        form = ItemForm(edited_item_data, instance=self.get_object())
        if form.is_valid():
            form.save()
            return HttpResponse(status=200)
        return JsonResponse(status=400)

    def patch(self):
        item = self.get_object()
        current_item_data = ItemForm(instance=item).to_json()
        edited_item_data = json.loads(self.request.body)
        updated_item_data = {**current_item_data, **edited_item_data}
        form = ItemForm(updated_item_data, instance=item)
        if form.is_valid():
            form.save()
            return HttpResponse(status=200)
        return JsonResponse(status=400)

    def delete(self):
        self.get_object().delete()
        return HttpResponse(status=200)

The process for the remaining views is nearly identical.

item_list_create_view()

transactions/views.py

def item_list_create_view(request):
    allowed_methods = ["GET", "POST"]
    view = ItemListCreateView(request)

    if request.method not in allowed_methods:
        return HttpResponseNotAllowed(allowed_methods)

    method = getattr(view, request.method.lower())
    return method()


class ItemListCreateView:
    def __init__(self, request):
        self.request = request

    def post(self):
        form = ItemForm(self.request.POST)
        if form.is_valid():
            form.save()
            return JsonResponse(status=201, data=form.to_json())
        else:
            return JsonResponse(status=400)

    def get(self):
        items = [ItemForm(instance=item).to_json() for item in Item.objects.all()]
        return JsonResponse(status=200, data=items, safe=False)

message_detail_view()

transactions/views.py

def message_detail_view(request, id):

    allowed_methods = ["GET", "DELETE"]
    view = MessageDetailView(request, id)

    if request.method not in allowed_methods:
        return HttpResponseNotAllowed(allowed_methods)

    method = getattr(view, request.method.lower())
    return method()


class MessageDetailView:
    def __init__(self, request, id):
        self.request = request
        self.id = id

    def get_object(self):
        return get_object_or_404(Message, id=self.id)

    def get(self):
        return JsonResponse(
            status=200, data=MessageForm(instance=self.get_object()).to_json()
        )

    def delete(self):
        self.get_object().delete()
        return HttpResponse(status=200)

message_list_create_view

transactions/views.py

def message_list_create_view(request):
    allowed_methods = ["GET", "POST"]
    view = MessageListCreateView(request)

    if request.method not in allowed_methods:
        return HttpResponseNotAllowed(allowed_methods)

    method = getattr(view, request.method.lower())
    return method()


class MessageListCreateView:
    def __init__(self, request):
        self.request = request

    def post(self):
        form = MessageForm(self.request.POST)
        if form.is_valid():
            message = form.save()
            return JsonResponse(status=201, data=form.to_json())
        else:
            return JsonResponse(status=400)

    def get(self):
        messages = [
            MessageForm(instance=message).to_json() for message in Message.objects.all()
        ]
        return JsonResponse(status=200, data=messages, safe=False)

Where we are now
Thus far we haven’t reduced complexity; in fact, we’ve added both methods and lines of code. But we’ve created a class that gets instantiated for each incoming request, along with different class methods to handle incoming HTTP methods.

This is the first step in isolating the view interface from the underlying functionality. As a result, the standalone view functions are now almost identical, except for the HTTP methods supported and the names of the various view classes. We’re only a few steps away from defining a single, generic interface method to replace the custom view functions.

First, we need to take the nearly-duplicate code from the view functions and move it into methods on the view classes.

.dispatch()

The view functions are currently responsible for dispatching requests. They receive a request object, determine if the appropriate view method exists, and either call that method or return an error response. Eventually we’ll have a BaseView class that defines a generic .dispatch() method used by all concrete view classes. For now though, we’ll create a unique .dispatch() method for each view class.

To understand how this works, take a look at ItemDetailView:

def item_detail_view(request, id):
    view = ItemDetailView(request, id)
    return view.dispatch()


class ItemDetailView:
    def __init__(self, request, id):
        ...

     def dispatch(self):
        allowed_methods = ["GET", "PUT", "PATCH", "DELETE"]

        if self.request.method not in allowed_methods:
            return HttpResponseNotAllowed(allowed_methods)

        method = getattr(self, self.request.method.lower())
        return method()

With the new .dispatch() method, item_detail_view() need only instantiate the view class and let .dispatch() handle the rest of the request process.

The code is nearly identical for the remaining classes.

ItemListCreateView

def item_list_create_view(request):
    view = ItemListCreateView(request)
    return view.dispatch()


class ItemListCreateView:
    def __init__(self, request):
            ...

    def dispatch(self):
        allowed_methods = ["GET", "POST"]

        if self.request.method not in allowed_methods:
            return HttpResponseNotAllowed(allowed_methods)

        method = getattr(self, self.request.method.lower())
        return method()

MessageDetailView

def message_detail_view(request, id):
    view = MessageDetailView(request, id)
    return view.dispatch()


class MessageDetailView:
    def __init__(self, request, id):
        ...

    def dispatch(self):
        allowed_methods = ["GET", "DELETE"]

        if self.request.method not in allowed_methods:
            return HttpResponseNotAllowed(allowed_methods)

        method = getattr(self, self.request.method.lower())
        return method()

MessageListCreateView

def message_list_create_view(request):
    view = MessageListCreateView(request)
    return view.dispatch()


class MessageListCreateView:
    def __init__(self, request):
        self.request = request

    def dispatch(self):
        allowed_methods = ["GET", "POST"]

        if self.request.method not in allowed_methods:
            return HttpResponseNotAllowed(allowed_methods)

        method = getattr(self, self.request.method.lower())
        return method()

We can /almost/ say we have class-based views. Except, our classes are still dependent on the original view functions. This works, but is an awkward design. We don’t want to define a new class /and/ create a standalone function every time time we create a new view. Ideally, this functionality would be generalized and moved to the base class.

The only challenge is that the URL resolver expects to call a function; when we define routes in project/urls.py, we’re telling Django what function to call when a request is received along a particular path. The resolver doesn’t know how to instantiate a class and then call .dispatch().

Put differently, the view class needs a method for the URL resolver to call, which then creates an instance of the same class.

We’ll handle this with a new class method named .as_view(). Let’s dive into the code for ItemDetailView, since that will illustrate what’s happening.

class ItemDetailView:
    def __init__(self, request, id):
        ...

      @classmethod
    def as_view(cls):

          def item_detail_view(request, id):
              view = ItemDetailView(request, id)
              return view.dispatch()

          return item_detail_view

When called, .as_view() will return the item_detail_view function. The updated route in urls.py now points to ItemDetailView.as_view().

path("items/", views.ItemDetailView.as_view()),

Let’s cover what’s going on step by step, because this can be tricky to understand.

  1. When Django is first initialized, it parses the urlpatterns variable in urls.py and creates mappings between the paths defined and the corresponding view functions.
  2. a. With function-based views, the view function in the path doesn’t include the trailing parentheses:
    path("items/", views.item_detail_view)
    This means that the mapping is between the path (“items/”) and the unexecuted view function.
    b. With class-based views, the view function in the path does include the trailing parentheses, indicating that the function is executed when first parsed. As a result, the mapping isn’t between the path and the .as_view() method; rather, it’s between the path and the output of .as_view(), which in this case is item_detail_view.
  3. Once mapped, the URL resolver treats function-based views and class-based views identically. It simply calls the mapped function and passes in the request object as well as any arguments or keyword arguments from the path. For both examples, item_detail_view() behaves identically. It’s a standalone function that creates an instance of the ItemDetailView class and calls .dispatch().

 

If this was waaaaaaay too much/little explanation, let me know in the comments and I’ll update this post accordingly.

Before moving on to the other classes, let’s generalize .as_view() a little bit more. Since the URL resolver doesn’t care about the name of the function returned from .as_view(), we can use the same generic name for all view classes.

class ItemDetailView:
    def __init__(self, request, id):
        ...

    @classmethod
    def as_view(cls):
        def view(request, *args, **kwargs):
            self = cls(request, *args, **kwargs)
            return self.dispatch()

        return view

The updated view() function creates an instance of whatever its calling class might be. We’ve also updated the function arguments to include *args and **kwargs, to properly handle whatever arguments might be received.

We can now add our generic .as_view() method to all view classes, and delete the original view functions.

This code is literally identical between classes, but for those who really want to see the exact changes, here it is for the remaining classes:

ItemListCreateView

class ItemListCreateView:
    def __init__(self, request):
        ...

    @classmethod
    def as_view(cls):
        def view(request, *args, **kwargs):
            self = cls(request, *args, **kwargs)
            return self.dispatch()

        return view

MessageDetailView

class MessageDetailView:
    def __init__(self, request, id):
        ...

    @classmethod
    def as_view(cls):
        def view(request, *args, **kwargs):
            self = cls(request, *args, **kwargs)
            return self.dispatch()

        return view

MessageListCreateView

class MessageListCreateView:
    def __init__(self, request):
        self.request = request

    @classmethod
    def as_view(cls):
        def view(request, *args, **kwargs):
            self = cls(request, *args, **kwargs)
            return self.dispatch()

        return view

Don’t forget to update urls.py to use the .as_view() methods.

urlpatterns = [
    path("items/", views.ItemDetailView.as_view()),
    path("items/", views.ItemListCreateView.as_view()),
    path("messages/", views.MessageDetailView.as_view()),
    path("messages/", views.MessageListCreateView.as_view()),
]

View Inheritance

If you’re copying and pasting identical methods between classes, it’s time to move shared functionality to a separate class.

Create a new BaseView class to contain the .as_view() method, and make your concrete view classes inherit from BaseView.

class BaseView:
    @classmethod
    def as_view(cls):
        def view(request, *args, **kwargs):
            self = cls(request, *args, **kwargs)
            return self.dispatch()

        return view
class ItemDetailView(BaseView):
...
class ItemListCreateView(BaseView):
...
class MessageDetailView(BaseView):
...
class MessageListCreateView(BaseView):
...

No additional methods are perfect copies of one another, but .__init__() and .dispatch() are quite close. They also provide low-level functionality that all views use, making them good candidates for BaseView. We just need to generalize them somewhat.

Currently, all of the classes’ __init__() methods save request as an instance attribute, and the detail views also save id. If we’re fine with view methods referencing self.kwargs["id"] and similar instead of self.id, we can use a base class method to save request, args, and kwargs as instance attributes.

class BaseView:

    def __init__(self, request, *args, **kwargs):
        self.request = request
        self.args = args
        self.kwargs = kwargs

You can now remove .__init__()from all child classes. Make sure to update all references of self.id to self.kwargs["id"].

BaseView.dispatch()

Time to move .dispatch() into the base view. Doing so will require us to modify the method so it works for any class. Take a look at the updated code below and then we’ll explain in more depth what’s taking place.

class BaseView:

    http_method_names = ["get", "post", "put", "patch", "delete"]  # Incomplete list.

      ...

    def dispatch(self):
        method = getattr(self, self.request.method.lower(), None)

          if not method or not callable(method):
            return HttpResponseNotAllowed(self._get_allowed_methods())

        return method()

    def _get_allowed_methods(self):
        return [
            m.upper() for m in self.http_method_names if hasattr(self, m)
        ]  # Lifted from base.py in Django

The new .dispatch() method needs to work with different view classes that support different HTTP method types. To accommodate different methods, we define a list of HTTP methods supported by all BaseView children. The list does not include all valid HTTP method types; rather, it includes the methods used in this tutorial.

Upon receipt of a request, .dispatch() first checks whether the instance has an attribute whose name matches the HTTP method. If not, it returns an error response which contains a list of allowed methods, found by crossing the http_method_names values with the instance’s attribute names.

For sake of attribution, the body of ._get_allowed_methods() is copied nearly verbatim from the base.py file in Django’s views.

We don’t need to modify BaseView any more. Like Django’s base view class, this is a simple class that only handles low level mechanisms that apply to all class-based views. The actual method handling is handled by higher-level child-classes and mixins.

Mixins

Mixins are an easy way to add functionality without adding long inheritance chains. They work especially well for methods that occur frequently but aren’t present in all class-based views.

ObjectDetailMixin

Our first mixin isolates the .get_object() method used in detail views. Currently, the .get_object() methods know the type of model the class references and they always use id as a lookup field.

ObjectDetailMixin solves these limitations with instance attributes. The model_class attribute defaults to None, meaning it must be explicitly set by the concrete class. The lookup_field attribute defaults to id, but is customizable.

It’s a bit tricky to pass a keyword argument wherein both the key and value aren’t known until runtime. We use dictionary unpacking to address this limitation.

class ObjectDetailMixin:
    model_class = None
    lookup_field = "id"

    def get_object(self):
        return get_object_or_404(
            self.model_class, **{self.lookup_field: self.kwargs.get(self.lookup_field)}
        )

With our new mixin, we need to update the detail classes. We’ll need to inherit from ObjectDetailMixin, define the model_class attribute, and remove the original .get_object() methods.

ItemDetailView

class ItemDetailView(BaseView, ObjectDetailMixin):

    model_class = Item

MessageDetailView

class MessageDetailView(BaseView, ObjectDetailMixin):

    model_class = Message

HTTP Method Mixins

We can continue with this pattern by taking our remaining view methods and putting each one in its own mixin. Let’s start with JsonDetailMixin, which handles the GET requests used in detail views. Since the type of form must now be set explicitly, we’ll add form_class as a new attribute that defaults to None. This new mixin inherits from ObjectDetailMixin because it makes use of .get_object().

JsonDetailMixin

class JsonDetailMixin(ObjectDetailMixin):
    form_class = None

    def get(self):
        return JsonResponse(
            status=200, data=self.form_class(instance=self.get_object()).to_json()
        )

Repeat this process for the remaining methods.

JsonUpdateMixin

class JsonUpdateMixin(ObjectDetailMixin):
    form_class = None

    def put(self):
        edited_object_data = json.loads(self.request.body)
        form = self.form_class(edited_object_data, instance=self.get_object())
        if form.is_valid():
            form.save()
            return HttpResponse(status=200)
        return JsonResponse(status=400)

JsonPartialUpdateMixin

class JsonPartialUpdateMixin(ObjectDetailMixin):
    form_class = None

    def patch(self):
        object = self.get_object()
        current_object_data = self.form_class(instance=object).to_json()
        edited_object_data = json.loads(self.request.body)
        updated_object_data = {**current_object_data, **edited_object_data}
        form = self.form_class(updated_object_data, instance=object)
        if form.is_valid():
            form.save()
            return HttpResponse(status=200)
        return JsonResponse(status=400)

JsonDestroyMixin

class JsonDestroyMixin(ObjectDetailMixin):
    form_class = None

    def delete(self):
        self.get_object().delete()
        return HttpResponse(status=200)

JsonCreateMixin

class JsonCreateMixin:
    form_class = None

    def post(self):
        form = self.form_class(self.request.POST)
        if form.is_valid():
            form.save()
            return JsonResponse(status=201, data=form.to_json())
        else:
            return JsonResponse(status=400)

JsonListMixin

class JsonListMixin:
    form_class = None
    queryset = None

    def get_queryset(self):
        return self.queryset

    def get(self):
        objects = [
            self.form_class(instance=object).to_json() for object in self.get_queryset()
        ]
        return JsonResponse(status=200, data=objects, safe=False)

Once the mixins are ready, make the view classes inherit from the appropriate mixins. Make sure to set the new attributes when necessary and remove the previous class methods.

Once the inheritance is complete, you should be left with the following concrete view classes. Note that the view classes no longer need to inherit directly from ObjectDetailMixin, as it’s already a parent of the other detail mixins.

class ItemDetailView(
    BaseView, JsonDetailMixin, JsonUpdateMixin, JsonPartialUpdateMixin, JsonDestroyMixin
):
    model_class = Item
    form_class = ItemForm

class ItemListCreateView(BaseView, JsonCreateMixin, JsonListMixin):
    form_class = ItemForm
    queryset = Item.objects.all()

class MessageDetailView(BaseView, JsonDetailMixin, JsonDestroyMixin):
    model_class = Message
    form_class = MessageForm

class MessageListCreateView(BaseView, JsonCreateMixin, JsonListMixin):
    form_class = MessageForm
    queryset = Message.objects.all()

This is quite a reduction in code! Sure, you’re left with more code than you started with, but you can add a whole new class based view with just three lines of code. That’s a major win and makes your code much easier to maintain.

Generics

We’re /almost/ done. But if you look at various classes inherited by the concrete view classes, you may notice a few patterns. Most notably:

 

Within Django projects, it’s common to create views with similar groups of functionality. To prevent the need to inherit from a base class and multiple mixins in every view, Django includes generic view classes. These are essentially just wrapper classes. They don’t necessarily define any functionality of their own. Instead, they combine commonly grouped mixins into a single class, meaning that your project only needs to inherit from one generic view.

Let’s create our own generic views. We’ll group common detail mixins into GenericJsonDetailView, and common list/create mixins into GenericJsonListCreateView.

class GenericJsonDetailView(
    BaseView, JsonDetailMixin, JsonUpdateMixin, JsonPartialUpdateMixin, JsonDestroyMixin
):
    pass

class GenericJsonListCreateView(BaseView, JsonCreateMixin, JsonListMixin):
    pass

Now update the concrete view classes to use the new generic views, where appropriate.

class ItemDetailView(GenericJsonDetailView):
    model_class = Item
    form_class = ItemForm

class ItemListCreateView(GenericJsonListCreateView):
    form_class = ItemForm
    queryset = Item.objects.all()

class MessageDetailView(BaseView, JsonDetailMixin, JsonDestroyMixin):
    model_class = Message
    form_class = MessageForm

class MessageListCreateView(GenericJsonListCreateView):
    form_class = MessageForm
    queryset = Message.objects.all()

Note that MessageDetailView doesn’t use one of our new generics. It made sense to use GenericJsonDetailView for ItemDetailView, since that view needed all available detail functionality. Because MessageDetailView only needs GET and DELETE functionality, which is (probably) a less common combination of methods, it makes more sense to inherit from the base class and mixins directly.

Concluding Thoughts

Hopefully this tutorial gave you a bit more understanding how the mechanisms of class-based views actually work, and how the different base classes, mixins, generics, and concrete view classes all fit together. Django’s class-based views can be overwhelming, but by studying the building blocks you can better appreciate that underneath it all, class-based views are just Python.

To understand the methods, attributes, and inheritance diagrams of Django’s actual class-based views, Classy Class Based Views is a fantastic resource. For Django Rest Framework, Classy DRF is equally excellent.

2 Responses

  1. Hi there!
    Good post 🙂
    Quick comments: you have some weird indentation in the first “as_view” snippet + the return value of this class method is not the right one. It should say `return item_detail_view`.
    Also, just below, the path might be in its own code block with syntax highlighting. (`path(“items//”, views.ItemDetailView.as_view()),`)

    Thanks again!

Leave a Reply to James Timmins Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.