Index: docs/ref/models/instances.txt
===================================================================
--- docs/ref/models/instances.txt	(revision 16112)
+++ docs/ref/models/instances.txt	(working copy)
@@ -113,8 +113,13 @@
 
 Finally, ``full_clean()`` will check any unique constraints on your model.
 
+.. _model-validate-unique
+
 .. method:: Model.validate_unique(exclude=None)
 
+.. versionadded:: 1.4
+   Prior to Django 1.4, a ``unique_together`` constraint would be ignored if any of the fields were listed in ``exclude``
+
 This method is similar to ``clean_fields``, but validates all uniqueness
 constraints on your model instead of individual field values. The optional
 ``exclude`` argument allows you to provide a list of field names to exclude
@@ -122,10 +127,13 @@
 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. If such a default-enabled field is listed in ``exclude``, all 
+``unique_together`` constraints that reference that field will also be ignored.
 
 
+
 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,15 @@
     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).  As of Django 1.2, it 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
@@ -667,10 +667,18 @@
 
         for model_class, unique_together in unique_togethers:
             for check in unique_together:
+                # if all fields are excluded, do not add to unique_checks
+                check_count = len(check)
                 for name in check:
-                    # If this is an excluded field, don't add this check.
                     if name in exclude:
-                        break
+                        check_count -= 1
+                        if check_count > 0:
+                            # if the default value is specified, do not add to unique_checks 
+                            default = self._meta.fields_dict[name].default
+                            if default is not NOT_PROVIDED:
+                                break
+                        else:
+                            break
                 else:
                     unique_checks.append((model_class, tuple(check)))
 
Index: django/db/models/options.py
===================================================================
--- django/db/models/options.py	(revision 16112)
+++ django/db/models/options.py	(working copy)
@@ -219,6 +219,14 @@
         return self._field_name_cache
     fields = property(_fields)
 
+    def _fields_dict(self):
+        try:
+            return self._fields_dict_cache
+        except AttributeError:
+            self._fields_dict_cache = dict([(field.name, field) for field in self.fields])
+            return self._fields_dict_cache
+    fields_dict = property(_fields_dict)
+
     def get_fields_with_model(self):
         """
         Returns a sequence of (field, model) pairs for all fields. The "model"
Index: tests/modeltests/model_forms/tests.py
===================================================================
--- tests/modeltests/model_forms/tests.py	(revision 16112)
+++ tests/modeltests/model_forms/tests.py	(working copy)
@@ -4,7 +4,8 @@
 from models import Category, Writer, Book, DerivedBook, Post, FlexibleDatePost
 from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm,
                    ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm,
-                   FlexDatePostForm)
+                   FlexDatePostForm, BirthdayPresentForm, DefaultBirthdayPresentForm,
+                   ExcludeAllBirthdayPresentForm)
 
 
 class IncompleteCategoryFormWithFields(forms.ModelForm):
@@ -68,6 +69,47 @@
         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
+        """
+        form = BirthdayPresentForm({'year': '1998', 'description':'a blue bicycle'})
+        form.instance.username = 'joe.smith'
+        self.assertTrue(form.is_valid())
+        form.save()
+        form = BirthdayPresentForm({'year': '1998', 'description':'a sweater'})
+        form.instance.username = 'joe.smith'
+        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_all(self):
+        """
+        ModelForm test of unique_together where all specified 
+        unique_together fields are excluded from the form
+        """
+        form = ExcludeAllBirthdayPresentForm({'description':'a rock'})
+        form.instance.year = '2010'
+        form.instance.username = 'fred.wilson'
+        self.assertTrue(form.is_valid())
+        form.save()
+        form = ExcludeAllBirthdayPresentForm({'description':'an empty box'})
+        form.instance.year = '2010'
+        form.instance.username = 'fred.wilson'
+        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)
 
 class ProductForm(ModelForm):
     class Meta:
@@ -42,3 +43,19 @@
 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 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,25 @@
     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'),)
+
+
 __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,39 @@
         # 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-t3": "c", 
+            "form-0-id": "1", 
+ 
+            "form-1-t1": "a", 
+            "form-1-t2": "b", 
+            "form-1-t3": "c", 
+            "form-1-id": "2", 
+ 
+            "form-2-t1": "a", 
+            "form-2-t2": "b", 
+            "form-2-t3": "c", 
+            "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, blank=True, null=True)       
+     
+    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:
