Skip to content

Commit b646fbe

Browse files
committed
[1.9.x] Fixed #14368 -- Allowed setting a reverse OneToOne relation to None.
Backport of 384ddbe from master
1 parent 4326ac6 commit b646fbe

File tree

2 files changed

+44
-18
lines changed

2 files changed

+44
-18
lines changed

django/db/models/fields/related_descriptors.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -376,14 +376,24 @@ def __set__(self, instance, value):
376376

377377
# If null=True, we can assign null here, but otherwise the value needs
378378
# to be an instance of the related class.
379-
if value is None and self.related.field.null is False:
380-
raise ValueError(
381-
'Cannot assign None: "%s.%s" does not allow null values.' % (
382-
instance._meta.object_name,
383-
self.related.get_accessor_name(),
379+
if value is None:
380+
if self.related.field.null:
381+
# Update the cached related instance (if any) & clear the cache.
382+
try:
383+
rel_obj = getattr(instance, self.cache_name)
384+
except AttributeError:
385+
pass
386+
else:
387+
delattr(instance, self.cache_name)
388+
setattr(rel_obj, self.related.field.name, None)
389+
else:
390+
raise ValueError(
391+
'Cannot assign None: "%s.%s" does not allow null values.' % (
392+
instance._meta.object_name,
393+
self.related.get_accessor_name(),
394+
)
384395
)
385-
)
386-
elif value is not None and not isinstance(value, self.related.related_model):
396+
elif not isinstance(value, self.related.related_model):
387397
raise ValueError(
388398
'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % (
389399
value,
@@ -392,7 +402,7 @@ def __set__(self, instance, value):
392402
self.related.related_model._meta.object_name,
393403
)
394404
)
395-
elif value is not None:
405+
else:
396406
if instance._state.db is None:
397407
instance._state.db = router.db_for_write(instance.__class__, instance=value)
398408
elif value._state.db is None:
@@ -401,18 +411,18 @@ def __set__(self, instance, value):
401411
if not router.allow_relation(value, instance):
402412
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
403413

404-
related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields)
405-
# Set the value of the related field to the value of the related object's related field
406-
for index, field in enumerate(self.related.field.local_related_fields):
407-
setattr(value, field.attname, related_pk[index])
414+
related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields)
415+
# Set the value of the related field to the value of the related object's related field
416+
for index, field in enumerate(self.related.field.local_related_fields):
417+
setattr(value, field.attname, related_pk[index])
408418

409-
# Set the related instance cache used by __get__ to avoid a SQL query
410-
# when accessing the attribute we just set.
411-
setattr(instance, self.cache_name, value)
419+
# Set the related instance cache used by __get__ to avoid a SQL query
420+
# when accessing the attribute we just set.
421+
setattr(instance, self.cache_name, value)
412422

413-
# Set the forward accessor cache on the related object to the current
414-
# instance to avoid an extra SQL query if it's accessed later on.
415-
setattr(value, self.related.field.get_cache_name(), instance)
423+
# Set the forward accessor cache on the related object to the current
424+
# instance to avoid an extra SQL query if it's accessed later on.
425+
setattr(value, self.related.field.get_cache_name(), instance)
416426

417427

418428
class ReverseManyToOneDescriptor(object):

tests/one_to_one/tests.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,22 @@ def test_reverse_object_cache(self):
186186
self.assertEqual(self.p1.restaurant, self.r1)
187187
self.assertEqual(self.p1.bar, self.b1)
188188

189+
def test_assign_none_reverse_relation(self):
190+
p = Place.objects.get(name="Demon Dogs")
191+
# Assigning None succeeds if field is null=True.
192+
ug_bar = UndergroundBar.objects.create(place=p, serves_cocktails=False)
193+
p.undergroundbar = None
194+
self.assertIsNone(ug_bar.place)
195+
ug_bar.save()
196+
ug_bar.refresh_from_db()
197+
self.assertIsNone(ug_bar.place)
198+
199+
def test_assign_none_null_reverse_relation(self):
200+
p = Place.objects.get(name="Demon Dogs")
201+
# Assigning None doesn't throw AttributeError if there isn't a related
202+
# UndergroundBar.
203+
p.undergroundbar = None
204+
189205
def test_related_object_cache(self):
190206
""" Regression test for #6886 (the related-object cache) """
191207

0 commit comments

Comments
 (0)