From 1754c2c8026ab592183821c3933972a8aa7acc66 Mon Sep 17 00:00:00 2001 From: Akash Kumar Sen Date: Thu, 15 Jun 2023 18:08:30 +0530 Subject: [PATCH] Refs #34634 -- Fixed creating diamond-shaped MTI objects with ancestors inherited from different paths. Co-authored-by: Simon Charette --- django/db/models/options.py | 24 +++++++++--------------- tests/model_inheritance/models.py | 6 ++++++ tests/model_inheritance/tests.py | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/django/db/models/options.py b/django/db/models/options.py index 00735e0de1..64e5ff53ea 100644 --- a/django/db/models/options.py +++ b/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 diff --git a/tests/model_inheritance/models.py b/tests/model_inheritance/models.py index aabfdfc03d..47aae186e0 100644 --- a/tests/model_inheritance/models.py +++ b/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") diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index f1ecacbe29..5bf1ef5db5 100644 --- a/tests/model_inheritance/tests.py +++ b/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