[1.8.x] Fixed #24328 -- cleaned up Options._get_fields() implementation

Backport of bad5f262bf from master
This commit is contained in:
Anssi Kääriäinen 2015-02-16 09:49:19 +02:00 committed by Tim Graham
parent 3a6c37fce4
commit 6f03a4ca91
5 changed files with 106 additions and 105 deletions

View File

@ -272,7 +272,8 @@ class ModelBase(type):
else:
# .. and abstract ones.
for field in parent_fields:
new_class.add_to_class(field.name, copy.deepcopy(field))
new_field = copy.deepcopy(field)
new_class.add_to_class(field.name, new_field)
# Pass any non-abstract parent classes onto child.
new_class._meta.parents.update(base._meta.parents)

View File

@ -22,6 +22,8 @@ from django.utils.lru_cache import lru_cache
from django.utils.text import camel_case_to_spaces
from django.utils.translation import override, string_concat
PROXY_PARENTS = object()
EMPTY_RELATION_TREE = tuple()
IMMUTABLE_WARNING = (
@ -473,8 +475,6 @@ class Options(object):
@cached_property
def _forward_fields_map(self):
res = {}
# call get_fields() with export_ordered_set=True in order to have a
# field_instance -> names map
fields = self._get_fields(reverse=False)
for field in fields:
res[field.name] = field
@ -566,6 +566,10 @@ class Options(object):
# included in the results.
if field.is_relation and field.many_to_one and field.related_model is None:
continue
# Relations to child proxy models should not be included.
if (field.model != self.model and
field.model._meta.concrete_model == self.concrete_model):
continue
names.add(field.name)
if hasattr(field, 'attname'):
@ -576,14 +580,13 @@ class Options(object):
def get_all_related_objects(self, local_only=False, include_hidden=False,
include_proxy_eq=False):
include_parents = local_only is False
include_parents = True if local_only is False else PROXY_PARENTS
fields = self._get_fields(
forward=False, reverse=True,
include_parents=include_parents,
include_hidden=include_hidden,
)
fields = (obj for obj in fields if not isinstance(obj.field, ManyToManyField))
if include_proxy_eq:
children = chain.from_iterable(c._relation_tree
for c in self.concrete_model._meta.proxied_children
@ -606,9 +609,10 @@ class Options(object):
@raise_deprecation(suggested_alternative="get_fields()")
def get_all_related_many_to_many_objects(self, local_only=False):
include_parents = True if local_only is not True else PROXY_PARENTS
fields = self._get_fields(
forward=False, reverse=True,
include_parents=local_only is not True, include_hidden=True
include_parents=include_parents, include_hidden=True
)
return [obj for obj in fields if isinstance(obj.field, ManyToManyField)]
@ -676,19 +680,16 @@ class Options(object):
all_models = self.apps.get_models(include_auto_created=True)
for model in all_models:
# Abstract model's fields are copied to child models, hence we will
# see the fields from the child models.
if model._meta.abstract:
continue
fields_with_relations = (
f for f in model._meta._get_fields(reverse=False)
f for f in model._meta._get_fields(reverse=False, include_parents=False)
if f.is_relation and f.related_model is not None
)
if model._meta.auto_created:
fields_with_relations = (
f for f in fields_with_relations
if not f.many_to_many
)
for f in fields_with_relations:
if not isinstance(f.rel.to, six.string_types):
# Set options_instance -> field
related_objects_graph[f.rel.to._meta].append(f)
for model in all_models:
@ -698,24 +699,14 @@ class Options(object):
# @cached_property). This means that the _meta._relation_tree is
# only called if related_objects is not in __dict__.
related_objects = related_objects_graph[model._meta]
# If related_objects are empty, it makes sense to set
# EMPTY_RELATION_TREE. This will avoid allocating multiple empty
# relation trees.
relation_tree = EMPTY_RELATION_TREE
if related_objects:
relation_tree = related_objects
model._meta.__dict__['_relation_tree'] = relation_tree
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().
return self.__dict__.get('_relation_tree', EMPTY_RELATION_TREE)
@cached_property
def _relation_tree(self):
# If cache is not present, populate the cache
self._populate_directed_relation_graph()
# It may happen, often when the registry is not ready, that a not yet
# registered model is queried. In this very rare case we simply return
# an EMPTY_RELATION_TREE. When the registry will be ready, cache will
# be flushed and this model will be computed properly.
return self.__dict__.get('_relation_tree', EMPTY_RELATION_TREE)
return self._populate_directed_relation_graph()
def _expire_cache(self, forward=True, reverse=True):
# This method is usually called by apps.cache_clear(), when the
@ -744,16 +735,40 @@ class Options(object):
- include_hidden: include fields that have a related_name that
starts with a "+"
"""
if include_parents is False:
include_parents = PROXY_PARENTS
return self._get_fields(include_parents=include_parents, include_hidden=include_hidden)
def _get_fields(self, forward=True, reverse=True, include_parents=True, include_hidden=False,
export_ordered_set=False):
seen_models=None):
"""
Internal helper function to return fields of the model.
* If forward=True, then fields defined on this model are returned.
* If reverse=True, then relations pointing to this model are returned.
* If include_hidden=True, then fields with is_hidden=True are returned.
* The include_parents argument toggles if fields from parent models
should be included. It has three values: True, False, and
PROXY_PARENTS. When set to PROXY_PARENTS, the call will return all
fields defined for the current model or any of its parents in the
parent chain to the model's concrete model.
"""
if include_parents not in (True, False, PROXY_PARENTS):
raise TypeError("Invalid argument for include_parents: %s" % (include_parents,))
# This helper function is used to allow recursion in ``get_fields()``
# 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 = False
if seen_models is None:
seen_models = set()
topmost_call = True
seen_models.add(self.model)
# Creates a cache key composed of all arguments
cache_key = (forward, reverse, include_parents, include_hidden, export_ordered_set)
cache_key = (forward, reverse, include_parents, include_hidden, topmost_call)
try:
# In order to avoid list manipulation. Always return a shallow copy
# of the results.
@ -761,76 +776,54 @@ class Options(object):
except KeyError:
pass
# Using an OrderedDict preserves the order of insertion. This is
# important when displaying a ModelForm or the contrib.admin panel
# and no specific ordering is provided.
fields = OrderedDict()
options = {
'include_parents': include_parents,
'include_hidden': include_hidden,
'export_ordered_set': True,
}
# Abstract models cannot hold reverse fields.
if reverse and not self.abstract:
if include_parents:
parent_list = self.get_parent_list()
# Recursively call _get_fields() on each parent, with the same
# options provided in this call.
for parent in self.parents:
for obj, _ in six.iteritems(parent._meta._get_fields(forward=False, **options)):
if obj.many_to_many:
# In order for a reverse ManyToManyRel object to be
# valid, its creation counter must be > 0 and must
# be in the parent list.
if not (obj.field.creation_counter < 0 and obj.related_model not in parent_list):
fields[obj] = True
elif not ((obj.field.creation_counter < 0 or obj.field.rel.parent_link)
and obj.related_model not in parent_list):
fields[obj] = True
fields = []
# Recursively call _get_fields() on each parent, with the same
# options provided in this call.
if include_parents is not False:
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):
continue
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:
continue
fields.append(obj)
if reverse:
# 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. If the model is a proxy model, then we also
# add the concrete model.
all_fields = (
self._relation_tree if not self.proxy else
chain(self._relation_tree, self.concrete_model._meta._relation_tree)
)
# Pull out all related objects from forward fields
for field in (f.rel for f in all_fields):
# from other models.
all_fields = self._relation_tree
for field in all_fields:
# If hidden fields should be included or the relation is not
# intentionally hidden, add to the fields dict.
if include_hidden or not field.hidden:
fields[field] = True
if include_hidden or not field.rel.hidden:
fields.append(field.rel)
if forward:
if include_parents:
for parent in self.parents:
# Add the forward fields of each parent.
fields.update(parent._meta._get_fields(reverse=False, **options))
fields.update(
(field, True,)
for field in chain(self.local_fields, self.local_many_to_many)
fields.extend(
field for field in chain(self.local_fields, self.local_many_to_many)
)
if not export_ordered_set:
# By default, fields contains field instances as keys and all
# possible names if the field instance as values. When
# _get_fields() is called, we only want to return field instances,
# so we just preserve the keys.
fields = list(fields.keys())
# Virtual fields are not inheritable, therefore they are inserted
# only when the recursive _get_fields() call comes to an end.
if forward:
fields.extend(self.virtual_fields)
fields = make_immutable_fields_list("get_fields()", fields)
# Store result into cache for later access
self._get_fields_cache[cache_key] = fields
# Virtual fields are recopied to each child model, and they get a
# different model as field.model in each child. Hence we have to
# add the virtual fields separately from the topmost call. If we
# did this recursively similar to local_fields, we would get field
# instances with field.model != self.model.
if topmost_call:
fields.extend(
f for f in self.virtual_fields
)
# In order to avoid list manipulation. Always
# return a shallow copy of the results
fields = make_immutable_fields_list("get_fields()", fields)
# Store result into cache for later access
self._get_fields_cache[cache_key] = fields
return fields

View File

@ -228,11 +228,9 @@ class RelationTreeTests(TestCase):
sorted([field.related_query_name() for field in Relation._meta._relation_tree
if not field.rel.field.rel.is_hidden()]),
sorted([
'fk_abstract_rel', 'fk_abstract_rel', 'fk_abstract_rel', 'fk_base_rel', 'fk_base_rel',
'fk_base_rel', 'fk_concrete_rel', 'fk_concrete_rel', 'fo_abstract_rel', 'fo_abstract_rel',
'fo_abstract_rel', 'fo_base_rel', 'fo_base_rel', 'fo_base_rel', 'fo_concrete_rel',
'fo_concrete_rel', 'm2m_abstract_rel', 'm2m_abstract_rel', 'm2m_abstract_rel',
'm2m_base_rel', 'm2m_base_rel', 'm2m_base_rel', 'm2m_concrete_rel', 'm2m_concrete_rel',
'fk_abstract_rel', 'fk_base_rel', 'fk_concrete_rel', 'fo_abstract_rel',
'fo_base_rel', 'fo_concrete_rel', 'm2m_abstract_rel',
'm2m_base_rel', 'm2m_concrete_rel'
])
)
# Testing hidden related objects
@ -243,9 +241,8 @@ class RelationTreeTests(TestCase):
'BasePerson_following_base+', 'BasePerson_following_base+', 'BasePerson_friends_abstract+',
'BasePerson_friends_abstract+', 'BasePerson_friends_base+', 'BasePerson_friends_base+',
'BasePerson_m2m_abstract+', 'BasePerson_m2m_base+', 'Relating_basepeople+',
'Relating_basepeople_hidden+', 'followers_abstract', 'followers_abstract', 'followers_abstract',
'followers_base', 'followers_base', 'followers_base', 'friends_abstract_rel_+', 'friends_abstract_rel_+',
'friends_abstract_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'person',
'Relating_basepeople_hidden+', 'followers_abstract',
'followers_base', 'friends_abstract_rel_+', 'friends_base_rel_+',
'person', 'relating_basepeople', 'relating_baseperson',
])
)

View File

@ -158,7 +158,7 @@ class ProxyTrackerUser(TrackerUser):
@python_2_unicode_compatible
class Issue(models.Model):
summary = models.CharField(max_length=255)
assignee = models.ForeignKey(TrackerUser)
assignee = models.ForeignKey(ProxyTrackerUser)
def __str__(self):
return ':'.join((self.__class__.__name__, self.summary,))

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.apps import apps
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
from django.core import checks, management
from django.core import checks, exceptions, management
from django.db import DEFAULT_DB_ALIAS, models
from django.db.models import signals
from django.test import TestCase, override_settings
@ -327,8 +327,18 @@ class ProxyModelTests(TestCase):
resp = StateProxy.objects.select_related().get(name='New South Wales')
self.assertEqual(resp.name, 'New South Wales')
def test_filter_proxy_relation_reverse(self):
tu = TrackerUser.objects.create(
name='Contributor', status='contrib')
with self.assertRaises(exceptions.FieldError):
TrackerUser.objects.filter(issue=None),
self.assertQuerysetEqual(
ProxyTrackerUser.objects.filter(issue=None),
[tu], lambda x: x
)
def test_proxy_bug(self):
contributor = TrackerUser.objects.create(name='Contributor',
contributor = ProxyTrackerUser.objects.create(name='Contributor',
status='contrib')
someone = BaseUser.objects.create(name='Someone')
Bug.objects.create(summary='fix this', version='1.1beta',