Commit b77f2631 authored by Gabe Jackson's avatar Gabe Jackson Committed by Anssi Kääriäinen
Browse files

Fixed #22207 -- Added support for GenericRelation reverse lookups

GenericRelation now supports an optional related_query_name argument.
Setting related_query_name adds a relation from the related object back to
the content type for filtering, ordering and other query operations.

Thanks to Loic Bistuer for spotting a couple of important issues in
his review.
parent c627da0c
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -168,7 +168,7 @@ class NestedObjects(Collector):

    def collect(self, objs, source=None, source_attr=None, **kwargs):
        for obj in objs:
            if source_attr:
            if source_attr and not source_attr.endswith('+'):
                related_name = source_attr % {
                    'class': source._meta.model_name,
                    'app_label': source._meta.app_label,
+15 −16
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ from django.core import checks
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.db import models, router, transaction, DEFAULT_DB_ALIAS
from django.db.models import signals, FieldDoesNotExist
from django.db.models import signals, FieldDoesNotExist, DO_NOTHING
from django.db.models.base import ModelBase
from django.db.models.fields.related import ForeignObject, ForeignObjectRel
from django.db.models.related import PathInfo
@@ -243,8 +243,10 @@ class GenericRelation(ForeignObject):
    def __init__(self, to, **kwargs):
        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
        kwargs['rel'] = GenericRel(
            self, to, related_name=kwargs.pop('related_name', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None),)
            self, to,
            related_query_name=kwargs.pop('related_query_name', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None),
        )
        # Override content-type/object-id field names on the related class
        self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
        self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
@@ -300,11 +302,16 @@ class GenericRelation(ForeignObject):
        return [(self.rel.to._meta.get_field_by_name(self.object_id_field_name)[0],
                 self.model._meta.pk)]

    def get_reverse_path_info(self):
    def get_path_info(self):
        opts = self.rel.to._meta
        target = opts.get_field_by_name(self.object_id_field_name)[0]
        return [PathInfo(self.model._meta, opts, (target,), self.rel, True, False)]

    def get_reverse_path_info(self):
        opts = self.model._meta
        from_opts = self.rel.to._meta
        return [PathInfo(from_opts, opts, (opts.pk,), self, not self.unique, False)]

    def get_choices_default(self):
        return super(GenericRelation, self).get_choices(include_blank=False)

@@ -312,13 +319,6 @@ class GenericRelation(ForeignObject):
        qs = getattr(obj, self.name).all()
        return smart_text([instance._get_pk_val() for instance in qs])

    def get_joining_columns(self, reverse_join=False):
        if not reverse_join:
            # This error message is meant for the user, and from user
            # perspective this is a reverse join along the GenericRelation.
            raise ValueError('Joining in reverse direction not allowed.')
        return super(GenericRelation, self).get_joining_columns(reverse_join)

    def contribute_to_class(self, cls, name):
        super(GenericRelation, self).contribute_to_class(cls, name, virtual_only=True)
        # Save a reference to which model this class is on for future use
@@ -326,9 +326,6 @@ class GenericRelation(ForeignObject):
        # Add the descriptor for the relation
        setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model))

    def contribute_to_related_class(self, cls, related):
        pass

    def set_attributes_from_rel(self):
        pass

@@ -527,5 +524,7 @@ def create_generic_related_manager(superclass):


class GenericRel(ForeignObjectRel):
    def __init__(self, field, to, related_name=None, limit_choices_to=None):
        super(GenericRel, self).__init__(field, to, related_name, limit_choices_to)
    def __init__(self, field, to, related_name=None, limit_choices_to=None, related_query_name=None):
        super(GenericRel, self).__init__(field=field, to=to, related_name=related_query_name or '+',
                                         limit_choices_to=limit_choices_to, on_delete=DO_NOTHING,
                                         related_query_name=related_query_name)
+4 −5
Original line number Diff line number Diff line
@@ -449,9 +449,7 @@ class Options(object):
        for f, model in self.get_fields_with_model():
            cache[f.name] = cache[f.attname] = (f, model, True, False)
        for f in self.virtual_fields:
            if hasattr(f, 'related'):
                cache[f.name] = cache[f.attname] = (
                    f.related, None if f.model == self.model else f.model, True, False)
            cache[f.name] = (f, None if f.model == self.model else f.model, True, False)
        if apps.ready:
            self._name_map = cache
        return cache
@@ -530,8 +528,9 @@ class Options(object):
        proxy_cache = cache.copy()
        for klass in self.apps.get_models(include_auto_created=True):
            if not klass._meta.swapped:
                for f in klass._meta.local_fields:
                    if f.rel and not isinstance(f.rel.to, six.string_types) and f.generate_reverse_relation:
                for f in klass._meta.local_fields + klass._meta.virtual_fields:
                    if (hasattr(f, 'rel') and f.rel and not isinstance(f.rel.to, six.string_types)
                            and f.generate_reverse_relation):
                        if self == f.rel.to._meta:
                            cache[f.related] = None
                            proxy_cache[f.related] = None
+23 −0
Original line number Diff line number Diff line
@@ -373,6 +373,15 @@ Reverse generic relations

        This class used to be defined in ``django.contrib.contenttypes.generic``.

    .. attribute:: related_query_name

        .. versionadded:: 1.7

        The relation on the related object back to this object doesn't exist by
        default. Setting ``related_query_name`` creates a relation from the
        related object back to this one. This allows querying and filtering
        from the related object.

If you know which models you'll be using most often, you can also add
a "reverse" generic relationship to enable an additional API. For example::

@@ -392,6 +401,20 @@ be used to retrieve their associated ``TaggedItems``::
    >>> b.tags.all()
    [<TaggedItem: django>, <TaggedItem: python>]

.. versionadded:: 1.7

Defining :class:`~django.contrib.contenttypes.fields.GenericRelation` with
``related_query_name`` set allows querying from the related object::

    tags = GenericRelation(TaggedItem, related_query_name='bookmarks')

This enables filtering, ordering, and other query operations on ``Bookmark``
from ``TaggedItem``::

    >>> # Get all tags belonging to books containing `django` in the url
    >>> TaggedItem.objects.filter(bookmarks__url__contains='django')
    [<TaggedItem: django>, <TaggedItem: python>]

Just as :class:`~django.contrib.contenttypes.fields.GenericForeignKey`
accepts the names of the content-type and object-ID fields as
arguments, so too does
+5 −0
Original line number Diff line number Diff line
@@ -1165,6 +1165,11 @@ Miscellaneous
* The ``shortcut`` view in ``django.contrib.contenttypes.views`` now supports
  protocol-relative URLs (e.g. ``//example.com``).

* :class:`~django.contrib.contenttypes.fields.GenericRelation` now supports an
  optional ``related_query_name`` argument. Setting ``related_query_name`` adds
  a relation from the related object back to the content type for filtering,
  ordering and other query operations.

.. _deprecated-features-1.7:

Features deprecated in 1.7
Loading