Commit c81d6935 authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #2217 -- Allowed raw objects to be used in __exact and __in query terms....

Fixed #2217 -- Allowed raw objects to be used in __exact and __in query terms. Existing use of primary keys in query terms is preserved.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@3246 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 0ad88636
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -78,6 +78,32 @@ class RelatedField(object):
        related = RelatedObject(other, cls, self)
        self.contribute_to_related_class(other, related)

    def get_db_prep_lookup(self, lookup_type, value):
        # If we are doing a lookup on a Related Field, we must be 
        # comparing object instances. The value should be the PK of value, 
        # not value itself.
        def pk_trace(value):
            # Value may be a primary key, or an object held in a relation. 
            # If it is an object, then we need to get the primary key value for
            # that object. In certain conditions (especially one-to-one relations),
            # the primary key may itself be an object - so we need to keep drilling
            # down until we hit a value that can be used for a comparison.
            v = value
            try:
                while True:
                    v = getattr(v, v._meta.pk.name)
            except AttributeError:
                pass
            return v 
            
        if lookup_type == 'exact':
            return [pk_trace(value)]
        if lookup_type == 'in':
            return [pk_trace(v) for v in value]
        elif lookup_type == 'isnull':
            return []
        raise TypeError, "Related Field has invalid lookup: %s" % lookup_type
            
    def _get_related_query_name(self, opts):
        # This method defines the name that can be used to identify this related object
        # in a table-spanning query. It uses the lower-cased object_name by default,
+39 −9
Original line number Diff line number Diff line
@@ -841,12 +841,14 @@ def lookup_inner(path, clause, value, opts, table, column):
        )

    if path:
        # There are elements left in the path. More joins are required.
        if len(path) == 1 and path[0] in (new_opts.pk.name, None) \
            and clause in ('exact', 'isnull') and not join_required:
            # If the last name query is for a key, and the search is for
            # isnull/exact, then the current (for N-1) or intermediate
            # (for N-N) table can be used for the search - no need to join an
            # extra table just to check the primary key.
            # If the next and final name query is for a primary key, 
            # and the search is for isnull/exact, then the current 
            # (for N-1) or intermediate (for N-N) table can be used 
            # for the search - no need to join an extra table just 
            # to check the primary key.
            new_table = current_table
        else:
            # There are 1 or more name queries pending, and we have ruled out
@@ -872,13 +874,41 @@ def lookup_inner(path, clause, value, opts, table, column):
        where.extend(where2)
        params.extend(params2)
    else:
        # Evaluate clause on current table.
        if name in (current_opts.pk.name, None) and clause in ('exact', 'isnull') and current_column:
            # If this is an exact/isnull key search, and the last pass
            # found/introduced a current/intermediate table that we can use to
            # optimize the query, then use that column name.
        # No elements left in path. Current element is the element on which 
        # the search is being performed. 
        
        if join_required:
            # Last query term is a RelatedObject 
            if field.field.rel.multiple:    
                # RelatedObject is from a 1-N relation.
                # Join is required; query operates on joined table.
                column = new_opts.pk.name            
                joins[backend.quote_name(new_table)] = (
                    backend.quote_name(new_opts.db_table),
                    "INNER JOIN",
                    "%s.%s = %s.%s" %
                        (backend.quote_name(current_table),
                        backend.quote_name(join_column),
                        backend.quote_name(new_table),
                        backend.quote_name(new_column))
                )
                current_table = new_table
            else:
                # RelatedObject is from a 1-1 relation, 
                # No need to join; get the pk value from the related object, 
                # and compare using that.
                column = current_opts.pk.name
        elif intermediate_table: 
            # Last query term is a related object from an N-N relation.
            # Join from intermediate table is sufficient.
            column = join_column
        elif name == current_opts.pk.name and clause in ('exact', 'isnull') and current_column:
            # Last query term is for a primary key. If previous iterations 
            # introduced a current/intermediate table that can be used to
            # optimize the query, then use that table and column name.
            column = current_column
        else:
            # Last query term was a normal field.
            column = field.column

        where.append(get_where_clause(clause, current_table + '.', column, value))
+4 −0
Original line number Diff line number Diff line
@@ -70,6 +70,10 @@ class RelatedObject(object):
        else:
            return [None] * self.field.rel.num_in_admin

    def get_db_prep_lookup(self, lookup_type, value):
        # Defer to the actual field definition for db prep
        return self.field.get_db_prep_lookup(lookup_type, value)
        
    def editable_fields(self):
        "Get the fields in this class that should be edited inline."
        return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field]
+22 −1
Original line number Diff line number Diff line
@@ -75,6 +75,10 @@ API_TESTS = """
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]
>>> Article.objects.filter(publications__pk=1)
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]
>>> Article.objects.filter(publications=1)
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]
>>> Article.objects.filter(publications=p1)
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]

>>> Article.objects.filter(publications__title__startswith="Science")
[<Article: NASA uses Python>, <Article: NASA uses Python>]
@@ -89,6 +93,13 @@ API_TESTS = """
>>> Article.objects.filter(publications__title__startswith="Science").distinct().count()
1

>>> Article.objects.filter(publications__in=[1,2]).distinct()
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]
>>> Article.objects.filter(publications__in=[1,p2]).distinct()
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]
>>> Article.objects.filter(publications__in=[p1,p2]).distinct()
[<Article: Django lets you build Web apps easily>, <Article: NASA uses Python>]

# Reverse m2m queries are supported (i.e., starting at the table that doesn't
# have a ManyToManyField).
>>> Publication.objects.filter(id__exact=1)
@@ -101,9 +112,19 @@ API_TESTS = """

>>> Publication.objects.filter(article__id__exact=1)
[<Publication: The Python Journal>]

>>> Publication.objects.filter(article__pk=1)
[<Publication: The Python Journal>]
>>> Publication.objects.filter(article=1)
[<Publication: The Python Journal>]
>>> Publication.objects.filter(article=a1)
[<Publication: The Python Journal>]

>>> Publication.objects.filter(article__in=[1,2]).distinct()
[<Publication: Highlights for Children>, <Publication: Science News>, <Publication: Science Weekly>, <Publication: The Python Journal>]
>>> Publication.objects.filter(article__in=[1,a2]).distinct()
[<Publication: Highlights for Children>, <Publication: Science News>, <Publication: Science Weekly>, <Publication: The Python Journal>]
>>> Publication.objects.filter(article__in=[a1,a2]).distinct()
[<Publication: Highlights for Children>, <Publication: Science News>, <Publication: Science Weekly>, <Publication: The Python Journal>]

# If we delete a Publication, its Articles won't be able to access it.
>>> p1.delete()
+24 −4
Original line number Diff line number Diff line
@@ -151,10 +151,20 @@ False
[<Article: John's second story>, <Article: This is a test>]

# Find all Articles for the Reporter whose ID is 1.
# Use direct ID check, pk check, and object comparison 
>>> Article.objects.filter(reporter__id__exact=1)
[<Article: John's second story>, <Article: This is a test>]
>>> Article.objects.filter(reporter__pk=1)
[<Article: John's second story>, <Article: This is a test>]
>>> Article.objects.filter(reporter=1)
[<Article: John's second story>, <Article: This is a test>]
>>> Article.objects.filter(reporter=r)
[<Article: John's second story>, <Article: This is a test>]

>>> Article.objects.filter(reporter__in=[1,2]).distinct()
[<Article: John's second story>, <Article: Paul's story>, <Article: This is a test>]
>>> Article.objects.filter(reporter__in=[r,r2]).distinct()
[<Article: John's second story>, <Article: Paul's story>, <Article: This is a test>]

# You need two underscores between "reporter" and "id" -- not one.
>>> Article.objects.filter(reporter_id__exact=1)
@@ -168,10 +178,6 @@ Traceback (most recent call last):
    ...
TypeError: Cannot resolve keyword 'reporter_id' into field

# "pk" shortcut syntax works in a related context, too.
>>> Article.objects.filter(reporter__pk=1)
[<Article: John's second story>, <Article: This is a test>]

# You can also instantiate an Article by passing
# the Reporter's ID instead of a Reporter object.
>>> a3 = Article(id=None, headline="This is a test", pub_date=datetime(2005, 7, 27), reporter_id=r.id)
@@ -200,6 +206,18 @@ TypeError: Cannot resolve keyword 'reporter_id' into field
[<Reporter: John Smith>]
>>> Reporter.objects.filter(article__pk=1)
[<Reporter: John Smith>]
>>> Reporter.objects.filter(article=1)
[<Reporter: John Smith>]
>>> Reporter.objects.filter(article=a)
[<Reporter: John Smith>]

>>> Reporter.objects.filter(article__in=[1,4]).distinct()
[<Reporter: John Smith>]
>>> Reporter.objects.filter(article__in=[1,a3]).distinct()
[<Reporter: John Smith>]
>>> Reporter.objects.filter(article__in=[a,a3]).distinct()
[<Reporter: John Smith>]

>>> Reporter.objects.filter(article__headline__startswith='This')
[<Reporter: John Smith>, <Reporter: John Smith>, <Reporter: John Smith>]
>>> Reporter.objects.filter(article__headline__startswith='This').distinct()
@@ -216,6 +234,8 @@ TypeError: Cannot resolve keyword 'reporter_id' into field
[<Reporter: John Smith>, <Reporter: John Smith>, <Reporter: John Smith>, <Reporter: John Smith>]
>>> Reporter.objects.filter(article__reporter__first_name__startswith='John').distinct()
[<Reporter: John Smith>]
>>> Reporter.objects.filter(article__reporter__exact=r).distinct()
[<Reporter: John Smith>]

# If you delete a reporter, his articles will be deleted.
>>> Article.objects.all()
Loading