Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Improvements to [8608] to fix an infinite loop (for exclude(generic_r…

…elation)).

Also comes with approximately 67% less stupidity in the table joins for
filtering on generic relations.

Fixed #5937, hopefully for good, this time.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8644 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 4cd03ef5d985a76236d47c4fd9c041531722e6c0 1 parent 1abfb1d
@malcolmt malcolmt authored
View
7 django/contrib/contenttypes/generic.py
@@ -157,15 +157,18 @@ def db_type(self):
# same db_type as well.
return None
- def extra_filters(self, pieces, pos):
+ def extra_filters(self, pieces, pos, negate):
"""
Return an extra filter to the queryset so that the results are filtered
on the appropriate content type.
"""
+ if negate:
+ return []
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
+ return [("%s__%s" % (prefix, self.content_type_field_name),
+ content_type)]
class ReverseGenericRelatedObjectsDescriptor(object):
"""
View
39 django/db/models/sql/query.py
@@ -1058,9 +1058,11 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
try:
field, target, opts, join_list, last, extra_filters = self.setup_joins(
- parts, opts, alias, True, allow_many, can_reuse=can_reuse)
+ parts, opts, alias, True, allow_many, can_reuse=can_reuse,
+ negate=negate, process_extras=process_extras)
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]),
+ can_reuse)
return
final = len(join_list)
penultimate = last.pop()
@@ -1196,7 +1198,8 @@ def add_q(self, q_object, used_aliases=None):
self.used_aliases = used_aliases
def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
- allow_explicit_fk=False, can_reuse=None):
+ allow_explicit_fk=False, can_reuse=None, negate=False,
+ process_extras=True):
"""
Compute the necessary table joins for the passage through the fields
given in 'names'. 'opts' is the Options class for the current model
@@ -1205,7 +1208,10 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
many-to-one joins will always create a new alias (necessary for
disjunctive filters). If can_reuse is not None, it's a list of aliases
that can be reused in these joins (nothing else can be reused in this
- case).
+ case). Finally, 'negate' is used in the same sense as for add_filter()
+ -- it indicates an exclude() filter, or something similar. It is only
+ passed in here so that it can be passed to a field's extra_filter() for
+ customised behaviour.
Returns the final field involved in the join, the target database
column (used for any 'where' constraint), the final 'opts' value and the
@@ -1271,8 +1277,8 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col),
()))
- if hasattr(field, 'extra_filters'):
- extra_filters.append(field.extra_filters(names, pos))
+ if process_extras and hasattr(field, 'extra_filters'):
+ extra_filters.extend(field.extra_filters(names, pos, negate))
if direct:
if m2m:
# Many-to-many field defined on the current model.
@@ -1295,10 +1301,15 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
int_alias = self.join((alias, table1, from_col1, to_col1),
dupe_multis, exclusions, nullable=True,
reuse=can_reuse)
- alias = self.join((int_alias, table2, from_col2, to_col2),
- dupe_multis, exclusions, nullable=True,
- reuse=can_reuse)
- joins.extend([int_alias, alias])
+ if int_alias == table2 and from_col2 == to_col2:
+ joins.append(int_alias)
+ alias = int_alias
+ else:
+ alias = self.join(
+ (int_alias, table2, from_col2, to_col2),
+ dupe_multis, exclusions, nullable=True,
+ reuse=can_reuse)
+ joins.extend([int_alias, alias])
elif field.rel:
# One-to-one or many-to-one field
if cached_data:
@@ -1391,7 +1402,7 @@ def update_dupe_avoidance(self, opts, col, alias):
except KeyError:
self.dupe_avoidance[ident, name] = set([alias])
- def split_exclude(self, filter_expr, prefix):
+ def split_exclude(self, filter_expr, prefix, can_reuse):
"""
When doing an exclude against any kind of N-to-many relation, we need
to use a subquery. This method constructs the nested query, given the
@@ -1399,10 +1410,11 @@ def split_exclude(self, filter_expr, prefix):
N-to-many relation field.
"""
query = Query(self.model, self.connection)
- query.add_filter(filter_expr)
+ query.add_filter(filter_expr, can_reuse=can_reuse)
query.set_start(prefix)
query.clear_ordering(True)
- self.add_filter(('%s__in' % prefix, query), negate=True, trim=True)
+ self.add_filter(('%s__in' % prefix, query), negate=True, trim=True,
+ can_reuse=can_reuse)
def set_limits(self, low=None, high=None):
"""
@@ -1614,6 +1626,7 @@ def set_start(self, start):
alias = self.get_initial_alias()
field, col, opts, joins, last, extra = self.setup_joins(
start.split(LOOKUP_SEP), opts, alias, False)
+ self.unref_alias(alias)
alias = joins[last[-1]]
self.select = [(alias, self.alias_map[alias][RHS_JOIN_COL])]
self.select_fields = [field]
View
4 tests/modeltests/generic_relations/models.py
@@ -131,8 +131,12 @@ def __unicode__(self):
[<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.order_by('common_name')
+[<Animal: Lion>, <Animal: Platypus>]
>>> Animal.objects.filter(tags__tag='fatty')
[<Animal: Platypus>]
+>>> Animal.objects.exclude(tags__tag='fatty')
+[<Animal: Lion>]
# If you delete an object with an explicit Generic relation, the related
# objects are deleted when the source object is deleted.
Please sign in to comment.
Something went wrong with that request. Please try again.