django - Best way to deal with validation checks in model creation - django

I am trying to figure out how to use validations when creating an object in django.
From my POV, there are 2 approaches:
Override the default validate_field method of the DRF serializers.
Add field-level validators to models and catch any IntegrityError or ValidationError exception when serializer calls .save() method of the model.
Both ways seem to have their cons.
By using approach 1 my models are left "unprotected" from any other .create() call of the model besides the serializers. Approach 2 deals with the above issue, but makes the code more complex since exception handling is neccessary in serializer's .create() method.
Is there anyone that has faced a similar issue and/or found a "cleaner" way to deal with this?

As far as i understood you need Django model validation. You can try this approach (I think, this is exactly what you want).
from django.core.exceptions import ValidationError
class Foo(models.Model):
name = models.CharField(max_length=255)
def clean(self):
raise ValidationError('Problem during validation')
f = Foo(name='test')
f.full_clean() # This is what you need. f.clean() will be enough, but f.full_clean() will make field level validations (run validators) and checking about uniqueness also.
f.save()
In general Django never makes model level validations(Does not call full_clean()), during model creation.
f = Foo(**fields_dict)
f.save() # no validation performed.
call it yourself if you want,,,
f = Foo(**fields_dict)
f.full_clean() # validation performed
f.save()
full_clean() method is performed automatically, if you use ModelForm class. This is additional hook by Django.

I think that moving the validations down to the model is the safest way to ensure the highest level of reliability, because the validations are going to be applied as long as they are done through Django (if you use serializers, validations wouldn't work if Django Rest Framework is skipped).
Model level validations can be:
Field level validations: You create a method that makes the validation, and set such method as field validator:
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_even(value):
if value % 2 != 0:
raise ValidationError(
_('%(value)s is not an even number'),
params={'value': value},
)
from django.db import models
class MyModel(models.Model):
even_field = models.IntegerField(validators=[validate_even])
Model level validations: You override model's clean() method and perform the required validations:
import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
class Article(models.Model):
...
def clean(self):
# Don't allow draft entries to have a pub_date.
if self.status == 'draft' and self.pub_date is not None:
raise ValidationError(_('Draft entries may not have a publication date.'))
# Set the pub_date for published items if it hasn't been set already.
if self.status == 'published' and self.pub_date is None:
self.pub_date = datetime.date.today()

Related

How to show a non_field_errors ValidationError raised in model.clean() in inline admin

Using Django 2.2 I added a custom clean() method to a model.
class MyModel(Model):
employees = models.ManyToManyField(Employee)
def clean(self):
raise ValidationError({'non_field_errors': _("Error message here.")})
super().clean()
I am showing this model in the Django admin using an InlineModelAdmin, TabularInline to be precise, code looks like this:
class MyModelInline(BaseModelTabularInline):
model = MyModel.employees.through
And in EmployeeAdmin:
#admin.register(Employee)
class EmployeeAdmin(BaseModelAdmin):
inlines = (MyModelInline)
I've hidden some code, don't know if my employer would like me to share it. But when I now try to save an instance of MyModel using the InlineModelAdmin, I get an error 'MyModelForm' has no field named 'non_field_errors'.
Can anyone tell me how to have this field available, so that I can display potential validation errors (coming from MyModel.clean())
PS.
I notice that raising ValidationErrors for other fields (that are on my model) works perfecly, example:
raise ValidationError({'confidential_level': _("Error message.")})
Use __all__ or better, from django.core.exceptions import NON_FIELD_ERRORS and use that (which by default is "__all__")

Django model validator not working on create

I have model with a field validator
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
class MyModel(model.Model):
name = models.CharField()
size = models.IntegerField(validators=[MinValueValidator(1),MaxValueValidator(10)])
The validator is working well in the django admin panel ,while I try to enter the value more than 10, it's showing me the error message 'Ensure this value is less than or equal to 10' and does not allow to save.
But, when I try in the django shell, the validator is not working, it allows to save the record, I don't know why is the validator not throwing error message here.
>>>form app.models import MyModel
>>>MyModel.objects.create(name="Some Name", size=15)
<MyModel: Some Name>
Can you please suggest me if anything I missed or any mistake i did here. Kindly help me to solve this problem, it will be very greatfull for me, Thanks in advance.
Django validation is mostly application level validation and not validation at DB level. Also Model validation is not run automatically on save/create of the model. If you want to validate your values at certain time in your code then you need to do it manually.
For example:
from django.core.exceptions import ValidationError
form app.models import MyModel
instance = MyModel(name="Some Name", size=15)
try:
instance.full_clean()
except ValidationError:
# Do something when validation is not passing
else:
# Validation is ok we will save the instance
instance.save()
More info you can see at django's documentation https://docs.djangoproject.com/en/1.10/ref/models/instances/#validating-objects
In administration it works automatically because all model forms (ModelForm) will run model validation process alongside form validation.
If you need to validate data because it is coming from untrusted source (user input) you need to use ModelForms and save the model only when the form is valid.
The validator only works when you are using models in a ModelForm.
https://docs.djangoproject.com/en/dev/ref/validators/#how-validators-are-run
You can perform model validation by overidding clean() and full_clean() methods
Validators work only with the Forms and model forms. Can't be used with the model definition because it runs at the app side not the DB side.
You can add this to your model and call it in save().
def save(self, *args, **kwargs):
self.run_validators()
super().save(*args, **kwargs)
def run_validators(self) -> None:
for field_name, field_value in model_to_dict(self).items():
model_field = getattr(UserSearchHistory, field_name)
field = getattr(model_field, 'field', object())
validators = getattr(field, 'validators', list())
for validator_func in validators:
if field_value is not None:
validator_func(field_value)
From django documentation:
Note that validators will not be run automatically when you save a
model, but if you are using a ModelForm, it will run your validators
on any fields that are included in your form.
https://docs.djangoproject.com/en/3.1/ref/validators/#how-validators-are-run
I ran into the same issue.
So the validators only work when you are using Forms and Model Form to fill it.
However, by creating in shell, you probably wanted to test the validators before going live.
So here is the additional piece of code to help in validating the validators.
>>>form app.models import MyModel
>>>MyModel.size.field.run_validators(value=<undesirable value>)
You can not run validator in creating you must run validation in instance if not exception occurred you must save it
It is worth mentioning that model field validators
like validate_color in here:
bg_color = models.CharField(
max_length=50, default="f0f2f5", validators=[validate_color]
)
work with restf_ramework (drf) Serializer class either.
https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py
so validators run when you call is_valid on ModelForm (from django) or is_valid on Serializer (from rest_framework).

Django Custom Validators Not Working

I wrote a custom validator, which will raise ValidationError if given field value is negative.
def validate_positive(value):
if value < 0:
raise ValidationError(
_('%(value) is negative number'),
params = {'value': value}
)
i added this to my model field via the field’s validators argument
class Book(models.Model):
user = models.ForeignKey(User,on_delete=models.CASCADE)
title = models.CharField(max_length=50)
price = models.IntegerField(default=0,validators=[validate_positive])
quantity = models.IntegerField(default=0,validators=[validate_positive])
But while creating object it's not raising any error if price is less than zero.
I don't know where i am doing wrong and i am new to django.
I am using Django 1.9.
Please help me .
Validators are used for forms, not for creating an object. If you're creating an object outside of a form then you need to provide an alternative way to validate input.
The easiest way to do this is to call the model's full_clean method before saving as shown in the docs
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
This is similar to what would happen from a form, and would call any validators on your model fields.
I ran into the same issue. So the validators only work when you are using Forms and Model Form to fill it.
You can validate the validators in the shell though.
python manage.py shell
>>>from app.models import Book
>>>Book.price.field.run_validators(value=<undesirable value>)
This would raise a validation error so you can be sure your validation is working.

Django custom field validator vs. clean

I would like to create TodayOrLaterDateField() which would subclass DateField() field as I am using this condition in many places. The purpose of this field would be avoiding putting dates from the past.
What is the most straightway way of doing this? I am confused with validator vs. clean method.
I've tried with clean() but when comparing value to datetime.date.today() I am getting "compare unicode object to date" error.
I'm using Django 1.3
Validators only validate, they don't return the improved format;
Clean methods both validate and return a (sometimes amended) value.
I think the way to go here is to just use a DateField with a validator as a inherited class of DateField with a default_validators set.
import datetime
from django.core import exceptions
from django.db import models
from django.utils.translation import ugettext_lazy as _
def validate_date_today_or_later(value):
'Place this in validators.py and import it to keep your model a bit cleaner'
if value < datetime.date.today():
raise exceptions.ValidationError(_('Date must be today or later'))
class TodayOrLaterDateField(models.DateField):
default_validators = [validate_date_today_or_later,]
edit:
You can apply the same validator to your form fields as well if you just want it there and not in your whole app.
You can extend models.DateField and override to_python method. Didn't tested on Django 1.3 but should work.
import datetime
from django.core import exceptions
from django.db import models
class TodayOrLaterDateField(models.DateField):
def to_python(self, value):
value = super(TodayOrLaterDateField, self).to_python(value)
if value < datetime.date.today():
raise exceptions.ValidationError(u'Date must be today or later')
return value

Django-Admin: list_filter attribute from UserProfile

I want to allow the admins of my site to filter users from a specific country on the Admin Site. So the natural thing to do would be something like this:
#admin.py
class UserAdmin(django.contrib.auth.admin.UserAdmin):
list_filter=('userprofile__country__name',)
#models.py
class UserProfile(models.Model)
...
country=models.ForeignKey('Country')
class Country(models.Model)
...
name=models.CharField(max_length=32)
But, because of the way Users and their UserProfiles are handled in django this leads to the following error:
'UserAdmin.list_filter[0]' refers to field 'userprofile__country__name' that is missing from model 'User'
How do I get around this limitation?
What you are looking for is custom admin FilterSpecs. The bad news is, the support for those might not supposed to ship soon (you can track the discussion here).
However, at the price of a dirty hack, you can workaround the limitation. Some highlights on how FilterSpecs are built before diving in the code :
When building the list of FilterSpec to display on the page, Django uses the list of fields you provided in list_filter
Those fields needs to be real fields on the model, not reverse relationship, nor custom properties.
Django maintains a list of FilterSpec classes, each associated with a test function.
For each fields in list_filter, Django will use the first FilterSpec class for which the test function returns True for the field.
Ok, now with this in mind, have a look at the following code. It is adapted from a django snippet. The organization of the code is left to your discretion, just keep in mind this should be imported by the admin app.
from myapp.models import UserProfile, Country
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
class ProfileCountryFilterSpec(ChoicesFilterSpec):
def __init__(self, f, request, params, model, model_admin):
ChoicesFilterSpec.__init__(self, f, request, params, model, model_admin)
# The lookup string that will be added to the queryset
# by this filter
self.lookup_kwarg = 'userprofile__country__name'
# get the current filter value from GET (we will use it to know
# which filter item is selected)
self.lookup_val = request.GET.get(self.lookup_kwarg)
# Prepare the list of unique, country name, ordered alphabetically
country_qs = Country.objects.distinct().order_by('name')
self.lookup_choices = country_qs.values_list('name', flat=True)
def choices(self, cl):
# Generator that returns all the possible item in the filter
# including an 'All' item.
yield { 'selected': self.lookup_val is None,
'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
'display': _('All') }
for val in self.lookup_choices:
yield { 'selected' : smart_unicode(val) == self.lookup_val,
'query_string': cl.get_query_string({self.lookup_kwarg: val}),
'display': val }
def title(self):
# return the title displayed above your filter
return _('user\'s country')
# Here, we insert the new FilterSpec at the first position, to be sure
# it gets picked up before any other
FilterSpec.filter_specs.insert(0,
# If the field has a `profilecountry_filter` attribute set to True
# the this FilterSpec will be used
(lambda f: getattr(f, 'profilecountry_filter', False), ProfileCountryFilterSpec)
)
# Now, how to use this filter in UserAdmin,
# We have to use one of the field of User model and
# add a profilecountry_filter attribute to it.
# This field will then activate the country filter if we
# place it in `list_filter`, but we won't be able to use
# it in its own filter anymore.
User._meta.get_field('email').profilecountry_filter = True
class MyUserAdmin(UserAdmin):
list_filter = ('email',) + UserAdmin.list_filter
# register the new UserAdmin
from django.contrib.admin import site
site.unregister(User)
site.register(User, MyUserAdmin)
It's clearly not a panacea but it will do the job, waiting for a better solution to come up.(for example, one that will subclass ChangeList and override get_filters).
Django 1.3 fixed it. You're now allowed to span relations in list_filter
https://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter