Fixed #8209: `ModelForm`s now validate unique constraints. Alex Gaynor did much of this work, and Brian Rosner helped as well.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@8805 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
5f31e9bd33
commit
ea05e61b2b
|
@ -3,9 +3,10 @@ Helper functions for creating Form classes from Django models
|
||||||
and database field objects.
|
and database field objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.utils.encoding import smart_unicode
|
from django.utils.encoding import smart_unicode
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
|
from django.utils.text import get_text_list
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from util import ValidationError, ErrorList
|
from util import ValidationError, ErrorList
|
||||||
from forms import BaseForm, get_declared_fields
|
from forms import BaseForm, get_declared_fields
|
||||||
|
@ -20,6 +21,7 @@ __all__ = (
|
||||||
'ModelMultipleChoiceField',
|
'ModelMultipleChoiceField',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_instance(form, instance, fields=None, fail_message='saved',
|
def save_instance(form, instance, fields=None, fail_message='saved',
|
||||||
commit=True, exclude=None):
|
commit=True, exclude=None):
|
||||||
"""
|
"""
|
||||||
|
@ -202,6 +204,76 @@ class BaseModelForm(BaseForm):
|
||||||
object_data.update(initial)
|
object_data.update(initial)
|
||||||
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
|
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
|
||||||
error_class, label_suffix, empty_permitted)
|
error_class, label_suffix, empty_permitted)
|
||||||
|
def clean(self):
|
||||||
|
self.validate_unique()
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
def validate_unique(self):
|
||||||
|
from django.db.models.fields import FieldDoesNotExist
|
||||||
|
unique_checks = list(self.instance._meta.unique_together[:])
|
||||||
|
form_errors = []
|
||||||
|
|
||||||
|
# Make sure the unique checks apply to actual fields on the ModelForm
|
||||||
|
for name, field in self.fields.items():
|
||||||
|
try:
|
||||||
|
f = self.instance._meta.get_field_by_name(name)[0]
|
||||||
|
except FieldDoesNotExist:
|
||||||
|
# This is an extra field that's not on the ModelForm, ignore it
|
||||||
|
continue
|
||||||
|
# MySQL can't handle ... WHERE pk IS NULL, so make sure we don't
|
||||||
|
# don't generate queries of that form.
|
||||||
|
is_null_pk = f.primary_key and self.cleaned_data[name] is None
|
||||||
|
if name in self.cleaned_data and f.unique and not is_null_pk:
|
||||||
|
unique_checks.append((name,))
|
||||||
|
|
||||||
|
# Don't run unique checks on fields that already have an error.
|
||||||
|
unique_checks = [check for check in unique_checks if not [x in self._errors for x in check if x in self._errors]]
|
||||||
|
|
||||||
|
for unique_check in unique_checks:
|
||||||
|
# Try to look up an existing object with the same values as this
|
||||||
|
# object's values for all the unique field.
|
||||||
|
|
||||||
|
lookup_kwargs = {}
|
||||||
|
for field_name in unique_check:
|
||||||
|
lookup_kwargs[field_name] = self.cleaned_data[field_name]
|
||||||
|
|
||||||
|
qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
|
||||||
|
|
||||||
|
# Exclude the current object from the query if we are editing an
|
||||||
|
# instance (as opposed to creating a new one)
|
||||||
|
if self.instance.pk is not None:
|
||||||
|
qs = qs.exclude(pk=self.instance.pk)
|
||||||
|
|
||||||
|
# This cute trick with extra/values is the most efficiant way to
|
||||||
|
# tell if a particular query returns any results.
|
||||||
|
if qs.extra(select={'a': 1}).values('a').order_by():
|
||||||
|
model_name = self.instance._meta.verbose_name.title()
|
||||||
|
|
||||||
|
# A unique field
|
||||||
|
if len(unique_check) == 1:
|
||||||
|
field_name = unique_check[0]
|
||||||
|
field_label = self.fields[field_name].label
|
||||||
|
# Insert the error into the error dict, very sneaky
|
||||||
|
self._errors[field_name] = ErrorList([
|
||||||
|
_("%(model_name)s with this %(field_label)s already exists.") % \
|
||||||
|
{'model_name': model_name, 'field_label': field_label}
|
||||||
|
])
|
||||||
|
# unique_together
|
||||||
|
else:
|
||||||
|
field_labels = [self.fields[field_name].label for field_name in unique_check]
|
||||||
|
field_labels = get_text_list(field_labels, _('and'))
|
||||||
|
form_errors.append(
|
||||||
|
_("%(model_name)s with this %(field_label)s already exists.") % \
|
||||||
|
{'model_name': model_name, 'field_label': field_labels}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the data from the cleaned_data dict since it was invalid
|
||||||
|
for field_name in unique_check:
|
||||||
|
del self.cleaned_data[field_name]
|
||||||
|
|
||||||
|
if form_errors:
|
||||||
|
# Raise the unique together errors since they are considered form-wide.
|
||||||
|
raise ValidationError(form_errors)
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
"""
|
"""
|
||||||
|
@ -246,18 +318,26 @@ class BaseModelFormSet(BaseFormSet):
|
||||||
queryset=None, **kwargs):
|
queryset=None, **kwargs):
|
||||||
self.queryset = queryset
|
self.queryset = queryset
|
||||||
defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
|
defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
|
||||||
if self.max_num > 0:
|
defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()]
|
||||||
qs = self.get_queryset()[:self.max_num]
|
|
||||||
else:
|
|
||||||
qs = self.get_queryset()
|
|
||||||
defaults['initial'] = [model_to_dict(obj) for obj in qs]
|
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
super(BaseModelFormSet, self).__init__(**defaults)
|
super(BaseModelFormSet, self).__init__(**defaults)
|
||||||
|
|
||||||
|
def _construct_form(self, i, **kwargs):
|
||||||
|
if i < self._initial_form_count:
|
||||||
|
kwargs['instance'] = self.get_queryset()[i]
|
||||||
|
return super(BaseModelFormSet, self)._construct_form(i, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.queryset is not None:
|
if not hasattr(self, '_queryset'):
|
||||||
return self.queryset
|
if self.queryset is not None:
|
||||||
return self.model._default_manager.get_query_set()
|
qs = self.queryset
|
||||||
|
else:
|
||||||
|
qs = self.model._default_manager.get_query_set()
|
||||||
|
if self.max_num > 0:
|
||||||
|
self._queryset = qs[:self.max_num]
|
||||||
|
else:
|
||||||
|
self._queryset = qs
|
||||||
|
return self._queryset
|
||||||
|
|
||||||
def save_new(self, form, commit=True):
|
def save_new(self, form, commit=True):
|
||||||
"""Saves and returns a new model instance for the given form."""
|
"""Saves and returns a new model instance for the given form."""
|
||||||
|
@ -359,6 +439,14 @@ class BaseInlineFormSet(BaseModelFormSet):
|
||||||
self._initial_form_count = 0
|
self._initial_form_count = 0
|
||||||
super(BaseInlineFormSet, self)._construct_forms()
|
super(BaseInlineFormSet, self)._construct_forms()
|
||||||
|
|
||||||
|
def _construct_form(self, i, **kwargs):
|
||||||
|
form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs)
|
||||||
|
if self.save_as_new:
|
||||||
|
# Remove the primary key from the form's data, we are only
|
||||||
|
# creating new instances
|
||||||
|
form.data[form.add_prefix(self._pk_field.name)] = None
|
||||||
|
return form
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns this FormSet's queryset, but restricted to children of
|
Returns this FormSet's queryset, but restricted to children of
|
||||||
|
|
|
@ -72,3 +72,4 @@ def sorted(in_value):
|
||||||
out_value = in_value[:]
|
out_value = in_value[:]
|
||||||
out_value.sort()
|
out_value.sort()
|
||||||
return out_value
|
return out_value
|
||||||
|
|
||||||
|
|
|
@ -338,6 +338,16 @@ parameter when declaring the form field::
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = Article
|
... model = Article
|
||||||
|
|
||||||
|
Overriding the clean() method
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
You can overide the ``clean()`` method on a model form to provide additional
|
||||||
|
validation in the same way you can on a normal form. However, by default the
|
||||||
|
``clean()`` method validates the uniqueness of fields that are marked as unique
|
||||||
|
on the model, and those marked as unque_together, if you would like to overide
|
||||||
|
the ``clean()`` method and maintain the default validation you must call the
|
||||||
|
parent class's ``clean()`` method.
|
||||||
|
|
||||||
Form inheritance
|
Form inheritance
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
@ -500,4 +510,4 @@ books of a specific author. Here is how you could accomplish this::
|
||||||
>>> from django.forms.models import inlineformset_factory
|
>>> from django.forms.models import inlineformset_factory
|
||||||
>>> BookFormSet = inlineformset_factory(Author, Book)
|
>>> BookFormSet = inlineformset_factory(Author, Book)
|
||||||
>>> author = Author.objects.get(name=u'Orson Scott Card')
|
>>> author = Author.objects.get(name=u'Orson Scott Card')
|
||||||
>>> formset = BookFormSet(instance=author)
|
>>> formset = BookFormSet(instance=author)
|
||||||
|
|
|
@ -117,9 +117,26 @@ class CommaSeparatedInteger(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.field
|
return self.field
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.slug
|
||||||
|
|
||||||
|
class Price(models.Model):
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"%s for %s" % (self.quantity, self.price)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('price', 'quantity'),)
|
||||||
|
|
||||||
class ArticleStatus(models.Model):
|
class ArticleStatus(models.Model):
|
||||||
status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True)
|
status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
__test__ = {'API_TESTS': """
|
__test__ = {'API_TESTS': """
|
||||||
>>> from django import forms
|
>>> from django import forms
|
||||||
>>> from django.forms.models import ModelForm, model_to_dict
|
>>> from django.forms.models import ModelForm, model_to_dict
|
||||||
|
@ -1132,8 +1149,42 @@ u'1,,2'
|
||||||
>>> f.clean('1')
|
>>> f.clean('1')
|
||||||
u'1'
|
u'1'
|
||||||
|
|
||||||
# Choices on CharField and IntegerField
|
# unique/unique_together validation
|
||||||
|
|
||||||
|
>>> class ProductForm(ModelForm):
|
||||||
|
... class Meta:
|
||||||
|
... model = Product
|
||||||
|
>>> form = ProductForm({'slug': 'teddy-bear-blue'})
|
||||||
|
>>> form.is_valid()
|
||||||
|
True
|
||||||
|
>>> obj = form.save()
|
||||||
|
>>> obj
|
||||||
|
<Product: teddy-bear-blue>
|
||||||
|
>>> form = ProductForm({'slug': 'teddy-bear-blue'})
|
||||||
|
>>> form.is_valid()
|
||||||
|
False
|
||||||
|
>>> form._errors
|
||||||
|
{'slug': [u'Product with this Slug already exists.']}
|
||||||
|
>>> form = ProductForm({'slug': 'teddy-bear-blue'}, instance=obj)
|
||||||
|
>>> form.is_valid()
|
||||||
|
True
|
||||||
|
|
||||||
|
# ModelForm test of unique_together constraint
|
||||||
|
>>> class PriceForm(ModelForm):
|
||||||
|
... class Meta:
|
||||||
|
... model = Price
|
||||||
|
>>> form = PriceForm({'price': '6.00', 'quantity': '1'})
|
||||||
|
>>> form.is_valid()
|
||||||
|
True
|
||||||
|
>>> form.save()
|
||||||
|
<Price: 1 for 6.00>
|
||||||
|
>>> form = PriceForm({'price': '6.00', 'quantity': '1'})
|
||||||
|
>>> form.is_valid()
|
||||||
|
False
|
||||||
|
>>> form._errors
|
||||||
|
{'__all__': [u'Price with this Price and Quantity already exists.']}
|
||||||
|
|
||||||
|
# Choices on CharField and IntegerField
|
||||||
>>> class ArticleForm(ModelForm):
|
>>> class ArticleForm(ModelForm):
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = Article
|
... model = Article
|
||||||
|
|
|
@ -73,6 +73,22 @@ class Restaurant(Place):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.slug
|
||||||
|
|
||||||
|
class Price(models.Model):
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
quantity = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"%s for %s" % (self.quantity, self.price)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('price', 'quantity'),)
|
||||||
|
|
||||||
class MexicanRestaurant(Restaurant):
|
class MexicanRestaurant(Restaurant):
|
||||||
serves_tacos = models.BooleanField()
|
serves_tacos = models.BooleanField()
|
||||||
|
|
||||||
|
@ -553,4 +569,56 @@ True
|
||||||
>>> type(_get_foreign_key(MexicanRestaurant, Owner))
|
>>> type(_get_foreign_key(MexicanRestaurant, Owner))
|
||||||
<class 'django.db.models.fields.related.ForeignKey'>
|
<class 'django.db.models.fields.related.ForeignKey'>
|
||||||
|
|
||||||
|
# unique/unique_together validation ###########################################
|
||||||
|
|
||||||
|
>>> FormSet = modelformset_factory(Product, extra=1)
|
||||||
|
>>> data = {
|
||||||
|
... 'form-TOTAL_FORMS': '1',
|
||||||
|
... 'form-INITIAL_FORMS': '0',
|
||||||
|
... 'form-0-slug': 'car-red',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(data)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
True
|
||||||
|
>>> formset.save()
|
||||||
|
[<Product: car-red>]
|
||||||
|
|
||||||
|
>>> data = {
|
||||||
|
... 'form-TOTAL_FORMS': '1',
|
||||||
|
... 'form-INITIAL_FORMS': '0',
|
||||||
|
... 'form-0-slug': 'car-red',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(data)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
False
|
||||||
|
>>> formset.errors
|
||||||
|
[{'slug': [u'Product with this Slug already exists.']}]
|
||||||
|
|
||||||
|
# unique_together
|
||||||
|
|
||||||
|
>>> FormSet = modelformset_factory(Price, extra=1)
|
||||||
|
>>> data = {
|
||||||
|
... 'form-TOTAL_FORMS': '1',
|
||||||
|
... 'form-INITIAL_FORMS': '0',
|
||||||
|
... 'form-0-price': u'12.00',
|
||||||
|
... 'form-0-quantity': '1',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(data)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
True
|
||||||
|
>>> formset.save()
|
||||||
|
[<Price: 1 for 12.00>]
|
||||||
|
|
||||||
|
>>> data = {
|
||||||
|
... 'form-TOTAL_FORMS': '1',
|
||||||
|
... 'form-INITIAL_FORMS': '0',
|
||||||
|
... 'form-0-price': u'12.00',
|
||||||
|
... 'form-0-quantity': '1',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(data)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
False
|
||||||
|
>>> formset.errors
|
||||||
|
[{'__all__': [u'Price with this Price and Quantity already exists.']}]
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
|
Loading…
Reference in New Issue