diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 76a2d35890..831e3c03db 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -55,7 +55,7 @@ def get_candidate_relations_to_delete(opts): # The candidate relations are the ones that come from N-1 and 1-1 relations. # N-N (i.e., many-to-many) relations aren't candidates for deletion. return ( - f for f in opts.concrete_model._meta.get_fields(include_hidden=True) + f for f in opts.get_fields(include_hidden=True) if f.auto_created and not f.concrete and (f.one_to_one or f.one_to_many) ) diff --git a/django/db/models/options.py b/django/db/models/options.py index 28773bc0b3..3d0a42e515 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -552,14 +552,10 @@ class Options(object): is set as a property on every model. """ related_objects_graph = defaultdict(list) - # Map of concrete models to all options of models it represents. - # Including its options and all its proxy model ones. - concrete_model_classes = defaultdict(list) all_models = self.apps.get_models(include_auto_created=True) for model in all_models: opts = model._meta - concrete_model_classes[opts.concrete_model].append(opts) # Abstract model's fields are copied to child models, hence we will # see the fields from the child models. if opts.abstract: @@ -570,7 +566,7 @@ class Options(object): ) for f in fields_with_relations: if not isinstance(f.remote_field.model, six.string_types): - related_objects_graph[f.remote_field.model._meta].append(f) + related_objects_graph[f.remote_field.model._meta.concrete_model._meta].append(f) for model in all_models: # Set the relation_tree using the internal __dict__. In this way @@ -578,9 +574,7 @@ class Options(object): # __dict__ takes precedence over a data descriptor (such as # @cached_property). This means that the _meta._relation_tree is # only called if related_objects is not in __dict__. - related_objects = list(chain.from_iterable( - related_objects_graph[opts] for opts in concrete_model_classes[model] - )) + related_objects = related_objects_graph[model._meta.concrete_model._meta] model._meta.__dict__['_relation_tree'] = related_objects # It seems it is possible that self is not in all_models, so guard # against that with default for get(). @@ -674,10 +668,10 @@ class Options(object): for obj in parent._meta._get_fields( forward=forward, reverse=reverse, include_parents=include_parents, include_hidden=include_hidden, seen_models=seen_models): - if hasattr(obj, 'parent_link') and obj.parent_link: + if getattr(obj, 'parent_link', False) and obj.model != self.concrete_model: continue fields.append(obj) - if reverse: + 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 # from other models. diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index 4868930bf8..1087c0bb55 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -238,6 +238,15 @@ But it didn't prohibit nested non-relation fields as it does now:: ... FieldError: Non-relational field given in select_related: 'name' +``_meta.get_fields()`` returns consistent reverse fields for proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before Django 1.10, the :meth:`~django.db.models.options.Options.get_fields` +method returned different reverse fields when called on a proxy model compared +to its proxied concrete class. This inconsistency was fixed by returning the +full set of fields pointing to a concrete class or one of its proxies in both +cases. + Miscellaneous ~~~~~~~~~~~~~ diff --git a/tests/model_meta/models.py b/tests/model_meta/models.py index a9c1f8f98d..7060cf521c 100644 --- a/tests/model_meta/models.py +++ b/tests/model_meta/models.py @@ -101,6 +101,10 @@ class ProxyPerson(Person): proxy = True +class PersonThroughProxySubclass(ProxyPerson): + pass + + class Relating(models.Model): # ForeignKey to BasePerson diff --git a/tests/model_meta/results.py b/tests/model_meta/results.py index 5975f06465..50cb59a169 100644 --- a/tests/model_meta/results.py +++ b/tests/model_meta/results.py @@ -1,4 +1,6 @@ -from .models import AbstractPerson, BasePerson, Person, Relating, Relation +from .models import ( + AbstractPerson, BasePerson, Person, ProxyPerson, Relating, Relation, +) TEST_RESULTS = { 'get_all_field_names': { @@ -329,11 +331,30 @@ TEST_RESULTS = { ('Relating_people_hidden+', None), ('followers_concrete', None), ('friends_inherited_rel_+', None), + ('personthroughproxysubclass', None), ('relating_people', None), ('relating_person', None), ('relating_proxyperson', None), ('relating_proxyperson_hidden+', None), ), + ProxyPerson: ( + ('+', Person), + ('_relating_people_hidden_+', Person), + ('Person_following_inherited+', Person), + ('Person_following_inherited+', Person), + ('Person_friends_inherited+', Person), + ('Person_friends_inherited+', Person), + ('Person_m2m_inherited+', Person), + ('Relating_people+', Person), + ('Relating_people_hidden+', Person), + ('followers_concrete', Person), + ('friends_inherited_rel_+', Person), + ('personthroughproxysubclass', Person), + ('relating_people', Person), + ('relating_person', Person), + ('relating_proxyperson', Person), + ('relating_proxyperson_hidden+', Person), + ), BasePerson: ( ('+', None), ('_relating_basepeople_hidden_+', None), @@ -366,6 +387,9 @@ TEST_RESULTS = { ('+', None), ('+', None), ('+', None), + ('+', None), + ('+', None), + ('+', None), ('BasePerson_m2m_abstract+', None), ('BasePerson_m2m_base+', None), ('Person_m2m_inherited+', None), @@ -411,6 +435,7 @@ TEST_RESULTS = { ('friends_abstract_rel_+', BasePerson), ('friends_base_rel_+', BasePerson), ('friends_inherited_rel_+', None), + ('personthroughproxysubclass', None), ('relating_basepeople', BasePerson), ('relating_baseperson', BasePerson), ('relating_people', None), @@ -418,6 +443,44 @@ TEST_RESULTS = { ('relating_proxyperson', None), ('relating_proxyperson_hidden+', None), ), + ProxyPerson: ( + ('+', BasePerson), + ('+', Person), + ('_relating_basepeople_hidden_+', BasePerson), + ('_relating_people_hidden_+', Person), + ('BasePerson_following_abstract+', BasePerson), + ('BasePerson_following_abstract+', BasePerson), + ('BasePerson_following_base+', BasePerson), + ('BasePerson_following_base+', BasePerson), + ('BasePerson_friends_abstract+', BasePerson), + ('BasePerson_friends_abstract+', BasePerson), + ('BasePerson_friends_base+', BasePerson), + ('BasePerson_friends_base+', BasePerson), + ('BasePerson_m2m_abstract+', BasePerson), + ('BasePerson_m2m_base+', BasePerson), + ('Person_following_inherited+', Person), + ('Person_following_inherited+', Person), + ('Person_friends_inherited+', Person), + ('Person_friends_inherited+', Person), + ('Person_m2m_inherited+', Person), + ('Relating_basepeople+', BasePerson), + ('Relating_basepeople_hidden+', BasePerson), + ('Relating_people+', Person), + ('Relating_people_hidden+', Person), + ('followers_abstract', BasePerson), + ('followers_base', BasePerson), + ('followers_concrete', Person), + ('friends_abstract_rel_+', BasePerson), + ('friends_base_rel_+', BasePerson), + ('friends_inherited_rel_+', Person), + ('personthroughproxysubclass', Person), + ('relating_basepeople', BasePerson), + ('relating_baseperson', BasePerson), + ('relating_people', Person), + ('relating_person', Person), + ('relating_proxyperson', Person), + ('relating_proxyperson_hidden+', Person), + ), BasePerson: ( ('+', None), ('_relating_basepeople_hidden_+', None), @@ -450,6 +513,9 @@ TEST_RESULTS = { ('+', None), ('+', None), ('+', None), + ('+', None), + ('+', None), + ('+', None), ('BasePerson_m2m_abstract+', None), ('BasePerson_m2m_base+', None), ('Person_m2m_inherited+', None), @@ -467,10 +533,18 @@ TEST_RESULTS = { 'get_all_related_objects_with_model_local': { Person: ( ('followers_concrete', None), + ('personthroughproxysubclass', None), ('relating_person', None), ('relating_people', None), ('relating_proxyperson', None), ), + ProxyPerson: ( + ('followers_concrete', Person), + ('personthroughproxysubclass', Person), + ('relating_person', Person), + ('relating_people', Person), + ('relating_proxyperson', Person), + ), BasePerson: ( ('followers_abstract', None), ('followers_base', None), @@ -497,10 +571,22 @@ TEST_RESULTS = { ('relating_baseperson', BasePerson), ('relating_basepeople', BasePerson), ('followers_concrete', None), + ('personthroughproxysubclass', None), ('relating_person', None), ('relating_people', None), ('relating_proxyperson', None), ), + ProxyPerson: ( + ('followers_abstract', BasePerson), + ('followers_base', BasePerson), + ('relating_baseperson', BasePerson), + ('relating_basepeople', BasePerson), + ('followers_concrete', Person), + ('personthroughproxysubclass', Person), + ('relating_person', Person), + ('relating_people', Person), + ('relating_proxyperson', Person), + ), BasePerson: ( ('followers_abstract', None), ('followers_base', None), diff --git a/tests/model_meta/tests.py b/tests/model_meta/tests.py index 6a881624c8..1dbfca0c8e 100644 --- a/tests/model_meta/tests.py +++ b/tests/model_meta/tests.py @@ -111,7 +111,10 @@ class RelatedObjectsTests(OptionsBaseTests): for field in model._meta.get_fields() if field.auto_created and not field.concrete ] - self.assertEqual(self._map_related_query_names(objects), expected) + self.assertEqual( + sorted(self._map_related_query_names(objects), key=self.key_name), + sorted(expected, key=self.key_name), + ) def test_related_objects_local(self): result_key = 'get_all_related_objects_with_model_local' @@ -121,7 +124,10 @@ class RelatedObjectsTests(OptionsBaseTests): for field in model._meta.get_fields(include_parents=False) if field.auto_created and not field.concrete ] - self.assertEqual(self._map_related_query_names(objects), expected) + self.assertEqual( + sorted(self._map_related_query_names(objects), key=self.key_name), + sorted(expected, key=self.key_name), + ) def test_related_objects_include_hidden(self): result_key = 'get_all_related_objects_with_model_hidden'