Index: docs/ref/models/instances.txt
===================================================================
--- docs/ref/models/instances.txt	(revision 16112)
+++ docs/ref/models/instances.txt	(working copy)
@@ -113,6 +113,8 @@
 
 Finally, ``full_clean()`` will check any unique constraints on your model.
 
+.. _model-validate-unique
+
 .. method:: Model.validate_unique(exclude=None)
 
 This method is similar to ``clean_fields``, but validates all uniqueness
@@ -122,10 +124,16 @@
 validation.
 
 Note that if you provide an ``exclude`` argument to ``validate_unique``, any
-``unique_together`` constraint that contains one of the fields you provided
-will not be checked.
+``unique_together`` constraint that has all of its fields listed in ``exclude``
+will be ignored.  An exception to this is the case of a field with its ``default`` 
+parameter defined, or ``blank`` set to True (which is equivalent to ``default=""``). 
+If such a default-enabled field is listed in ``exclude``, all ``unique_together`` 
+constraints that reference that field will also be ignored.
 
+.. versionadded:: 1.4
+   Prior to Django 1.4, a ``unique_together`` constraint would be ignored if any of the fields were listed in ``exclude``
 
+
 Saving objects
 ==============
 
Index: docs/ref/models/options.txt
===================================================================
--- docs/ref/models/options.txt	(revision 16112)
+++ docs/ref/models/options.txt	(working copy)
@@ -231,13 +231,17 @@
     This is a list of lists of fields that must be unique when considered together.
     It's used in the Django admin and is enforced at the database level (i.e., the
     appropriate ``UNIQUE`` statements are included in the ``CREATE TABLE``
-    statement).
+    statement).  
 
+.. versionadded:: 1.2
+   As of Django 1.2, ``unique_together`` is also used in ModelForm's :ref:`model validation <model-validate-unique>`.
+
     For convenience, unique_together can be a single list when dealing with a single
     set of fields::
 
         unique_together = ("driver", "restaurant")
 
+
 ``verbose_name``
 ----------------
 
Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py	(revision 16112)
+++ django/db/models/base.py	(working copy)
@@ -7,7 +7,7 @@
 import django.db.models.manager     # Imported to register signal handler.
 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
 from django.core import validators
-from django.db.models.fields import AutoField, FieldDoesNotExist
+from django.db.models.fields import AutoField, FieldDoesNotExist, NOT_PROVIDED
 from django.db.models.fields.related import (OneToOneRel, ManyToOneRel,
     OneToOneField, add_lazy_relation)
 from django.db.models.query import Q
@@ -666,13 +666,21 @@
                 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)))
+            for unique_tuple in unique_together:
+                perform_check = False
+                for name in unique_tuple:
+                    # if any of the fields are NOT excluded, then validate unique
+                    if name not in exclude:
+                        perform_check = True
+                    # however, if there is a default value specified among excluded, don't
+                    else:
+                        field = self._meta.get_field_by_name(name)[0]
+                        if field.default is not NOT_PROVIDED or field.blank:
+                            perform_check = False
+                            break
+                        
+                if perform_check:
+                    unique_checks.append((model_class, tuple(unique_tuple)))
 
         # These are checks for the unique_for_<date/year/month>.
         date_checks = []
Index: tests/modeltests/model_forms/tests.py
===================================================================
--- tests/modeltests/model_forms/tests.py	(revision 16112)
+++ tests/modeltests/model_forms/tests.py	(working copy)
@@ -1,10 +1,12 @@
 import datetime
 from django.test import TestCase
 from django import forms
-from models import Category, Writer, Book, DerivedBook, Post, FlexibleDatePost
+from models import (Category, Writer, Book, DerivedBook, Post, FlexibleDatePost,
+                   BirthdayPresent)
 from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm,
                    ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm,
-                   FlexDatePostForm)
+                   FlexDatePostForm, BirthdayPresentForm, DefaultBirthdayPresentForm,
+                   BlankBirthdayPresentForm, ExcludeAllBirthdayPresentForm)
 
 
 class IncompleteCategoryFormWithFields(forms.ModelForm):
@@ -68,6 +70,60 @@
         self.assertEqual(len(form.errors), 1)
         self.assertEqual(form.errors['__all__'], [u'Price with this Price and Quantity already exists.'])
 
+    def test_excluded_unique_together(self):
+        """
+        ModelForm test of unique_together where a unique_together 
+        field is excluded from the form
+        """
+        new_instance = BirthdayPresent(username='joe.smith')
+        form = BirthdayPresentForm({'year': '1998', 'description':'a blue bicycle'}, 
+                                   instance=new_instance)
+        self.assertTrue(form.is_valid())
+        form.save()
+        new_instance = BirthdayPresent(username='joe.smith')
+        form = BirthdayPresentForm({'year': '1998', 'description':'a sweater'},
+                                   instance=new_instance)
+        self.assertFalse(form.is_valid())
+        self.assertEqual(len(form.errors), 1)
+        self.assertEqual(form.errors['__all__'], [u'Birthday present with this Username and Year already exists.'])
+
+    def test_excluded_unique_together_default(self):
+        """
+        ModelForm test of unique_together where a unique_together 
+        field with a default value is excluded from the form 
+        """
+        form = DefaultBirthdayPresentForm({'year': '2000', 'description':'a blue bicycle'})
+        self.assertTrue(form.is_valid())
+        form.save()
+        form = DefaultBirthdayPresentForm({'year': '2000', 'description':'a sweater'})
+        self.assertTrue(form.is_valid())
+
+    def test_excluded_unique_together_blank(self):
+        """
+        ModelForm test of unique_together where a unique_together 
+        field with blank set to True is excluded from the form 
+        """
+        form = BlankBirthdayPresentForm({'year': '2000', 'description':'a blue bicycle'})
+        self.assertTrue(form.is_valid())
+        form.save()
+        form = BlankBirthdayPresentForm({'year': '2000', 'description':'a sweater'})
+        self.assertTrue(form.is_valid())
+
+    def test_excluded_unique_together_all(self):
+        """
+        ModelForm test of unique_together where all specified 
+        unique_together fields are excluded from the form
+        """
+        new_instance = BirthdayPresent(username='fred.wilson', year='2010')
+        form = ExcludeAllBirthdayPresentForm({'description':'a rock'}, 
+                                             instance=new_instance)
+        self.assertTrue(form.is_valid())
+        form.save()
+        new_instance = BirthdayPresent(username='fred.wilson', year='2010')
+        form = ExcludeAllBirthdayPresentForm({'description':'an empty box'}, 
+                                             instance=new_instance)
+        self.assertTrue(form.is_valid())
+
     def test_unique_null(self):
         title = 'I May Be Wrong But I Doubt It'
         form = BookForm({'title': title, 'author': self.writer.pk})
Index: tests/modeltests/model_forms/mforms.py
===================================================================
--- tests/modeltests/model_forms/mforms.py	(revision 16112)
+++ tests/modeltests/model_forms/mforms.py	(working copy)
@@ -2,7 +2,8 @@
 from django.forms import ModelForm
 
 from models import (Product, Price, Book, DerivedBook, ExplicitPK, Post,
-        DerivedPost, Writer, FlexibleDatePost)
+        DerivedPost, Writer, FlexibleDatePost, BirthdayPresent, 
+        DefaultBirthdayPresent, BlankBirthdayPresent)
 
 class ProductForm(ModelForm):
     class Meta:
@@ -42,3 +43,24 @@
 class FlexDatePostForm(ModelForm):
     class Meta:
         model = FlexibleDatePost
+
+class BirthdayPresentForm(ModelForm):
+    class Meta:
+        model = BirthdayPresent
+        exclude = ('username',)
+
+class DefaultBirthdayPresentForm(ModelForm):
+    class Meta:
+        model = DefaultBirthdayPresent
+        exclude = ('username',)
+
+class BlankBirthdayPresentForm(ModelForm):
+    class Meta:
+        model = BlankBirthdayPresent
+        exclude = ('username',)
+
+class ExcludeAllBirthdayPresentForm(ModelForm):
+    class Meta:
+        model = BirthdayPresent
+        exclude = ('username', 'year',)
+
Index: tests/modeltests/model_forms/models.py
===================================================================
--- tests/modeltests/model_forms/models.py	(revision 16112)
+++ tests/modeltests/model_forms/models.py	(working copy)
@@ -248,6 +248,34 @@
     subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True)
     posted = models.DateField(blank=True, null=True)
 
+class BirthdayPresent(models.Model):
+    """used to test unique_together validation"""
+    username = models.CharField(max_length=30)
+    year = models.IntegerField()
+    description = models.CharField(max_length=200) 
+
+    class Meta:
+        unique_together = ('username', 'year')
+
+class DefaultBirthdayPresent(models.Model):
+    """used to test unique_together validation"""
+    username = models.CharField(max_length=30, default='jeremy.jones')
+    year = models.IntegerField()
+    description = models.CharField(max_length=200) 
+
+    class Meta:
+        unique_together = (('username', 'year'),)
+
+class BlankBirthdayPresent(models.Model):
+    """used to test unique_together validation"""
+    username = models.CharField(max_length=30, blank=True)
+    year = models.IntegerField()
+    description = models.CharField(max_length=200) 
+
+    class Meta:
+        unique_together = (('username', 'year'),)
+
+
 __test__ = {'API_TESTS': """
 >>> from django import forms
 >>> from django.forms.models import ModelForm, model_to_dict
Index: tests/modeltests/model_formsets/tests.py
===================================================================
--- tests/modeltests/model_formsets/tests.py	(revision 16112)
+++ tests/modeltests/model_formsets/tests.py	(working copy)
@@ -1066,8 +1066,8 @@
         self.assertEqual(formset._non_form_errors,
             [u'Please correct the duplicate data for price and quantity, which must be unique.'])
 
-        # Only the price field is specified, this should skip any unique checks since
-        # the unique_together is not fulfilled. This will fail with a KeyError if broken.
+        # Only the price field is specified, this will fail in the unique checks since
+        # the unique_together is partially included.
         FormSet = modelformset_factory(Price, fields=("price",), extra=2)
         data = {
             'form-TOTAL_FORMS': '2',
@@ -1077,7 +1077,9 @@
             'form-1-price': '24',
         }
         formset = FormSet(data)
-        self.assertTrue(formset.is_valid())
+        self.assertFalse(formset.is_valid())
+        self.assertEqual(formset._non_form_errors,
+            [u'Please correct the duplicate data for price and quantity, which must be unique.'])
 
         FormSet = inlineformset_factory(Author, Book, extra=0)
         author = Author.objects.create(pk=1, name='Charles Baudelaire')
Index: tests/regressiontests/admin_views/tests.py
===================================================================
--- tests/regressiontests/admin_views/tests.py	(revision 16112)
+++ tests/regressiontests/admin_views/tests.py	(working copy)
@@ -36,7 +36,7 @@
     Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast,
     Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
     Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
-    Question, Answer, Inquisition, Actor, FoodDelivery,
+    Question, Answer, Inquisition, Actor, FoodDelivery, UniqueTogether,
     RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory)
 
 
@@ -1403,6 +1403,36 @@
         # 1 select per object = 3 selects
         self.assertEqual(response.content.count("<select"), 4)
 
+    def test_partial_unique_together(self): 
+        """ Ensure that no IntegrityError is raised when editing some (but 
+            not all) of the values specified as unique_together. Refs #13091. 
+        """ 
+        UniqueTogether.objects.create(t1='a', t2='b', t3='c') 
+        UniqueTogether.objects.create(t1='b', t2='b', t3='a') 
+        UniqueTogether.objects.create(t1='c', t2='a', t3='c') 
+        data = { 
+            "form-TOTAL_FORMS": "3", 
+            "form-INITIAL_FORMS": "3", 
+            "form-MAX_NUM_FORMS": "0", 
+ 
+            "form-0-t1": "a", 
+            "form-0-t2": "b", 
+            "form-0-id": "1", 
+ 
+            "form-1-t1": "a", 
+            "form-1-t2": "b", 
+            "form-1-id": "2", 
+ 
+            "form-2-t1": "a", 
+            "form-2-t2": "b", 
+            "form-2-id": "3", 
+ 
+            "_save": "Save", 
+        } 
+        response = self.client.post('/test_admin/admin/admin_views/uniquetogether/', data) 
+        self.assertContains(response, 'Please correct the errors below.') 
+        self.assertContains(response, 'Unique together with this T1, T2 and T3 already exists.') 
+         
     def test_post_messages(self):
         # Ticket 12707: Saving inline editable should not show admin
         # action warnings
Index: tests/regressiontests/admin_views/models.py
===================================================================
--- tests/regressiontests/admin_views/models.py	(revision 16112)
+++ tests/regressiontests/admin_views/models.py	(working copy)
@@ -803,6 +803,19 @@
     list_display_links = ('title', 'id') # 'id' in list_display_links
     list_editable = ('content', )
 
+class UniqueTogether(models.Model):  
+    t1 = models.CharField(max_length=255, blank=True, null=True)  
+    t2 = models.CharField(max_length=255, blank=True, null=True)  
+    t3 = models.CharField(max_length=255)       
+     
+    class Meta:  
+        unique_together = ['t1','t2','t3']  
+                  
+class UniqueTogetherAdmin(admin.ModelAdmin):  
+    list_display = ['t3', 't1', 't2']  
+    list_editable = ['t1', 't2',] 
+
+
 admin.site.register(Article, ArticleAdmin)
 admin.site.register(CustomArticle, CustomArticleAdmin)
 admin.site.register(Section, save_as=True, inlines=[ArticleInline])
@@ -845,6 +858,7 @@
 admin.site.register(CoverLetter, CoverLetterAdmin)
 admin.site.register(Story, StoryAdmin)
 admin.site.register(OtherStory, OtherStoryAdmin)
+admin.site.register(UniqueTogether, UniqueTogetherAdmin)
 
 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
 # That way we cover all four cases:
