Form Validation - Clean Method Interfering with PATCH Requests - python

I have a form that looks like this:
class ContactForm(forms.ModelForm):
error_messages = {
'duplicate_name': 'A backup contact with that name already exists for this location',
'missing_location': 'No location supplied.'
}
class Meta:
fields = ('name', 'notification_preference', 'email', 'phone')
model = Contact
········
def clean_name(self):
# FIXME: Location validation shouldn't be happening here, but it's difficult to get
# form fields for foreign key relationships to play nicely with Tastypie
print dir(self)
if 'location' in self.data:
location = Location.objects.get(pk=self.uri_to_pk(self.data['location']))
else:
raise forms.ValidationError(self.error_messages['missing_location'])
# No duplicate names in a given location
if 'name' in self.cleaned_data and Contact.objects.filter(name=self.cleaned_data['name'], location=location).exists():
raise forms.ValidationError(self.error_messages['duplicate_name'])
return self.cleaned_data
I'm using it to validate calls to my TastyPie API. The clean_name method is meant to prevent POST requests from happening if they post a contact with the same name to the same location. This works perfectly as long as I'm making a POST request.
If I make a PATCH however, changing, say, the email and phone field on an already existent contact, the clean_name logic is still fired. Since the name already exists for a given location, it raises a validation error.
Should I be overriding something other than clean_name? Can I change the way PATCH works so it ignores certain validations?

Yes, if you're checking values between fields, I'd recommend implementing a general def clean(self, data) that checks the values don't conflict.
Regarding checking for duplicates, I'd advise you to use an .exclude(pk=instance.pk) in your exists() queryset to prevent mistakenly detecting an update the the model as a duplicate. Looking at the tastypie validation source, it adds the appropriate self.instance for the object being updated.
qs = Contact.objects.filter(name=self.cleaned_data.get('name', ''), location=location)
if self.instance and self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise forms.ValidationError(...)

Related

How should i auto fill a field and make it readonly in django?

I`m new to django and i was doing a test for my knowledge.
Found a lot of duplicates in here and web but nothing useful
I'm trying to make a ForeignKey field which gets filled due to the other fields that user fills, and make it unchangeable for the user.
I thought that I should use overriding save() method but couldn't figure that at all.
How should I do that auto-fill and read-only thing?
Your approach is right. Override the save method and if self.pk is not None raise an exception if your field has changed. You can use django model utils to easily track changes in your model: https://django-model-utils.readthedocs.io/en/latest/utilities.html#field-tracker
Principle:
class MyModel(models.Model):
#....
some_field = models.Foreignkey(...)
tracker = FieldTracker()
def save(*args, **kwargs):
if self.pk is None:
# new object is being created
self.some_field = SomeForeignKeyObject
else:
if self.tracker.has_changed("some_field"):
raise Exception("Change is not allowed")
super().save(*args, **kwargs)

What's the standard way of saving something only if its foreign key exists?

I'm using Python 3.7 and Django . I have the following model, with a foreign key to another model ...
class ArticleStat(models.Model):
objects = ArticleStatManager()
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
...
def save(self, *args, **kwargs):
if self.article.exists():
try:
article_stat = ArticleStat.objects.get(article=self.article, elapsed_time_in_seconds=self.elapsed_time_in_seconds)
self.id = article_stat.id
super().save(*args, **kwargs, update_fields=["hits"])
except ObjectDoesNotExist:
super().save(*args, **kwargs)
I only want to save this if the related foreign key exists, otherwise, I've noticed errors result. What's the standard Django/Python way of doing something like this? I thought I read I could use ".exists()" (Check if an object exists), but instead I get an error
AttributeError: 'Article' object has no attribute 'exists'
Edit: This is the unit test I have to check this ...
id = 1
article = Article.objects.get(pk=id)
self.assertTrue(article, "A pre-condition of this test is that an article exist with id=" + str(id))
articlestat = ArticleStat(article=article, elapsed_time_in_seconds=250, hits=25)
# Delete the article
article.delete()
# Attempt to save ArticleStat
articlestat.save()
If you want to be sure Article exists in ArticleStat's save method you can try to get it from your database and not just test self.article.
Quoting Alex Martelli:
" ... Grace Murray Hopper's famous motto, "It's easier to ask forgiveness than permission", has many useful applications -- in Python, ... "
I think using try .. except .. else is more pythonic and I will do something like that:
from django.db import models
class ArticleStat(models.Model):
...
article = models.ForeignKey(
Article, on_delete=models.CASCADE, related_name='articlestats'
)
def save(self, *args, **kwargs):
try:
article = Article.objects.get(pk=self.article_id)
except Article.DoesNotExist:
pass
else:
try:
article_stat = ArticleStat.objects.get(
article=article,
elapsed_time_in_seconds=self.elapsed_time_in_seconds
)
self.id = article_stat.id
super().save(*args, **kwargs, update_fields=["hits"])
except ArticleStat.DoesNotExist:
super().save(*args, **kwargs)
If you are using a relational database, foreign key constraints will be added automatically post-migration. save method may not need any customization.
class ArticleStat(models.Model):
objects = ArticleStatManager()
article = models.ForeignKey(
Article, on_delete=models.CASCADE, related_name='articlestats'
)
Use the following code to create ArticleStats
from django.db import IntegrityError
try:
ArticleStats.objects.create(article=article, ...)
except IntegrityError:
pass
If article_id is valid, ArticleStats objects get created else IntegrityError is raised.
article = Article.objects.get(id=1)
article.delete()
try:
ArticleStats.objects.create(article=article, ...)
print("article stats is created")
except IntegrityError:
print("article stats is not created")
# Output
article stats is not created
Note: Tested on MySQL v5.7, Django 1.11
article field on your ArticleStat model is not optional. You can't save your ArticleStat object without the ForeignKey to Article
Here is a similar code, item is a ForeignKey to the Item model, and it is required.
class Interaction(TimeStampedModel, models.Model):
...
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='interactions')
type = models.IntegerField('Type', choices=TYPE_CHOICES)
...
If I try to save an object of Interaction from the shell without selecting a ForeignKey to the item, I receive an IntegrityError.
~ interaction = Interaction()
~ interaction.save()
~ IntegrityError: null value in column "item_id" violates not-null constraint
You don't need a check self.article.exists(). Django and Database will require that field and will not let you save the object without it.
You should read about ForeignKey field in Django Docs
You can just test the value of the article field. If it's not set, I believe it defaults to None.
if self.article: # Value is set
If you want this ForeignKey field to be optional (which it sounds like you do), you need to set blank=True and null=True on that field. This will allow the field to be blank (in validation) and will set null on the field when it's not there.
As mentioned in the comments below, your database is likely enforcing the fact that the field is required, and refuses to remove the article instance.
As other answers have pointed out, the Article ForeignKey is required on your ArticleStat model and saving will automatically fail without a valid Article instance. The best way to fail gracefully with non-valid input is using Form validation with Django's Forms API. Or if handling serialized data with Django Rest Framework, using a Serializer, which is the Form-like equivalent for JSON data. That way you don't need to overwrite the save method unless you have a specific requirement.
So far nobody has mentioned the correct usage of .exists(). It is a method of a queryset not a model instance, which is why you get the error you mentioned above when trying to apply it to an individual model instance with self.article.exists(). To check the existence of an object, simply use .filter instead of .get. If your Article (pk=1) exists then:
Article.objects.filter(pk=1)
Will return a queryset with one Article in it:
<Queryset: [Article: 1]>
and
Article.objects.filter(pk=1).exists()
Will return True. Whereas if the item does not exist the query will return an empty queryset and .exists() will return False, rather than raising an exception (as attempting to .get() a non-existent object does). This still applies if the pk previously existed and has been deleted.
EDIT: Just noticed that your ArticleStat's on_delete behaviour is currently set to CASCADE. This means that when an Article is deleted, the related ArticleStat is also deleted. So I think you must have been misinterpreting the errors/difficulties you mentioned in reply to #jonah-bishop's answer when trying if self.article:. For a minimal fix, if you still want to keep the ArticleStat after an Article is deleted, change the on_delete keyword to models.SET_NULL, and as per Jonah's answer, add the extra keywords null=True, blank=True:
article = models.ForeignKey(Article, on_delete=models.SET_NULL, related_name='articlestats', null=True, blank=True)
Then there should be no problem simply doing if self.article: to check for a valid ForeignKey object relation. But using Forms/Serializers is still better practice though.
The code of last line articlestat.save() will fail if the article instance has been deleted. Django and database will check the article automatically for you if you are using relation database like mysql or sqlite3.
During the migrations, a constraint will be created. For example:
shell>>> python manage.py sqlmigrate <appname> 0001
CREATE TABLE impress_impress ...
...
ALTER TABLE `impress_impress` ADD CONSTRAINT
`impress_impress_target_id_73acd523_fk_account_myuser_id` FOREIGN KEY (`target_id`)
REFERENCES `account_myuser` (`id`);
...
So if you want to save the articlestat without article, an error will be raised.
You can call .full_clean() before .save()
from django.core.exceptions import ValidationError
class ArticleStat(models.Model):
#...
def save(self, *args, **kwargs):
try:
self.full_clean()
except ValidationError as e:
# dont save
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
else:
super().save(*args, **kwargs)

Order of Serializer Validation in Django REST Framework

Situation
While working with validation in the Django REST Framework's ModelSerializer, I have noticed that the Meta.model fields are always validated, even when it does not necessarily make sense to do so. Take the following example for a User model's serialization:
I have an endpoint that creates a user. As such, there is a password field and a confirm_password field. If the two fields do not match, the user cannot be created. Likewise, if the requested username already exists, the user cannot be created.
The user POSTs improper values for each of the fields mentioned above
An implementation of validate has been made in the serializer (see below), catching the non-matching password and confirm_password fields
Implementation of validate:
def validate(self, data):
if data['password'] != data.pop('confirm_password'):
raise serializers.ValidationError("Passwords do not match")
return data
Problem
Even when the ValidationError is raised by validate, the ModelSerializer still queries the database to check to see if the username is already in use. This is evident in the error-list that gets returned from the endpoint; both the model and non-field errors are present.
Consequently, I would like to know how to prevent model validation until after non-field validation has finished, saving me a call to my database.
Attempt at solution
I have been trying to go through the DRF's source to figure out where this is happening, but I have been unsuccessful in locating what I need to override in order to get this to work.
Since most likely your username field has unique=True set, Django REST Framework automatically adds a validator that checks to make sure the new username is unique. You can actually confirm this by doing repr(serializer()), which will show you all of the automatically generated fields, which includes the validators.
Validation is run in a specific, undocumented order
Field deserialization called (serializer.to_internal_value and field.run_validators)
serializer.validate_[field] is called for each field
Serializer-level validators are called (serializer.run_validation followed by serializer.run_validators)
serializer.validate is called
So the problem that you are seeing is that the field-level validation is called before your serializer-level validation. While I wouldn't recommend it, you can remove the field-level validator by setting extra_kwargs in your serilalizer's meta.
class Meta:
extra_kwargs = {
"username": {
"validators": [],
},
}
You will need to re-implement the unique check in your own validation though, along with any additional validators that have been automatically generated.
I was also trying to understand how the control flows during serializer validation and after carefully going through the source code of djangorestframework-3.10.3 I came up with below request flow diagram. I have described the flow and what happens in the flow to the best of my understanding without going into too much detail as it can be looked up from source.
Ignore the incomplete method signatures. Only focusing on what methods are called on what classes.
Assuming you have an overridden is_valid method on your serializer class (MySerializer(serializers.Serializer)) when you call my_serializer.is_valid() the following takes place.
MySerializer.is_valid() is executed.
Assuming you are calling the super class (BaseSerializer) is_valid method (like: super(MySerializer, self).is_valid(raise_exception) in your MySerializer.is_valid() method, that will be called.
Now since MySerializer is extending serializers.Serializer, the run_validation() method from serializer.Serializers is called. This is validating only the data dict the first. So we haven't yet started field level validations.
Then the validate_empty_values from fields.Field gets called. This again happens on the entire data and not a single field.
Then the Serializer.to_internal_method is called.
Now we loop over each fields defined on the serializer. And for each field, first we call the field.run_validation() method. If the field has overridden the Field.run_validation() method then that will be called first. In case of a CharField it is overridden and calls the run_validation method of Field base class. Step 6-2 in the figure.
On that field we again call the Field.validate_empty_values()
The to_internal_value of the type of field is called next.
Now there is a call to the Field.run_validators() method. I presume this is where the additional validators that we add on the field by specifying the validators = [] field option get executed one by one
Once all this is done, we are back to the Serializer.to_internal_value() method. Now remember that we are doing the above for each field within that for loop. Now the custom field validators you wrote in your serializer (methods like validate_field_name) are run. If an exception occurred in any of the previous steps, your custom validators wont run.
read_only_defaults()
update validate data with defaults I think
run object level validators. I think the validate() method on your object is run here.
I don't believe the above solutions work any more. In my case, my model has fields 'first_name' and 'last_name', but the API will only receive 'name'.
Setting 'extra_kwargs' and 'validators' in the Meta class seems to have no effect, first_name and last_name are allways deemed required, and validators are always called. I can't overload the first_name/last_name character fields with
anotherrepfor_first_name = serializers.CharField(source=first_name, required=False)
as the names make sense. After many hours of frustration, I found the only way I could override the validators with a ModelSerializer instance was to override the class initializer as follows (forgive the incorrect indentation):
class ContactSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=True)
class Meta:
model = Contact
fields = [ 'name', 'first_name', 'last_name', 'email', 'phone', 'question' ]
def __init__(self, *args, **kwargs):
self.fields['first_name'] = serializers.CharField(required=False, allow_null=True, allow_blank=True)
self.fields['last_name'] = serializers.CharField(required=False, allow_null=True, allow_blank=True)
return super(ContactSerializer, self).__init__(*args, **kwargs)
def create(self, validated_data):
return Contact.objects.create()
def validate(self, data):
"""
Remove name after getting first_name, last_name
"""
missing = []
for k in ['name', 'email', 'question']:
if k not in self.fields:
missing.append(k)
if len(missing):
raise serializers.ValidationError("Ooops! The following fields are required: %s" % ','.join(missing))
from nameparser import HumanName
names = HumanName(data['name'])
names.capitalize()
data['last_name'] = names.last
if re.search(r'\w+', names.middle):
data['first_name'] = ' '.join([names.first, names.middle])
else:
data['first_name'] = names.first
del(data['name'])
return data
Now the doc says that allowing blank and null with character fields is a no no, but this is a serializer, not a model, and as the API gets called by all kinds of cowboys, I need to cover my bases.
Here's the approach that worked for me.
Use a sentinel error type that gets caught in an overridden view function
The sentinel is raised from the custom serializer
The sentinel error type:
from django.core.exceptions import ValidationError
class CustomValidationErrors(ValidationError):
""" custom validation error for the api view to catch the status code """
And in the serializer we override errors, _errors, validated_data, and _validated_data, as well as is_valid:
class CustomSerializer(serializers.ModelSerializer):
# fields that usually run validation before parent serializer validation
child_field1 = Child1Serializer()
child_field2 = Child2Serializer()
# override DRF fields
errors = {}
_errors = None
validated_data = {}
_validated_data = []
def is_valid(self, *args, **kwargs):
# override drf.serializers.Serializer.is_valid
# and raise CustomValidationErrors from parent validate
self.validate(self.initial_data)
return not bool(self.errors)
def validate(self, attrs):
self._errors = {}
if len(attrs.get("child_field1", {}).get("name", "")) > 100:
self._errors.update({"child_field1": {"name": "child 1 name > 100"}})
if len(attrs.get("child_field2", {}).get("description", "")) > 1000:
self._errors.update({"child_field2.description": "child 2 description > 100"})
if len(self._errors):
# set the overriden DRF values
self.errors = self._errors
# raise the sentinel error type
raise CustomValidationErrors(self._errors)
# set the overriden DRF serializer values
self._errors = None
self.validated_data = attrs
self._validated_data = [[k, v] for k, v in attrs.items()]
return attrs
class Meta:
model = CustomModel
And in the view we can override the default method, and catch the sentinel error type:
class CustomSerializerView(ListCreateAPIView):
serializer_class = CustomeSerializer
def post(self, *args, **kwargs):
try:
# if this fails for any exception
# other than CustomValidationErrors
# it will return the default error
return super().post(*args, **kwargs)
except CustomValidationErrors as e:
############
# returns a 400 with the following
# {"child_field1":
# [[{"name": "child 1 name > 100"}]],
# "child_field2.description":
# [["child 2 description > 100"]]
# }
############
return Response(e.error_dict, status=400)
drf version:
djangorestframework==3.11.0

Django REST Framework PATCH fails on required fields

We are creating an API that needs to allow a user to update a record. In many cases, like a status update or name change, only one field will change. This seems an appropriate use-case scenario for a PATCH request. As I understand it this is a 'partial' update.
We've implemented Django's REST Framework and run into this issue. For a record such as a "AccountUser" I want to change a name field only so I send the following request:
PATCH /api/users/1/ HTTP/1.1
Host: localhost
X-CSRFToken: 111122223333444455556666
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cache
{ "fullname": "John Doe" }
The record obviously has other attributes including a couple of 'related' fields such as 'account' which is required for a new record. When submit the request, the response is a 400 error with the following body:
{ "account": [ "This field is required." ] }
The serializer for the user looks like this:
class AccountUserSerializer(serializers.ModelSerializer):
account = serializers.PrimaryKeyRelatedField()
class Meta:
model = AccountUser
fields = ('id', 'account', 'fullname', ... )
depth = 1
And the model looks like this:
class AccountUser(models.Model):
''' Account User'''
fullname = models.CharField(max_length=200,
null=True,blank=True)
account = models.ForeignKey(Account,
on_delete=models.PROTECT
)
objects = AccountUserManager()
def __unicode__(self):
return self.email
class Meta:
db_table = 'accounts_account_user'
Am I doing something wrong here or is it wrong to expect to be able to update a single field on a record this way. Thanks! This community rocks!
EDIT:
Requested - AccountUserManager:
class AccountUserManager(BaseUserManager):
def create_user(self, email, account_name):
username = hash_email_into_username(email)
...
account = Account.objects.get(name=account_name)
account_user = AccountUser(email=email,user=user,account=account)
account_user.save()
return account_user
It doesn't look like your manager is filtering the user. I'd encourage you to use pdb and set a breakpoint in your view code and step through to see why its attempting to create a new record. I can vouch that we use PATCH to complete partial updates all the time and only send a few fields to update without issue.
Only other thought is that you're some how sending a value for account (like null) that's triggering the validation error even though you're listed example only shows sending the fullname field.
See my answer about partial updates. Also you can see the drf docs and this one docs
In my case, all I had to do was add required=False to my serializer field's arguments.
A novice mistake to be sure, but I thought I'd mention it here in case it helps anyone.
I came here because I got into a similar problem. CREATE is fine but PATCH isn't.
Since OP doesn't post the ViewSets and I suspend if he is using a custom ViewSets, which I got a mistake when override the update function:
class MyViewSet(ModelViewSet):
def update(self, request, *args, **kwargs):
// Do some checking
return super().update(request, args, kwargs)
My mistake is in the call to super(). Missing the asterisks. This fix my problem
return super().update(request, *args, **kwargs)
you can simply add this line after the fields variable in serializers.py
class AccountUserSerializer(serializers.ModelSerializer):
account = serializers.PrimaryKeyRelatedField()
class Meta:
model = AccountUser
fields = ('id', 'account', 'fullname', ... )
depth = 1
extra_kwargs = {
'account' : {'required' : False}
}

Django: How to override unique_together error message?

In a model's Meta class, I define a unique_together. I have a ModelForm based on this model. When I call is_valid on this ModelForm, an error will automatically raised if unique_together validation fails. That's all good.
Now my problem is that I'm not satisfied with the default unique_together error message. I want to override it. How can I do that? For a field related error, I can easily do that by setting error_messages on the field parameters. But unique_together is a non field error. How can I override a non field error message?
You can do this since Django 1.7
from django.forms import ModelForm
from django.core.exceptions import NON_FIELD_ERRORS
class ArticleForm(ModelForm):
class Meta:
error_messages = {
NON_FIELD_ERRORS: {
'unique_together': "%(model_name)s's %(field_labels)s are not unique.",
}
}
Update 2016/10/20: See jifeng-yin's even nicer answer below for Django >= 1.7
The nicest way to override these error messages might be to override the unique_error_message method on your model. Django calls this method to get the error message whenever it encounters a uniqueness issue during validation.
You can just handle the specific case you want and let all other cases be handled by Django as usual:
def unique_error_message(self, model_class, unique_check):
if model_class == type(self) and unique_check == ('field1', 'field2'):
return 'My custom error message'
else:
return super(Project, self).unique_error_message(model_class, unique_check)
For DRF serializers you can use this
from rest_framework import serializers
class SomeSerializer(serializers.ModelSerializer):
class Meta:
model = Some
validators = [
serializers.UniqueTogetherValidator(
queryset=model.objects.all(),
fields=('field1', 'field2'),
message="Some custom message."
)
]
Here is the original source.
After a quick check, it seems that unique_together validation errors are hard-coded deep in django.db.models.Model.unique_error_message :
def unique_error_message(self, model_class, unique_check):
opts = model_class._meta
model_name = capfirst(opts.verbose_name)
# A unique field
if len(unique_check) == 1:
field_name = unique_check[0]
field_label = capfirst(opts.get_field(field_name).verbose_name)
# Insert the error into the error dict, very sneaky
return _(u"%(model_name)s with this %(field_label)s already exists.") % {
'model_name': unicode(model_name),
'field_label': unicode(field_label)
}
# unique_together
else:
field_labels = map(lambda f: capfirst(opts.get_field(f).verbose_name), unique_check)
field_labels = get_text_list(field_labels, _('and'))
return _(u"%(model_name)s with this %(field_label)s already exists.") % {
'model_name': unicode(model_name),
'field_label': unicode(field_labels)
}
So maybe you should try to override this method from your model, to insert your own message !?
However, I haven't tried, and it seems a rather brutal solution ! But if you don't have something better, you might try...
Notice: A lot had changed in Django since this answer. So better check other answers...
If what sebpiq is true( since i do not check source code), then there is one
possible solution you can do, but it is the hard way...
You can define a validation rule in your form, as it described here
You can see examples of validation with more than one field, so by using this method, you can define a unique together check before standard django unique check executed...
Or the worst one, you can do a validation in your view before you try to save the objects...
You might take a look at overriding django/db/models/base.py:Model._perform_unique_checks() in your model.
In that method you can get the "original" errors:
errors = super(MyModel, self)._perform_unique_checks(unique_checks)
-- then modify and return them upwards.

Categories