Login

Validation for 'unique' and 'unique_together' constraints (different version)

Author:
miracle2k
Posted:
July 24, 2007
Language:
Python
Version:
.96
Score:
2 (after 2 ratings)

This is a slightly different and extendend version of this snippet:

http://www.djangosnippets.org/snippets/260/

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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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_<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).
    """
    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

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Please login first before commenting.