Showing custom model validation exceptions in the Django admin site - django

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)

Related

how to display models.full_clean() ValidationError in django admin?

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.

How do I prevent Django's admin interface from silently suppressing errors?

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')

Django Admin change_list forcing search_form to reject given seach keywords

Problem:
I'd like to enforce the change_list_view when opening the page with "?q=" (that is with no value or whitespace) and simply load the page by not executing queries at all.
Possible ways:
My question is how can I do that by overriding the queryset, get_queryset, or get_search_results methods ?
Any other means are welcomed?
Expectations:
The end goal is to:
start with a page displaying 0 results
no database activity
the search text input focused
underneath a message instructing the user the it may start typing the thing it seeks.
Thanks.
You can't override ModelAdmin.get_queryset() because it is used in the edit/delete views. So you have to inherit ChangeList class and override get_queryset() in it:
from django.contrib import messages
from django.contrib.admin.views.main import ChangeList
class MyChangeList(ChangeList):
def get_queryset(self, request):
queryset = super(MyChangeList, self).get_queryset(request)
if not request.GET.get('q', ''):
messages.add_message(request, messages.INFO, 'Start typing.')
queryset = queryset.none()
return queryset
class MyAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
return MyChangeList

How to limit access to the UpdateView of an object to the creator of that object

Django and programming noob here. I've made an application I'd like to deploy, but I need to figure out how to limit access to the UpdateView to the creator of that object, and I'm stumped.
Currently a user can use the CreateView .../universities/create/ to create a university object, but then any user can use .../universities/update/ to edit that object. I want to configure this so only the user who is the creator (any user with the ManytoMany attribute 'administrator') of that university has access to the UpdateView for their university object.
Any advice would be appreciated. I've spent a few days on this and I haven't made much traction...thanks for reading.
models.py
class University(models.Model):
name = models.CharField(max_length=100)
about = models.TextField()
administrators = models.ManyToManyField(User)
profile_picture = models.FileField(upload_to=get_upload_file_name, blank=True)
def __unicode__(self):
return unicode(self.name)
def get_absolute_url(self):
return reverse('university_detail', kwargs={'pk': str(self.id)})
views.py
class UniversityCreateView(CreateView):
model = University
form_class = UniversityForm
template_name = 'university_create.html'
def form_valid(self, form):
f = form.save(commit=False)
f.save()
return super(UniversityCreateView, self).form_valid(form)
class UniversityUpdateView(UpdateView):
model = University
form_class = UniversityForm
template_name='university_form.html'
You can use UserPassesTestMixin as the documentation says:
limit access based on certain permissions or some other test
just implement test_func(self) that returns True if the user should enter the view.
You might write a code like this:
class UniversityUpdateView(UserPassesTestMixin,UpdateView):
def test_func(self):
return self.request.user.administrators_set.filter(pk=self.get_object().pk).exists()
model = University
form_class = UniversityForm
template_name='university_form.html'
youll have to include permission decorators on your views , further info is here https://docs.djangoproject.com/en/dev/topics/auth/ , & https://docs.djangoproject.com/en/dev/topics/auth/default/#topic-authorization
so if you want to limit your updateview to any user with the ManytoMany attribute 'administrator', youll have to do something like this:
views.py
from appname.users.decorators import requiresGroup
from django.contrib.auth.decorators import login_required
class UniversityUpdateView(UpdateView):
model = University
form_class = UniversityForm
template_name='university_form.html'
#method_decorator(requiresGroup("groupname" , login_url='/accounts/login/'))
def dispatch(self, request, *args, **kwargs):
return super(UniversityUpdateView, self).dispatch(request, *args, **kwargs)
also if you havent already youll have to include the following at the top of your models.py
from django.contrib.auth.modes import user
though Ill assume its there as youve defined your administrators with the user model
then go to the group seetings in the django admin ( should be a url like localhost/admin/auth/group , add your special adminstrator group name, then go to the admin user section (localhost/admin/auth/user), then make sure they have been put into the adminstrator group
then replace "groupname" in the #requiresGroup decorator with the actual name of the user group
the #requiresGroup decorator isnt a standard decorator, so it has to be written
make a folder path and file like appname/users.decorators.py
then in decorators.py write
from functools import update_wrapper , wraps
from django.utils.decorators import available_attrs
from django.http import HttpResponse, HttpResponseRedirect
def requiresGroup(groupname):
def decorator(view_function):
def _wrapped_view(request,*args,**kwargs):
if request.user.groups.filter(name=groupname).count()!=1:
return HttpResponseRedirect("/")
else:
return view_function(request,*args,**kwargs)
return wraps(view_function,assigned=available_attrs(view_function))(_wrapped_view)
return decorator
hope this helped
edit: made a mistake, put the decorators above the class, they should be in a function inside the class, noticed my mistake almost immediately so hopefully I havent caused any trouble
You can override the get method of your class based view (in this case UniversityUpdateView). Then in the method check if user has rights to access the page and if not raise exception or redirect the user to another page. If the user has enough rights to access the page then just let the normal behavior go on.
class UniversityUpdateView(UpdateView):
model = University
form_class = UniversityForm
template_name='university_form.html'
def get(self, request, *args, **kwargs):
if request.user.groups.filter(name=groupname).count()!=1:
return HttpResponseRedirect("/")
return super().get(request, *args, **kwargs)

Raising ValidationError in a custom field's to_python() method breaks admin forms

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.