Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #10159 -- `F()` expressions now work on geographic fields. The …

…tests are in `relatedapp`, which has been retrofitted to work with Oracle (minus the prior offending tests). I'm back.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@9963 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 9b8c712ca714d218e1ab7414237058d1d0f189ac 1 parent 7b55da0
Justin Bronn authored March 03, 2009
36  django/contrib/gis/db/models/sql/query.py
@@ -278,40 +278,6 @@ def get_select_format(self, fld):
278 278
         return sel_fmt
279 279
 
280 280
     # Private API utilities, subject to change.
281  
-    def _check_geo_field(self, model, name_param):
282  
-        """
283  
-        Recursive utility routine for checking the given name parameter
284  
-        on the given model.  Initially, the name parameter is a string,
285  
-        of the field on the given model e.g., 'point', 'the_geom'.
286  
-        Related model field strings like 'address__point', may also be
287  
-        used.
288  
-
289  
-        If a GeometryField exists according to the given name parameter
290  
-        it will be returned, otherwise returns False.
291  
-        """
292  
-        if isinstance(name_param, basestring):
293  
-            # This takes into account the situation where the name is a
294  
-            # lookup to a related geographic field, e.g., 'address__point'.
295  
-            name_param = name_param.split(sql.constants.LOOKUP_SEP)
296  
-            name_param.reverse() # Reversing so list operates like a queue of related lookups.
297  
-        elif not isinstance(name_param, list):
298  
-            raise TypeError
299  
-        try:
300  
-            # Getting the name of the field for the model (by popping the first
301  
-            # name from the `name_param` list created above).
302  
-            fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop())
303  
-        except (FieldDoesNotExist, IndexError):
304  
-            return False
305  
-        # TODO: ManyToManyField?
306  
-        if isinstance(fld, GeometryField):
307  
-            return fld # A-OK.
308  
-        elif isinstance(fld, ForeignKey):
309  
-            # ForeignKey encountered, return the output of this utility called
310  
-            # on the _related_ model with the remaining name parameters.
311  
-            return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation.
312  
-        else:
313  
-            return False
314  
-
315 281
     def _field_column(self, field, table_alias=None):
316 282
         """
317 283
         Helper function that returns the database column for the given field.
@@ -339,4 +305,4 @@ def _geo_field(self, field_name=None):
339 305
         else:
340 306
             # Otherwise, check by the given field name -- which may be
341 307
             # a lookup to a _related_ geographic field.
342  
-            return self._check_geo_field(self.model, field_name)
  308
+            return GeoWhereNode._check_geo_field(self.model._meta, field_name)
77  django/contrib/gis/db/models/sql/where.py
... ...
@@ -1,7 +1,11 @@
1  
-import datetime
  1
+from django.db import connection
2 2
 from django.db.models.fields import Field
  3
+from django.db.models.sql.constants import LOOKUP_SEP
  4
+from django.db.models.sql.expressions import SQLEvaluator
3 5
 from django.db.models.sql.where import WhereNode
4 6
 from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
  7
+from django.contrib.gis.db.models.fields import GeometryField
  8
+qn = connection.ops.quote_name
5 9
 
6 10
 class GeoAnnotation(object):
7 11
     """
@@ -37,9 +41,35 @@ def add(self, data, connector):
37 41
             # Not a geographic field, so call `WhereNode.add`.
38 42
             return super(GeoWhereNode, self).add(data, connector)
39 43
         else:
40  
-            # `GeometryField.get_db_prep_lookup` returns a where clause
41  
-            # substitution array in addition to the parameters.
42  
-            where, params = field.get_db_prep_lookup(lookup_type, value)
  44
+            if isinstance(value, SQLEvaluator):
  45
+                # Getting the geographic field to compare with from the expression.
  46
+                geo_fld = self._check_geo_field(value.opts, value.expression.name)
  47
+                if not geo_fld:
  48
+                    raise ValueError('No geographic field found in expression.')
  49
+
  50
+                # Get the SRID of the geometry field that the expression was meant 
  51
+                # to operate on -- it's needed to determine whether transformation 
  52
+                # SQL is necessary.
  53
+                srid = geo_fld._srid
  54
+
  55
+                # Getting the quoted representation of the geometry column that
  56
+                # the expression is operating on.
  57
+                geo_col = '%s.%s' % tuple(map(qn, value.cols[value.expression]))
  58
+
  59
+                # If it's in a different SRID, we'll need to wrap in 
  60
+                # transformation SQL.
  61
+                if not srid is None and srid != field._srid and SpatialBackend.transform:
  62
+                    placeholder = '%s(%%s, %s)' % (SpatialBackend.transform, field._srid)
  63
+                else:
  64
+                    placeholder = '%s'
  65
+
  66
+                # Setting these up as if we had called `field.get_db_prep_lookup()`.
  67
+                where =  [placeholder % geo_col]
  68
+                params = ()
  69
+            else:
  70
+                # `GeometryField.get_db_prep_lookup` returns a where clause
  71
+                # substitution array in addition to the parameters.
  72
+                where, params = field.get_db_prep_lookup(lookup_type, value)
43 73
 
44 74
             # The annotation will be a `GeoAnnotation` object that
45 75
             # will contain the necessary geometry field metadata for
@@ -64,3 +94,42 @@ def make_atom(self, child, qn):
64 94
             # If not a GeometryField, call the `make_atom` from the 
65 95
             # base class.
66 96
             return super(GeoWhereNode, self).make_atom(child, qn)
  97
+
  98
+    @classmethod
  99
+    def _check_geo_field(cls, opts, lookup):
  100
+        """
  101
+        Utility for checking the given lookup with the given model options.  
  102
+        The lookup is a string either specifying the geographic field, e.g.
  103
+        'point, 'the_geom', or a related lookup on a geographic field like
  104
+        'address__point'.
  105
+
  106
+        If a GeometryField exists according to the given lookup on the model
  107
+        options, it will be returned.  Otherwise returns None.
  108
+        """
  109
+        # This takes into account the situation where the lookup is a
  110
+        # lookup to a related geographic field, e.g., 'address__point'.
  111
+        field_list = lookup.split(LOOKUP_SEP)
  112
+
  113
+        # Reversing so list operates like a queue of related lookups,
  114
+        # and popping the top lookup.
  115
+        field_list.reverse()
  116
+        fld_name = field_list.pop()
  117
+
  118
+        try:
  119
+            geo_fld = opts.get_field(fld_name)
  120
+            # If the field list is still around, then it means that the
  121
+            # lookup was for a geometry field across a relationship --
  122
+            # thus we keep on getting the related model options and the
  123
+            # model field associated with the next field in the list 
  124
+            # until there's no more left.
  125
+            while len(field_list):
  126
+                opts = geo_fld.rel.to._meta
  127
+                geo_fld = opts.get_field(field_list.pop())
  128
+        except (FieldDoesNotExist, AttributeError):
  129
+            return False
  130
+
  131
+        # Finally, make sure we got a Geographic field and return.
  132
+        if isinstance(geo_fld, GeometryField):
  133
+            return geo_fld
  134
+        else:
  135
+            return False
7  django/contrib/gis/tests/__init__.py
@@ -21,12 +21,7 @@ def geo_suite():
21 21
         'test_measure',
22 22
         ]
23 23
     if HAS_GDAL:
24  
-        if oracle:
25  
-            # TODO: There's a problem with `select_related` and GeoQuerySet on
26  
-            # Oracle -- e.g., GeoModel.objects.distance(geom, field_name='fk__point')
27  
-            # doesn't work so we don't test `relatedapp`.
28  
-            test_models += ['distapp', 'layermap']
29  
-        elif postgis:
  24
+        if oracle or postgis:
30 25
             test_models += ['distapp', 'layermap', 'relatedapp']
31 26
         elif mysql:
32 27
             test_models += ['relatedapp', 'layermap']
11  django/contrib/gis/tests/relatedapp/models.py
@@ -20,3 +20,14 @@ class DirectoryEntry(models.Model):
20 20
     listing_text = models.CharField(max_length=50)
21 21
     location = models.ForeignKey(AugmentedLocation)
22 22
     objects = models.GeoManager()
  23
+
  24
+class Parcel(models.Model):
  25
+    name = models.CharField(max_length=30)
  26
+    city = models.ForeignKey(City)
  27
+    center1 = models.PointField()
  28
+    # Throwing a curveball w/`db_column` here.
  29
+    center2 = models.PointField(srid=2276, db_column='mycenter') 
  30
+    border1 = models.PolygonField()
  31
+    border2 = models.PolygonField(srid=2276)
  32
+    objects = models.GeoManager()
  33
+    def __unicode__(self): return self.name
131  django/contrib/gis/tests/relatedapp/tests.py
... ...
@@ -1,8 +1,10 @@
1 1
 import os, unittest
2 2
 from django.contrib.gis.geos import *
3  
-from django.contrib.gis.tests.utils import no_mysql, postgis
  3
+from django.contrib.gis.db.backend import SpatialBackend
  4
+from django.contrib.gis.db.models import F, Extent, Union
  5
+from django.contrib.gis.tests.utils import no_mysql, no_oracle
4 6
 from django.conf import settings
5  
-from models import City, Location, DirectoryEntry
  7
+from models import City, Location, DirectoryEntry, Parcel
6 8
 
7 9
 cities = (('Aurora', 'TX', -97.516111, 33.058333),
8 10
           ('Roswell', 'NM', -104.528056, 33.387222),
@@ -14,11 +16,10 @@ class RelatedGeoModelTest(unittest.TestCase):
14 16
     def test01_setup(self):
15 17
         "Setting up for related model tests."
16 18
         for name, state, lon, lat in cities:
17  
-            loc = Location(point=Point(lon, lat))
18  
-            loc.save()
19  
-            c = City(name=name, state=state, location=loc)
20  
-            c.save()
21  
-            
  19
+            loc = Location.objects.create(point=Point(lon, lat))
  20
+            c = City.objects.create(name=name, state=state, location=loc)
  21
+
  22
+    @no_oracle # TODO: Fix select_related() problems w/Oracle and pagination.
22 23
     def test02_select_related(self):
23 24
         "Testing `select_related` on geographic models (see #7126)."
24 25
         qs1 = City.objects.all()
@@ -33,28 +34,21 @@ def test02_select_related(self):
33 34
                 self.assertEqual(Point(lon, lat), c.location.point)
34 35
         
35 36
     @no_mysql
  37
+    @no_oracle # Pagination problem is implicated in this test as well.
36 38
     def test03_transform_related(self):
37 39
         "Testing the `transform` GeoQuerySet method on related geographic models."
38 40
         # All the transformations are to state plane coordinate systems using
39 41
         # US Survey Feet (thus a tolerance of 0 implies error w/in 1 survey foot).
40  
-        if postgis:
  42
+        if SpatialBackend.postgis:
41 43
             tol = 3
42  
-            nqueries = 4 # +1 for `postgis_lib_version`
43 44
         else:
44 45
             tol = 0
45  
-            nqueries = 3
46 46
             
47 47
         def check_pnt(ref, pnt):
48 48
             self.assertAlmostEqual(ref.x, pnt.x, tol)
49 49
             self.assertAlmostEqual(ref.y, pnt.y, tol)
50 50
             self.assertEqual(ref.srid, pnt.srid)
51 51
 
52  
-        # Turning on debug so we can manually verify the number of SQL queries issued.
53  
-        # DISABLED: the number of queries count testing mechanism is way too brittle.
54  
-        #dbg = settings.DEBUG
55  
-        #settings.DEBUG = True
56  
-        from django.db import connection
57  
-
58 52
         # Each city transformed to the SRID of their state plane coordinate system.
59 53
         transformed = (('Kecksburg', 2272, 'POINT(1490553.98959621 314792.131023984)'),
60 54
                        ('Roswell', 2257, 'POINT(481902.189077221 868477.766629735)'),
@@ -63,40 +57,111 @@ def check_pnt(ref, pnt):
63 57
 
64 58
         for name, srid, wkt in transformed:
65 59
             # Doing this implicitly sets `select_related` select the location.
  60
+            # TODO: Fix why this breaks on Oracle.
66 61
             qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point'))
67 62
             check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) 
68  
-        #settings.DEBUG= dbg
69  
-
70  
-        # Verifying the number of issued SQL queries.
71  
-        #self.assertEqual(nqueries, len(connection.queries))
72 63
 
73 64
     @no_mysql
74 65
     def test04_related_aggregate(self):
75 66
         "Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models."
76  
-        if postgis:
77  
-            # One for all locations, one that excludes Roswell.
78  
-            all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
79  
-            txpa_extent = (-97.51611328125, 33.0583305358887,-79.4607315063477, 40.1847610473633)
80  
-            e1 = City.objects.extent(field_name='location__point')
81  
-            e2 = City.objects.exclude(name='Roswell').extent(field_name='location__point')
82  
-            for ref, e in [(all_extent, e1), (txpa_extent, e2)]:
83  
-                for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val)
84  
-
85  
-        # The second union is for a query that has something in the WHERE clause.
86  
-        ref_u1 = GEOSGeometry('MULTIPOINT(-104.528056 33.387222,-97.516111 33.058333,-79.460734 40.18476)', 4326)
87  
-        ref_u2 = GEOSGeometry('MULTIPOINT(-97.516111 33.058333,-79.460734 40.18476)', 4326)
  67
+
  68
+        # This combines the Extent and Union aggregates into one query
  69
+        aggs = City.objects.aggregate(Extent('location__point'), Union('location__point'))
  70
+
  71
+        # One for all locations, one that excludes Roswell.
  72
+        all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
  73
+        txpa_extent = (-97.51611328125, 33.0583305358887,-79.4607315063477, 40.1847610473633)
  74
+        e1 = City.objects.extent(field_name='location__point')
  75
+        e2 = City.objects.exclude(name='Roswell').extent(field_name='location__point')
  76
+        e3 = aggs['location__point__extent']
  77
+
  78
+        # The tolerance value is to four decimal places because of differences
  79
+        # between the Oracle and PostGIS spatial backends on the extent calculation.
  80
+        tol = 4
  81
+        for ref, e in [(all_extent, e1), (txpa_extent, e2), (all_extent, e3)]:
  82
+            for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val, tol)
  83
+
  84
+        # These are the points that are components of the aggregate geographic
  85
+        # union that is returned.
  86
+        p1 = Point(-104.528056, 33.387222)
  87
+        p2 = Point(-97.516111, 33.058333)
  88
+        p3 = Point(-79.460734, 40.18476)
  89
+        
  90
+        # Creating the reference union geometry depending on the spatial backend,
  91
+        # as Oracle will have a different internal ordering of the component 
  92
+        # geometries than PostGIS.  The second union aggregate is for a union
  93
+        # query that includes limiting information in the WHERE clause (in other
  94
+        # words a `.filter()` precedes the call to `.unionagg()`).
  95
+        if SpatialBackend.oracle:
  96
+            ref_u1 = MultiPoint(p3, p1, p2, srid=4326)
  97
+            ref_u2 = MultiPoint(p3, p2, srid=4326)
  98
+        else:
  99
+            ref_u1 = MultiPoint(p1, p2, p3, srid=4326)
  100
+            ref_u2 = MultiPoint(p2, p3, srid=4326)
  101
+        
88 102
         u1 = City.objects.unionagg(field_name='location__point')
89 103
         u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point')
  104
+        u3 = aggs['location__point__union']
  105
+
90 106
         self.assertEqual(ref_u1, u1)
91 107
         self.assertEqual(ref_u2, u2)
  108
+        self.assertEqual(ref_u1, u3)
92 109
         
93 110
     def test05_select_related_fk_to_subclass(self):
94 111
         "Testing that calling select_related on a query over a model with an FK to a model subclass works"
95 112
         # Regression test for #9752.
96 113
         l = list(DirectoryEntry.objects.all().select_related())
97 114
 
98  
-    # TODO: Related tests for KML, GML, and distance lookups.
  115
+    def test6_f_expressions(self):
  116
+        "Testing F() expressions on GeometryFields."
  117
+        # Constructing a dummy parcel border and getting the City instance for
  118
+        # assigning the FK.
  119
+        b1 = GEOSGeometry('POLYGON((-97.501205 33.052520,-97.501205 33.052576,-97.501150 33.052576,-97.501150 33.052520,-97.501205 33.052520))', srid=4326)
  120
+        pcity = City.objects.get(name='Aurora')
  121
+
  122
+        # First parcel has incorrect center point that is equal to the City;
  123
+        # it also has a second border that is different from the first as a
  124
+        # 100ft buffer around the City.
  125
+        c1 = pcity.location.point
  126
+        c2 = c1.transform(2276, clone=True)
  127
+        b2 = c2.buffer(100)
  128
+        p1 = Parcel.objects.create(name='P1', city=pcity, center1=c1, center2=c2, border1=b1, border2=b2)
  129
+
  130
+        # Now creating a second Parcel where the borders are the same, just
  131
+        # in different coordinate systems.  The center points are also the
  132
+        # the same (but in different coordinate systems), and this time they
  133
+        # actually correspond to the centroid of the border.
  134
+        c1 = b1.centroid
  135
+        c2 = c1.transform(2276, clone=True)
  136
+        p2 = Parcel.objects.create(name='P2', city=pcity, center1=c1, center2=c2, border1=b1, border2=b1)
  137
+
  138
+        # Should return the second Parcel, which has the center within the
  139
+        # border.
  140
+        qs = Parcel.objects.filter(center1__within=F('border1'))
  141
+        self.assertEqual(1, len(qs))
  142
+        self.assertEqual('P2', qs[0].name)
99 143
         
  144
+        if not SpatialBackend.mysql:
  145
+            # This time center2 is in a different coordinate system and needs
  146
+            # to be wrapped in transformation SQL.
  147
+            qs = Parcel.objects.filter(center2__within=F('border1'))
  148
+            self.assertEqual(1, len(qs))
  149
+            self.assertEqual('P2', qs[0].name)
  150
+        
  151
+        # Should return the first Parcel, which has the center point equal
  152
+        # to the point in the City ForeignKey.
  153
+        qs = Parcel.objects.filter(center1=F('city__location__point'))
  154
+        self.assertEqual(1, len(qs))
  155
+        self.assertEqual('P1', qs[0].name)
  156
+
  157
+        if not SpatialBackend.mysql:
  158
+            # This time the city column should be wrapped in transformation SQL.
  159
+            qs = Parcel.objects.filter(border2__contains=F('city__location__point'))
  160
+            self.assertEqual(1, len(qs))
  161
+            self.assertEqual('P1', qs[0].name)
  162
+
  163
+    # TODO: Related tests for KML, GML, and distance lookups.
  164
+
100 165
 def suite():
101 166
     s = unittest.TestSuite()
102 167
     s.addTest(unittest.makeSuite(RelatedGeoModelTest))

0 notes on commit 9b8c712

Please sign in to comment.
Something went wrong with that request. Please try again.