Browse Source

Refs #28147 -- Fixed setting of OneToOne and Foreign Key fields to None when using attnames.

Regression in 519016e5f25d7c0a040015724f9920581551cab0.
Jon Dufresne 5 years ago
parent
commit
4122d9d3f1

+ 4 - 4
django/db/models/fields/related.py

@@ -19,9 +19,9 @@ from django.utils.translation import gettext_lazy as _
 from . import Field
 from .mixins import FieldCacheMixin
 from .related_descriptors import (
-    ForwardManyToOneDescriptor, ForwardOneToOneDescriptor,
-    ManyToManyDescriptor, ReverseManyToOneDescriptor,
-    ReverseOneToOneDescriptor,
+    ForeignKeyDeferredAttribute, ForwardManyToOneDescriptor,
+    ForwardOneToOneDescriptor, ManyToManyDescriptor,
+    ReverseManyToOneDescriptor, ReverseOneToOneDescriptor,
 )
 from .related_lookups import (
     RelatedExact, RelatedGreaterThan, RelatedGreaterThanOrEqual, RelatedIn,
@@ -764,7 +764,7 @@ class ForeignKey(ForeignObject):
     By default ForeignKey will target the pk of the remote model but this
     behavior can be changed by using the ``to_field`` argument.
     """
-
+    descriptor_class = ForeignKeyDeferredAttribute
     # Field flags
     many_to_many = False
     many_to_one = True

+ 8 - 0
django/db/models/fields/related_descriptors.py

@@ -67,9 +67,17 @@ from django.core.exceptions import FieldError
 from django.db import connections, router, transaction
 from django.db.models import Q, signals
 from django.db.models.query import QuerySet
+from django.db.models.query_utils import DeferredAttribute
 from django.utils.functional import cached_property
 
 
+class ForeignKeyDeferredAttribute(DeferredAttribute):
+    def __set__(self, instance, value):
+        if instance.__dict__.get(self.field.attname) != value and self.field.is_cached(instance):
+            self.field.delete_cached_value(instance)
+        instance.__dict__[self.field.attname] = value
+
+
 class ForwardManyToOneDescriptor:
     """
     Accessor to the related object on the forward side of a many-to-one or

+ 4 - 0
docs/releases/3.0.txt

@@ -490,6 +490,10 @@ Miscellaneous
 * ``intword`` template filter now translates ``1.0`` as a singular phrase and
   all other numeric values as plural. This may be incorrect for some languages.
 
+* Assigning a value to a model's :class:`~django.db.models.ForeignKey` or
+  :class:`~django.db.models.OneToOneField` ``'_id'`` attribute now unsets the
+  corresponding field. Accessing the field afterwards will result in a query.
+
 .. _deprecated-features-3.0:
 
 Features deprecated in 3.0

+ 12 - 0
tests/many_to_one/tests.py

@@ -168,6 +168,18 @@ class ManyToOneTests(TestCase):
         parent.bestchild_id = child2.pk
         self.assertTrue(Parent.bestchild.is_cached(parent))
 
+    def test_assign_fk_id_none(self):
+        parent = Parent.objects.create(name='jeff')
+        child = Child.objects.create(name='frank', parent=parent)
+        parent.bestchild = child
+        parent.save()
+        parent.bestchild_id = None
+        parent.save()
+        self.assertIsNone(parent.bestchild_id)
+        self.assertFalse(Parent.bestchild.is_cached(parent))
+        self.assertIsNone(parent.bestchild)
+        self.assertTrue(Parent.bestchild.is_cached(parent))
+
     def test_selects(self):
         self.r.article_set.create(headline="John's second story", pub_date=datetime.date(2005, 7, 29))
         self.r2.article_set.create(headline="Paul's story", pub_date=datetime.date(2006, 1, 17))

+ 9 - 0
tests/one_to_one/tests.py

@@ -217,6 +217,15 @@ class OneToOneTests(TestCase):
         b.place_id = self.p2.pk
         self.assertTrue(UndergroundBar.place.is_cached(b))
 
+    def test_assign_o2o_id_none(self):
+        b = UndergroundBar.objects.create(place=self.p1)
+        b.place_id = None
+        b.save()
+        self.assertIsNone(b.place_id)
+        self.assertFalse(UndergroundBar.place.is_cached(b))
+        self.assertIsNone(b.place)
+        self.assertTrue(UndergroundBar.place.is_cached(b))
+
     def test_related_object_cache(self):
         """ Regression test for #6886 (the related-object cache) """