Monday, November 20, 2017

Crash validating related model created using inline admin

Leave a Comment

I'm creating a one-to-one model to extend the functionality of an existing model type, but I want it to only allow creating the extension model in certain cases. I enforce this constraint by throwing a ValidationError in the full_clean on the new Extended model. This works great when I create Extended models using Extended's ModelAdmin directly (it highlights the a field if it's the wrong type), but when I use StackedInline to inline Extended creation in As ModelAdmin, and A is the wrong type, the form fails to catch the ValidationError and I get the message A server error occurred. Please contact the administrator.

This is how I have the models set up:

# models.py from django.db import models  class A(models.Model):     type = models.IntegerField(...)  class Extended(models.Model)     a = models.OneToOneField(A)      def clean_fields(self, **kwargs):         if self.a.type != 3:             raise ValidationError({'a': ["a must be of type 3"]})         super(Extended, self).clean_fields(**kwargs)      def save(self, *args, **kwargs):         self.full_clean()         super(Extended, self).save(*args, **kwargs)  # admin.py from django.contrib import admin  class ExtendedInline(admin.StackedInline):     model = Extended  @admin.register(A) class AAdmin(admin.ModelAdmin):     inlines = (ExtendedInline,) 

The full traceback:

Traceback (most recent call last):   File "/usr/local/lib/python2.7/wsgiref/handlers.py", line 85, in run     self.result = application(self.environ, self.start_response)   File "/usr/local/lib/python2.7/site-packages/django/contrib/staticfiles/handlers.py", line 63, in __call__     return self.application(environ, start_response)   File "/usr/local/lib/python2.7/site-packages/whitenoise/base.py", line 66, in __call__     return self.application(environ, start_response)   File "/usr/local/lib/python2.7/site-packages/django/core/handlers/wsgi.py", line 189, in __call__     response = self.get_response(request)   File "/usr/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 218, in get_response     response = self.handle_uncaught_exception(request, resolver, sys.exc_info())   File "/usr/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 132, in get_response     response = wrapped_callback(request, *callback_args, **callback_kwargs)   File "/usr/local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 618, in wrapper     return self.admin_site.admin_view(view)(*args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 110, in _wrapped_view     response = view_func(request, *args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/views/decorators/cache.py", line 57, in _wrapped_view_func     response = view_func(request, *args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/contrib/admin/sites.py", line 233, in inner     return view(request, *args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 1521, in change_view     return self.changeform_view(request, object_id, form_url, extra_context)   File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 34, in _wrapper     return bound_func(*args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 110, in _wrapped_view     response = view_func(request, *args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 30, in bound_func     return func.__get__(self, type(self))(*args2, **kwargs2)   File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 145, in inner     return func(*args, **kwargs)   File "/usr/local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 1470, in changeform_view     self.save_related(request, form, formsets, not add)   File "/usr/local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 1104, in save_related     self.save_formset(request, form, formset, change=change)   File "/usr/local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 1092, in save_formset     formset.save()   File "/usr/local/lib/python2.7/site-packages/django/forms/models.py", line 636, in save     return self.save_existing_objects(commit) + self.save_new_objects(commit)   File "/usr/local/lib/python2.7/site-packages/django/forms/models.py", line 767, in save_new_objects     self.new_objects.append(self.save_new(form, commit=commit))   File "/usr/local/lib/python2.7/site-packages/django/forms/models.py", line 900, in save_new     obj.save()   File "/code/app/models.py", line 162, in save     self.full_clean()   File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 1171, in full_clean     raise ValidationError(errors) ValidationError: {'a': [u'a must be of type 3']} 

I'm currently using Django version 1.8

2 Answers

Answers 1

Inline foreignkeys are excluded on the calls to full_clean() that happen in modelform validators, so your ValidationError is not being caught by the form's is_valid() call.

From django/forms/models.py:

def _post_clean(self):     opts = self._meta      exclude = self._get_validation_exclusions()      try:         self.instance = construct_instance(self, self.instance, opts.fields, exclude)     except ValidationError as e:         self._update_errors(e)      # Foreign Keys being used to represent inline relationships     # are excluded from basic field value validation. This is for two     # reasons: firstly, the value may not be supplied (#12507; the     # case of providing new values to the admin); secondly the     # object being referred to may not yet fully exist (#12749).     # However, these fields *must* be included in uniqueness checks,     # so this can't be part of _get_validation_exclusions().     for name, field in self.fields.items():         if isinstance(field, InlineForeignKeyField):             exclude.append(name)      try:         self.instance.full_clean(exclude=exclude, validate_unique=False)     except ValidationError as e:         self._update_errors(e)      # Validate uniqueness if needed.     if self._validate_unique:         self.validate_unique() 

They're being caught in save() instead (where you call full_clean without the exclusion), which is too late.

Move your validation to clean() instead:

def clean(self):     if self.a.type != 3:         raise ValidationError({'a': ["a must be of type 3"]}) 

Then there will be no need to call full_clean from your save method. This method is where any validation of this sort should go as per the docs.

Answers 2

Though not ideal, spinkus posted an answer that's also fixing my crash. It involves overriding the changeform_view method in AAdmin:

@admin.register(A) class AAdmin(admin.ModelAdmin):     inlines = (ExtendedInline,)      def changeform_view(self, request, object_id=None, form_url='', extra_context=None):         # Need to override to catch ValidationError in pre_save and save hooks.         try:             return super(AAdmin, self).changeform_view(request, object_id, form_url, extra_context)         except ValidationError as e:             self.message_user(request, '\n'.join(e.messages), level=messages.ERROR)             return HttpResponseRedirect(form_url) 

This causes AAdmin to show an error message at the top of the form instead of crashing. Unfortunately, it clears the user's other changes and doesn't do field-level highlighting.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment