How to test uniqueness in a Model.clean() function? - python

I have a model with a UniqueConstraint:
class MyModel(models.Model)
name = models.CharField()
title = models.CharField()
class Meta:
constraints = [ models.UniqueConstraint(
fields=['name', 'title'],
name="unique_name_and_title") ]
This works fine and raises an IntegrityError when 2 objects with the same values are created.
The problem is UniqueConstraint doesn't present a pretty ValidationError to the user. Usually, I would add these in the Model.clean() class, but if I do this then it will fail on an Update because the instance being updated will already be present:
def clean(self):
if MyModel.objects.filter(title=self.title, name=self.name):
raise ValidationError({'title':'An object with this name+title already exists'})
I How do I create a ValidationError that passes if it's an UPDATE not an INSERT?
I know I could do this on a ModelForm and use self.instance to check if the instance already exists, but I want to apply this to the Model class and not have to rely on a ModelForm.

You can exclude the object from the queryset you check:
def clean(self):
qs = MyModel.objects.exclude(pk=self.pk).filter(title=self.title, name=self.name)
if qs.exists():
raise ValidationError({'title':'An object with this name+title already exists'})
return super().clean()
If the object is not yet saved, it will check for .exclude(pk=None), but that will not exclude any objects, since the primary key is non-nullable.
It is more efficient to use .exists() [Django-doc] here, since it limits the bandwidth from the database to the Django/Python layer.

Related

Is it best practice to validate fields in ModelSerializer for the fields that already have validators in Django model by accessing the the database?

for example I have a django model as
class User(models.Model):
email = models.EmailField(required=True, unique=True)
Isnt it redundant and against DRY principle to validate again in the ModelSerializer as following?
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
def validate_email(self, email):
try:
User.objects.get(email=email)
raise serializers.ValidationError("Email address already used")
except User.DoesNotExist:
return email
The validate_email method feels kind of against the DRY PRINCIPLE and wrong in this context since we have to access Database to validate in this method.
please correct me.
You don't have to validate the data again in the serializer if you have validation already in model level. In your case, the difference is the error message that you'll get from the API.
By default, DRF return {'field_name':['This field must be unique']} response in case of unique validation fail
Quoting #dirkgroten's comment
Note that to override the generic error message, you don't need to re-validate either. Just use the extra_kwargs attribute on the serializer as explained here

DRF validator doesn't return false on uniqueness check

I have a user serializer in DRF that looks like this:
class UserSerializer(serializers.ModelSerializer):
class Meta: # password should exist only if POST
model = User
fields = ['first_name', 'last_name',
'password', 'email', 'username']
write_only_fields = ['password']
And this is what it looks like when I checked the shell.
UserSerializer():
first_name = CharField(allow_blank=True, max_length=30, required=False)
last_name = CharField(allow_blank=True, max_length=150, required=False)
password = CharField(max_length=128)
email = EmailField(allow_blank=True, label='Email address', max_length=254, required=False)
username = CharField(help_text='Required. 150 characters or fewer. Letters, digits and #/./+/-/_ only.', max_length=150, validators=[<django.contrib.auth.validators.UnicodeUsernameValidator object>, <UniqueValidator(queryset=User.objects.all())>])
In my view if I check is_valid() on a serializer with data that already exists in the database, the function returns True when it should return False and then a django error is raised:
django.db.utils.IntegrityError: duplicate key value violates unique constraint "auth_user_username_key"
DETAIL: Key (username)=(myrandomusername) already exists.
Why is this happening?
Serializer does not care about whether there is an exception at the lower level, it only cares about the serialization/deserialization. So when you pass a username in POST that already exists, the IntegrityError is raised on the model layer (after serializer passed the data), not on serializer so it has no idea of it.
Serializer only checked if the deserialization goes on properly i.e. all the data you passed conform to the definition of fields in the serializer. If they are valid, it will pass it on to the next step.
Also, Serializer.is_valid only handles ValidationError, and keeps a dictionary to refer the errors. For errors, it decides whether to raise a ValidationError (from the errors) or not based on raise_exception.
You should look at the create method of the serializer (ModelSerializer and subclasses) to handle database level exceptions, as all object creation logic of ModelSerializer goes in there. (Also look at update method for updating).
DRF provides ModelSerializer to ensure the creation and update of model objects from deserialized data, it should be treated as an extension to the definition of basic serializers.

Django Rest Framework: allow a serializer field to be created, but not edited

Right now, DRF's read_only argument on a Serializer constructor means you can neither create nor update the field, while the write_only argument on a Serializer constructor allows the field to be created OR updated, but prevents the field from being output when serializing the representation.
Is there any (elegant) way to have a Serializer field that can be created, exactly once, when the model in question is created (when the create() is called on the Serializer), but cannot that later be modified via update?
NB: Yes, I've seen this solution, but honestly I find it ugly and un-Pythonic. Is there a better way?
class TodoModifySerializer(ModelSerializer):
def to_internal_value(self, data):
data = super(TodoModifySerializer, self).to_internal_value(data)
if self.instance:
# update
for x in self.create_only_fields:
data.pop(x)
return data
class Meta:
model = Todo
fields = ('id', 'category', 'title', 'content')
create_only_fields = ('title',)
you can do it in to_internal_value method by remove this data when update
By "not elegant", I'm assuming you only want one serializer for both creates and updates. You could perhaps consider overriding the update method of your serializer and remove the create_only_field from validated_data before saving:
class MySerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
validated_data.pop('create_only_field')
return super().update(instance, validated_data)
class Meta:
model = MyModel
fields = ('id', 'field_one', 'field_two', 'create_only_field')
You would, however, have to supply the old (or some) field value when updating your model.
I don't think there's any, you either specify it like that or make your own serializer, inheriting from DRF's serializer.
In order to make the field REQUIRED and read-only on update I've handled it on field validation.
class MyUserProfileSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username', required=True)
first_name = serializers.CharField(source='user.first_name')
last_name = serializers.CharField(source='user.last_name')
class Meta:
model = UserProfile
fields = ['user', 'username', 'first_name', 'last_name', 'phone']
read_only_fields = []
def validate_username(self, value):
if not self.instance and not value: ## Creation and value not provided
raise serializers.ValidationError('The username is required on user profile creation.')
elif value and self.instance != value: ## Update and value differs from existing
raise serializers.ValidationError('The username cannot be modified.')
return value
You can also make the field optional in case of edition, but you need to set the field as required=False and do validation in validate() method, since validate_username() wouldn't be called in creation if not included in the payload.

Where to write the save function in Django?

Where should I write my save() function in Django: in models.py in a model class, or in forms.py in a form?
For example :
models.py
class Customer(models.Model):
name = models.CharField(max_length=200)
created_by = models.ForeignKey(User)
def save():
........ some code to override it.......
forms.py
class Addcustomer(forms.ModelForm):
class Meta:
model = Customer
fields = ('name',)
def save():
........code to override it....
Where should I override my save function?
It really depends on what you are trying to achieve. Default realization of ModelForm's save calls Model's save. But it is usually better to override it on form because it also runs validation. So if you are already using form I would suggest overriding ModelForm.save. And by overriding I mean extending using super
Here is default realization of ModelForm.save
def save(self, commit=True):
"""
Save this form's self.instance object if commit=True. Otherwise, add
a save_m2m() method to the form which can be called after the instance
is saved manually at a later time. Return the model instance.
"""
if self.errors: # there validation is done
raise ValueError(
"The %s could not be %s because the data didn't validate." % (
self.instance._meta.object_name,
'created' if self.instance._state.adding else 'changed',
)
)
if commit:
# If committing, save the instance and the m2m data immediately.
self.instance.save()
self._save_m2m()
else:
# If not committing, add a method to the form to allow deferred
# saving of m2m data.
self.save_m2m = self._save_m2m
return self.instance
save.alters_data = True

Disassociate Foreign Key on django model for validation during editing

I have the following model:
class Event(models.Model):
assigned_to= models.ForeignKey(user)
(...) # Some other fields
I create a Event instance
event = Event(assigned_to=someuser, **kwargs)
event.save()
If I have to use the Model validation features while editing the event,
event.assigned_to = None
for doing event.full_clean(), django complains that it cannot set null a foreign key relation.
Cannot assign None: "Event.assigned_to" does not allow null values.
How should I get around this to get errors using the ValidationError raised during the full_clean() for a None specified to the foreignkey field.
I managed to generate errors by using null=True on ForeignKey as #Mad Wombat suggested, and by overriding the clean method on the model to check if the assigned_to on the instance is not None
def clean(self):
if self.assinged_to is None:
raise ValidationError({'assigned_to': "Whatever error"})
As full_clean() calls the clean() method, it raised the validation error as needed.

Categories