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.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8608 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-08-27 05:22:33 +00:00
parent 6e51f05112
commit d70c8907cd
3 changed files with 45 additions and 15 deletions

View File

@ -166,6 +166,16 @@ class GenericRelation(RelatedField, Field):
# same db_type as well. # same db_type as well.
return None 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): class ReverseGenericRelatedObjectsDescriptor(object):
""" """
This class provides the functionality that makes the related-object This class provides the functionality that makes the related-object

View File

@ -647,8 +647,8 @@ class Query(object):
pieces = name.split(LOOKUP_SEP) pieces = name.split(LOOKUP_SEP)
if not alias: if not alias:
alias = self.get_initial_alias() alias = self.get_initial_alias()
field, target, opts, joins, last = self.setup_joins(pieces, opts, field, target, opts, joins, last, extra = self.setup_joins(pieces,
alias, False) opts, alias, False)
alias = joins[-1] alias = joins[-1]
col = target.column col = target.column
if not field.rel: if not field.rel:
@ -1006,7 +1006,7 @@ class Query(object):
used, next, restricted, new_nullable, dupe_set, avoid) used, next, restricted, new_nullable, dupe_set, avoid)
def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, 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: Add a single filter to the query. The 'filter_expr' is a pair:
(filter_string, value). E.g. ('name__contains', 'fred') (filter_string, value). E.g. ('name__contains', 'fred')
@ -1026,6 +1026,10 @@ class Query(object):
will be a set of table aliases that can be reused in this filter, even 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 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. (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 arg, value = filter_expr
parts = arg.split(LOOKUP_SEP) parts = arg.split(LOOKUP_SEP)
@ -1053,8 +1057,8 @@ class Query(object):
allow_many = trim or not negate allow_many = trim or not negate
try: try:
field, target, opts, join_list, last = self.setup_joins(parts, opts, field, target, opts, join_list, last, extra_filters = self.setup_joins(
alias, True, allow_many, can_reuse=can_reuse) parts, opts, alias, True, allow_many, can_reuse=can_reuse)
except MultiJoin, e: except MultiJoin, e:
self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level])) self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]))
return return
@ -1152,6 +1156,10 @@ class Query(object):
if can_reuse is not None: if can_reuse is not None:
can_reuse.update(join_list) 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): def add_q(self, q_object, used_aliases=None):
""" """
@ -1207,6 +1215,7 @@ class Query(object):
last = [0] last = [0]
dupe_set = set() dupe_set = set()
exclusions = set() exclusions = set()
extra_filters = []
for pos, name in enumerate(names): for pos, name in enumerate(names):
try: try:
exclusions.add(int_alias) exclusions.add(int_alias)
@ -1262,6 +1271,8 @@ class Query(object):
exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col), 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 direct:
if m2m: if m2m:
# Many-to-many field defined on the current model. # Many-to-many field defined on the current model.
@ -1365,7 +1376,7 @@ class Query(object):
if pos != len(names) - 1: if pos != len(names) - 1:
raise FieldError("Join on field %r not permitted." % name) 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): def update_dupe_avoidance(self, opts, col, alias):
""" """
@ -1437,7 +1448,7 @@ class Query(object):
opts = self.get_meta() opts = self.get_meta()
try: try:
for name in field_names: 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, name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
True) True)
final_alias = joins[-1] final_alias = joins[-1]
@ -1601,7 +1612,7 @@ class Query(object):
""" """
opts = self.model._meta opts = self.model._meta
alias = self.get_initial_alias() 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) start.split(LOOKUP_SEP), opts, alias, False)
alias = joins[last[-1]] alias = joins[last[-1]]
self.select = [(alias, self.alias_map[alias][RHS_JOIN_COL])] self.select = [(alias, self.alias_map[alias][RHS_JOIN_COL])]

View File

@ -82,7 +82,7 @@ __test__ = {'API_TESTS':"""
>>> eggplant = Vegetable(name="Eggplant", is_yucky=True) >>> eggplant = Vegetable(name="Eggplant", is_yucky=True)
>>> bacon = Vegetable(name="Bacon", is_yucky=False) >>> bacon = Vegetable(name="Bacon", is_yucky=False)
>>> quartz = Mineral(name="Quartz", hardness=7) >>> quartz = Mineral(name="Quartz", hardness=7)
>>> for o in (lion, platypus, eggplant, bacon, quartz): >>> for o in (platypus, lion, eggplant, bacon, quartz):
... o.save() ... o.save()
# Objects with declared GenericRelations can be tagged directly -- the API # Objects with declared GenericRelations can be tagged directly -- the API
@ -95,6 +95,8 @@ __test__ = {'API_TESTS':"""
<TaggedItem: yellow> <TaggedItem: yellow>
>>> lion.tags.create(tag="hairy") >>> lion.tags.create(tag="hairy")
<TaggedItem: hairy> <TaggedItem: hairy>
>>> platypus.tags.create(tag="fatty")
<TaggedItem: fatty>
>>> lion.tags.all() >>> lion.tags.all()
[<TaggedItem: hairy>, <TaggedItem: yellow>] [<TaggedItem: hairy>, <TaggedItem: yellow>]
@ -124,25 +126,29 @@ __test__ = {'API_TESTS':"""
>>> tag1.content_object = platypus >>> tag1.content_object = platypus
>>> tag1.save() >>> tag1.save()
>>> platypus.tags.all() >>> platypus.tags.all()
[<TaggedItem: shiny>] [<TaggedItem: fatty>, <TaggedItem: shiny>]
>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id) >>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
[<TaggedItem: clearish>] [<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 # If you delete an object with an explicit Generic relation, the related
# objects are deleted when the source object is deleted. # objects are deleted when the source object is deleted.
# Original list of tags: # Original list of tags:
>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()] >>> [(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() >>> lion.delete()
>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()] >>> [(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 # If Generic Relation is not explicitly defined, any related objects
# remain after deletion of the source object. # remain after deletion of the source object.
>>> quartz.delete() >>> quartz.delete()
>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()] >>> [(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 # If you delete a tag, the objects using the tag are unaffected
# (other than losing a tag) # (other than losing a tag)
@ -151,7 +157,9 @@ __test__ = {'API_TESTS':"""
>>> bacon.tags.all() >>> bacon.tags.all()
[<TaggedItem: salty>] [<TaggedItem: salty>]
>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()] >>> [(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) >>> ctype = ContentType.objects.get_for_model(lion)
>>> Animal.objects.filter(tags__content_type=ctype) >>> Animal.objects.filter(tags__content_type=ctype)
@ -192,6 +200,7 @@ __test__ = {'API_TESTS':"""
>>> Comparison.objects.all() >>> Comparison.objects.all()
[<Comparison: tiger is stronger than None>] [<Comparison: tiger is stronger than None>]
# GenericInlineFormSet tests ################################################## # GenericInlineFormSet tests ##################################################
>>> from django.contrib.contenttypes.generic import generic_inlineformset_factory >>> from django.contrib.contenttypes.generic import generic_inlineformset_factory
@ -207,7 +216,7 @@ __test__ = {'API_TESTS':"""
>>> for form in formset.forms: >>> for form in formset.forms:
... print form.as_p() ... 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-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-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> <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>