Unique constraints for single fields are validated in a clean_FIELD, instead of globally in the form's clean() method, so that the error messages are correctly assigned to each field.

Additionally, you can specify mappings for unique_together constraints to assign those error messages to a specific field as well (instead of having them in non_field_errors(), where they would normally be.

fromdjangoimportnewformsasformsfromdjango.newformsimportValidationErrorfromdjango.utils.translationimportugettextas_fromdjango.utils.textimportforce_unicode""" Validates a single unique constraint which spans one or multiple fields. Raises a newforms.ValidationError on failure. Parameters: * form: The form instance to be validated. * model: The django model the form's class represents. * object: The model instance the form instance represents. * fields: A sequence of fieldnames that belong to the unique constraint to be checked. * data: A dict that is expected to contain a cleaned value for each for item in "fields". If it is None, form.cleaned_data is used. This parameter is useful, for example, if form.cleaned_data does not yet contain the latest data, e.g. if called from within a form's clean() method. * errormsg_callback: See add_unique_constraint_validations. The second value in the return tuple will have no effect unless used together with that function."""defvalidate_unique_constraint(form,model,object,fields,data=None,errormsg_callback=None):# used if errormsg_callback is not specifieddefdefault_error_callback(fields):iflen(fields)>1:l=[force_unicode(model._meta.get_field(n).verbose_name)forninfields]return_('The fields "%s" and "%s" must be unique together.')% \
(', '.join(l[:-1]),l[-1])else:return_('The field "%s" must be unique.')% \
model._meta.get_field(fields[0]).verbose_nameiferrormsg_callback==None:errormsg_callback=default_error_callback# build a filter to query for other objects with the same data for# the unique fields of this constraint. Basically, we merge the# "data" and "fields" parameters here.filter={}forfieldinfields:iffieldnotindata:# No cleaned data for the field means either that the field is# nullable and was left empty or that the field itself did not# validate.return# add this field to the query. None values need to be queried as# NULL, or it won't work with certain field types (like datetime# and related).ifdata[field]isNone:filter[field+'__isnull']=Trueelse:filter[field]=data[field]# use the filter to find objects matching the unique constraint. exclude# a possible instance of the form's model.query_set=model.objects.filter(**filter)ifobjectisnotNone:query_set=query_set.exclude(pk=object._get_pk_val)# if query gives a result, the unique constraint was violatedifquery_set.count()>0:# retrieve error message from callbackerrormsg=errormsg_callback(fields)ifisinstance(errormsg,tuple):errormsg,blame_field=errormsgelse:blame_field=None# raise validation errore=ValidationError(errormsg)ifblame_field:e.blame_field=blame_fieldraisee""" Adds validators for unique and unique_together constraints to a form class. Based on a snippet by "mp": http://www.djangosnippets.org/snippets/260/ Parameters: * form: Must be a form class, which must have a attribute _model, which refers to the django model the form is based on. If the form is created by form_for_model() or form_for_instance(), this will already be the case. * object: If this form represents an already existing object (e.g. if created by form_for_instance), you have to pass that object as well. This is necessary, as there is unfortunately no clean way to access the instance the form class is based on. One could use form.save(commit=False), but even that only works until the first error occured during the validation process. * blame_map: A nested tuple/list construct that allows to blame unique_together validation failures to one particular field. If a unique_together constraint is not found in the blame_map, any validation errors for that constraint will be added to the non-field errors list of the form. Format: ( (['field1', 'field2', 'field3'], 'field1'), (['field1', 'field2'], 'field2'), ... ) * errormsg_callback: Allows customization of the error messages. Will receive one parameter - the list of fields the failed constraint consists of. Should return the final error message. It can also return a tuple in the form (errormsg, fieldname), in which case the error will be blamed on the specified field. This is an alternative mechanism to the blame_map parameter. The callback has precedence."""defadd_unique_constraint_validations(form,object=None,blame_map=[],errormsg_callback=None):""" klass, name: the class and method name to wrap newfunc: the new function to take the wrapped method's place. needs to accept (in this order): * a "self" parameter. * an "value" parameter which will contain the return value of the wrapped method which is called first. nfargs, nfkwargs: keyword and non-keyword arguments to be passed to the new AND the old function. """defwrap_method(klass,name,newfunc,nfargs=[],nfkwargs={}):# wrapper function that will first call the old, then the new methoddefwrapped(self,oldmethod,*args,**kwargs):ifoldmethodisNone:value=Noneelse:value=oldmethod(self,*args,**kwargs)returnnewfunc(self,value,*args,**kwargs)# assign the new methodoldmethod=getattr(klass,name,None)setattr(klass,name,lambdaself:wrapped(self,oldmethod,*nfargs,**nfkwargs))""" Searches the blame_map parameter for the specified list fields, and returns the field name they should be mapped to, or None. """deffind_in_blame_map(fields):forsrc_fields,dst_fieldinblame_map:# be sure to have lists (and copies of them!) before trying to sortiflist(src_fields).sort()==list(fields).sort():returndst_fieldreturnNone""" Wrapper method around a form.clean_<field>() method. Validates the unique constraint of a single field (parameter "field" contains the field name). Please note that we have to explicitly pass the field name instead of just referring to an outer variable of the parent function -otherwise, each clean_field() method will refer to the same field (the value the outer variable last had, i.e. the last field of the form). """defclean_field(self,value,field):# if there as a previous clean method on this field, continue# with the value it returned. otherwise, start with what# cleaned_data currently contains.ifvalueisNone:value=self.cleaned_data[field]data={field:value}# validate the unique constraint on this fieldvalidate_unique_constraint(self,self._model,object,[field],data,errormsg_callback=errormsg_callback)# return the value determined before (not modified by us)returnvalue""" Wrapper method for a form.clean(). Validates all unique_together constraints of the form class passed to this (parent) function. For details on blame_map, see the doc of the parent function. """defclean(self,value,blame_map={}):# in case there was no previous clean() method, use cleaned_data -# otherwise we can use what the previous one returned (which is# stored in the value parameter).ifvalueisNone:data=self.cleaned_dataelse:data=value# validate each unique_together constraintforconstrained_fieldsinform._model._meta.unique_together:try:validate_unique_constraint(self,self._model,object,constrained_fields,data,errormsg_callback=errormsg_callback)exceptValidationError,e:field_to_blame=getattr(e,'blame_field',None)or \
find_in_blame_map(constrained_fields)iffield_to_blame:self._errors[field_to_blame]=e.messages# no need to remove the field from cleaned_data, as# cleaned_data will be deleted completely anyway soon (# because there are errors)else:# re-raise exception, so it will be applied to the# non-field-errors list.raisee# return the previous cleaned data (not modified by us)returndata""" Function body """# check "unique" constraint on each field in the formforfieldinform.base_fields:ifform._model._meta.get_field(field).unique:wrap_method(form,'clean_'+field,clean_field,nfkwargs={'field':field})# check "unique_together" constraints defined in the modelifform._model._meta.unique_together:wrap_method(form,'clean',clean)""" Wrappers around form_for_* which add validation methods to unique_constraints."""defform_for_model(model,**kwargs):fromdjango.newforms.modelsimportform_for_modelform=form_for_model(model,**kwargs)add_unique_constraint_validations(form)returnformdefform_for_instance(instance,**kwargs):fromdjango.newforms.modelsimportform_for_instanceform=form_for_instance(instance,**kwargs)add_unique_constraint_validations(form,instance)returnform