Browse Source

Fixed #31486 -- Deprecated passing unsaved objects to related filters.

Co-Authored-By: Hasan Ramezani <hasan.r67@gmail.com>
Albert Defler 3 years ago
parent
commit
2b6a3baebe

+ 12 - 0
django/db/models/fields/related_lookups.py

@@ -1,3 +1,5 @@
+import warnings
+
 from django.db.models.lookups import (
     Exact,
     GreaterThan,
@@ -7,6 +9,7 @@ from django.db.models.lookups import (
     LessThan,
     LessThanOrEqual,
 )
+from django.utils.deprecation import RemovedInDjango50Warning
 
 
 class MultiColSource:
@@ -40,6 +43,15 @@ def get_normalized_value(value, lhs):
     from django.db.models import Model
 
     if isinstance(value, Model):
+        if value.pk is None:
+            # When the deprecation ends, replace with:
+            # raise ValueError(
+            #     "Model instances passed to related filters must be saved."
+            # )
+            warnings.warn(
+                "Passing unsaved model instances to related filters is deprecated.",
+                RemovedInDjango50Warning,
+            )
         value_list = []
         sources = lhs.output_field.path_infos[-1].target_fields
         for source in sources:

+ 2 - 0
docs/internals/deprecation.txt

@@ -85,6 +85,8 @@ details on these changes.
   objects without providing the ``chunk_size`` argument will no longer be
   allowed.
 
+* Passing unsaved model instances to related filters will no longer be allowed.
+
 .. _deprecation-removed-in-4.1:
 
 4.1

+ 3 - 0
docs/releases/4.1.txt

@@ -484,6 +484,9 @@ Miscellaneous
   versions, no prefetching was done. Providing a value for ``chunk_size``
   signifies that the additional query per chunk needed to prefetch is desired.
 
+* Passing unsaved model instances to related filters is deprecated. In Django
+  5.0, the exception will be raised.
+
 Features removed in 4.1
 =======================
 

+ 29 - 1
tests/queries/tests.py

@@ -12,7 +12,8 @@ from django.db.models.expressions import RawSQL
 from django.db.models.sql.constants import LOUTER
 from django.db.models.sql.where import NothingNode, WhereNode
 from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
-from django.test.utils import CaptureQueriesContext
+from django.test.utils import CaptureQueriesContext, ignore_warnings
+from django.utils.deprecation import RemovedInDjango50Warning
 
 from .models import (
     FK1,
@@ -1899,6 +1900,19 @@ class Queries5Tests(TestCase):
         self.assertEqual(Ranking.objects.filter(author__in=authors).get(), self.rank3)
         self.assertEqual(authors.count(), 1)
 
+    def test_filter_unsaved_object(self):
+        # These tests will catch ValueError in Django 5.0 when passing unsaved
+        # model instances to related filters becomes forbidden.
+        # msg = "Model instances passed to related filters must be saved."
+        msg = "Passing unsaved model instances to related filters is deprecated."
+        company = Company.objects.create(name="Django")
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            Employment.objects.filter(employer=Company(name="unsaved"))
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            Employment.objects.filter(employer__in=[company, Company(name="unsaved")])
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            StaffUser.objects.filter(staff=Staff(name="unsaved"))
+
 
 class SelectRelatedTests(TestCase):
     def test_tickets_3045_3288(self):
@@ -3211,6 +3225,7 @@ class ExcludeTests(TestCase):
             [self.j1, self.j2],
         )
 
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_exclude_unsaved_o2o_object(self):
         jack = Staff.objects.create(name="jack")
         jack_staff = StaffUser.objects.create(staff=jack)
@@ -3221,6 +3236,19 @@ class ExcludeTests(TestCase):
             StaffUser.objects.exclude(staff=unsaved_object), [jack_staff]
         )
 
+    def test_exclude_unsaved_object(self):
+        # These tests will catch ValueError in Django 5.0 when passing unsaved
+        # model instances to related filters becomes forbidden.
+        # msg = "Model instances passed to related filters must be saved."
+        company = Company.objects.create(name="Django")
+        msg = "Passing unsaved model instances to related filters is deprecated."
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            Employment.objects.exclude(employer=Company(name="unsaved"))
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            Employment.objects.exclude(employer__in=[company, Company(name="unsaved")])
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            StaffUser.objects.exclude(staff=Staff(name="unsaved"))
+
 
 class ExcludeTest17600(TestCase):
     """