Django Admin Actions on single object

Not the same as the topic starter asked, but this snippet allows to have Single Object action from on the list page with minimum amount of code

BaseAction code

class AdminActionError(Exception):
    pass


class AdminObjectAction:
    """Base class for Django Admin actions for single object"""

    short_description = None
    exp_obj_state = {}

    def __init__(self, modeladmin, request, queryset):
        self.admin = modeladmin
        self.request = request
        self.queryset = queryset
        self.__call__()

    def validate_qs(self):
        count = self.queryset.count()
        if count != 1:
            self.error("You must select one object for this action.")

        if self.exp_obj_state:
            if self.queryset.filter(**self.exp_obj_state).count() != 1:
                self.error(f'Selected object does not meet the requirements: {self.exp_obj_state}')

    def error(self, msg):
        raise AdminActionError(msg)

    def get_object(self):
        return self.queryset.get()

    def process_object_action(self, obj):
        pass

    def validate_obj(self, obj):
        pass

    def __call__(self, *args, **kwargs):
        try:
            self.validate_qs()
            obj = self.get_object()
            self.validate_obj(obj)
        except AdminActionError as e:
            self.admin.message_user(self.request, f"Failed: {e}", level=messages.ERROR)
        else:
            with transaction.atomic():
                result = self.process_object_action(obj)

            self.admin.message_user(self.request, f"Success: {self.short_description}, {result}")

Custom Action [minimum amount of code]

class RenewSubscriptionAction(AdminObjectAction):
    short_description = 'Renew subscription'

    exp_obj_state = {
        'child': None,
        'active_status': True,        
    }

    def process_object_action(self, obj):
        manager = RenewManager(user=obj.user, subscription=obj)
        return manager.process()

AdminClass

class SomeAdmin(admin.ModelAdmin):
    actions = [RenewSubscriptionAction]

The built-in admin actions operate on a queryset.

You can use a calable for the action you whant or to show something else:

class ProductAdmin(admin.ModelAdmin):
    list_display ('name' )
    readonly_fields('detail_url)


def detail_url(self, instance):
    url = reverse('product_detail', kwargs={'pk': instance.slug})
    response = format_html("""<a href="{0}">{0}</a>""", product_detail)
    return response

or using forms

class ProductForm(forms.Form):
    name = forms.Charfield()

    def form_action(self, product, user):
        return Product.value(
            id=product.pk,
            user= user,
        .....

        )


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):

# render buttons and links to
def product_actions(self, obj):
        return format_html(
            '<a class="button" href="{}">Action1</a>&nbsp;'
            '<a class="button" href="{}">Action 2</a>',
            reverse('admin:product-action-1', args=[obj.pk]),
            reverse('admin:aproduct-action-3', args=[obj.pk]),
        )

for more details about using forms


If you realy need per-single object, I suggest you to use this solution, eg:

class Gallery(TimeStampedModel):
    title = models.CharField(max_length=200)
    attachment = models.FileField(upload_to='gallery/attachment/%Y/%m/%d')

    def __str__(self):
        return self.title

    def process_button(self):
        return ('<button id="%(id)s class="btn btn-default process_btn" '
                'data-value="%(value)s>Process</button>' % {'id': self.pk, 'value': self.attachment.url})
    process_button.short_description = 'Action'
    process_button.allow_tags = True

In your admin.py, insert process_button into list_display;

class GalleryAdmin(admin.ModelAdmin):
    list_display = ['title', 'process_button', 'created']
    search_fields = ['title', 'pk']
    ....

    class Media:
        js = ('path/to/yourfile.js', )

Then, inside yourfile.js, you can also process it..

$('.process_btn').click(function(){
    var id = $(this).attr('id');       // single object id
    var value = $(this).data('value'); // single object value
    ...
});

Hope it helpful..


Create a template for your model in your app.

templates/admin/<yourapp>/<yourmodel>/change_form.html

With this example content to add a button when changing an existing object.

{% extends "admin/change_form.html" %}
{% block submit_buttons_bottom %}
    {{ block.super }}
    {% if original %} {# Only show if changing #}
        <div class="submit-row">
            <a href="{% url 'custom-model-action' original.pk %}">
                 Another action
            </a>
        </div>
    {% endif %}
{% endblock %}

Link that action to any url and redirect back to your model change object view. More information about extending admin templates.

Update: Added complete common use case for custom action on existing object

urls.py

urlpatterns = [
    url(r'^custom_model_action/(?P<object_pk>\d+)/$',
        core_views.custom_model_action, name='custom-model-action')
]

views.py

from django.urls import reverse
from django.contrib import messages
from django.http import HttpResponse, HttpResponseRedirect

def custom_model_action(request, object_pk):
    messages.info(request, 'Performed custom action!')
    return HttpResponseRedirect(
       reverse('admin:<yourapp>_<yourmodel>_change', args=[object_pk])
    )