diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 54c064a56e..57c8bdefd0 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -824,13 +824,8 @@ class ForeignKey(RelatedField, Field): def validate(self, value, model_instance): if self.rel.parent_link: return - # Don't validate the field if a value wasn't supplied. This is - # generally the case when saving new inlines in the admin. - # See #12507. - if value is None: - return super(ForeignKey, self).validate(value, model_instance) - if not value: + if value is None: return qs = self.rel.to._default_manager.filter(**{self.rel.field_name:value}) diff --git a/django/forms/models.py b/django/forms/models.py index 4ac3f950ba..a0ef2de67f 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -316,12 +316,23 @@ class BaseModelForm(BaseForm): return self.cleaned_data def _post_clean(self): - exclude = self._get_validation_exclusions() opts = self._meta - # Update the model instance with self.cleaned_data. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) + exclude = self._get_validation_exclusions() + + # Foreign Keys being used to represent inline relationships + # are excluded from basic field value validation. This is for two + # reasons: firstly, the value may not be supplied (#12507; the + # case of providing new values to the admin); secondly the + # object being referred to may not yet fully exist (#12749). + # However, these fields *must* be included in uniqueness checks, + # so this can't be part of _get_validation_exclusions(). + for f_name, field in self.fields.items(): + if isinstance(field, InlineForeignKeyField): + exclude.append(f_name) + # Clean the model instance's fields. try: self.instance.clean_fields(exclude=exclude) @@ -762,6 +773,7 @@ class BaseInlineFormSet(BaseModelFormSet): unique_check = [field for field in unique_check if field != self.fk.name] return super(BaseInlineFormSet, self).get_unique_error_message(unique_check) + def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): """ Finds and returns the ForeignKey from model to parent if there is one diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py index bca582b9ab..5a12e0743c 100644 --- a/tests/regressiontests/admin_inlines/models.py +++ b/tests/regressiontests/admin_inlines/models.py @@ -102,6 +102,29 @@ admin.site.register(Holder2, HolderAdmin, inlines=[InnerInline2]) # only Inline media admin.site.register(Holder3, inlines=[InnerInline3]) +# Models for #12749 + +class Person(models.Model): + firstname = models.CharField(max_length=15) + +class OutfitItem(models.Model): + name = models.CharField(max_length=15) + +class Fashionista(models.Model): + person = models.OneToOneField(Person, primary_key=True) + weaknesses = models.ManyToManyField(OutfitItem, through='ShoppingWeakness', blank=True) + +class ShoppingWeakness(models.Model): + fashionista = models.ForeignKey(Fashionista) + item = models.ForeignKey(OutfitItem) + +class InlineWeakness(admin.TabularInline): + model = ShoppingWeakness + extra = 1 + +admin.site.register(Fashionista, inlines=[InlineWeakness]) + + __test__ = {'API_TESTS': """ # Regression test for #9362 diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index ffcbe1e4ba..c27873ceec 100644 --- a/tests/regressiontests/admin_inlines/tests.py +++ b/tests/regressiontests/admin_inlines/tests.py @@ -3,7 +3,7 @@ from django.test import TestCase # local test models from models import Holder, Inner, InnerInline from models import Holder2, Inner2, Holder3, Inner3 - +from models import Person, OutfitItem, Fashionista class TestInline(TestCase): fixtures = ['admin-views-users.xml'] @@ -48,6 +48,22 @@ class TestInline(TestCase): # The '+' is dropped from the autogenerated form prefix (Author_books+) self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"') + def test_inline_primary(self): + person = Person.objects.create(firstname='Imelda') + item = OutfitItem.objects.create(name='Shoes') + # Imelda likes shoes, but can't cary her own bags. + data = { + 'shoppingweakness_set-TOTAL_FORMS': 1, + 'shoppingweakness_set-INITIAL_FORMS': 0, + 'shoppingweakness_set-MAX_NUM_FORMS': 0, + '_save': u'Save', + 'person': person.id, + 'max_weight': 0, + 'shoppingweakness_set-0-item': item.id, + } + response = self.client.post('/test_admin/admin/admin_inlines/fashionista/add/', data) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(Fashionista.objects.filter(person__firstname='Imelda')), 1) class TestInlineMedia(TestCase): fixtures = ['admin-views-users.xml']