Commit bad5f262 authored by Anssi Kääriäinen's avatar Anssi Kääriäinen Committed by Tim Graham
Browse files

Fixed #24328 -- cleaned up Options._get_fields() implementation

parent bed504d7
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -239,7 +239,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)
+85 −92
Original line number Diff line number Diff line
@@ -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()
        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:
                    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

                # 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)
            )
            # 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
                )

        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)
        # 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

        # In order to avoid list manipulation. Always
        # return a shallow copy of the results
        return fields
+5 −8
Original line number Diff line number Diff line
@@ -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',
            ])
        )
+1 −1
Original line number Diff line number Diff line
@@ -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,))
+12 −2
Original line number Diff line number Diff line
@@ -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.core.urlresolvers import reverse
from django.db import DEFAULT_DB_ALIAS, models
from django.db.models import signals
@@ -328,8 +328,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',