瀏覽代碼

Refs #34634 -- Fixed creating diamond-shaped MTI objects with ancestors inherited from different paths.

Co-authored-by: Simon Charette <charette.s@gmail.com>
Akash Kumar Sen 1 年之前
父節點
當前提交
1754c2c802
共有 3 個文件被更改,包括 38 次插入15 次删除
  1. 9 15
      django/db/models/options.py
  2. 6 0
      tests/model_inheritance/models.py
  3. 23 0
      tests/model_inheritance/tests.py

+ 9 - 15
django/db/models/options.py

@@ -864,7 +864,7 @@ class Options:
         reverse=True,
         include_parents=True,
         include_hidden=False,
-        seen_models=None,
+        topmost_call=True,
     ):
         """
         Internal helper function to return fields of the model.
@@ -885,13 +885,6 @@ class Options:
         # implementation and to provide a fast way for Django's internals to
         # access specific subsets of fields.
 
-        # We must keep track of which models we have already seen. Otherwise we
-        # could include the same field multiple times from different models.
-        topmost_call = seen_models is None
-        if topmost_call:
-            seen_models = set()
-        seen_models.add(self.model)
-
         # Creates a cache key composed of all arguments
         cache_key = (forward, reverse, include_parents, include_hidden, topmost_call)
 
@@ -906,12 +899,11 @@ class Options:
         # Recursively call _get_fields() on each parent, with the same
         # options provided in this call.
         if include_parents is not False:
+            # In diamond inheritance it is possible that we see the same model
+            # from two different routes. In that case, avoid adding fields from
+            # the same parent again.
+            parent_fields = set()
             for parent in self.parents:
-                # In diamond inheritance it is possible that we see the same
-                # model from two different routes. In that case, avoid adding
-                # fields from the same parent again.
-                if parent in seen_models:
-                    continue
                 if (
                     parent._meta.concrete_model != self.concrete_model
                     and include_parents == PROXY_PARENTS
@@ -922,13 +914,15 @@ class Options:
                     reverse=reverse,
                     include_parents=include_parents,
                     include_hidden=include_hidden,
-                    seen_models=seen_models,
+                    topmost_call=False,
                 ):
                     if (
                         not getattr(obj, "parent_link", False)
                         or obj.model == self.concrete_model
-                    ):
+                    ) and obj not in parent_fields:
                         fields.append(obj)
+                        parent_fields.add(obj)
+
         if reverse and not self.proxy:
             # Tree is computed once and cached until the app cache is expired.
             # It is composed of a list of fields pointing to the current model

+ 6 - 0
tests/model_inheritance/models.py

@@ -106,6 +106,12 @@ class ItalianRestaurant(Restaurant):
     serves_gnocchi = models.BooleanField(default=False)
 
 
+class ItalianRestaurantCommonParent(ItalianRestaurant, Place):
+    place_ptr_two = models.OneToOneField(
+        Place, on_delete=models.CASCADE, parent_link=True
+    )
+
+
 class Supplier(Place):
     customers = models.ManyToManyField(Restaurant, related_name="provider")
 

+ 23 - 0
tests/model_inheritance/tests.py

@@ -15,6 +15,7 @@ from .models import (
     GrandChild,
     GrandParent,
     ItalianRestaurant,
+    ItalianRestaurantCommonParent,
     MixinModel,
     Parent,
     ParkingLot,
@@ -158,6 +159,28 @@ class ModelInheritanceTests(TestCase):
         with self.assertNumQueries(4):
             common_child.save()
 
+    def test_create_diamond_mti_common_parent(self):
+        with self.assertNumQueries(4):
+            italian_restaurant_child = ItalianRestaurantCommonParent.objects.create(
+                name="Ristorante Miron",
+                address="1234 W. Ash",
+            )
+
+        self.assertEqual(
+            italian_restaurant_child.italianrestaurant_ptr.place_ptr,
+            italian_restaurant_child.place_ptr_two,
+        )
+        self.assertEqual(
+            italian_restaurant_child.italianrestaurant_ptr.restaurant_ptr,
+            italian_restaurant_child.restaurant_ptr,
+        )
+        self.assertEqual(
+            italian_restaurant_child.restaurant_ptr.place_ptr,
+            italian_restaurant_child.place_ptr_two,
+        )
+        self.assertEqual(italian_restaurant_child.name, "Ristorante Miron")
+        self.assertEqual(italian_restaurant_child.address, "1234 W. Ash")
+
     def test_update_parent_filtering(self):
         """
         Updating a field of a model subclass doesn't issue an UPDATE