Custom Django admin actions with an intermediate page

2020-02-04

nice pic

To create a custom action in django admin is pretty straight forward. You must define a function which is then referenced from the models admin definition. This function will accept three arguments:

  1. The modeladmin

  2. A HTTP request

  3. A queryset of the selected models

An example of a Ordermodel with a custom action to update status for all selected items may look like this:

Python# admin.py

class OrderAdmin(admin.ModelAdmin):
    actions = ['update_status']

    def update_status(self, request, queryset):
        queryset.update(status='NEW_STATUS')

    update_status.short_description = "Update status"

The short_description property is used for customizing the label in the action dropdown box.

Adding an intermediate page

To add an intermediate page you will continue from the code above, but instead of executing the update right away you will treat it like a view and return a HTTP response including a new template and context.

To begin with just return a template with an empty context:

Python# admin.py
from django.shortcuts import render

class OrderAdmin(admin.ModelAdmin):
    actions = ['update_status']

    def update_status(self, request, queryset):
        return render(request,
                      'admin/order_intermediate.html',
                      context={})

    update_status.short_description = "Update status"

To keep the admin look & feel extend from djangos own admin base template:

Python# 'admin/order_intermediate.html'
{% extends "admin/base_site.html" %}

{% block content %}
Are you sure you want to execute this action ?
{% endblock %}

Try the above code and you will land on an intermediate page asking you if you want to perform the selected action, however you wont have to option to actually do anything. Let's fix that.

Adding a form to execute our action

First of lets add a simple form to our template. We could use a django form from our context but for now lets code everything by hand. There are three important form properties that you have to include:

  1. An action key, which should map to your custom admin action

  2. An apply key, which can be anything really. This is to catch our submission later on in the view

  3. The selected items, in the form of _selected_action which should be each of the selected items pks.

Our updated template now looks like this:

Python# 'admin/order_intermediate.html'
{% extends "admin/base_site.html" %}

{% block content %}
<form action="" method="post">
  {% csrf_token %}
<p>
Are you sure you want to execute this action on the selected items?
</p>  
  {% for order in orders %}
    <p>
      {{ order }}
    </p>
    <input type="hidden" name="_selected_action" value="{{ order.pk }}" />
  {% endfor %}

  <input type="hidden" name="action" value="update_status" />
  <input type="submit" name="apply" value="Update status"/>
</form>
{% endblock %}

Note how we added hidden fields for the _selected_action values as well as the action property.

Now to catch this form submit in our admin action method:

Python# admin.py
from django.shortcuts import render
from django.http import HttpResponseRedirect

class OrderAdmin(admin.ModelAdmin):
    actions = ['update_status']

    def update_status(self, request, queryset):
        # All requests here will actually be of type POST 
        # so we will need to check for our special key 'apply' 
        # rather than the actual request type
        if 'apply' in request.POST:
            # The user clicked submit on the intermediate form.
            # Perform our update action:
            queryset.update(status='NEW_STATUS')
            
            # Redirect to our admin view after our update has 
            # completed with a nice little info message saying 
            # our models have been updated:
            self.message_user(request,
                              "Changed status on {} orders".format(queryset.count()))
            return HttpResponseRedirect(request.get_full_path())
                        
        return render(request,
                      'admin/order_intermediate.html',
                      context={'orders':queryset})

    update_status.short_description = "Update status"

Thats it!