https://docs.djangoproject.com/en/4.0/ref/models/instances/#validating-objects
from django.core.exceptions import ValidationError
try:
article.full_clean()
except ValidationError as e:
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
There tell us can Display them to a user, how to display errors in Admin?
When I do nothing:
When Settings.py Debug = True, it always render a ValidationError at
/admin/xxx/xxx/xxx/change/ page.
When Settings.py Debug = False, it always render a HTTP 500 page.
Some code in models.py:
def clean(self):
try:
if self.get_previous_state:
if self.get_previous_state.state_choice == self.state_choice and \
self.get_previous_state.state_location == self.state_location:
raise ValidationError({"state_choice": f"提交的状态与当前状态冲突({self.product_entity.rfidtag.EPC_bank})"},
params={'state_choice': self.state_choice})
except Exception as e: # except the RelatedObjectDoesNotExist Exception to ValidationError
raise ValidationError(e)
def save(self, force_insert=True, *args, **kwargs):
self.pk = None # force_insert 具有 pk unique 限制
self.previous_state = self.get_previous_state
self.full_clean()
super(ProductEntityState, self).save(force_insert=True, *args, **kwargs)
sucess condition
error condition
What is the context of that code? Django will automatically show the errors for you in the admin when creating/editing the object through the admin. You don't have to manually call full_clean() as the auto-generated ModelForm from the admin will do so.
I fixed this bug by myself.
Because django admin just call formsets.all_valid() to validtion in ModelAdmin._changeform_view().
ModelAdmin._changeform_view() uses transaction.atomic().
When formsets.all_valid(), the froms which will be deleted didn't be commit.
Now, the previous state is the instance which will be deleted.It will not raise the ValidationError, the formsets.all_valid() will return True.
So ModelAdmin._changeform_view() will call from.instance.save() in foreach formsets.
This time, the previous state was be changed. So it will raise the ValidationError.
But ModelAdmin._changeform_view() didn't expect ValidationError in ModelAdmin.save_related(). it will expect by django\core\handlers\exception.py and call response_for_exception().
I cover the ModelAdmin._create_formsets() and fill the previous_state in all formsets.instance.
Notice: when you get form, you should watch which model not call the model.clean(), I use form.changed_data to get instance.
Then, when ModelAdmin._create_formsets() call formsets.all_valid(). It will return False, and raise ValidationError in your page.
Related
In the get_object method of class views, can I direct the user to a template instead of returning the object if an if statement fails?
Currently raise Http404("Some message.") works good but it doesn't look nice, I want to use my own template.
I'm trying to do this but with templates:
def get_object(self):
product = Product.objects.get(slug=self.kwargs.get('slug'))
if product.deleted == False:
if product.out_of_stock == False:
return product
else:
raise Http404("This product is sold out.")
# return reverse("404-error", kwargs={"error": "sold-out"})
# return render(request, "custom_404.html", {"error": "sold_out"})
else:
raise Http404("This product is no longer available.")
# return reverse("404-error", kwargs={"error": "deleted"})
# return render(request, "custom_404.html", {"error": "deleted"})
My main goal is to just avoid getting the object. I know I can perform the if statement in the get_context_data method, however I wasn't sure for objects containing sensitive data if there would be any way for a user to access it once it's in the get_object, so I just wanted to avoid getting the object altogether if the condition fails and display a template to the user.
You can use your own view when a 404 error occurs, first create a custom view:
views
from django.shortcuts import render
def handler404(request, *args, **kwargs):
return render(request, template_name='custom_404.html', status=404)
Now you need to override the default 404 view, adds this in your main urls.py file:
urls.py
handler404 = 'appname.views.handler404' # Replaces appname with the name of the app that contains the custom view
Now you can simply raise a Http404 exception to show your custom template (you can keep your actual code).
Suppose I have a simple Django model:
class MyModel(Model):
created = models.DateTimeField(default=datetime.utcnow)
#property
def my_property(self):
raise ValueError('Is anyone there')
Then in my admin GUI, I show my_property as a read-only field.
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
...
readonly_fields = ['my_property']
To my surprise, if any exception is raised in generating a value in the Django admin GUI, Django suppresses the error and just shows a "-" as the value.
It took me some time to realize this was the case.
Is there a way to force Django to show exceptions when exceptions occur?
It happens because of this implementation for AdminReadOnlyField:
field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
try:
f, attr, value = lookup_field(field, obj, model_admin)
except (AttributeError, ValueError, ObjectDoesNotExist):
result_repr = self.empty_value_display
So it will silently suppress those 3 exceptions.
One solution could be to create a decorator like:
from functools import wraps
class ReadonlyFieldError(Exception):
pass
def catch_field_exception(func):
"""
Django silenty suppresses AttributeError, ValueError, ObjectDoesNotExist exceptions that occur during processing of
custom readonly admin fields.
Add this decorator to re-raise exception so it is not missed
:param func:
:return:
"""
#wraps(func)
def wrapped_func(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
raise ReadonlyFieldError('Exception occurred while processing Admin field "{}": {}'.format(func.__name__,
str(e))) from e
return wrapped_func
Then decorate your method with this:
class MyModel(Model):
created = models.DateTimeField(default=datetime.utcnow)
#catch_field_exception
#property
def my_property(self):
raise ValueError('Is anyone there')
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?
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).
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')
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...
I have a custom field called CustomField to wrap a class called Example like so:
from django import forms
from django.db import models
from . import Example
class CustomField(models.CharField):
__metaclass__ = models.SubfieldBase
def to_python(self, value):
try:
return Example(value)
except Example.InvalidValueException:
raise forms.ValidationError('bad value!')
def get_prep_value(self, value):
return unicode(value)
def formfield(self, **kwargs):
defaults = {'form_class': forms.CharField}
defaults.update(kwargs)
return super(CustomField, self).formfield(**defaults)
The problem is in admin forms: when ValidationError is raised in the clean() or validate() methods for a field, the error message is displayed neatly next to the respective input field in the form. However, when it's raised in the to_python() method like in my custom field, an invalid value breaks the form ungracefully and displays an error page with the traceback about ValidationError. How do I get my field to work correctly with the Django admin forms?
EDIT: Based on a suggestion below I tried:
class CustomFieldForm(forms.CharField):
def validate(self, value):
raise forms.ValidationError('test')
And changed the formfield() method to use the class like so:
def formfield(self, **kwargs):
defaults = {'form_class': CustomFieldForm}
defaults.update(kwargs)
return super(CustomField, self).formfield(**defaults)
Yet the to_python() method still gets called before the CustomFieldForm.validate() method and the Example.InvalidValueException raised in the former method in case of an invalid value breaks the admin form.
I've found the solution thanks to ojii pointing me in the right direction. Explanation is in the docs about SubfieldBase. In short, because I used __metaclass__ = models.SubfieldBase, I needed to subclass forms.CharField and raise the ValidationError from its to_python() method, and then use this in my field's formfield() method instead of plain forms.CharField.
You should not raise a forms.ValidationError in a model field. Rather, in the field class returned in formfield, implement a validate method that accepts a value parameter. That method can raise form validation errors.
I have a booking model that needs to check if the item being booked out is available. I would like to have the logic behind figuring out if the item is available centralised so that no matter where I save the instance this code validates that it can be saved.
At the moment I have this code in a custom save function of my model class:
def save(self):
if self.is_available(): # my custom check availability function
super(MyObj, self).save()
else:
# this is the bit I'm stuck with..
raise forms.ValidationError('Item already booked for those dates')
This works fine - the error is raised if the item is unavailable, and my item is not saved. I can capture the exception from my front end form code, but what about the Django admin site? How can I get my exception to be displayed like any other validation error in the admin site?
In django 1.2, model validation has been added.
You can now add a "clean" method to your models which raise ValidationError exceptions, and it will be called automatically when using the django admin.
The clean() method is called when using the django admin, but NOT called on save().
If you need to use the clean() method outside of the admin, you will need to explicitly call clean() yourself.
http://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#validating-objects
So your clean method could be something like this:
from django.core.exceptions import ValidationError
class MyModel(models.Model):
def is_available(self):
#do check here
return result
def clean(self):
if not self.is_available():
raise ValidationError('Item already booked for those dates')
I haven't made use of it extensively, but seems like much less code than having to create a ModelForm, and then link that form in the admin.py file for use in django admin.
Pretty old post, but I think "use custom cleaning" is still the accepted answer. But it is not satisfactory. You can do as much pre checking as you want you still may get an exception in Model.save(), and you may want to show a message to the user in a fashion consistent with a form validation error.
The solution I found was to override ModelAdmin.changeform_view(). In this case I'm catching an integrity error generated somewhere down in the SQL driver:
from django.contrib import messages
from django.http import HttpResponseRedirect
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
try:
return super(MyModelAdmin, self).changeform_view(request, object_id, form_url, extra_context)
except IntegrityError as e:
self.message_user(request, e, level=messages.ERROR)
return HttpResponseRedirect(form_url)
The best way is put the validation one field is use the ModelForm... [ forms.py]
class FormProduct(forms.ModelForm):
class Meta:
model = Product
def clean_photo(self):
if self.cleaned_data["photo"] is None:
raise forms.ValidationError(u"You need set some imagem.")
And set the FORM that you create in respective model admin [ admin.py ]
class ProductAdmin(admin.ModelAdmin):
form = FormProduct
I've also tried to solve this and there is my solution- in my case i needed to deny any changes in related_objects if the main_object is locked for editing.
1) custom Exception
class Error(Exception):
"""Base class for errors in this module."""
pass
class EditNotAllowedError(Error):
def __init__(self, msg):
Exception.__init__(self, msg)
2) metaclass with custom save method- all my related_data models will be based on this:
class RelatedModel(models.Model):
main_object = models.ForeignKey("Main")
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.main_object.is_editable():
super(RelatedModel, self).save(*args, **kwargs)
else:
raise EditNotAllowedError, "Closed for editing"
3) metaform - all my related_data admin forms will be based on this (it will ensure that admin interface will inform user without admin interface error):
from django.forms import ModelForm, ValidationError
...
class RelatedModelForm(ModelForm):
def clean(self):
cleaned_data = self.cleaned_data
if not cleaned_data.get("main_object")
raise ValidationError("Closed for editing")
super(RelatedModelForm, self).clean() # important- let admin do its work on data!
return cleaned_data
To my mind it is not so much overhead and still pretty straightforward and maintainable.
from django.db import models
from django.core.exceptions import ValidationError
class Post(models.Model):
is_cleaned = False
title = models.CharField(max_length=255)
def clean(self):
self.is_cleaned = True
if something():
raise ValidationError("my error message")
super(Post, self).clean()
def save(self, *args, **kwargs):
if not self.is_cleaned:
self.full_clean()
super(Post, self).save(*args, **kwargs)