浏览代码

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

Co-Authored-By: Hasan Ramezani <hasan.r67@gmail.com>
Albert Defler 3 年之前
父节点
当前提交
2b6a3baebe
共有 4 个文件被更改,包括 46 次插入1 次删除
  1. 12 0
      django/db/models/fields/related_lookups.py
  2. 2 0
      docs/internals/deprecation.txt
  3. 3 0
      docs/releases/4.1.txt
  4. 29 1
      tests/queries/tests.py

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

@@ -1,3 +1,5 @@
+import warnings
+
 from django.db.models.lookups import (
 from django.db.models.lookups import (
     Exact,
     Exact,
     GreaterThan,
     GreaterThan,
@@ -7,6 +9,7 @@ from django.db.models.lookups import (
     LessThan,
     LessThan,
     LessThanOrEqual,
     LessThanOrEqual,
 )
 )
+from django.utils.deprecation import RemovedInDjango50Warning
 
 
 
 
 class MultiColSource:
 class MultiColSource:
@@ -40,6 +43,15 @@ def get_normalized_value(value, lhs):
     from django.db.models import Model
     from django.db.models import Model
 
 
     if isinstance(value, 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 = []
         value_list = []
         sources = lhs.output_field.path_infos[-1].target_fields
         sources = lhs.output_field.path_infos[-1].target_fields
         for source in sources:
         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
   objects without providing the ``chunk_size`` argument will no longer be
   allowed.
   allowed.
 
 
+* Passing unsaved model instances to related filters will no longer be allowed.
+
 .. _deprecation-removed-in-4.1:
 .. _deprecation-removed-in-4.1:
 
 
 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``
   versions, no prefetching was done. Providing a value for ``chunk_size``
   signifies that the additional query per chunk needed to prefetch is desired.
   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
 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.constants import LOUTER
 from django.db.models.sql.where import NothingNode, WhereNode
 from django.db.models.sql.where import NothingNode, WhereNode
 from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
 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 (
 from .models import (
     FK1,
     FK1,
@@ -1899,6 +1900,19 @@ class Queries5Tests(TestCase):
         self.assertEqual(Ranking.objects.filter(author__in=authors).get(), self.rank3)
         self.assertEqual(Ranking.objects.filter(author__in=authors).get(), self.rank3)
         self.assertEqual(authors.count(), 1)
         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):
 class SelectRelatedTests(TestCase):
     def test_tickets_3045_3288(self):
     def test_tickets_3045_3288(self):
@@ -3211,6 +3225,7 @@ class ExcludeTests(TestCase):
             [self.j1, self.j2],
             [self.j1, self.j2],
         )
         )
 
 
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_exclude_unsaved_o2o_object(self):
     def test_exclude_unsaved_o2o_object(self):
         jack = Staff.objects.create(name="jack")
         jack = Staff.objects.create(name="jack")
         jack_staff = StaffUser.objects.create(staff=jack)
         jack_staff = StaffUser.objects.create(staff=jack)
@@ -3221,6 +3236,19 @@ class ExcludeTests(TestCase):
             StaffUser.objects.exclude(staff=unsaved_object), [jack_staff]
             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):
 class ExcludeTest17600(TestCase):
     """
     """