How to add the custom button which executes a Django admin action to change form page?
Asked Answered
P

5

34

I have already defined a custom admin action for my model which works perfectly as expected. I also looked at multiple ways of adding a button to the admin change form page here on SO. The only step that I am missing is how do I make a button in the change form page execute my custom admin action with current object?

The goal is to allow admin to inspect every object individually and perform an action on them without needing to go back to list view, selecting the inspected object, and executing the action from the list.

My custom admin action looks like this:

def admin_apply_change(modeladmin, request, queryset):
    # loop over objects in query set and perform action

I am assuming there is a simple and clean way of calling this action in admin change form, where the queryset would only contain the currently opened object the admin is looking at.

NOTE: It would be preferable if the button is at the bottom of the change form, next to Save button instead of being at top with History which is not very visible.

Solution:

See the answer below by Remi for the solution. In order to make it work the following corrections are needed:

  1. In the override of response_change initialization of some variables is missing:

    opts = self.model._meta
    pk_value = obj._get_pk_val()
    preserved_filters = self.get_preserved_filters(request)
    
  2. New inclusion tag custom_submit_row should be placed in templatetags and not in admin (see docs for custom templatetags)

  3. This is the oversight you could lose some time on. In change_form.html you not only have to change the suggested line:

    {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
    

    but also the more important line at the bottom where submit_row appears:

    {% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
    

    It is located just above the javascript block in change_form.html.

Pattern answered 20/1, 2016 at 10:24 Comment(0)
N
23

You could take a look at the change_form_template and set it to a custom template of yours and override the response_change method:

class MyModelAdmin(admin.ModelAdmin):

    # A template for a customized change view:
    change_form_template = 'path/to/your/custom_change_form.html'

    def response_change(self, request, obj):
        opts = self.model._meta
        pk_value = obj._get_pk_val()
        preserved_filters = self.get_preserved_filters(request)

        if "_customaction" in request.POST:
            # handle the action on your obj
            redirect_url = reverse('admin:%s_%s_change' %
                               (opts.app_label, opts.model_name),
                               args=(pk_value,),
                               current_app=self.admin_site.name)
             redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
             return HttpResponseRedirect(redirect_url)
        else:
             return super(MyModelAdmin, self).response_change(request, obj)

Copy the change_form.html from your site-packages/django/contrib/admin/templates/change_form.html and edit the line 40

 {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}

to

 {% if save_on_top %}{% block submit_buttons_top %}{% custom_submit_row %}{% endblock %}{% endif %}

Also check the line:

 {% block submit_buttons_bottom %}{% submit_row %}{% endblock %}

just above the javascript block.

Then you can register a new inclusion tag somewhere in your admin.py or add it to templatetags:

@register.inclusion_tag('path/to/your/custom_submit_line.html', takes_context=True)
def custom_submit_row(context):
    """
    Displays the row of buttons for delete and save.
    """
    opts = context['opts']
    change = context['change']
    is_popup = context['is_popup']
    save_as = context['save_as']
    ctx = {
        'opts': opts,
        'show_delete_link': (
            not is_popup and context['has_delete_permission'] and
            change and context.get('show_delete', True)
        ),
        'show_save_as_new': not is_popup and change and save_as,
        'show_save_and_add_another': (
            context['has_add_permission'] and not is_popup and
            (not save_as or context['add'])
        ),
        'show_save_and_continue': not is_popup and context['has_change_permission'],
        'is_popup': is_popup,
        'show_save': True,
        'preserved_filters': context.get('preserved_filters'),
    }
    if context.get('original') is not None:
        ctx['original'] = context['original']
    return ctx

The contents of your custom_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_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 %}

<input type="submit" value="{% trans 'Custom Action' %}"  name="_customaction" />

</div>

It is a lot of code, but mostly copy/paste. Hope that helps.

Nitrogen answered 20/1, 2016 at 12:17 Comment(1)
Thank you. This solved my problem along with some minor corrections. See them in my question update. It would be nice if you could update your answer to incorporate these corrections.Pattern
E
15

Most people probably do this without thinking, though it wasn't clear from the answer that the admin change form should be simply extended rather than overwritten entirely.

custom_change_form.html

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

{% if save_on_top %}{% block submit_buttons_top %}{% custom_submit_row %}{% endblock %}{% endif %}

{% block submit_buttons_bottom %}{% custom_submit_row %}{% endblock %}
Enucleate answered 30/12, 2016 at 0:3 Comment(0)
E
5

Alternatively, you can just extend the submit_line.html file adding your custom button (both at the top and in the bottom of the change page).

Your file templates/adminyour_app_name/your_model_name.html will start with:

{% extends "admin/submit_line.html" %}
{% load i18n admin_urls %}
<div class="submit-row">
  {% block submit-row %}  
     ... YOUR BUTTONS HERE ...
  {% endblock %}
</div>
Eutherian answered 31/7, 2019 at 17:19 Comment(2)
I did try this, but I already have a custom change form, so I guess it's not compatible with that.Hubie
If you customized the change_form template, I suggest you to put the code directly inside that templateEutherian
T
3

To change page in Django Admin, you can add the custom button which runs an admin action.

For example first, copy submit_line.html from django/contrib/admin/templates/admin/submit_line.html in your virtual environment to templates/admin/, templates/admin/app1/ or templates/admin/app1/person/ to override it as shown below. *submit_line.html in templates/admin/, templates/admin/app1/ or templates/admin/app1/person/ applies to all admins in all apps, all admins in only app1 or only person admin in only app1 respectively and you can see the original submit_line.html:

Django Project
 |-core
 |  └-settings.py
 |-app1
 |  |-models.py
 |  └-admin.py 
 |-app2
 └-templates
    └-admin
       |-app1
       |  |-person
       |  |  └-submit_line.html # Or
       |  |-model1
       |  |-model2
       |  └-submit_line.html # Or
       |-app2
       └-submit_line.html # Or

Then, add {% if custom_button %}<input ... to submit_line.html as shown below:

# "templates/admin/submit_line.html" Or
# "templates/admin/app1/submit_line.html" Or
# "templates/admin/app1/person/submit_line.html"

# ...
{% if show_delete_link and original %}
    {% 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">{% translate "Delete" %}</a></p>
{% endif %} 
{# ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ #}
{% if custom_button %}<input type="submit" value="{% translate 'Custom button' %}" name="_custom_button">{% endif %}
{# ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ #}
{% if show_save_as_new %}<input type="submit" value="{% translate 'Save as new' %}" name="_saveasnew">{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% translate 'Save and add another' %}" name="_addanother">{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% translate 'Save and add another' %}" name="_addanother">{% endif %}
# ...

And, set BASE_DIR / 'templates' to 'DIRS' in TEMPLATES in settings.py as shown below:

# "core/settings.py"

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / 'templates' # Here
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

And, there is Person model in models.py as shown below:

# "app1/models.py"

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=20)

    def __str__(self):
        return self.name

And, there is Person admin with uppercase admin action in admin.py as shown below:

# "app1/admin.py"

from django.contrib import admin, messages
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    actions = ("uppercase", )

    # Here
    @admin.action(description='Make selected persons uppercase')
    def uppercase(modeladmin, request, queryset):
        for obj in queryset:
            obj.name = obj.name.upper()
            obj.save()
        messages.success(request, "Successfully made uppercase!")

So, if you use uppercase admin action as shown below:

enter image description here

Then, you can make the selected persons uppercase as shown below:

enter image description here

And, a custom button is not displayed yet on change page as shown below:

enter image description here

Now, override change_view() and response_change() in Person admin as shown below. *You can see the original change_view() and response_change():

# "app1/admin.py"

from django.contrib import admin, messages
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    actions = ("uppercase", )

    @admin.action(description='Make selected persons uppercase')
    def uppercase(modeladmin, request, queryset):
        for obj in queryset:
            obj.name = obj.name.upper()
            obj.save()
        messages.success(request, "Successfully made uppercase!")

    # Here
    def change_view(self, request, object_id, form_url="", extra_context=None):
        extra_context = extra_context or {}
        extra_context['custom_button'] = True
        return self.changeform_view(request, object_id, form_url, extra_context)

    # Here
    def response_change(self, request, obj):
        if "_custom_button" in request.POST:
            queryset = self.get_queryset(request).filter(id=obj.id)
            self.uppercase(request, queryset)
        return super().response_change(request, obj)

Then, a custom button is displayed on change page as shown below, then if you click on the custom button:

enter image description here

Then, you can make the person uppercase as shown below:

enter image description here

In addition, override add_view() and response_add() in Person admin as shown below. *You can see the original add_view() and response_add():

# "app1/admin.py"

from django.contrib import admin, messages
from .models import Person

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    actions = ("uppercase", )

    @admin.action(description='Make selected persons uppercase')
    def uppercase(modeladmin, request, queryset):
        for obj in queryset:
            obj.name = obj.name.upper()
            obj.save()
        messages.success(request, "Successfully made uppercase!")

    # Here
    def add_view(self, request, form_url="", extra_context=None):
        extra_context = extra_context or {}
        extra_context['custom_button'] = True
        return self.changeform_view(request, None, form_url, extra_context)

    # Here
    def response_add(self, request, obj, post_url_continue=None):
        if "_custom_button" in request.POST:
            queryset = self.get_queryset(request).filter(id=obj.id)
            self.uppercase(request, queryset)
        return super().response_add(request, obj, post_url_continue)

Then, a custom button is displayed on add page as shown below, then if you click on the custom button:

enter image description here

Then, you can make the person uppercase as shown below:

enter image description here

Therefore answered 15/5, 2023 at 11:1 Comment(0)
C
2

Based on Remi's answer a cleaner solution for templates to override submit-row in submit_line.html template.

{% extends "admin/submit_line.html" %}
{% load i18n admin_urls %}
<div class="submit-row">
{% block submit-row %}
{{ block.super }}

{% if custom_buttons_template %}{% include custom_buttons_template %}{% endif %}

{% endblock %}

In change_view you can add custom_buttons_template into the context so you can customize custom buttons on each model separatly.

def change_view(self, request, object_id, form_url='', extra_context=None):
    extra_context = extra_context or {}
    extra_context['custom_buttons_template'] = 'admin/test.html'
    return super(TransactionFileAdmin, self).change_view(
        request, object_id, form_url, extra_context=extra_context,
    )

After this my template will load admin/test.html and insert the content directly into submit line before Save button. If you would like to place your buttons elsewhere you can copy full submit_line.html and put custom_buttons_template anywhere.

Circumspection answered 7/10, 2021 at 20:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.