Author: mtredinnick
Date: 2008-08-27 00:22:33 -0500 (Wed, 27 Aug 2008)
New Revision: 8608
Modified:
django/trunk/django/contrib/contenttypes/generic.py
django/trunk/django/db/models/sql/query.py
django/trunk/tests/modeltests/generic_relations/models.py
Log:
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.
Modified: django/trunk/django/contrib/contenttypes/generic.py
===================================================================
--- django/trunk/django/contrib/contenttypes/generic.py 2008-08-27 05:22:25 UTC
(rev 8607)
+++ django/trunk/django/contrib/contenttypes/generic.py 2008-08-27 05:22:33 UTC
(rev 8608)
@@ -166,6 +166,16 @@
# 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
Modified: django/trunk/django/db/models/sql/query.py
===================================================================
--- django/trunk/django/db/models/sql/query.py 2008-08-27 05:22:25 UTC (rev
8607)
+++ django/trunk/django/db/models/sql/query.py 2008-08-27 05:22:33 UTC (rev
8608)
@@ -647,8 +647,8 @@
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:
@@ -1006,7 +1006,7 @@
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')
@@ -1026,6 +1026,10 @@
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)
@@ -1053,8 +1057,8 @@
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
@@ -1152,6 +1156,10 @@
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):
"""
@@ -1207,6 +1215,7 @@
last = [0]
dupe_set = set()
exclusions = set()
+ extra_filters = []
for pos, name in enumerate(names):
try:
exclusions.add(int_alias)
@@ -1262,6 +1271,8 @@
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.
@@ -1365,7 +1376,7 @@
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):
"""
@@ -1437,7 +1448,7 @@
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]
@@ -1601,7 +1612,7 @@
"""
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])]
Modified: django/trunk/tests/modeltests/generic_relations/models.py
===================================================================
--- django/trunk/tests/modeltests/generic_relations/models.py 2008-08-27
05:22:25 UTC (rev 8607)
+++ django/trunk/tests/modeltests/generic_relations/models.py 2008-08-27
05:22:33 UTC (rev 8608)
@@ -82,7 +82,7 @@
>>> 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
@@ -95,6 +95,8 @@
<TaggedItem: yellow>
>>> lion.tags.create(tag="hairy")
<TaggedItem: hairy>
+>>> platypus.tags.create(tag="fatty")
+<TaggedItem: fatty>
>>> lion.tags.all()
[<TaggedItem: hairy>, <TaggedItem: yellow>]
@@ -124,25 +126,29 @@
>>> 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)
@@ -151,8 +157,10 @@
>>> 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)
[<Animal: Platypus>]
@@ -192,6 +200,7 @@
>>> Comparison.objects.all()
[<Comparison: tiger is stronger than None>]
+
# GenericInlineFormSet tests ##################################################
>>> from django.contrib.contenttypes.generic import
>>> generic_inlineformset_factory
@@ -207,7 +216,7 @@
>>> 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>
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---