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

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

Backport of bad5f262 from master
parent 3a6c37fc
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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)
+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.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',