summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2015-10-08 18:48:57 -0400
committerTim Graham <timograham@gmail.com>2015-10-09 10:59:57 -0400
commitb646fbe4a77c10314ffcac31ff2f10a42e69c220 (patch)
treefff5c7b31289e1928097721d7d3e59045b6447f3
parent4326ac687ed9d38d92999157f57df95380baa989 (diff)
[1.9.x] Fixed #14368 -- Allowed setting a reverse OneToOne relation to None.
Backport of 384ddbec1b73a4636f234da3894fde8f8420bb63 from master
-rw-r--r--django/db/models/fields/related_descriptors.py46
-rw-r--r--tests/one_to_one/tests.py16
2 files changed, 44 insertions, 18 deletions
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) """