How do I add custom actions to a change model form in Django Admin?

As pointed out, there is not a way and needs to be hacked. Here's I think an elegant hack for adding custom actions to both the list and change form views. It doesn't actually save the form just execute whatever custom action you want against the current object and return you back to the same change form page.

from django.db.models import Model

from django.contrib import admin, messages
from django.contrib.admin.options import (
    unquote,
    csrf_protect_m,
    HttpResponseRedirect,
)


class ArticleAdmin(admin.ModelAdmin):
    change_form_template = 'book/admin_change_form_book.html'

    actions = ['make_published']

    def make_published(self, request, queryset):
        if isinstance(queryset, Model):
            obj = queryset
            obj.status = 'p'
            obj.save()
            updated_count = 1
        else:
            updated_count = queryset.update(status='p')

        msg = "Marked {} new objects from existing".format(updated_count)
        self.message_user(request, msg, messages.SUCCESS)

    make_published.short_description = "Mark selected stories as published"

    @csrf_protect_m
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        if request.method == 'POST' and '_make_published' in request.POST:
            obj = self.get_object(request, unquote(object_id))
            self.make_published(request, obj)
            return HttpResponseRedirect(request.get_full_path())

        return admin.ModelAdmin.changeform_view(
            self, request,
            object_id=object_id,
            form_url=form_url,
            extra_context=extra_context,
        )

Now you can add an <input> for the action to the custom template view (this example uses book/admin_change_form_book.html in change_form_template)

{% extends 'admin/change_form.html' %}

{% block form_top %}
<input
    type="submit"
    name="_make_published"            
    value="Mark Published"
    class="grp-button grp-default"
>
{% endblock %}

If you look at the admin/change_form.html (i.e. "django/contrib/admin/templates/admin/change_form.html") you can insert this custom <input> action anywhere between the <form...> </form> tags on the page. Including these blocks:

  • {% block form_top %}
  • {% block after_related_objects %}
  • {% block submit_buttons_bottom %}

Django Admin does not provide a way to add custom actions for change forms.

However, you can get what you want with a few hacking.

First you will have to override the submit row.

your_app/templates/admin/submit_line.html

{% load i18n admin_urls %}
<div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
{% if show_delete_link %}
    {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
    <p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p>
{% endif %}
{% if show_save_and_copy %}<input type="submit" value="{% trans 'Create a new item based on this one' %}" name="_save_and_copy" />{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
</div>

In the above template, I just added the line {% if show_save_and_copy %}<input type="submit" value="{% trans 'Create a new item based on this one' %}" name="_save_and_copy" />{% endif %}. All other line are from default django implementation.

Then you will have to handle your button '_save_and_copy'

your_app/admin.py

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect

class ArticleAdmin(admin.ModelAdmin):

    def render_change_form(self, request, context, *args, **kwargs):
        """We need to update the context to show the button."""
        context.update({'show_save_and_copy': True})
        return super().render_change_form(request, context, *args, **kwargs)

    def response_post_save_change(self, request, obj):
        """This method is called by `self.changeform_view()` when the form
        was submitted successfully and should return an HttpResponse.
        """
        # Check that you clicked the button `_save_and_copy`
        if '_save_and_copy' in request.POST:
            # Create a copy of your object
            # Assuming you have a method `create_from_existing()` in your manager
            new_obj = self.model.objects.create_from_existing(obj)

            # Get its admin url
            opts = self.model._meta
            info = self.admin_site, opts.app_label, opts.model_name
            route = '{}:{}_{}_change'.format(*info)
            post_url = reverse(route, args=(new_obj.pk,))

            # And redirect
            return HttpResponseRedirect(post_url)
        else:
            # Otherwise, use default behavior
            return super().response_post_save_change(request, obj)

This example is for your specific case, it's up to you to make it more generic if you need to.

That being said, for your specific case you can also just click "Save and continue" to save your work, and then click "Save as new" to make a copy of it. Don't you ?