Django: How to write a bulk “custom action” view using CBVs
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:
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):
return self.name
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):
continue
# Delete the corresponding student record:
student_obj = student.get('id') # The value at key 'id' is an instance of `Student` model
student_obj.delete()
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: {{ cohort.name }}</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">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Student</th>
<th scope="col">Age</th>
</tr>
</thead>
<tbody>
{% for student_form in form.forms %}
<tr>
{% for hidden_field in student_form.hidden_fields %}
{{ hidden_field.errors }}
{{ hidden_field }}
{% endfor %}
<td>{{ student_form.selected }}</td>
<td>{{ student_form.instance.name }}</td>
<td>{{ student_form.instance.age }} yrs</td>
</tr>
{% endfor %}
</tbody>
</table>
<input type="submit" value="Delete" class="btn btn-primary" />
</form>
Once we put it all together, below is what the user is presented with:
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!
Resources
- Django Formsets
- My previous article: how to write a bulk edit page in Django