diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 2398b166bb..e9caa2680f 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -376,14 +376,24 @@ class ReverseOneToOneDescriptor(object): # If null=True, we can assign null here, but otherwise the value needs # to be an instance of the related class. - if value is None and self.related.field.null is False: - raise ValueError( - 'Cannot assign None: "%s.%s" does not allow null values.' % ( - instance._meta.object_name, - self.related.get_accessor_name(), + if value is None: + if self.related.field.null: + # Update the cached related instance (if any) & clear the cache. + try: + rel_obj = getattr(instance, self.cache_name) + except AttributeError: + pass + else: + delattr(instance, self.cache_name) + setattr(rel_obj, self.related.field.name, None) + else: + raise ValueError( + 'Cannot assign None: "%s.%s" does not allow null values.' % ( + instance._meta.object_name, + self.related.get_accessor_name(), + ) ) - ) - elif value is not None and not isinstance(value, self.related.related_model): + elif not isinstance(value, self.related.related_model): raise ValueError( 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % ( value, @@ -392,7 +402,7 @@ class ReverseOneToOneDescriptor(object): self.related.related_model._meta.object_name, ) ) - elif value is not None: + else: if instance._state.db is None: instance._state.db = router.db_for_write(instance.__class__, instance=value) elif value._state.db is None: @@ -401,18 +411,18 @@ class ReverseOneToOneDescriptor(object): if not router.allow_relation(value, instance): raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value) - related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields) - # Set the value of the related field to the value of the related object's related field - for index, field in enumerate(self.related.field.local_related_fields): - setattr(value, field.attname, related_pk[index]) + related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields) + # Set the value of the related field to the value of the related object's related field + for index, field in enumerate(self.related.field.local_related_fields): + setattr(value, field.attname, related_pk[index]) - # Set the related instance cache used by __get__ to avoid a SQL query - # when accessing the attribute we just set. - setattr(instance, self.cache_name, value) + # Set the related instance cache used by __get__ to avoid a SQL query + # when accessing the attribute we just set. + setattr(instance, self.cache_name, value) - # Set the forward accessor cache on the related object to the current - # instance to avoid an extra SQL query if it's accessed later on. - setattr(value, self.related.field.get_cache_name(), instance) + # Set the forward accessor cache on the related object to the current + # instance to avoid an extra SQL query if it's accessed later on. + setattr(value, self.related.field.get_cache_name(), instance) class ReverseManyToOneDescriptor(object): diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index 25d20dc2c9..ee5d7415e3 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -186,6 +186,22 @@ class OneToOneTests(TestCase): self.assertEqual(self.p1.restaurant, self.r1) self.assertEqual(self.p1.bar, self.b1) + def test_assign_none_reverse_relation(self): + p = Place.objects.get(name="Demon Dogs") + # Assigning None succeeds if field is null=True. + ug_bar = UndergroundBar.objects.create(place=p, serves_cocktails=False) + p.undergroundbar = None + self.assertIsNone(ug_bar.place) + ug_bar.save() + ug_bar.refresh_from_db() + self.assertIsNone(ug_bar.place) + + def test_assign_none_null_reverse_relation(self): + p = Place.objects.get(name="Demon Dogs") + # Assigning None doesn't throw AttributeError if there isn't a related + # UndergroundBar. + p.undergroundbar = None + def test_related_object_cache(self): """ Regression test for #6886 (the related-object cache) """