[Fixed]-Django Admin: has_delete_permission Ignored for "Delete" Action

16👍

According to has_delete_permission‘s docstring:

def has_delete_permission(self, request, obj=None):
    """
    Returns True if the given request has permission to change the given
    Django model instance, ...
    """

This means has_delete_permission is executed per request, not per object. On a bulk action, obj is not set. However you may examine request:

def has_delete_permission(self, request, obj=None):
    if request.POST and request.POST.get('action') == 'delete_selected':
        return '1' not in request.POST.getlist('_selected_action')
    return obj is None or obj.pk != 1

Note that the above works because the delete_selected action takes has_delete_permission into account.

You may also want to provide some details about the error:

from django.contrib import messages

def has_delete_permission(self, request, obj=None):
    if request.POST and request.POST.get('action') == 'delete_selected':
        if '1' in request.POST.getlist('_selected_action'):
            messages.add_message(request, messages.ERROR, (
                "Widget #1 is protected, please remove it from your selection "
                "and try again."
            ))
            return False
        return True
    return obj is None or obj.pk != 1

I guess has_delete_permission is called per request rather than per object for performance reasons. In the general case, it is useless to make a SELECT query and loop over has_delete_permission (which may be time consuming according to what it does) prior to running the DELETE query. And when it’s relevant to do so, it’s up to the developer to take the necessary steps.

9👍

You can replace the admin’s implementation of the delete_selected action with your own. Something like:

from django.contrib.admin import actions

class WidgetAdmin(admin.ModelAdmin):
    actions = [delete_selected]

    def delete_selected(self, request, queryset):
        # Handle this however you like. You could raise PermissionDenied,
        # or just remove it, and / or use the messages framework...
        queryset = queryset.exclude(pk=1)

        actions.delete_selected(self, request, queryset)
    delete_selected.short_description = "Delete stuff"

See the documentation for more details.

Leave a comment