'Catch exception on save in Django admin?

I have a pre_save signal handler on a bunch of models, which write to a different database. If something goes wrong, I'd like to abort the whole save, or failing that give a message to the user.

Based on Display custom message from signal in the admin, I wrote a mixin with methods like:

class SafeSaveMixin(object):
    def save_model(self, request, *args, **kwargs):
        try:
            return super(SafeSaveMixin, self).save_model(request, *args, **kwargs)
        except Exception as e:
            self.message_user(request, e, messages.ERROR)

This allows me to throw an Exception from the pre_save handler and show the message to the user. The problem is, even though this winds up skipping the actual Model.save(), the admin console doesn't see anything, so it still reports the object as successfully saved.

If I changed the pre_save handler to a post_save handler, that would allow the base Model.save() to occur and at least Django would report the correct state of things, but the information I need in the other database is based on the previous state of the object, so I need to get to it before the save.

I've also considered stuffing the error message into the object itself in the pre_save and pulling it out in the mixin's save_model() -- but this gets more complicated in the other ModelAdmin save methods like save_formset().

Is there any good way to do this?



Solution 1:[1]

I've come up with this, which gets mixed in to the ModelAdmin class:

class InternalModelAdminMixin:
    """Mixin to catch all errors in the Django Admin and map them to user-visible errors."""
    def change_view(self, request, object_id, form_url='', extra_context=None):
        try:
            return super().change_view(request, object_id, form_url, extra_context)
        except Exception as e:
            self.message_user(request, 'Error changing model: %s' % e.msg, level=logging.ERROR)
            # This logic was cribbed from the `change_view()` handling here:
            # django/contrib/admin/options.py:response_post_save_add()
            # There might be a simpler way to do this, but it seems to do the job.
            return HttpResponseRedirect(request.path)

This doesn't interfere with the actual model save process, and simply prevents the 500 error redirect. (Note this will disable the debug stacktrace handling. You could add some conditional handling to add that back in).

Solution 2:[2]

Catching this kind of errors should not be desirable. This could mean you expose delicate information to your users, e.g. about database (if there is an IntegrityError). As this bypasses the normal error handling, you might also miss messages that inform you about errors.

If there's some check required for wrong/incomplete data a user has entered, then the way to go is to do this in def clean(self)

def clean(self):
    cleaned_data = super(ContactForm, self).clean()
    field_value = cleaned_data.get('field_name')
    if not field_value:
        raise ValidationError('No value for field_name')

Solution 3:[3]

class YourModel(models.Model):
    def clean(self):
        # do some validation
        # or raise ValidationError(...)
        pass   

    def save(self, *args, **kwargs):
        self.full_clean()
        super(YourModel, self).save(*args, **kwargs)

It usually works, and you should not do any validation in other place.

But if you use RelatedField with InlineAdmin, and validation by related instance. Sometimes it will be wrong, Django won't render your Exceptions.

Like this question: how to display models.full_clean() ValidationError in django admin?

It confuse me about two month. And make me saw lots sources code.

Just now,I fixed it in my project(maybe...)

Hope no bug...

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Symmetric
Solution 2 Hussam
Solution 3 954