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:
parent
6e51f05112
commit
d70c8907cd
|
@ -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
|
||||||
|
|
|
@ -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])]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue