mirror of https://github.com/django/django.git
Improvements to [8608] to fix an infinite loop (for exclude(generic_relation)).
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
This commit is contained in:
parent
1abfb1df19
commit
4cd03ef5d9
|
@ -157,15 +157,18 @@ class GenericRelation(RelatedField, Field):
|
||||||
# same db_type as well.
|
# same db_type as well.
|
||||||
return None
|
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
|
Return an extra filter to the queryset so that the results are filtered
|
||||||
on the appropriate content type.
|
on the appropriate content type.
|
||||||
"""
|
"""
|
||||||
|
if negate:
|
||||||
|
return []
|
||||||
ContentType = get_model("contenttypes", "contenttype")
|
ContentType = get_model("contenttypes", "contenttype")
|
||||||
content_type = ContentType.objects.get_for_model(self.model)
|
content_type = ContentType.objects.get_for_model(self.model)
|
||||||
prefix = "__".join(pieces[:pos + 1])
|
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):
|
class ReverseGenericRelatedObjectsDescriptor(object):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1058,9 +1058,11 @@ class Query(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
field, target, opts, join_list, last, extra_filters = self.setup_joins(
|
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:
|
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
|
return
|
||||||
final = len(join_list)
|
final = len(join_list)
|
||||||
penultimate = last.pop()
|
penultimate = last.pop()
|
||||||
|
@ -1196,7 +1198,8 @@ class Query(object):
|
||||||
self.used_aliases = used_aliases
|
self.used_aliases = used_aliases
|
||||||
|
|
||||||
def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
|
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
|
Compute the necessary table joins for the passage through the fields
|
||||||
given in 'names'. 'opts' is the Options class for the current model
|
given in 'names'. 'opts' is the Options class for the current model
|
||||||
|
@ -1205,7 +1208,10 @@ class Query(object):
|
||||||
many-to-one joins will always create a new alias (necessary for
|
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
|
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
|
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
|
Returns the final field involved in the join, the target database
|
||||||
column (used for any 'where' constraint), the final 'opts' value and the
|
column (used for any 'where' constraint), the final 'opts' value and the
|
||||||
|
@ -1271,8 +1277,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'):
|
if process_extras and hasattr(field, 'extra_filters'):
|
||||||
extra_filters.append(field.extra_filters(names, pos))
|
extra_filters.extend(field.extra_filters(names, pos, negate))
|
||||||
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.
|
||||||
|
@ -1295,10 +1301,15 @@ class Query(object):
|
||||||
int_alias = self.join((alias, table1, from_col1, to_col1),
|
int_alias = self.join((alias, table1, from_col1, to_col1),
|
||||||
dupe_multis, exclusions, nullable=True,
|
dupe_multis, exclusions, nullable=True,
|
||||||
reuse=can_reuse)
|
reuse=can_reuse)
|
||||||
alias = self.join((int_alias, table2, from_col2, to_col2),
|
if int_alias == table2 and from_col2 == to_col2:
|
||||||
dupe_multis, exclusions, nullable=True,
|
joins.append(int_alias)
|
||||||
reuse=can_reuse)
|
alias = int_alias
|
||||||
joins.extend([int_alias, 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:
|
elif field.rel:
|
||||||
# One-to-one or many-to-one field
|
# One-to-one or many-to-one field
|
||||||
if cached_data:
|
if cached_data:
|
||||||
|
@ -1391,7 +1402,7 @@ class Query(object):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.dupe_avoidance[ident, name] = set([alias])
|
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
|
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
|
to use a subquery. This method constructs the nested query, given the
|
||||||
|
@ -1399,10 +1410,11 @@ class Query(object):
|
||||||
N-to-many relation field.
|
N-to-many relation field.
|
||||||
"""
|
"""
|
||||||
query = Query(self.model, self.connection)
|
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.set_start(prefix)
|
||||||
query.clear_ordering(True)
|
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):
|
def set_limits(self, low=None, high=None):
|
||||||
"""
|
"""
|
||||||
|
@ -1614,6 +1626,7 @@ class Query(object):
|
||||||
alias = self.get_initial_alias()
|
alias = self.get_initial_alias()
|
||||||
field, col, opts, joins, last, extra = 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)
|
||||||
|
self.unref_alias(alias)
|
||||||
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])]
|
||||||
self.select_fields = [field]
|
self.select_fields = [field]
|
||||||
|
|
|
@ -131,8 +131,12 @@ __test__ = {'API_TESTS':"""
|
||||||
[<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.
|
# 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.objects.filter(tags__tag='fatty')
|
||||||
[<Animal: Platypus>]
|
[<Animal: Platypus>]
|
||||||
|
>>> Animal.objects.exclude(tags__tag='fatty')
|
||||||
|
[<Animal: Lion>]
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
Loading…
Reference in New Issue