Extending Django's Model Validation
by Justin Michalicek on Nov. 5, 2011, 9:47 a.m. UTCToday I found myself in need of extending the way Django does model validation. What I needed was a partial unique index. Unfortunately Django doesn't have support for that built in. What I ended up doing was overriding my model's validate_unique() method. This wasn't terribly difficult, but did have some complications due to a bug in django.core.exceptions.ValidationError or unclear documentation. The examples in the documentation only show ValidationError being raised with a string passed into it.
The problem that with this is that part of the code for ValidationError assumes that you did not pass in a string but instead passed in a dict. In fact the comments in __init()__ actually say that a string is what is usually passed in.
def __init__(self, message, code=None, params=None): import operator from django.utils.encoding import force_unicode """ ValidationError can be passed any object that can be printed (usually a string), a list of objects or a dictionary. """ if isinstance(message, dict): self.message_dict = message # Reduce each list of messages into a single list. message = reduce(operator.add, message.values()) if isinstance(message, list): self.messages = [force_unicode(msg) for msg in message] else: self.code = code self.params = params message = force_unicode(message) self.messages = [message]
But even though we could pass in a dict, list, or string it blows up if you pass a string. This bug is a bit old and the line numbers are no longer correct it seems, but this error is happening in Django 1.3.1. I'll dig up relevant info from a stack trace later, if I remember.
So to resolve the issue I looked at the format of the dict that ValidationError was looking at if the dict exists. It is a dict keyed to the name of the attribute that the error is related to and the value is a list of error messages for that attribute. This is actually the behavior I wanted anyway, since just passing a string, the error can't be tied to the attribute which is having a problem. The model below will fail a call to full_clean() every time but illustrates how to raise the ValidationError so that Django behaves properly. This can be done from validate_unique() as I did since my real use case was validating uniqueness or it can be done from clean().
class MyModel(models.Model): my_attribute = models.CharField(max_length=2) def validate_unique(self, *args, **kwargs): from django.core.exceptions import ValidationError # This example will always fail to validate raise ValidationError({'my_attribute': ('''something has gone wrong!''',),}) super(MyModel, self).validate_unique(*args, **kwargs)
In my case there is still a need for further checks. This validates the uniqueness, but it is not atomic with the actual save so a different process could sneak in and do a save which makes the current model no longer unique in between my model validating uniqueness and actually saving. My current solution to this is to manually add a partial unique index to the table so that the database layer will throw and exception when I call save() if this situation happens.