Reputation: 10224
I have the following (simplified) data structure:
Site
-> Zone
-> Room
-> name
I want the name of each Room to be unique for each Site.
I know that if I just wanted uniqueness for each Zone, I could do:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
class Meta:
unique_together = ('name', 'zone')
But I can't do what I really want, which is:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
class Meta:
unique_together = ('name', 'zone__site')
I tried adding a validate_unique method, as suggested by this question:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, exclude=None):
qs = Room.objects.filter(name=self.name)
if qs.filter(zone__site=self.zone__site).exists():
raise ValidationError('Name must be unique per site')
models.Model.validate_unique(self, exclude=exclude)
but I must be misunderstanding the point/implementation of validate_unique, because it is not being called when I save a Room object.
What would be the correct way to implement this check?
Upvotes: 34
Views: 20636
Reputation: 175
I think @Seperman solution is quite good but as @roskakori said it won't work when editing. Also in my opinion it can be done better.
So problem with this approach is that we already overwrite Model validate_unique method, so if we extend model with field that has to be unique it will be validate in a way we defined in our method. But if we have it in mind and still want to use it, why we call it in save method instead of setting name as unique? Full solution with this approach looks like this:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255, unique=True)
def validate_unique(self, exclude=None):
if (
Room.objects
.exclude(id=self.id)
.filter(name=self.name, zone__site=self.zone__site)
.exists()
):
raise ValidationError({'name': ['Name must be unique per site.',]})
In my opinion the best approach is to not overwrite Model method, because in big application we can easily forget about it and have terrible headache when new unique field appear in our model. Change method name to something unique in terms what Model contains and call it in save method then.
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique_name(self, exclude=None):
if (
Room.objects
.exclude(id=self.id)
.filter(name=self.name, zone__site=self.zone__site)
.exists()
):
raise ValidationError({'name': ['Name must be unique per site.',]})
def save(self, *args, **kwargs):
self.validate_unique_name()
super().save(*args, **kwargs)
Upvotes: 3
Reputation: 3346
The solution by @Seperman only works when you want to save()
the instance for the first time. When updating it will think of itself as a duplicate. The following takes this into account by excluding its own id
. In case this is the first time the instance will be saved, the Python id
is None
, which will exclude nothing, and consequently still work as intended.
This also merges the code for the conditions, uses the field specific error suggested by @aki33524 and updates the syntax to Python 3's super
.
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, exclude=None):
if (
Room.objects
.exclude(id=self.id)
.filter(name=self.name, zone__site=self.zone__site)
.exists()
):
raise ValidationError({'name': ['Name must be unique per site.',]})
def save(self, *args, **kwargs):
self.validate_unique()
super().save(*args, **kwargs)
Upvotes: 1
Reputation: 4530
Methods are not called on their own when saving the model.
One way to do this is to have a custom save method that calls the validate_unique
method when a model is saved:
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, exclude=None):
qs = Room.objects.filter(name=self.name)
if qs.filter(zone__site=self.zone__site).exists():
raise ValidationError('Name must be unique per site')
def save(self, *args, **kwargs):
self.validate_unique()
super(Room, self).save(*args, **kwargs)
Upvotes: 26
Reputation: 186
class Room(models.Model):
zone = models.ForeignKey(Zone)
name = models.CharField(max_length=255)
def validate_unique(self, *args, **kwargs):
super(Room, self).validate_unique(*args, **kwargs)
qs = Room.objects.filter(name=self.name)
if qs.filter(zone__site=self.zone__site).exists():
raise ValidationError({'name':['Name must be unique per site',]})
I needed to make similar program. It worked.
Upvotes: 8
Reputation: 15776
The Django Validation objects documentation explains the steps involved in validation including this snippet
Note that full_clean() will not be called automatically when you call your model's save() method
If the model instance is being created as a result of using a ModelForm
, then validation will occur when the form is validated.
There are a some options in how you handle validation.
full_clean()
manually before saving.Override the save()
method of the model to perform validation on every save. You can choose how much validation should occur here, whether you want full validation or only uniqueness checks.
class Room(models.Model):
def save(self, *args, **kwargs):
self.full_clean()
super(Room, self).save(*args, **kwargs)
Use a Django pre_save signal handler which will automatically perform validation before a save. This provides a very simple way to add validation on exisiting models without any additional model code.
# In your models.py
from django.db.models.signals import pre_save
def validate_model_signal_handler(sender, **kwargs):
"""
Signal handler to validate a model before it is saved to database.
"""
# Ignore raw saves.
if not kwargs.get('raw', False):
kwargs['instance'].full_clean()
pre_save.connect(validate_model_signal_handler,
sender=Room,
dispatch_uid='validate_model_room')
Upvotes: 7