1. 程式人生 > >How to Add Custom Action Buttons to Django Admin

How to Add Custom Action Buttons to Django Admin

The Admin

Before we add fancy buttons we need to set up a basic admin page for our Account model:

# admin.py
from django.contrib import admin
from .models import Account
@admin.register(Account)
class AccountAdmin(admin.ModelAdmin):
date_heirarchy = (
'modified',
)
list_display = (
'id',
'user',
'modified',
'balance',
'account_actions',
)
readonly_fields = (
'id',
'user',
'modified',
'balance',
'account_actions',
)
list_select_related = (
'user',
)

def account_actions
(self, obj):
# TODO: Render action buttons

Side Note: We can make the list view much better — add a link to the user and to the account actions, add search fields and many more but this post is not about that. I previously wrote about performance considerations in the admin interface when scaling a Django app to hundreds of thousands of users

and there are some nice tricks there that can make even this simple view much nicer.

Adding the Action Buttons

We want to add action buttons for each account and have them link to a page with a form. Luckily, Django has a function to add URL’s so let’s use it to add the routes and corresponding buttons:

# admin.py
from django.utils.html import format_html
from django.core.urlresolvers import reverse
class AccountAdmin(admin.ModelAdmin):
...
def get_urls(self):
urls = super().get_urls()
custom_urls = [
url(
r'^(?P<account_id>.+)/deposit/$',
self.admin_site.admin_view(self.process_deposit),
name='account-deposit',
),
url(
r'^(?P<account_id>.+)/withdraw/$',
self.admin_site.admin_view(self.process_withdraw),
name='account-withdraw',
),
]
return custom_urls + urls
    def account_actions(self, obj):
return format_html(
'<a class="button" href="{}">Deposit</a>&nbsp;'
'<a class="button" href="{}">Withdraw</a>',
reverse('admin:account-deposit', args=[obj.pk]),
reverse('admin:account-withdraw', args=[obj.pk]),
)
account_actions.short_description = 'Account Actions'
account_actions.allow_tags = True

We render two buttons, each linking to a view that executes a corresponding process_deposit/withdraw function. The two views will render an intermediate page with the relevant form. When the form is submitted the view will redirect back to our detail page or inform the user of an error.

A nice feature of using the account_actions field is that it is available in both the detail and the list view because it’s a regular field in the admin.

The function that handles the actual action:

# admin.py
from django.http import HttpResponseRedirect
from django.template.response import TemplateResponse
from .forms import DepositForm, WithdrawForm
class AccountAdmin(admin.ModelAdmin):
...
def process_deposit(self, request, account_id, *args, **kwargs):
return self.process_action(
request=request,
account_id=account_id,
action_form=DepositForm,
action_title='Deposit'
,
)
   def process_withdraw(self, request, account_id, *args, **kwargs):
return self.process_action(
request=request,
account_id=account_id,
action_form=WithdrawForm,
action_title='Withdraw',
)

def process_action(
self,
request,
account_id,
action_form,
action_title
):
account = self.get_object(request, account_id)
        if request.method != 'POST':
form = action_form()
        else:
form = action_form(request.POST)
if form.is_valid():
try:
form.save(account, request.user)
                except errors.Error as e:
# If save() raised, the form will a have a non
# field error containing an informative message.
pass
                else:
self.message_user(request, 'Success')
url = reverse(
'admin:account_account_change',
args=[account.pk],
current_app=self.admin_site.name,
)
return HttpResponseRedirect(url)
        context = self.admin_site.each_context(request)
context['opts'] = self.model._meta
context['form'] = form
context['account'] = account
context['title'] = action_title
        return TemplateResponse(
request,
'admin/account/account_action.html',
context,
)

We wrote a function called process_action that accepts the form, the title of the action and the account id, and handles the form submission. The two functions, process_withdraw and process_deposit, are used to set the relevant context for each operation.

There is some Django admin boilerplate here that is required by the Django admin site. No point in digging too deep into it because it’s not relevant to us at this point.

Only thing left to do is to add the template of the intermediate page containing the form. Once again, no need to work too hard — Django already has a detail page template we can extend:

# templates/admin/account/account_action.html
{% extends "admin/change_form.html" %}
{% load i18n admin_static admin_modify %}
{% block content %}
<div id="content-main">
  <form action="" method="POST">
{% csrf_token %}
    {% if form.non_field_errors|length > 0 %}
<p class="errornote">
"Please correct the errors below."
</p>
{{ form.non_field_errors }}
{% endif %}
    <fieldset class="module aligned">
{% for field in form %}
<div class="form-row">
{{ field.errors }}
{{ field.label_tag }}
{{ field }}
{% if field.field.help_text %}
<p class="help">
{{ field.field.help_text|safe }}
</p>
{% endif %}
</div>
{% endfor %}
</fieldset>
    <div class="submit-row">
<input type="submit" class="default" value="Submit”>
</div>
  </form>
</div>
{% endblock %}

This is it!

Staff members can now easily deposit and withdraw directly from the admin interface. No need to create an expensive dashboard or ssh to the server.

I promised we will do it in 100 lines and we did it in less!

Profit!