How to show database errors to user in Django Admin

Question:

Background: my Django application sits onto top of a pre-existing Postgresql database. This database has a very complex network of triggers and constraints.

Question: In the Django Admin, if a user causes a DatabaseError on save, I would like to show the error back to them in a user friendly format, similar to the builtin forms.ValidationError.

Example (this doesn’t work, it causes a 500):

def save_model(self, request, obj, form, change):
    try:
        obj.save()
    except DatabaseError as e:
        raise forms.ValidationError(e)

Expected Result:

Shown to user in Admin, "Database Error: ID 58574 - Price is outside customers requested range. Cannot add or update a child row: a foreign key constraint fails."

Asked By: keithhackbarth

||

Answers:

You need to slightly change your logic if possible. What you need is custom AdminModel.form. All validation should be done there. See the note for save_model():

ModelAdmin.save_model() and ModelAdmin.delete_model() must save/delete
the object, they are not for veto purposes, rather they allow you to
perform extra operations.

But if your circumstances are so that you can’t do all validation inside the form I’d subclass ModelAdmin and override def add_view(), def change_view() and def changelist_view() like so:

from django.contrib import admin
from django import forms
from django.contrib.admin import helpers
from django.contrib.admin.options import csrf_protect_m, IS_POPUP_VAR
from django.utils.translation import ugettext as _
from django.utils.encoding import force_text

# for nonfield errors to show correctly
from django.forms.forms import NON_FIELD_ERRORS

from .models import TestModel


class TestModelAdmin(admin.ModelAdmin):

    def save_model(self, request, obj, form, change):

        raise Exception('test exception')

    @csrf_protect_m
    def add_view(self, request, form_url='', extra_context=None):
        try:
            return super(TestModelAdmin, self).add_view(request, form_url, extra_context)
        except Exception as e:
            pass

        # mimic parent class on error

        model = self.model
        opts = model._meta

        ModelForm = self.get_form(request)
        formsets = []
        inline_instances = self.get_inline_instances(request, None)
        form = ModelForm(request.POST, request.FILES)
        form.is_valid()

        # make faked nonfield error
        # see http://stackoverflow.com/questions/8598247/how-to-append-error-message-to-form-non-field-errors-in-django
        form._errors[NON_FIELD_ERRORS] = form.error_class([e.message])

        # We may handle exception here (just to save indentation)
        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
            self.get_prepopulated_fields(request),
            self.get_readonly_fields(request),
            model_admin=self)
        media = self.media + adminForm.media

        inline_admin_formsets = []
        for inline, formset in zip(inline_instances, formsets):
            fieldsets = list(inline.get_fieldsets(request))
            readonly = list(inline.get_readonly_fields(request))
            prepopulated = dict(inline.get_prepopulated_fields(request))
            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
                fieldsets, prepopulated, readonly, model_admin=self)
            inline_admin_formsets.append(inline_admin_formset)
            media = media + inline_admin_formset.media

        context = {
            'title': _('Add %s') % force_text(opts.verbose_name),
            'adminform': adminForm,
            'is_popup': IS_POPUP_VAR in request.REQUEST,
            'media': media,
            'inline_admin_formsets': inline_admin_formsets,
            'errors': helpers.AdminErrorList(form, formsets),
            'app_label': opts.app_label,
            'preserved_filters': self.get_preserved_filters(request),
        }
        context.update(extra_context or {})
        return self.render_change_form(request, context, form_url=form_url, add=True)

admin.site.register(TestModel, TestModelAdmin)

My models.py:

from django.db import models

class TestModel(models.Model):

    text = models.TextField()

You see, there’s no easy way of hooking inside save_model() so you’ll have to copy-paste part of form preparation code.

Answered By: twil

@twil — Thanks for your help. You put me on the right track. Really appreciate your help. However, the solution didn’t work out of box. Didn’t actually show errors in my test case or work with change_view. Here’s want I ended up working with.

from django.contrib.admin import ModelAdmin
from django.db import DatabaseError, IntegrityError
from django.contrib import messages


class ShowValidationAdmin(ModelAdmin):

    def add_view(self, request, form_url='', extra_context=None):
        try:
            return super(ShowValidationAdmin, self).add_view(request, form_url, extra_context)
        except (IntegrityError, DatabaseError) as e:

            request.method = 'GET'
            messages.error(request, e.message)
            return super(ShowValidationAdmin, self).add_view(request, form_url, extra_context)

    def change_view(self, request, object_id, form_url='', extra_context=None):
        try:
            return super(ShowValidationAdmin, self).change_view(request, object_id, form_url, extra_context)
        except (IntegrityError, DatabaseError) as e:

            request.method = 'GET'
            messages.error(request, e.message)
            return super(ShowValidationAdmin, self).change_view(request, object_id, form_url, extra_context)

Note: That this version also seems to work cross version (django 1.3 – 1.6). Let me know if anyone has a better approach. I’ll wait to award bounty.

Answered By: keithhackbarth

Try this:

 from django.core.exceptions import ValidationError
    def save_model(self, request, obj, form, change):
        try:
            obj.save()
        except DatabaseError as e:
            raise ValidationError(e)
Answered By: Bradia

Old question, but nobody mentioned ModelAdmin.message_user() yet, which also uses the messages framework.

For example:

from django.contrib import admin, messages

class MyModelAdmin(admin.ModelAdmin):
    ...
    
    def save_model(self, request, *args, **kwargs):
        try:
            super().save_model(request, *args, **kwargs)
        except DatabaseError as e:
            messages.set_level(request=request, level=messages.ERROR)            
            self.message_user(request=request, message=e, level=messages.ERROR)

This would show your message as follows (assuming that’s the error string in e):

error message

The set_level call suppresses the default success message. Also see e.g. here and here.

Answered By: djvg

In save_model(), you can handle DatabaseError exception separately and differently for adding or changing an object as shown below:

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    
    def save_model(self, request, obj, form, change): # Here
        last_part_of_path = request.path.split('/')[-2]

        if last_part_of_path == "add":
            try: # ↓ I intentionally raise an exception
                raise DatabaseError("Add Error")
                obj.save()
            except DatabaseError as e:
                messages.set_level(request, messages.ERROR)
                messages.error(request, e)

        if last_part_of_path == "change":
            try: # ↓ I intentionally raise an exception
                raise DatabaseError("Change Error")
                obj.save()
            except DatabaseError as e:
                messages.set_level(request, messages.ERROR)
                messages.error(request, e)
Answered By: Kai – Kazuya Ito
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.