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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
Please login first before commenting.