from django import newforms as forms from django.newforms import ValidationError from django.utils.translation import ugettext as _ from django.utils.text import force_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. """ def validate_unique_constraint(form, model, object, fields, data=None, errormsg_callback=None): # used if errormsg_callback is not specified def default_error_callback(fields): if len(fields) > 1: l = [force_unicode(model._meta.get_field(n).verbose_name) for n in fields] 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_name if errormsg_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 = {} for field in fields: if field not in data: # 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). if data[field] is None: filter[field+'__isnull'] = True else: 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) if object is not None: query_set = query_set.exclude(pk=object._get_pk_val) # if query gives a result, the unique constraint was violated if query_set.count() > 0: # retrieve error message from callback errormsg = errormsg_callback(fields) if isinstance(errormsg, tuple): errormsg, blame_field = errormsg else: blame_field = None # raise validation error e = ValidationError(errormsg) if blame_field: e.blame_field = blame_field raise e """ 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. """ def add_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. """ def wrap_method(klass, name, newfunc, nfargs=[], nfkwargs={}): # wrapper function that will first call the old, then the new method def wrapped(self, oldmethod, *args, **kwargs): if oldmethod is None: value = None else: value = oldmethod(self, *args, **kwargs) return newfunc(self, value, *args, **kwargs) # assign the new method oldmethod = getattr(klass, name, None) setattr(klass, name, lambda self: 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. """ def find_in_blame_map(fields): for src_fields, dst_field in blame_map: # be sure to have lists (and copies of them!) before trying to sort if list(src_fields).sort() == list(fields).sort(): return dst_field return None """ Wrapper method around a form.clean_() 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). """ def clean_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. if value is None: value = self.cleaned_data[field] data = {field: value} # validate the unique constraint on this field validate_unique_constraint(self, self._model, object, [field], data, errormsg_callback=errormsg_callback) # return the value determined before (not modified by us) return value """ 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. """ def clean(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). if value is None: data = self.cleaned_data else: data = value # validate each unique_together constraint for constrained_fields in form._model._meta.unique_together: try: validate_unique_constraint(self, self._model, object, constrained_fields, data, errormsg_callback=errormsg_callback) except ValidationError, e: field_to_blame = getattr(e, 'blame_field', None) or \ find_in_blame_map(constrained_fields) if field_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. raise e # return the previous cleaned data (not modified by us) return data """ Function body """ # check "unique" constraint on each field in the form for field in form.base_fields: if form._model._meta.get_field(field).unique: wrap_method(form, 'clean_'+field, clean_field, nfkwargs={'field': field}) # check "unique_together" constraints defined in the model if form._model._meta.unique_together: wrap_method(form, 'clean', clean) """ Wrappers around form_for_* which add validation methods to unique_constraints. """ def form_for_model(model, **kwargs): from django.newforms.models import form_for_model form = form_for_model(model, **kwargs) add_unique_constraint_validations(form) return form def form_for_instance(instance, **kwargs): from django.newforms.models import form_for_instance form = form_for_instance(instance, **kwargs) add_unique_constraint_validations(form, instance) return form