Search code examples
pythondjangodjango-admindjango-signals

Django admin not handling ProtectedError exception


I have a Django application that has a model like this:

class Foo(models.Model):
    name = models.CharField(max_length=100)

I have a specific instance in this model that it's name is 'bar' (for example) and I want to prevent this instance from being deleted.

I've created a signal receiver like this:

def protect_foo_bar(sender, instance, using, **kwargs):
    if instance.title != 'bar':
        pass
    else:
        raise ProtectedError(protected_objects=instance, msg='You cannot delete this object')

and I've connected this receiver to pre_delete signal like this:

pre_delete.connect(receiver=protect_foo_bar, dispatch_uid='protect_foo_bar_signal',
                           sender='app_name.foo')

When I try to delete this specific object from Django's admin panel, it returns an exception (Error 500). Is it possible to force the admin panel to show an error like you cannot delete this object and not returning an exception to the user?

EDIT:

Here's the traceback:

Traceback:

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\core\handlers\exception.py" in inner
  41.             response = get_response(request)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\core\handlers\base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\core\handlers\base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\contrib\admin\options.py" in wrapper
  551.                 return self.admin_site.admin_view(view)(*args, **kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\utils\decorators.py" in _wrapped_view
  149.                     response = view_func(request, *args, **kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\views\decorators\cache.py" in _wrapped_view_func
  57.         response = view_func(request, *args, **kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\contrib\admin\sites.py" in inner
  224.             return view(request, *args, **kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\utils\decorators.py" in _wrapper
  67.             return bound_func(*args, **kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\utils\decorators.py" in _wrapped_view
  149.                     response = view_func(request, *args, **kwargs)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\utils\decorators.py" in bound_func
  63.                 return func.__get__(self, type(self))(*args2, **kwargs2)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\contrib\admin\options.py" in changelist_view
  1584.                 response = self.response_action(request, queryset=cl.get_queryset(request))

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\contrib\admin\options.py" in response_action
  1286.             response = func(self, request, queryset)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\contrib\admin\actions.py" in delete_selected
  49.             queryset.delete()

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\db\models\query.py" in delete
  614.         deleted, _rows_count = collector.delete()

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\db\models\deletion.py" in delete
  279.                         sender=model, instance=obj, using=self.using

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\dispatch\dispatcher.py" in send
  193.             for receiver in self._live_receivers(sender)

File "C:\Users\asus\PycharmProjects\AleTaha\env\lib\site-packages\django\dispatch\dispatcher.py" in <listcomp>
  193.             for receiver in self._live_receivers(sender)

File "C:/Users/asus/PycharmProjects/AleTaha\content\signals.py" in protect_content_category
  8.         raise ProtectedError(protected_objects=instance, msg='دسته‌ی «همه» را نمی‌توان حذف کرد.')

Exception Type: ProtectedError at /admin/content/contentcategory/
Exception Value: ('دسته\u200cی «همه» را نمی\u200cتوان حذف کرد.', <ContentCategory: همه>)

Solution

  • Yes, you can override the delete_view() and response_action() methods of your ModelAdmin. I would also suggest to override the has_delete_permission() method so that the "Delete" button won't appear at all:

    class MyModelAdmin(admin.ModelAdmin):
    
        def delete_view(self, request, object_id, extra_context=None):
            try:
                return super().delete_view(request, object_id, extra_context)
            except ProtectedError:
                msg = "you cannot delete this object"
                self.message_user(request, msg, messages.ERROR)
                opts = self.model._meta
                return_url = reverse(
                    'admin:%s_%s_change' % (opts.app_label, opts.model_name),
                    args=(object_id,),
                    current_app=self.admin_site.name,
                )
                return HttpResponseRedirect(return_url)
    
        def response_action(self, request, queryset):
            try:
                return super().response_action(request, queryset)
            except ProtectedError:
                msg = "you cannot delete this object"
                self.message_user(request, msg, messages.ERROR)
                opts = self.model._meta
                return_url = reverse(
                    'admin:%s_%s_changelist' % (opts.app_label, opts.model_name),
                    current_app=self.admin_site.name,
                )
                return HttpResponseRedirect(return_url)
    
        def has_delete_permission(self, request, obj=None)
            return super().has_delete_permission(request, obj) and (
                not obj or obj.name != 'bar'
            )