Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py	(revision 12756)
+++ django/db/models/base.py	(working copy)
@@ -682,37 +682,50 @@
         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.
 
@@ -732,7 +745,7 @@
             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)
@@ -744,13 +757,13 @@
                     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
@@ -763,7 +776,7 @@
                 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:
@@ -783,8 +796,8 @@
             '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
Index: django/forms/models.py
===================================================================
--- django/forms/models.py	(revision 12756)
+++ django/forms/models.py	(working copy)
@@ -488,7 +488,7 @@
 
         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 @@
         # 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_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):
Index: tests/modeltests/model_forms/tests.py
===================================================================
--- tests/modeltests/model_forms/tests.py	(revision 12756)
+++ tests/modeltests/model_forms/tests.py	(working copy)
@@ -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 @@
         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())
+
Index: tests/modeltests/model_forms/models.py
===================================================================
--- tests/modeltests/model_forms/models.py	(revision 12756)
+++ tests/modeltests/model_forms/models.py	(working copy)
@@ -101,7 +101,7 @@
         from PIL import Image, _imaging
     except ImportError:
         import Image, _imaging
-    
+
     test_images = True
 
     class ImageFile(models.Model):
@@ -181,6 +181,18 @@
     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 @@
     def __unicode__(self):
         return self.name
 
+class DerivedPost(Post):
+    pass
+
 class BigInt(models.Model):
     biggie = models.BigIntegerField()
 
@@ -1424,41 +1439,6 @@
 >>> 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 @@
 >>> 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 @@
 <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)
Index: tests/modeltests/model_forms/mforms.py
===================================================================
--- tests/modeltests/model_forms/mforms.py	(revision 0)
+++ tests/modeltests/model_forms/mforms.py	(revision 0)
@@ -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

Property changes on: tests/modeltests/model_forms/mforms.py
___________________________________________________________________
Added: svn:executable
   + *
Added: svn:eol-style
   + native

Index: tests/modeltests/validation/test_unique.py
===================================================================
--- tests/modeltests/validation/test_unique.py	(revision 12756)
+++ tests/modeltests/validation/test_unique.py	(working copy)
@@ -9,26 +9,34 @@
     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()
         )
 
