Photo by Maik Jonietz on Unsplash

Django: How to write a bulk “custom action” view using CBVs

Adrien Van Thong
4 min readApr 20, 2023


In a previous article, I showcased how to leverage Django’s built-in capabilities to create a bulk edit form for the fields in a set of records for a Django model.

I’ve recently had a need for a use case in a similar vein, but with a different application: implementing a new page to let users perform a custom action (in this case, deleting) for related records in bulk.

What do I mean by this? In the previous example where our simple application was managing cohorts of students in a summer camp, for example, a councilor may need the capability to delete a set of students in a cohort (i.e. at the end of the month) by selecting multiple checkboxes and clicking a submit button.

Of course, we want to accomplish all of this using class-based-views in Django.

Here is what we ultimately want the end goal to look like:

Screenshot showing what will ultimately be built using this tutorial
Bulk Delete form UI

Existing models and views

Recall that in that previous article, our models and views were structured in the following manner:

from django.db import models

class Cohort(models.Model):
name = models.CharField(max_length=128)
teacher = models.CharField(max_length=128)
description = models.CharField(max_length=254)

def __str__(self):

class Student(models.Model):
name = models.CharField(max_length=128)
age = models.IntegerField()
active = models.BooleanField(default=True)
cohort = models.ForeignKey(Cohort, on_delete=models.CASCADE)
fav_snack = models.CharField(max_length=64, blank=True, null=True)
fav_color = models.CharField(max_length=32, blank=True, null=True)
fav_activity = models.CharField(max_length=32, blank=True, null=True)

For this article we’ll create a new View which will allow the user to select a group of Student objects in the current Cohort to be deleted.

Custom Form for Student checkboxes

We’ll start by creating a new ModelForm for the Student model which only has a single checkbox field in it, which the user will use to select which Students to delete.

from django import forms

class StudentsCheckboxForm(forms.ModelForm):
selected = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}))

class Meta:
model = Student
fields = []

Note that the only field in this form is the custom Boolean form field we’ve created, as we’re not planning to use this form to edit any of the Model fields. The fields property in the meta-class is also left blank for this reason. The new Boolean field will represent the checkboxes for each of the Student records.

Crafting the new UpdateView

Next, we need to create a new UpdateView in the style of the one we created in the previous article, using a formset factory to generate the list of Student entries in the current Cohort. The new View will leverage the inlineformset_factory method described in the previous article, and connect it to the new StudentCheckboxForm we just created.

from django.views.generic import UpdateView
from django import forms

class StudentBulkDisable(UpdateView):
template_name = 'student_bulk_delete.html'
model = Cohort
form_class = forms.inlineformset_factory(Cohort, Student, StudentsCheckboxForm, formset=StudentFormSet, extra=0)
slug_field = 'id'
slug_url_kwarg = 'id'
context_object_name = 'cohort'

Next, comes the most important part of this process: writing our custom form_valid method which will handle what to do after the user submits the form — specifically, delete the Student records.

class StudentBulkDisable(UpdateView):
< ... >
def form_valid(self, form):
for student in form.cleaned_data:
# Was this student's checkbox checked?
if not student.get('selected', None):
# Delete the corresponding student record:
student_obj = student.get('id') # The value at key 'id' is an instance of `Student` model
return HttpResponseRedirect(reverse_lazy('cohort-bulk-delete', kwargs={"id": self.get_object().pk}))

We iterate over form.cleaned_data as that is what contains all the individual checkboxes that were present on the page: both the checked and unchecked ones.

Next, the if statement skips over any unchecked records, as the user did not select those.

Finally, if the current record was selected by the user, we get the instance of the corresponding Student record, and delete it. In your own application, you will want to replace this with whatever business logic is appropriate for your use case.

Creating the form elements on the template side

Finally, we need to create the corresponding template for this view. I’ll skim over most details as most were already covered in the prior article.

First, we include the necessary management_form and hidden_fields. Next, we’ll iterate over form.forms which is a list of the individuals records(forms) generated by our factory. The model data for each record can be accessed via the instance property on each form.

<h1>Bulk Delete for Cohort: {{ }}</h1>
<b>Teacher</b>: {{ cohort.teacher }}
<p>{{ cohort.description }}</p>

<form method="post" enctype="multipart/form-data" action="">
{% 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">
<th scope="col"></th>
<th scope="col">Student</th>
<th scope="col">Age</th>
{% for student_form in form.forms %}
{% for hidden_field in student_form.hidden_fields %}
{{ hidden_field.errors }}
{{ hidden_field }}
{% endfor %}
<td>{{ student_form.selected }}</td>
<td>{{ }}</td>
<td>{{ student_form.instance.age }} yrs</td>
{% endfor %}
<input type="submit" value="Delete" class="btn btn-primary" />

Once we put it all together, below is what the user is presented with:

Screenshot showing what will ultimately be built using this tutorial
Our complete bulk delete form!

And that’s it! Clicking the delete button deletes all the checked student records. We’ve now leveraged Django’s class-based-views to easily create a bulk action form with as little boilerplate code as possible.

What do you think of this approach? Sound off in the comments!