Commits

Anonymous committed 5c08e4c

Fixed #5937 -- When filtering on generic relations, restrict the target objects to those with the right content type.

This isn't a complete solution to this class of problem, but it will do for
1.0, which only has generic relations as a multicolumn type. A more general
multicolumn solution will be available after that release.

  • Participants
  • Parent commits 2539721

Comments (0)

Files changed (3)

django/contrib/contenttypes/generic.py

         # same db_type as well.
         return None
 
+    def extra_filters(self, pieces, pos):
+        """
+        Return an extra filter to the queryset so that the results are filtered
+        on the appropriate content type.
+        """
+        ContentType = get_model("contenttypes", "contenttype")
+        content_type = ContentType.objects.get_for_model(self.model)
+        prefix = "__".join(pieces[:pos + 1])
+        return "%s__%s" % (prefix, self.content_type_field_name), content_type
+
 class ReverseGenericRelatedObjectsDescriptor(object):
     """
     This class provides the functionality that makes the related-object

django/db/models/sql/query.py

         pieces = name.split(LOOKUP_SEP)
         if not alias:
             alias = self.get_initial_alias()
-        field, target, opts, joins, last = self.setup_joins(pieces, opts,
-                alias, False)
+        field, target, opts, joins, last, extra = self.setup_joins(pieces,
+                opts, alias, False)
         alias = joins[-1]
         col = target.column
         if not field.rel:
                     used, next, restricted, new_nullable, dupe_set, avoid)
 
     def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
-            can_reuse=None):
+            can_reuse=None, process_extras=True):
         """
         Add a single filter to the query. The 'filter_expr' is a pair:
         (filter_string, value). E.g. ('name__contains', 'fred')
         will be a set of table aliases that can be reused in this filter, even
         if we would otherwise force the creation of new aliases for a join
         (needed for nested Q-filters). The set is updated by this method.
+
+        If 'process_extras' is set, any extra filters returned from the table
+        joining process will be processed. This parameter is set to False
+        during the processing of extra filters to avoid infinite recursion.
         """
         arg, value = filter_expr
         parts = arg.split(LOOKUP_SEP)
         allow_many = trim or not negate
 
         try:
-            field, target, opts, join_list, last = self.setup_joins(parts, opts,
-                    alias, True, allow_many, can_reuse=can_reuse)
+            field, target, opts, join_list, last, extra_filters = self.setup_joins(
+                    parts, opts, alias, True, allow_many, can_reuse=can_reuse)
         except MultiJoin, e:
             self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]))
             return
 
         if can_reuse is not None:
             can_reuse.update(join_list)
+        if process_extras:
+            for filter in extra_filters:
+                self.add_filter(filter, negate=negate, can_reuse=can_reuse,
+                        process_extras=False)
 
     def add_q(self, q_object, used_aliases=None):
         """
         last = [0]
         dupe_set = set()
         exclusions = set()
+        extra_filters = []
         for pos, name in enumerate(names):
             try:
                 exclusions.add(int_alias)
                 exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col),
                         ()))
 
+            if hasattr(field, 'extra_filters'):
+                extra_filters.append(field.extra_filters(names, pos))
             if direct:
                 if m2m:
                     # Many-to-many field defined on the current model.
         if pos != len(names) - 1:
             raise FieldError("Join on field %r not permitted." % name)
 
-        return field, target, opts, joins, last
+        return field, target, opts, joins, last, extra_filters
 
     def update_dupe_avoidance(self, opts, col, alias):
         """
         opts = self.get_meta()
         try:
             for name in field_names:
-                field, target, u2, joins, u3 = self.setup_joins(
+                field, target, u2, joins, u3, u4 = self.setup_joins(
                         name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
                         True)
                 final_alias = joins[-1]
         """
         opts = self.model._meta
         alias = self.get_initial_alias()
-        field, col, opts, joins, last = self.setup_joins(
+        field, col, opts, joins, last, extra = self.setup_joins(
                 start.split(LOOKUP_SEP), opts, alias, False)
         alias = joins[last[-1]]
         self.select = [(alias, self.alias_map[alias][RHS_JOIN_COL])]

tests/modeltests/generic_relations/models.py

 >>> eggplant = Vegetable(name="Eggplant", is_yucky=True)
 >>> bacon = Vegetable(name="Bacon", is_yucky=False)
 >>> quartz = Mineral(name="Quartz", hardness=7)
->>> for o in (lion, platypus, eggplant, bacon, quartz):
+>>> for o in (platypus, lion, eggplant, bacon, quartz):
 ...     o.save()
 
 # Objects with declared GenericRelations can be tagged directly -- the API
 <TaggedItem: yellow>
 >>> lion.tags.create(tag="hairy")
 <TaggedItem: hairy>
+>>> platypus.tags.create(tag="fatty")
+<TaggedItem: fatty>
 
 >>> lion.tags.all()
 [<TaggedItem: hairy>, <TaggedItem: yellow>]
 >>> tag1.content_object = platypus
 >>> tag1.save()
 >>> platypus.tags.all()
-[<TaggedItem: shiny>]
+[<TaggedItem: fatty>, <TaggedItem: shiny>]
 >>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
 [<TaggedItem: clearish>]
 
+# Queries across generic relations respect the content types. Even though there are two TaggedItems with a tag of "fatty", this query only pulls out the one with the content type related to Animals.
+>>> Animal.objects.filter(tags__tag='fatty')
+[<Animal: Platypus>]
+
 # If you delete an object with an explicit Generic relation, the related
 # objects are deleted when the source object is deleted.
 # Original list of tags:
 >>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
-[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: vegetable>, 2), (u'hairy', <ContentType: animal>, 1), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 2), (u'yellow', <ContentType: animal>, 1)]
+[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: vegetable>, 2), (u'fatty', <ContentType: animal>, 1), (u'hairy', <ContentType: animal>, 2), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 1), (u'yellow', <ContentType: animal>, 2)]
 
 >>> lion.delete()
 >>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
-[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: vegetable>, 2), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 2)]
+[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: vegetable>, 2), (u'fatty', <ContentType: animal>, 1), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 1)]
 
 # If Generic Relation is not explicitly defined, any related objects
 # remain after deletion of the source object.
 >>> quartz.delete()
 >>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
-[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: vegetable>, 2), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 2)]
+[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: vegetable>, 2), (u'fatty', <ContentType: animal>, 1), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 1)]
 
 # If you delete a tag, the objects using the tag are unaffected
 # (other than losing a tag)
 >>> bacon.tags.all()
 [<TaggedItem: salty>]
 >>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
-[(u'clearish', <ContentType: mineral>, 1), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 2)]
+[(u'clearish', <ContentType: mineral>, 1), (u'fatty', <ContentType: animal>, 1), (u'salty', <ContentType: vegetable>, 2), (u'shiny', <ContentType: animal>, 1)]
+
+>>> TaggedItem.objects.filter(tag='fatty').delete()
 
 >>> ctype = ContentType.objects.get_for_model(lion)
 >>> Animal.objects.filter(tags__content_type=ctype)
 >>> Comparison.objects.all()
 [<Comparison: tiger is stronger than None>]
 
+
 # GenericInlineFormSet tests ##################################################
 
 >>> from django.contrib.contenttypes.generic import generic_inlineformset_factory
 >>> for form in formset.forms:
 ...     print form.as_p()
 <p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" value="shiny" maxlength="50" /></p>
-<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" value="5" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" value="..." id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>
 <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p>
 <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>