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.
0 comments:
Post a Comment