Photo by Braden Jarvis on Unsplash

Simplify your Django project with custom admin actions

Adrien Van Thong

--

One of the many ways in which Django makes administrating our projects easier is by providing us with the Django Admin portal. Every Django user will have used this at one point — a visual entry point into the underlying database. In most Django projects I’ve worked on, we utilized the Django admin site as the place where administrators can perform actions that require elevated privileges, and have visibility into more fields than the regular interface.

Most who are familiar with the Django admin site will recognize the dropdown menu at the top of every model page with a single action in it: “delete selected items”. Did you know that it is very easy to customize that dropdown and populate it with additional custom actions to be performed on the selected items?

In this article, I’ll be showing how to create new custom actions to add to that dropdown menu.

How to create a Custom Admin Action

For this article, I’ll be using the model below as our example (which we also used in a prior article), which stores information about software releases.

# models.py
from django.db import models

class Release(models.Model):
name = models.CharField(max_length=128)
release_date = models.DateField(auto_now_add=True)
version = models.PositiveIntegerField()
enabled = models.BooleanField(default=True)

With our model defined, let’s create a very straightforward and simple admin class to help us manage it, below:

# admin.py
from django.contrib import admin
from .models import Release

@admin.register(Release)
class ReleaseAdmin(admin.ModelAdmin):
list_display = ('name', 'enabled', 'release_date', 'version')
list_filter = ('enabled',)
search_fields = ('name', 'version')
ordering = ('release_date',)

With our simple Model and Admin setup, we can now manage our releases via the Django admin site:

Django Admin site for the `Release` model

Now that we have the Django admin site set up with our model, we want to expand its functionality to let our administrator enable and disable these releases in bulk. We’ll accomplish this using a custom admin action.

As with most things Django, custom actions are extremely simple to setup. The first step is to create a new class method inside our existing admin class which takes in a request object, and a queryset. The request object represent the user’s HTTP request, similar to the one we’re used to seeing in all our CBVs. The queryset parameter represents all the records which the user selected which they want the custom action to operate on.

Let’s create our first method, enable below. It will enable all the selected releases all at once:

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

The method quite simply leverages the existing queryset and applies an update to all the records in it to set the enabled field to True.

We also want to give the admin user some feedback about the operation:

def enable(self, request, queryset):
queryset.update(enabled=True)
self.message_user(request, f'{queryset.count()} releases enabled.')

This change connects the request object that was passed in and feeds it into the Django messages framework — which then prints a styled success message back to the user.

The next step is the most important one: we need to tell our existing admin class about the new custom action. We do so by adding our method name to the actions list field:

class ReleaseAdmin(admin.ModelAdmin):
actions = ['enable']

Finally, we decorate the method using the @admin.action decorator so we can set the description for the action (the text that will show up in the actions dropdown) and define which permission classes can perform the action. Here is what our complete Admin class now looks like:

from django.contrib import admin
from .models import Release

@admin.register(Release)
class ReleaseAdmin(admin.ModelAdmin):
list_display = ('name', 'enabled', 'release_date', 'version')
list_filter = ('enabled',)
search_fields = ('name', 'version')
ordering = ('release_date',)
actions = ['enable']

@admin.action(description='Enable selected releases', permissions=['change'])
def enable(self, request, queryset):
queryset.update(enabled=True)
self.message_user(request, f'{queryset.count()} releases enabled.')

If we also wanted to add error handling, we can roll up any warning or errors back to the user using the same messages framework, for example:

self.message_user(request, f'{queryset.count()} releases enabled.')
self.message_user(request, 'This is a sample error', level='ERROR')
self.message_user(request, 'This is a sample warning', level='WARNING')

The messages above would produce the following output on the page:

Sample messages shown on the admin panel

Of course, a disable custom action can also be added which would accomplish the same thing in the other direction:

# admin.py
from django.contrib import admin
from .models import Release

@admin.register(Release)
class ReleaseAdmin(admin.ModelAdmin):
list_display = ('name', 'enabled', 'release_date', 'version')
list_filter = ('enabled',)
search_fields = ('name', 'version')
ordering = ('release_date',)
actions = ['enable', 'disable']


@admin.action(description='Enable selected releases', permissions=['change'])
def enable(self, request, queryset):
queryset.update(enabled=True)
self.message_user(request, f'{queryset.count()} releases enabled.')

@admin.action(description='Disable selected releases', permissions=['change'])
def disable(self, request, queryset):
queryset.update(enabled=False)
self.message_user(request, f'{queryset.count()} releases disabled.')

With these changes now implemented, this is what our administrators now see as the available actions in the Django admin site:

New actions for “enable” and “disable” now appear in the action dropdown.

It is worth noting that in cases where the custom action has more complex logic, you should consider encapsulating it in a model method or model manager and calling it directly from the custom action. This way it is in keeping with DRY principles if that same logic needs to be accessed from elsewhere in the project.

Unit testing admin custom actions

With our custom admin logic implemented, most developers would naturally ask how to unit test this new functionality. We can once again use Django’s native unit testing architecture to accomplish everything we need to.

As we would with any REST test case, we instantiate a client and log in, making sure to use a super user who can access the admin site. Our setup method would look as follows:

from django.test import TestCase, Client

class CustomDjangoAdmin(TestCase):
def setUp(self):
# Create admin user and log in as that user:
self.client = Client()
self.admin_user = User.objects.create_superuser(username='admin', password='admin', email='test@example.com')
self.client.login(username='admin', password='admin')

With the admin user created and logged in, in our test case, we can make an HTTP call directly into the custom action, passing in the PKs of the records we want to act upon. For example:

from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME

def test_enable(self):
data = {
'action': 'enable',
ACTION_CHECKBOX_NAME: [PKS_GO_HERE],
}
response = self.client.post(reverse('admin:app_release_changelist'), data)
self.assertEqual(response.status_code, 302)

To ensure forward-compatibility, we import the name of the checkbox field as the key, and the value is a list of record PKs.

Next, we reverse-lookup the URL of the admin site for the model in question. This follows the format app_model_changelist. The expected response code is HTTP 302 on success, as Django will forward the client back to the admin list page.

Next, we can create a few sample Release objects to play with and put it all together into a cohesive test, for example:

# tests/test_custom_admin.py
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.urls import reverse
from .models import Release


class CustomDjangoAdmin(TestCase):
def setUp(self):
# Create admin user and log in as that user:
self.client = Client()
self.admin_user = User.objects.create_superuser(username='admin', password='admin', email='test@example.com')
self.client.login(username='admin', password='admin')
# Create some releases to be used for testing:
self.release1 = Release.objects.create(name='Release 1', version=1_002_123, enabled=False)
self.release2 = Release.objects.create(name='Release 2', version=2_003_000, enabled=False)
self.release3 = Release.objects.create(name='Release 3', version=3_005_007, enabled=False)

def test_enable(self):
# Invoke the custom admin command:
data = {
'action': 'enable',
ACTION_CHECKBOX_NAME: [self.release1.pk, self.release2.pk],
}
response = self.client.post(reverse('admin:app_release_changelist'), data)
self.assertEqual(response.status_code, 302)
# Validate that the enabled flag was correctly changed:
self.release1.refresh_from_db()
self.release2.refresh_from_db()
self.release3.refresh_from_db()
self.assertTrue(self.release1.enabled)
self.assertTrue(self.release2.enabled)
self.assertFalse(self.release3.enabled)

def test_disable(self):
# Set all releases to enabled:
Release.objects.all().update(enabled=True)
# Invoke the custom admin command:
data = {
'action': 'disable',
ACTION_CHECKBOX_NAME: [self.release1.pk, self.release2.pk],
}
response = self.client.post(reverse('admin:app_release_changelist'), data)
self.assertEqual(response.status_code, 302)
# Validate that the enabled flag was correctly changed:
self.release1.refresh_from_db()
self.release2.refresh_from_db()
self.release3.refresh_from_db()
self.assertFalse(self.release1.enabled)
self.assertFalse(self.release2.enabled)
self.assertTrue(self.release3.enabled)

That’s all there is to creating custom actions in the Django admin site! Thanks to Django’s native framework, we added another really useful feature to our project.

To quickly summarize, the three easy steps we followed were:

  1. Create a new method in the ModelAdmin class which takes in the request object and queryset for the selected records. This method performs the custom action logic.
  2. Decorate the new method with the @admin.action decorator to set a description and control permissions.
  3. Add the the new method name to the actions field as a string.

For more in-depth learning on this topic, I recommend consulting the resources outlined in the next section.

Recommended Reading

--

--

No responses yet