Let's say that I have a model where the row that has an ID of 1 is special and should not be able to be deleted, but all the other rows are fine to delete. Here is my attempt at implementing that logic:
from django.db import models
class Widget(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
from django.contrib import admin
from .models import Widget
class WidgetAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return obj is None or obj.pk != 1
admin.site.register(Widget, WidgetAdmin)
The above code removes the "Delete" button from the change form when obj.pk
is 1
, which is what I want. However, on the change list, if I check the checkbox for the row with an ID of 1 and then use the "Delete selected widgets" action, I am able to delete that row. I want to prevent that, but still allow all of the other rows to be deleted with the "Delete selected widgets" action. How can I do this?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With