Django Unleashed

Unleashing the Full Potential of Web Development

Photo by Robert Lukeman on Unsplash

Writing a custom action CBV

Adrien Van Thong
Django Unleashed
Published in
5 min readMar 3, 2025

--

In last month’s article, I wrote about the importance of the setup and dispatch methods in Django CBVs and the role they play in the greater framework.

For this article, I will be diving deeper on that subject by showing a real-life example of how to utilize these methods to create a reusable component for a Django app. In this particular instance, I will be writing a CBV Mixin which other CBVs can inherit from to add custom workflow actions to that view.

The environment

Before we dive into implementing the new functionality, let’s define the models we’ll be working with today. In this particular case, we’ll reuse the Release model from a prior article — this model tracks various software releases for a development group.

This time we’ll add a custom method called release_to_public() which atomically updates all the requisite fields for us when a release is made generally available, shown below:

from django.db import models

class Release(models.Model):
STATUS_CHOICES = [
('dev', 'Development Build'),
('internal', 'Internal Release'),
('restricted', 'Restricted Release'),
('ga', 'Generally Available Release'),
('cancelled', 'Cancelled'),
]
name = models.CharField(max_length=128)
build_date = models.DateField(auto_now_add=True)
version = models.PositiveIntegerField()
release_date = models.DateField(null=True, blank=True)
status = models.CharField(max_length=32, choices=STATUS_CHOICES)
enabled = models.BooleanField(default=True)

def release_to_public(self):
self.status = 'ga'
self.release_date = datetime.now().date()
self.enabled = True
self.save()

We’ll create a very simple and straight-forward DetailView for this model:

from django.views.generic import DetailView
from .models import Release

class ReleaseDetailView(DetailView):
model = Release
template_name = 'release_detail.html'

And finally, a straightforward template to display the various fields:

{% extends 'base.html' %}
{% block content %}
<div class="col-md-8 offset-2">
<h1>{{ release.name }}</h1>
<dl>
<dt>Version</dt>
<dd>{{ release.version }}</dd>
<dt>Status</dt>
<dd>{{ release.get_status_display }}</dd>
<dt>Build Date</dt>
<dd>{{ release.build_date }}</dd>
<dt>Release Date</dt>
<dd>{{ release.release_date }}</dd>
<dt>State</dt>
<dd>{{ release.enabled|yesno:"Enabled,Disabled" }}</dd>
</dl>
</div>
{% endblock %}

My goal with this article is to create an easy way to extend our View by adding a button to the page which would provide the user the ability to invoke the model’s release_to_public method from the detail page above.

Extending our view with a custom action

Remember our dispatch() method from the last article? This use case provides us with an excellent opportunity to dive deeper into this method!

Let’s start by adding a new button to our template which will load the same page but with an added “action” GET param, which will indicate to our CBV we want it to run a custom action:

<a href="{% url 'release-detail' release.pk %}?action=release_to_public" class="btn btn-primary">Release to public</a>

Next, let’s encapsulate our custom action into a separate method. In this particular case, we’ll simply call the model’s release_to_public method and return that value:

    def release_to_public(self):
return self.get_object().release_to_public()

Next is where the dispatch() method comes in. We’ll need to overwrite this method to re-route our view to execute our new custom action method before the dispatcher loads the detail page as normal. Since we’re passing in the GET param to determine the custom action is being run, we’ll use that to determine whether to run the custom action or load the page as normal. Once the custom action is run we’ll reload the page without the custom action param:

    def dispatch(self, request, *args, **kwargs):
action = request.GET.get('action', None)
if action == 'release_to_public':
self.release_to_public()
return HttpResponseRedirect(self.get_object().get_absolute_url())
return super().dispatch(request, *args, **kwargs)

Putting it all together, this is what our DetailView now looks like:

from django.views.generic import DetailView
from .models import Release

class ReleaseDetailView(DetailView):
model = Release
template_name = 'release_detail.html'

def dispatch(self, request, *args, **kwargs):
action = request.GET.get('action', None)
if action == 'release_to_public':
self.release_to_public()
return HttpResponseRedirect(self.get_object().get_absolute_url())
return super().dispatch(request, *args, **kwargs)

def release_to_public(self):
return self.get_object().release_to_public()

We now have a working “release to public” button on our page — hooray! But what if we want to add more buttons to the page which add other custom actions to the object?

Arbitrary custom actions

Now that our custom action is working, let’s try to add some other custom actions to our view — for example, enable and disable. We can start by defining new methods in our DetailView for each of these new actions, for example:

    def enable(self):
object: Release = self.get_object()
object.enabled = True
object.save()

def disable(self):
object: Release = self.get_object()
object.enabled = False
object.save()

We’ll also need to adjust our dispatch method to be able to handle the new actions — namely the if clause will need to expand to check against a list of possible actions, and the call to the method is replaced with a call to the method named by the action GET attribute:

    def dispatch(self, request, *args, **kwargs):
action = request.GET.get('action', None)
if action in ['release_to_public', 'enable', 'disable']:
getattr(self, action)()
return HttpResponseRedirect(self.get_object().get_absolute_url())
return super().dispatch(request, *args, **kwargs)

Finally, we add the two new buttons to the template:

<a href="{% url 'release-details' release.pk %}?action=enable" class="btn btn-outline-primary">Enable</a>
<a href="{% url 'release-details' release.pk %}?action=disable" class="btn btn-outline-primary">Disable</a>

Our detail page now has three fully functioning buttons which perform custom actions against our object.

Let’s not stop there — since we live by the DRY mantra let’s to abstract this logic into a reusable Mixin that we can leverage for all our other CBVs.

Abstracting away the logic into a new Mixin

Now that we’ve already written most of the work, abstracting this out into a reusable mixin will be extremely easy. We only need to pull out the dispatch method and make some modest changes to allow subclasses to customize the list of callable custom actions:

class CustomActionsMixin:
custom_actions: list = None

def dispatch(self, request, *args, **kwargs):
action = request.GET.get('action', None)
if action in self.custom_actions:
getattr(self, action)()
return HttpResponseRedirect(self.get_object().get_absolute_url())
return super().dispatch(request, *args, **kwargs)

With this abstraction set up, our DetailView code is now simpler:

from django.views.generic import DetailView
from .models import Release
from .mixins import CustomActionsMixin

class ReleaseDetailView(CustomActionsMixin, DetailView):
model = Release
template_name = 'release_detail.html'
custom_actions = ['release_to_public', 'enable', 'disable']

def release_to_public(self):
return self.get_object().release_to_public()

def enable(self):
object: Release = self.get_object()
object.enabled = True
object.save()

def disable(self):
object: Release = self.get_object()
object.enabled = False
object.save()

As shown above, don’t forget to update the class definition to inherit from our new Mixin class!

Voila! With our new mixin we can very easily create additional DetailView CBVs for other models with their own custom actions without having to copy the dispatch customizations around to each CBV. One possible alternative implementation of this could use decorators to designate which class methods are custom actions, instead of manually defining a list of strings in each CBV.

That’s it! It’s that simple! In the example above, we made use of the CBV dispatch method and CBV Mixins to create a reusable framework that any of our other CBVs can hook into to gain the ability to specify custom actions for that model. Don’t forget to keep your custom logic in an easily accessible module (i.e. model method) so they can be individually unit tested or called in from other parts of the application.

Hope you found this helpful. Happy coding!

--

--

Django Unleashed
Django Unleashed

Published in Django Unleashed

Unleashing the Full Potential of Web Development

No responses yet