Ticket #16187: 16187_uqly.diff

File 16187_uqly.diff, 12.4 KB (added by Anssi Kääriäinen, 13 years ago)

An alternate way to resolve the lookup.

  • django/db/models/sql/query.py

    diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
    index 61fd2be..fe59710 100644
    a b from django.core.exceptions import FieldError  
    2626
    2727__all__ = ['Query', 'RawQuery']
    2828
     29FOLLOW_FORWARD = 'FORWARD'
     30FOLLOW_REVERSE = 'REVERSE'
     31
    2932class RawQuery(object):
    3033    """
    3134    A single raw SQL query
    class Query(object):  
    10141017        # Add the aggregate to the query
    10151018        aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary)
    10161019
     1020    def get_field_by_name(self, opts, filter_name, allow_explicit_fk=True):
     1021        """
     1022        A wrapper to opts.get_field_by_name, so that 'foo_id' -> 'foo'
     1023        translation does not clutter the main code. TODO: Move to Options.
     1024        """
     1025        try:
     1026            field, model, direct, m2m = opts.get_field_by_name(filter_name)
     1027        except FieldDoesNotExist:
     1028            for f in opts.fields:
     1029                if allow_explicit_fk and filter_name == f.attname:
     1030                    # XXX: A hack to allow foo_id to work in values() for
     1031                    # backwards compatibility purposes. If we dropped that
     1032                    # feature, this could be removed.
     1033                    field, model, direct, m2m = opts.get_field_by_name(f.name)
     1034                    break
     1035            else:
     1036                names = opts.get_all_field_names() + self.aggregate_select.keys()
     1037                raise FieldError("Cannot resolve keyword %r into field. "
     1038                        "Choices are: %s" % (filter_name, ", ".join(names)))
     1039        return field, model, direct, m2m
     1040
     1041    def filter_chain_to_relations(self, filter_chain):
     1042        """
     1043        Turns the passed in filter_chain (for example [related_model, id])
     1044        into relations and final_field.
     1045
     1046        The relations will be in the format
     1047            (field, direction)
     1048        Direction itself is either FOLLOW_FORWARD or FOLLOW_REVERSE
     1049        The format is slightly inconvenient in that the direct flag informs if
     1050        the field lives in the related model or in the previous model of the
     1051        join chain. The fields in relations list will always be ForeignKeys,
     1052        OneToOneKeys or GenericForeignKeys(?).
     1053
     1054        The final field will be the last field in the chain.
     1055
     1056        Note that one filter_chain part can lead to multiple relations due to
     1057        model inheritance and many_to_many filtering.
     1058        """
     1059        relations, final_field = [], None
     1060        opts = self.model._meta
     1061        field = None
     1062        for pos, filter_name in enumerate(filter_chain):
     1063            if filter_name == 'pk':
     1064                filter_name = opts.pk.name
     1065            try:
     1066                field, model, direct, m2m = self.get_field_by_name(opts, filter_name)
     1067            except FieldError:
     1068                # We have reached the lookup part of the filter chain
     1069                if field is None:
     1070                    # We didn't resolve a single field.
     1071                    raise
     1072                return relations, field, filter_chain[pos:]
     1073            if model:
     1074                # The field lives on a base class of the current model.
     1075                # Skip the chain of proxy to the concrete proxied model
     1076                proxied_model = get_proxied_model(opts)
     1077                # Process the internal one-to-one chain into
     1078                # relations
     1079                for int_model in opts.get_base_chain(model):
     1080                    if int_model is proxied_model:
     1081                        # skip proxy models
     1082                        opts = int_model._meta
     1083                        continue
     1084                    # Note that we do not care that there might be unecessary
     1085                    # relations here (stepping through intermediatry relations to get
     1086                    # to a field in the base chain, even though the field's value is
     1087                    # guaranteed to stay the same throughout the chain). We can prune
     1088                    # them later on if necessary.
     1089                    o2o_field = opts.get_ancestor_link(int_model)
     1090                    relations.append((o2o_field, FOLLOW_FORWARD))
     1091                    opts = int_model._meta
     1092            if m2m and (direct and field.rel.through):
     1093                # For some strange reason GenericRelations pretend to be
     1094                # m2m fields, although they are not. The above field.rel.through
     1095                # check ensures we do not end up here: proper fix - refactor
     1096                # generic relations
     1097                if not direct:
     1098                    # This nice little line fetches related model's field
     1099                    field = field.field
     1100                through_model = field.rel.through
     1101                through_opts = through_model._meta
     1102                through_field1_name = field.m2m_field_name()
     1103                field1, _, _, _ = through_opts.get_field_by_name(through_field1_name)
     1104                through_field2_name = field.m2m_reverse_field_name()
     1105                field2, _, _, _ = through_opts.get_field_by_name(through_field2_name)
     1106                if direct:
     1107                    relations.append((field1, FOLLOW_FORWARD))
     1108                    relations.append((field2, FOLLOW_FORWARD))
     1109                    opts = field.rel.to._meta
     1110                else:
     1111                    relations.append((field2, FOLLOW_REVERSE))
     1112                    relations.append((field1, FOLLOW_REVERSE))
     1113                    opts = field.opts
     1114            else:
     1115                if direct and field.rel:
     1116                    # Local foreign key
     1117                    relations.append((field, FOLLOW_FORWARD))
     1118                    opts = field.rel.to._meta
     1119                elif direct:
     1120                    # This is a local field which is not a relation -> the final field.
     1121                    return relations, field, filter_chain[pos+1:]
     1122                else:
     1123                    # ForeignKey or OneToOneKey defined in the target model.
     1124                    field = field.field
     1125                    relations.append((field, FOLLOW_REVERSE))
     1126                    opts = field.opts
     1127
     1128        # We end up in this case when querying 'related_field=val'
     1129        return relations, field, []
     1130
    10171131    def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
    10181132            can_reuse=None, process_extras=True, force_having=False):
    10191133        """
    class Query(object):  
    10401154        joining process will be processed. This parameter is set to False
    10411155        during the processing of extra filters to avoid infinite recursion.
    10421156        """
    1043         arg, value = filter_expr
    1044         parts = arg.split(LOOKUP_SEP)
     1157        filter_path, value = filter_expr
     1158        parts = filter_path.split(LOOKUP_SEP)
    10451159        if not parts:
    1046             raise FieldError("Cannot parse keyword query %r" % arg)
     1160            raise FieldError("Cannot parse keyword query %r" % filter_path)
     1161       
     1162        aggregate_lookup = None
     1163        lookup_type = 'exact'
     1164        for alias, aggregate in self.aggregates.items():
     1165            if alias == LOOKUP_SEP.join(parts[0:-1]):
     1166                 aggregate_lookup = aggregate
     1167                 lookup_type = parts[-1]
     1168                 # aggregates could support custom lookups, too.
     1169                 if lookup_type not in self.query_terms:
     1170                     raise Exception('TODO: correct exception type and message')
     1171                 break
     1172            if alias == filter_path:
     1173                 aggregate_lookup = aggregate
     1174                 break
     1175
     1176        if not aggregate_lookup:
     1177            _ , final_field, lookup_parts = self.filter_chain_to_relations(parts)
     1178            # we could use the final field and lookup_parts for doing custom lookups
     1179            # not done yet.
     1180            if len(lookup_parts) == 1:
     1181                if lookup_parts[0] not in self.query_terms:
     1182                    raise FieldError(
     1183                        "Join on field %r not permitted. "
     1184                        "Did you misspell %r for the lookup type?"
     1185                        % (final_field.name, lookup_parts[0])
     1186                    )
     1187                lookup_type = lookup_parts[0]
     1188                parts = parts[0:-1]
     1189            if len(lookup_parts) > 1:
     1190                if not hasattr(final_field, 'rel'):
     1191                    raise FieldError("Join on field %r not permitted." % final_field.name)
     1192                else:
     1193                    names = (final_field.rel.to._meta.get_all_field_names() +
     1194                             self.aggregate_select.keys())
     1195                    raise FieldError("Cannot resolve keyword %r into field. "
     1196                          "Choices are: %s" % (lookup_parts[0], ", ".join(names)))
    10471197
    1048         # Work out the lookup type and remove it from 'parts', if necessary.
    1049         if len(parts) == 1 or parts[-1] not in self.query_terms:
     1198        if lookup_type == None:
    10501199            lookup_type = 'exact'
    1051         else:
    1052             lookup_type = parts.pop()
    10531200
    10541201        # By default, this is a WHERE clause. If an aggregate is referenced
    10551202        # in the value, the filter will be promoted to a HAVING
    class Query(object):  
    10691216            value = SQLEvaluator(value, self)
    10701217            having_clause = value.contains_aggregate
    10711218
    1072         for alias, aggregate in self.aggregates.items():
    1073             if alias in (parts[0], LOOKUP_SEP.join(parts)):
    1074                 entry = self.where_class()
    1075                 entry.add((aggregate, lookup_type, value), AND)
    1076                 if negate:
    1077                     entry.negate()
    1078                 self.having.add(entry, connector)
    1079                 return
     1219        if aggregate_lookup:
     1220            entry = self.where_class()
     1221            entry.add((aggregate, lookup_type, value), AND)
     1222            if negate:
     1223                entry.negate()
     1224            self.having.add(entry, connector)
     1225            return
     1226
    10801227
    10811228        opts = self.get_meta()
    10821229        alias = self.get_initial_alias()
  • tests/modeltests/aggregation/tests.py

    diff --git a/tests/modeltests/aggregation/tests.py b/tests/modeltests/aggregation/tests.py
    index 6f68800..7b43464 100644
    a b class BaseAggregateTestCase(TestCase):  
    437437        vals = Author.objects.filter(pk=1).aggregate(Count("friends__id"))
    438438        self.assertEqual(vals, {"friends__id__count": 2})
    439439
    440         books = Book.objects.annotate(num_authors=Count("authors__name")).filter(num_authors__ge=2).order_by("pk")
     440        books = Book.objects.annotate(num_authors=Count("authors__name")).filter(num_authors=2).order_by("pk")
    441441        self.assertQuerysetEqual(
    442442            books, [
    443443                "The Definitive Guide to Django: Web Development Done Right",
  • tests/modeltests/lookup/tests.py

    diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py
    index 33eeae7..6470f2a 100644
    a b class LookupTests(TestCase):  
    469469            self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' "
    470470                             "into field. Choices are: author, headline, id, pub_date, tag")
    471471        try:
     472            Article.objects.filter(author__foo__bar=1)
     473            self.fail('FieldError not raised')
     474        except FieldError, ex:
     475            self.assertEqual(str(ex), "Cannot resolve keyword 'foo' "
     476                             "into field. Choices are: article, id, name")
     477        try:
    472478            Article.objects.filter(headline__starts='Article')
    473479            self.fail('FieldError not raised')
    474480        except FieldError, ex:
    475481            self.assertEqual(str(ex), "Join on field 'headline' not permitted. "
    476482                             "Did you misspell 'starts' for the lookup type?")
    477483
     484
    478485    def test_regex(self):
    479486        # Create some articles with a bit more interesting headlines for testing field lookups:
    480487        for a in Article.objects.all():
  • tests/regressiontests/null_queries/tests.py

    diff --git a/tests/regressiontests/null_queries/tests.py b/tests/regressiontests/null_queries/tests.py
    index ab66ff6..e1b1c98 100644
    a b class NullQueriesTests(TestCase):  
    4040        self.assertRaises(ValueError, Choice.objects.filter, id__gt=None)
    4141
    4242        # Can't use None on anything other than __exact
    43         self.assertRaises(ValueError, Choice.objects.filter, foo__gt=None)
     43        self.assertRaises(FieldError, Choice.objects.filter, foo__gt=None)
    4444
    4545        # Related managers use __exact=None implicitly if the object hasn't been saved.
    4646        p2 = Poll(question="How?")
Back to Top