Commit 47a82220 authored by Karen Tracey's avatar Karen Tracey
Browse files

Fixed #12881: Corrected handling of inherited unique constraints. Thanks for report fgaudin.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@12797 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 883329ec
Loading
Loading
Loading
Loading
+39 −26
Original line number Diff line number Diff line
@@ -707,37 +707,50 @@ class Model(object):
        if exclude is None:
            exclude = []
        unique_checks = []
        for check in self._meta.unique_together:

        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(tuple(check))
                    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:

        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((name,))
                    unique_checks.append((model_class, (name,)))
                if f.unique_for_date:
                date_checks.append(('date', name, f.unique_for_date))
                    date_checks.append((model_class, 'date', name, f.unique_for_date))
                if f.unique_for_year:
                date_checks.append(('year', name, f.unique_for_year))
                    date_checks.append((model_class, 'year', name, f.unique_for_year))
                if f.unique_for_month:
                date_checks.append(('month', name, 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 @@ class Model(object):
            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 @@ class Model(object):
                    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 @@ class Model(object):
                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 @@ class Model(object):
            '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
+5 −5
Original line number Diff line number Diff line
@@ -488,7 +488,7 @@ class BaseModelFormSet(BaseFormSet):

        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 @@ class BaseModelFormSet(BaseFormSet):
        # 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 @@ class BaseModelFormSet(BaseFormSet):
    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 −0
Original line number Diff line number Diff line
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
+16 −113
Original line number Diff line number Diff line
@@ -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 @@ True
>>> 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 @@ True
>>> 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 @@ ValidationError: [u'Select a valid choice. z is not one of the available choices
<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)
+140 −1
Original line number Diff line number Diff line
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 @@ class ValidationTest(TestCase):
        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())
Loading