Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #12881: Corrected handling of inherited unique constraints. Tha…

…nks for report fgaudin.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12797 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 47a822207f937ccafe37c0b99ab37bf2157fb4f7 1 parent 883329e
Karen Tracey kmtracey authored
65 django/db/models/base.py
View
@@ -707,37 +707,50 @@ def _get_unique_checks(self, exclude=None):
if exclude is None:
exclude = []
unique_checks = []
- for check in self._meta.unique_together:
- for name in check:
- # If this is an excluded field, don't add this check.
- if name in exclude:
- break
- else:
- unique_checks.append(tuple(check))
+
+ unique_togethers = [(self.__class__, self._meta.unique_together)]
+ for parent_class in self._meta.parents.keys():
+ if parent_class._meta.unique_together:
+ unique_togethers.append((parent_class, parent_class._meta.unique_together))
+
+ for model_class, unique_together in unique_togethers:
+ for check in unique_together:
+ for name in check:
+ # If this is an excluded field, don't add this check.
+ if name in exclude:
+ break
+ else:
+ unique_checks.append((model_class, tuple(check)))
# These are checks for the unique_for_<date/year/month>.
date_checks = []
# Gather a list of checks for fields declared as unique and add them to
# the list of checks.
- for f in self._meta.fields:
- name = f.name
- if name in exclude:
- continue
- if f.unique:
- unique_checks.append((name,))
- if f.unique_for_date:
- date_checks.append(('date', name, f.unique_for_date))
- if f.unique_for_year:
- date_checks.append(('year', name, f.unique_for_year))
- if f.unique_for_month:
- date_checks.append(('month', name, f.unique_for_month))
+
+ fields_with_class = [(self.__class__, self._meta.local_fields)]
+ for parent_class in self._meta.parents.keys():
+ fields_with_class.append((parent_class, parent_class._meta.local_fields))
+
+ for model_class, fields in fields_with_class:
+ for f in fields:
+ name = f.name
+ if name in exclude:
+ continue
+ if f.unique:
+ unique_checks.append((model_class, (name,)))
+ if f.unique_for_date:
+ date_checks.append((model_class, 'date', name, f.unique_for_date))
+ if f.unique_for_year:
+ date_checks.append((model_class, 'year', name, f.unique_for_year))
+ if f.unique_for_month:
+ date_checks.append((model_class, 'month', name, f.unique_for_month))
return unique_checks, date_checks
def _perform_unique_checks(self, unique_checks):
errors = {}
- for unique_check in unique_checks:
+ for model_class, 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.
@@ -757,7 +770,7 @@ def _perform_unique_checks(self, unique_checks):
if len(unique_check) != len(lookup_kwargs.keys()):
continue
- qs = self.__class__._default_manager.filter(**lookup_kwargs)
+ qs = model_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)
@@ -769,13 +782,13 @@ def _perform_unique_checks(self, unique_checks):
key = unique_check[0]
else:
key = NON_FIELD_ERRORS
- errors.setdefault(key, []).append(self.unique_error_message(unique_check))
+ errors.setdefault(key, []).append(self.unique_error_message(model_class, unique_check))
return errors
def _perform_date_checks(self, date_checks):
errors = {}
- for lookup_type, field, unique_for in date_checks:
+ for model_class, lookup_type, field, unique_for in date_checks:
lookup_kwargs = {}
# there's a ticket to add a date lookup, we can remove this special
# case if that makes it's way in
@@ -788,7 +801,7 @@ def _perform_date_checks(self, date_checks):
lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(date, lookup_type)
lookup_kwargs[field] = getattr(self, field)
- qs = self.__class__._default_manager.filter(**lookup_kwargs)
+ qs = model_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 not getattr(self, '_adding', False) and self.pk is not None:
@@ -808,8 +821,8 @@ def date_error_message(self, lookup_type, field, unique_for):
'lookup': lookup_type,
}
- def unique_error_message(self, unique_check):
- opts = self._meta
+ def unique_error_message(self, model_class, unique_check):
+ opts = model_class._meta
model_name = capfirst(opts.verbose_name)
# A unique field
10 django/forms/models.py
View
@@ -488,7 +488,7 @@ def validate_unique(self):
errors = []
# Do each of the unique checks (unique and unique_together)
- for unique_check in all_unique_checks:
+ for uclass, unique_check in all_unique_checks:
seen_data = set()
for form in self.forms:
# if the form doesn't have cleaned_data then we ignore it,
@@ -512,7 +512,7 @@ def validate_unique(self):
# iterate over each of the date checks now
for date_check in all_date_checks:
seen_data = set()
- lookup, field, unique_for = date_check
+ uclass, lookup, field, unique_for = date_check
for form in self.forms:
# if the form doesn't have cleaned_data then we ignore it,
# it's already invalid
@@ -556,9 +556,9 @@ def get_unique_error_message(self, unique_check):
def get_date_error_message(self, date_check):
return ugettext("Please correct the duplicate data for %(field_name)s "
"which must be unique for the %(lookup)s in %(date_field)s.") % {
- 'field_name': date_check[1],
- 'date_field': date_check[2],
- 'lookup': unicode(date_check[0]),
+ 'field_name': date_check[2],
+ 'date_field': date_check[3],
+ 'lookup': unicode(date_check[1]),
}
def get_form_error(self):
32 tests/modeltests/model_forms/mforms.py
View
@@ -0,0 +1,32 @@
+from django.forms import ModelForm
+
+from models import Product, Price, Book, DerivedBook, ExplicitPK, Post, DerivedPost
+
+class ProductForm(ModelForm):
+ class Meta:
+ model = Product
+
+class PriceForm(ModelForm):
+ class Meta:
+ model = Price
+
+class BookForm(ModelForm):
+ class Meta:
+ model = Book
+
+class DerivedBookForm(ModelForm):
+ class Meta:
+ model = DerivedBook
+
+class ExplicitPKForm(ModelForm):
+ class Meta:
+ model = ExplicitPK
+ fields = ('key', 'desc',)
+
+class PostForm(ModelForm):
+ class Meta:
+ model = Post
+
+class DerivedPostForm(ModelForm):
+ class Meta:
+ model = DerivedPost
129 tests/modeltests/model_forms/models.py
View
@@ -101,7 +101,7 @@ def __unicode__(self):
from PIL import Image, _imaging
except ImportError:
import Image, _imaging
-
+
test_images = True
class ImageFile(models.Model):
@@ -181,6 +181,18 @@ class Book(models.Model):
class Meta:
unique_together = ('title', 'author')
+class BookXtra(models.Model):
+ isbn = models.CharField(max_length=16, unique=True)
+ suffix1 = models.IntegerField(blank=True, default=0)
+ suffix2 = models.IntegerField(blank=True, default=0)
+
+ class Meta:
+ unique_together = (('suffix1', 'suffix2'))
+ abstract = True
+
+class DerivedBook(Book, BookXtra):
+ pass
+
class ExplicitPK(models.Model):
key = models.CharField(max_length=20, primary_key=True)
desc = models.CharField(max_length=20, blank=True, unique=True)
@@ -199,6 +211,9 @@ class Post(models.Model):
def __unicode__(self):
return self.name
+class DerivedPost(Post):
+ pass
+
class BigInt(models.Model):
biggie = models.BigIntegerField()
@@ -1424,41 +1439,6 @@ def __unicode__(self):
>>> f.cleaned_data
{'field': u'1'}
-# 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.']}
-
This Price instance generated by this form is not valid because the quantity
field is required, but the form is valid because the field is excluded from
the form. This is for backwards compatibility.
@@ -1495,51 +1475,6 @@ def __unicode__(self):
>>> form.instance.pk is None
True
-# Unique & unique together with null values
->>> class BookForm(ModelForm):
-... class Meta:
-... model = Book
->>> w = Writer.objects.get(name='Mike Royko')
->>> form = BookForm({'title': 'I May Be Wrong But I Doubt It', 'author' : w.pk})
->>> form.is_valid()
-True
->>> form.save()
-<Book: Book object>
->>> form = BookForm({'title': 'I May Be Wrong But I Doubt It', 'author' : w.pk})
->>> form.is_valid()
-False
->>> form._errors
-{'__all__': [u'Book with this Title and Author already exists.']}
->>> form = BookForm({'title': 'I May Be Wrong But I Doubt It'})
->>> form.is_valid()
-True
->>> form.save()
-<Book: Book object>
->>> form = BookForm({'title': 'I May Be Wrong But I Doubt It'})
->>> form.is_valid()
-True
-
-# Test for primary_key being in the form and failing validation.
->>> class ExplicitPKForm(ModelForm):
-... class Meta:
-... model = ExplicitPK
-... fields = ('key', 'desc',)
->>> form = ExplicitPKForm({'key': u'', 'desc': u'' })
->>> form.is_valid()
-False
-
-# Ensure keys and blank character strings are tested for uniqueness.
->>> form = ExplicitPKForm({'key': u'key1', 'desc': u''})
->>> form.is_valid()
-True
->>> form.save()
-<ExplicitPK: key1>
->>> form = ExplicitPKForm({'key': u'key1', 'desc': u''})
->>> form.is_valid()
-False
->>> sorted(form.errors.items())
-[('__all__', [u'Explicit pk with this Key and Desc already exists.']), ('desc', [u'Explicit pk with this Desc already exists.']), ('key', [u'Explicit pk with this Key already exists.'])]
-
# Choices on CharField and IntegerField
>>> class ArticleForm(ModelForm):
... class Meta:
@@ -1605,38 +1540,6 @@ def __unicode__(self):
<tr><th><label for="id_description">Description:</label></th><td><input type="text" name="description" id="id_description" /></td></tr>
<tr><th><label for="id_url">The URL:</label></th><td><input id="id_url" type="text" name="url" maxlength="40" /></td></tr>
-### Validation on unique_for_date
-
->>> p = Post.objects.create(title="Django 1.0 is released", slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3))
->>> class PostForm(ModelForm):
-... class Meta:
-... model = Post
-
->>> f = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'})
->>> f.is_valid()
-False
->>> f.errors
-{'title': [u'Title must be unique for Posted date.']}
->>> f = PostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'})
->>> f.is_valid()
-True
->>> f = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'})
->>> f.is_valid()
-True
->>> f = PostForm({'slug': "Django 1.0", 'posted': '2008-01-01'})
->>> f.is_valid()
-False
->>> f.errors
-{'slug': [u'Slug must be unique for Posted year.']}
->>> f = PostForm({'subtitle': "Finally", 'posted': '2008-09-30'})
->>> f.is_valid()
-False
->>> f.errors
-{'subtitle': [u'Subtitle must be unique for Posted month.']}
->>> f = PostForm({'subtitle': "Finally", "title": "Django 1.0 is released", "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p)
->>> f.is_valid()
-True
-
# Clean up
>>> import shutil
>>> shutil.rmtree(temp_storage_dir)
141 tests/modeltests/model_forms/tests.py
View
@@ -1,6 +1,8 @@
+import datetime
from django.test import TestCase
from django import forms
-from models import Category
+from models import Category, Writer, Book, DerivedBook, Post
+from mforms import ProductForm, PriceForm, BookForm, DerivedBookForm, ExplicitPKForm, PostForm, DerivedPostForm
class IncompleteCategoryFormWithFields(forms.ModelForm):
@@ -35,3 +37,140 @@ def test_validates_with_replaced_field_excluded(self):
form = IncompleteCategoryFormWithExclude(data={'name': 'some name', 'slug': 'some-slug'})
assert form.is_valid()
+# unique/unique_together validation
+class UniqueTest(TestCase):
+ def setUp(self):
+ self.writer = Writer.objects.create(name='Mike Royko')
+
+ def test_simple_unique(self):
+ form = ProductForm({'slug': 'teddy-bear-blue'})
+ self.assertTrue(form.is_valid())
+ obj = form.save()
+ form = ProductForm({'slug': 'teddy-bear-blue'})
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['slug'], [u'Product with this Slug already exists.'])
+ form = ProductForm({'slug': 'teddy-bear-blue'}, instance=obj)
+ self.assertTrue(form.is_valid())
+
+ def test_unique_together(self):
+ """ModelForm test of unique_together constraint"""
+ form = PriceForm({'price': '6.00', 'quantity': '1'})
+ self.assertTrue(form.is_valid())
+ form.save()
+ form = PriceForm({'price': '6.00', 'quantity': '1'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['__all__'], [u'Price with this Price and Quantity already exists.'])
+
+ def test_unique_null(self):
+ title = 'I May Be Wrong But I Doubt It'
+ form = BookForm({'title': title, 'author': self.writer.pk})
+ self.assertTrue(form.is_valid())
+ form.save()
+ form = BookForm({'title': title, 'author': self.writer.pk})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['__all__'], [u'Book with this Title and Author already exists.'])
+ form = BookForm({'title': title})
+ self.assertTrue(form.is_valid())
+ form.save()
+ form = BookForm({'title': title})
+ self.assertTrue(form.is_valid())
+
+ def test_inherited_unique(self):
+ title = 'Boss'
+ Book.objects.create(title=title, author=self.writer, special_id=1)
+ form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'special_id': u'1', 'isbn': '12345'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['special_id'], [u'Book with this Special id already exists.'])
+
+ def test_inherited_unique_together(self):
+ title = 'Boss'
+ form = BookForm({'title': title, 'author': self.writer.pk})
+ self.assertTrue(form.is_valid())
+ form.save()
+ form = DerivedBookForm({'title': title, 'author': self.writer.pk, 'isbn': '12345'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['__all__'], [u'Book with this Title and Author already exists.'])
+
+ def test_abstract_inherited_unique(self):
+ title = 'Boss'
+ isbn = '12345'
+ dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn)
+ form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': isbn})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['isbn'], [u'Derived book with this Isbn already exists.'])
+
+ def test_abstract_inherited_unique_together(self):
+ title = 'Boss'
+ isbn = '12345'
+ dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn)
+ form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': '9876', 'suffix1': u'0', 'suffix2': u'0'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['__all__'], [u'Derived book with this Suffix1 and Suffix2 already exists.'])
+
+ def test_explicitpk_unspecified(self):
+ """Test for primary_key being in the form and failing validation."""
+ form = ExplicitPKForm({'key': u'', 'desc': u'' })
+ self.assertFalse(form.is_valid())
+
+ def test_explicitpk_unique(self):
+ """Ensure keys and blank character strings are tested for uniqueness."""
+ form = ExplicitPKForm({'key': u'key1', 'desc': u''})
+ self.assertTrue(form.is_valid())
+ form.save()
+ form = ExplicitPKForm({'key': u'key1', 'desc': u''})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 3)
+ self.assertEqual(form.errors['__all__'], [u'Explicit pk with this Key and Desc already exists.'])
+ self.assertEqual(form.errors['desc'], [u'Explicit pk with this Desc already exists.'])
+ self.assertEqual(form.errors['key'], [u'Explicit pk with this Key already exists.'])
+
+ def test_unique_for_date(self):
+ p = Post.objects.create(title="Django 1.0 is released",
+ slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3))
+ form = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['title'], [u'Title must be unique for Posted date.'])
+ form = PostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'})
+ self.assertTrue(form.is_valid())
+ form = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'})
+ self.assertTrue(form.is_valid())
+ form = PostForm({'slug': "Django 1.0", 'posted': '2008-01-01'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['slug'], [u'Slug must be unique for Posted year.'])
+ form = PostForm({'subtitle': "Finally", 'posted': '2008-09-30'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors['subtitle'], [u'Subtitle must be unique for Posted month.'])
+ form = PostForm({'subtitle': "Finally", "title": "Django 1.0 is released",
+ "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p)
+ self.assertTrue(form.is_valid())
+
+ def test_inherited_unique_for_date(self):
+ p = Post.objects.create(title="Django 1.0 is released",
+ slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3))
+ form = DerivedPostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['title'], [u'Title must be unique for Posted date.'])
+ form = DerivedPostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'})
+ self.assertTrue(form.is_valid())
+ form = DerivedPostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'})
+ self.assertTrue(form.is_valid())
+ form = DerivedPostForm({'slug': "Django 1.0", 'posted': '2008-01-01'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertEqual(form.errors['slug'], [u'Slug must be unique for Posted year.'])
+ form = DerivedPostForm({'subtitle': "Finally", 'posted': '2008-09-30'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors['subtitle'], [u'Subtitle must be unique for Posted month.'])
+ form = DerivedPostForm({'subtitle': "Finally", "title": "Django 1.0 is released",
+ "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p)
+ self.assertTrue(form.is_valid())
+
18 tests/modeltests/validation/test_unique.py
View
@@ -9,26 +9,34 @@ class GetUniqueCheckTests(unittest.TestCase):
def test_unique_fields_get_collected(self):
m = UniqueFieldsModel()
self.assertEqual(
- ([('id',), ('unique_charfield',), ('unique_integerfield',)], []),
+ ([(UniqueFieldsModel, ('id',)),
+ (UniqueFieldsModel, ('unique_charfield',)),
+ (UniqueFieldsModel, ('unique_integerfield',))],
+ []),
m._get_unique_checks()
)
def test_unique_together_gets_picked_up_and_converted_to_tuple(self):
m = UniqueTogetherModel()
self.assertEqual(
- ([('ifield', 'cfield',),('ifield', 'efield'), ('id',), ], []),
+ ([(UniqueTogetherModel, ('ifield', 'cfield',)),
+ (UniqueTogetherModel, ('ifield', 'efield')),
+ (UniqueTogetherModel, ('id',)), ],
+ []),
m._get_unique_checks()
)
def test_primary_key_is_considered_unique(self):
m = CustomPKModel()
- self.assertEqual(([('my_pk_field',)], []), m._get_unique_checks())
+ self.assertEqual(([(CustomPKModel, ('my_pk_field',))], []), m._get_unique_checks())
def test_unique_for_date_gets_picked_up(self):
m = UniqueForDateModel()
self.assertEqual((
- [('id',)],
- [('date', 'count', 'start_date'), ('year', 'count', 'end_date'), ('month', 'order', 'end_date')]
+ [(UniqueForDateModel, ('id',))],
+ [(UniqueForDateModel, 'date', 'count', 'start_date'),
+ (UniqueForDateModel, 'year', 'count', 'end_date'),
+ (UniqueForDateModel, 'month', 'order', 'end_date')]
), m._get_unique_checks()
)
Please sign in to comment.
Something went wrong with that request. Please try again.