Sitemap
Django Unleashed

Unleashing the Full Potential of Web Development

Photo by Joel Holland on Unsplash

Django Tutorial: Multiple Record Custom Actions Mixin

6 min readApr 2, 2025

--

In my previous article, I wrote about how to create a new Django Mixin to enhance CBVs with the ability to perform a custom action on a single object. This article can be considered “part 2” where I will demonstrate how to write a new mixin that will perform a bulk action against multiple records.

I previously wrote about this particular topic in a prior article using formsets. In this article, I will accomplish the same outcome using a different method — neither approach is better than the other, though depending on the situation one may fit more naturally in your environment.

What are Mixins?

As a quick catch-up, mix-ins are customizable features that can be added-on to any existing CBV. Unlike fully-fledged View classes, Mixins are not a complete package and cannot be used as standalone as views — rather, they are singular pieces of functionality that enhance existing Views.

In the previous article in this series, we wrote our own custom actions mixin, which could be added to any View class to add new capabilities to that view:

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)

As shown in the article, any existing View could also inherit this Mixin to gain the capability to add customization actions that act upon the singular record.

Now, let’s create a new Mixin that does the same thing, but this time acts upon multiple records at once.

The Environment

We’ll start by re-using the same Release model from the previous article, included below for convenience:

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()

Whereas in the previous article, we wrote a ReleaseDetailView CBV which displayed information about a singular Release record, this time we’ll create a new ReleaseListView which will display information about multiple Release records at once. We’ll start by creating our bare-bones ListView as seen below:

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

class ReleaseListView(ListView):
model = Release
template_name = 'release_list.html'

This simple ListView will show the user a page listing out all our Release records at once.

Creating a new Mixin for handling multiple records

The approach for handling multiple records is going to be different. Since the user will be selecting multiple records, we’ll need to leverage a form with checkboxes — which means this mixin will be overwriting the form_valid method. This time, we’ll leave the dispatch method unmodified.

First, we need to define some class variables to keep track of the kwargs containing the action verb and the selected records. Let’s call them action_kwarg and ids_kwarg respectively. We’ll also need a list of tuples to keep track of the custom actions and their descriptions, called actions.

Next, let’s create a new method which will filter the current QuerySet down to have only the records selected by the user:

from django.db.models.query import QuerySet

def get_selected_records(self) -> QuerySet:
"""
Returns a QuerySet object containing the records selected by the user.
"""
return self.get_queryset().filter(pk__in=self.request.POST.getlist(self.ids_kwarg))

Afterwards, we overwrite the form_valid method and utilize our new method above to act upon all the selected records:

def form_valid(self, form):
"""
Determine the method to call to perform the custom action, then redirect to the client.
"""
queryset = self.get_selected_records()
action = self.request.POST.get(self.action_kwarg, None)
if action and any(action == available_action[0] for available_action in self.actions):
callable = getattr(self, action)
callable(self.request, queryset)
else:
return HttpResponseNotFound(f"Action '{action}' not found.")
return HttpResponseRedirect(self.get_success_url())

The overwritten form_valid method gets the QuerySet of user-selected records from our new method, then determines which action to take upon them based on the action type selected by the user. The custom method is invoked based on the action type passed in by the user. Lastly, once the custom action is complete, we redirect the user to the success_url.

If this logic all sounds familiar, it is heavily inspired by the Django admin bulk actions functionality we covered in a previous article.

Finally, we’ll need to dynamically generate our Form class to provide the user with a dropdown to select the custom action they want to perform. To do this we’ll overwrite the get_form_class method:

from typing import Type
from django.forms.models import BaseModelForm

def get_form_class(self) -> Type[BaseModelForm]:
"""
Create a new form class to use for the custom action form, populated with the available actions.
"""
class CustomActionForm(forms.Form):
action = forms.ChoiceField(choices=self.actions, required=True)
return CustomActionForm

Here is what our new Mixin looks like when we put it all together:

from typing import Type
from django.db.models.query import QuerySet
from django.forms.models import BaseModelForm
from django.http import HttpResponseRedirect, HttpResponseNotFound

class MultipleObjectCustomActionMixin:
# Callable of the actions that can be performed:
actions = ()
# Name of the kwarg variable that contains the list of PKs to act upon:
ids_kwarg = 'pk'
# Name of the kwarg variable that contains the action the client wants performed:
action_kwarg = 'action'

def get_form_class(self) -> Type[BaseModelForm]:
class CustomActionForm(forms.Form):
action = forms.ChoiceField(choices=self.actions, required=True)
return CustomActionForm

def get_selected_records(self) -> QuerySet:
return self.get_queryset().filter(pk__in=self.request.POST.getlist(self.ids_kwarg))

def form_valid(self, form):
queryset = self.get_selected_records()
action = self.request.POST.get(self.action_kwarg, None)
if action and any(action == available_action[0] for available_action in self.actions):
callable = getattr(self, action)
callable(self.request, queryset)
else:
return HttpResponseNotFound(f"Action '{action}' not found.")
return HttpResponseRedirect(self.get_success_url())

Next, let’s update our existing CBV to leverage our new mixin!

Updating the ReleaseListView

With our mixin complete, let’s plug it into our existing ReleaseListView. As noted in the previous article, we’ll need to also inherit FormView as well as the new mixin to provide the user with the requisite checkboxes to select the records they want to modify.

As with the single object custom action mixin, we’ll also need to define our custom action method in the CBV- though this time, it’ll need to leverage queryset kwarg instead of get_object since we’re dealing with multiple records.

from django.urls import reverse_lazy
from django.views.generic import ListView, FormView
from .models import Release
from .mixins import MultipleObjectCustomActionMixin


class ReleaseListView(FormView, MultipleObjectCustomActionMixin, ListView):
model = Release
template_name = 'release_list.html'
actions = (
('release_to_public', 'Make all selected releases available to public'),
('enable', 'Enable all selected releases'),
('disable', 'Disable all selected releases'),
)
success_url = reverse_lazy('all-releases')

def release_to_public(self, request, queryset):
for release in queryset:
release.release_to_public()

def enable(self, request, queryset):
queryset.update(enabled=True)

def disable(self, request, queryset):
queryset.update(enabled=False)

Finally, we update the template code to add the form and checkboxes.

{% extends 'base.html' %}
{% block title %}Releases{% endblock %}

{% block content %}
<h1>Releases</h1>

<div class="row">
<div class="col">
<form method="post">
{% for hidden_field in form.hidden_fields %}
{{ hidden_field.errors }}
{{ hidden_field }}
{% endfor %}

{% csrf_token %}

{{ form.management_form }}
{{ form.non_form_errors }}

<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Name</th>
<th scope="col">Version</th>
<th scope="col">Build Date</th>
<th scope="col">Release Date</th>
<th scope="col">Status</th>
<th scope="col">State</th>
</tr>
</thead>
<tbody>
{% for release in release_list %}
<tr>
<td>
<div class="custom-control custom-checkbox"><input class="form-check-input" type="checkbox" name="pk" value="{{ release.pk }}" /></div>
</td>
<td><a href="{% url "release-details" release.pk %}">{{ release.name }}</a></td>
<td>{{ release.version }}</td>
<td>{{ release.build_date }}</td>
<td>{{ release.release_date }}</td>
<td>{{ release.get_status_display }}</td>
<td>{{ release.enabled|yesno:"Enabled,Disabled"}}
</tr>
{% endfor %}
</tbody>
</table>

<div class="row">
<div class="col-md-1">
<button type="submit" class="btn btn-outline-dark">Submit</button>
</div>
<div class="col-md-11 text-right">
{{ form.action }}
</div>
</div>
</form>
</div>
</div>
{% endblock %}

We now have a CBV generating our new list page with the ability to perform bulk actions against any number of records:

The dropdown under the table allows the user to select which action to perform on the selected items

Using Django mixins, we’re created another highly reusable component for our application which enhances any CBV by providing it the ability to quickly define a variety of custom actions that a user can apply to multiple records. By encapsulating this logic into a mixin, we’ve made it so that we can very easily leverage this again in future CBVs!

Readers who are wanting an extra challenge can attempt to integrate the ReleaseListView with django-tables to significantly simplify the template code (hint: I’ve previously written about django-tables in another article)

What do you think of this new Mixin? Do you prefer it over my previous approach of using formsets? Do you have a completely different approach to solve this? Sound off in the comments below!

Recommended Reading

--

--

Django Unleashed
Django Unleashed

Published in Django Unleashed

Unleashing the Full Potential of Web Development

Responses (1)