From a7d964ab87ad7352af3e33b8f3c12e4643a80f02 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 19 Aug 2014 18:47:23 +0200 Subject: [PATCH] Replaced no_spatialite by connection features Refs #22632. Thanks Tim Graham for the review. --- django/contrib/gis/db/backends/base.py | 48 +++++++++++++++++++ django/contrib/gis/db/backends/mysql/base.py | 1 + .../contrib/gis/db/backends/postgis/base.py | 2 +- .../gis/db/backends/spatialite/base.py | 4 +- .../gis/db/backends/spatialite/operations.py | 19 +++++++- django/contrib/gis/tests/distapp/tests.py | 8 ++-- django/contrib/gis/tests/geoapp/models.py | 17 ++++--- .../contrib/gis/tests/geoapp/test_regress.py | 4 +- django/contrib/gis/tests/geoapp/tests.py | 46 +++++------------- django/contrib/gis/tests/relatedapp/tests.py | 9 ++-- django/contrib/gis/tests/utils.py | 3 -- docs/ref/contrib/gis/db-api.txt | 4 +- docs/releases/1.8.txt | 3 ++ 13 files changed, 105 insertions(+), 63 deletions(-) diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index e281ba073e3..b7dbee49083 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -11,8 +11,56 @@ from django.utils.encoding import python_2_unicode_compatible class BaseSpatialFeatures(object): gis_enabled = True + + # Does the database contain a SpatialRefSys model to store SRID information? has_spatialrefsys_table = True + # Can the `distance` GeoQuerySet method be applied on geodetic coordinate systems? + supports_distance_geodetic = True + # Does the database supports `left` and `right` lookups? + supports_left_right_lookups = False + # Is the database able to count vertices on polygons (with `num_points`)? + supports_num_points_poly = True + + # The following properties indicate if the database GIS extensions support + # certain methods (dwithin, force_rhr, geohash, ...) + @property + def has_dwithin_lookup(self): + return 'dwithin' in self.connection.ops.distance_functions + + @property + def has_force_rhr_method(self): + return bool(self.connection.ops.force_rhr) + + @property + def has_geohash_method(self): + return bool(self.connection.ops.geohash) + + @property + def has_make_line_method(self): + return bool(self.connection.ops.make_line) + + @property + def has_perimeter_method(self): + return bool(self.connection.ops.perimeter) + + @property + def has_reverse_method(self): + return bool(self.connection.ops.reverse) + + @property + def has_snap_to_grid_method(self): + return bool(self.connection.ops.snap_to_grid) + + # Specifies whether the Collect and Extent aggregates are supported by the database + @property + def supports_collect_aggr(self): + return 'Collect' in self.connection.ops.valid_aggregates + + @property + def supports_extent_aggr(self): + return 'Extent' in self.connection.ops.valid_aggregates + class BaseSpatialOperations(object): """ diff --git a/django/contrib/gis/db/backends/mysql/base.py b/django/contrib/gis/db/backends/mysql/base.py index 077a8a1dc12..1a1221032b7 100644 --- a/django/contrib/gis/db/backends/mysql/base.py +++ b/django/contrib/gis/db/backends/mysql/base.py @@ -10,6 +10,7 @@ from django.contrib.gis.db.backends.mysql.operations import MySQLOperations class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): has_spatialrefsys_table = False + supports_num_points_poly = False class DatabaseWrapper(MySQLDatabaseWrapper): diff --git a/django/contrib/gis/db/backends/postgis/base.py b/django/contrib/gis/db/backends/postgis/base.py index abeb4556eb3..cb2cbe53aa6 100644 --- a/django/contrib/gis/db/backends/postgis/base.py +++ b/django/contrib/gis/db/backends/postgis/base.py @@ -11,7 +11,7 @@ from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures): - pass + supports_left_right_lookups = True class DatabaseWrapper(Psycopg2DatabaseWrapper): diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py index 3bb949c4aa4..f3c22bdb202 100644 --- a/django/contrib/gis/db/backends/spatialite/base.py +++ b/django/contrib/gis/db/backends/spatialite/base.py @@ -16,7 +16,9 @@ from django.utils import six class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures): - pass + supports_distance_geodetic = False + # SpatiaLite can only count vertices in LineStrings + supports_num_points_poly = False class DatabaseWrapper(SQLiteDatabaseWrapper): diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 70649124137..b0862251eef 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -65,17 +65,25 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): name = 'spatialite' spatialite = True version_regex = re.compile(r'^(?P\d)\.(?P\d)\.(?P\d+)') - valid_aggregates = {'Extent', 'Union'} + + @property + def valid_aggregates(self): + if self.spatial_version >= 3: + return {'Collect', 'Extent', 'Union'} + else: + return {'Union'} Adapter = SpatiaLiteAdapter Adaptor = Adapter # Backwards-compatibility alias. area = 'Area' centroid = 'Centroid' + collect = 'Collect' contained = 'MbrWithin' difference = 'Difference' distance = 'Distance' envelope = 'Envelope' + extent = 'Extent' intersection = 'Intersection' length = 'GLength' # OpenGis defines Length, but this conflicts with an SQLite reserved keyword num_geom = 'NumGeometries' @@ -180,6 +188,15 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): agg_name = aggregate.__class__.__name__ return agg_name in self.valid_aggregates + def convert_extent(self, box): + """ + Convert the polygon data received from Spatialite to min/max values. + """ + shell = Geometry(box).shell + xmin, ymin = shell[0][:2] + xmax, ymax = shell[2][:2] + return (xmin, ymin, xmax, ymax) + def convert_geom(self, wkt, geo_field): """ Converts geometry WKT returned from a SpatiaLite aggregate. diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index 19af28f4e94..348fe6f6ccc 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -7,7 +7,7 @@ from django.db.models import Q from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.measure import D # alias for Distance from django.contrib.gis.tests.utils import ( - mysql, oracle, postgis, spatialite, no_oracle, no_spatialite + mysql, oracle, postgis, spatialite, no_oracle ) from django.test import TestCase, skipUnlessDBFeature @@ -50,7 +50,7 @@ class DistanceTest(TestCase): self.assertEqual(1, Interstate.objects.count()) self.assertEqual(1, SouthTexasInterstate.objects.count()) - @no_spatialite + @skipUnlessDBFeature("has_dwithin_lookup") def test_dwithin(self): """ Test the `dwithin` lookup type. @@ -139,7 +139,7 @@ class DistanceTest(TestCase): self.assertAlmostEqual(m_distances[i], c.distance.m, tol) self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) - @no_spatialite + @skipUnlessDBFeature("supports_distance_geodetic") def test_distance_geodetic(self): """ Test the `distance` GeoQuerySet method on geodetic coordinate systems. @@ -354,7 +354,7 @@ class DistanceTest(TestCase): i10 = SouthTexasInterstate.objects.length().get(name='I-10') self.assertAlmostEqual(len_m2, i10.length.m, 2) - @no_spatialite + @skipUnlessDBFeature("has_perimeter_method") def test_perimeter(self): """ Test the `perimeter` GeoQuerySet method. diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py index 2550854b76a..1e07e00d9dc 100644 --- a/django/contrib/gis/tests/geoapp/models.py +++ b/django/contrib/gis/tests/geoapp/models.py @@ -1,5 +1,5 @@ from django.contrib.gis.db import models -from django.contrib.gis.tests.utils import mysql, spatialite +from django.contrib.gis.tests.utils import mysql from django.utils.encoding import python_2_unicode_compatible # MySQL spatial indices can't handle NULL geometries. @@ -58,15 +58,14 @@ class Truth(models.Model): app_label = 'geoapp' -if not spatialite: +class Feature(NamedModel): + geom = models.GeometryField() - class Feature(NamedModel): - geom = models.GeometryField() - class MinusOneSRID(models.Model): - geom = models.PointField(srid=-1) # Minus one SRID. +class MinusOneSRID(models.Model): + geom = models.PointField(srid=-1) # Minus one SRID. - objects = models.GeoManager() + objects = models.GeoManager() - class Meta: - app_label = 'geoapp' + class Meta: + app_label = 'geoapp' diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index 117c786cddd..c1a17326a25 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from datetime import datetime from django.contrib.gis.geos import HAS_GEOS -from django.contrib.gis.tests.utils import no_mysql, no_spatialite from django.contrib.gis.shortcuts import render_to_kmz from django.db.models import Count, Min from django.test import TestCase, skipUnlessDBFeature @@ -39,8 +38,7 @@ class GeoRegressionTests(TestCase): }] render_to_kmz('gis/kml/placemarks.kml', {'places': places}) - @no_spatialite - @no_mysql + @skipUnlessDBFeature("supports_extent_aggr") def test_extent(self): "Testing `extent` on a table with a single point. See #11827." pnt = City.objects.get(name='Pueblo').point diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 4fdbf8e8bce..085111a138a 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -8,7 +8,7 @@ from django.db import connection from django.contrib.gis import gdal from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import ( - no_mysql, no_oracle, no_spatialite, mysql, oracle, postgis, spatialite) + no_mysql, no_oracle, mysql, oracle, postgis, spatialite) from django.test import TestCase, skipUnlessDBFeature from django.utils import six @@ -17,8 +17,6 @@ if HAS_GEOS: Point, LineString, LinearRing, Polygon, GeometryCollection) from .models import Country, City, PennsylvaniaCity, State, Track - -if HAS_GEOS and not spatialite: from .models import Feature, MinusOneSRID @@ -156,7 +154,6 @@ class GeoModelTest(TestCase): c = City() self.assertEqual(c.point, None) - @no_spatialite # SpatiaLite does not support abstract geometry columns def test_geometryfield(self): "Testing the general GeometryField." Feature(name='Point', geom=Point(1, 1)).save() @@ -266,9 +263,7 @@ class GeoLookupTest(TestCase): self.assertEqual('Texas', qs[0].name) # Only PostGIS has `left` and `right` lookup types. - @no_mysql - @no_oracle - @no_spatialite + @skipUnlessDBFeature("supports_left_right_lookups") def test_left_right_lookups(self): "Testing the 'left' and 'right' lookup types." # Left: A << B => true if xmax(A) < xmin(B) @@ -451,8 +446,7 @@ class GeoQuerySetTest(TestCase): for country in countries: self.assertIsInstance(country.envelope, Polygon) - @no_mysql - @no_spatialite # SpatiaLite does not have an Extent function + @skipUnlessDBFeature("supports_extent_aggr") def test_extent(self): "Testing the `extent` GeoQuerySet method." # Reference query: @@ -466,9 +460,7 @@ class GeoQuerySetTest(TestCase): for val, exp in zip(extent, expected): self.assertAlmostEqual(exp, val, 4) - @no_mysql - @no_oracle - @no_spatialite + @skipUnlessDBFeature("has_force_rhr_method") def test_force_rhr(self): "Testing GeoQuerySet.force_rhr()." rings = ( @@ -483,13 +475,9 @@ class GeoQuerySetTest(TestCase): s = State.objects.force_rhr().get(name='Foo') self.assertEqual(rhr_rings, s.force_rhr.coords) - @no_mysql - @no_oracle - @no_spatialite + @skipUnlessDBFeature("has_geohash_method") def test_geohash(self): "Testing GeoQuerySet.geohash()." - if not connection.ops.geohash: - return # Reference query: # SELECT ST_GeoHash(point) FROM geoapp_city WHERE name='Houston'; # SELECT ST_GeoHash(point, 5) FROM geoapp_city WHERE name='Houston'; @@ -501,7 +489,7 @@ class GeoQuerySetTest(TestCase): def test_geojson(self): "Testing GeoJSON output from the database using GeoQuerySet.geojson()." - # Only PostGIS 1.3.4+ and SpatiaLite 3.0+ support GeoJSON. + # Only PostGIS and SpatiaLite 3.0+ support GeoJSON. if not connection.ops.geojson: self.assertRaises(NotImplementedError, Country.objects.all().geojson, field_name='mpoly') return @@ -520,17 +508,15 @@ class GeoQuerySetTest(TestCase): # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 0) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Pueblo'; self.assertEqual(pueblo_json, City.objects.geojson().get(name='Pueblo').geojson) - # 1.3.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; - # 1.4.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; + # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; # This time we want to include the CRS by using the `crs` keyword. self.assertEqual(houston_json, City.objects.geojson(crs=True, model_att='json').get(name='Houston').json) - # 1.3.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Victoria'; - # 1.4.x: SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; + # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; # This time we include the bounding box by using the `bbox` keyword. self.assertEqual(victoria_json, City.objects.geojson(bbox=True).get(name='Victoria').geojson) - # 1.(3|4).x: SELECT ST_AsGeoJson("geoapp_city"."point", 5, 3) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Chicago'; + # SELECT ST_AsGeoJson("geoapp_city"."point", 5, 3) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Chicago'; # Finally, we set every available keyword. self.assertEqual(chicago_json, City.objects.geojson(bbox=True, crs=True, precision=5).get(name='Chicago').geojson) @@ -581,9 +567,7 @@ class GeoQuerySetTest(TestCase): self.assertEqual('-104.609252,38.255001', ptown.kml) # Only PostGIS has support for the MakeLine aggregate. - @no_mysql - @no_oracle - @no_spatialite + @skipUnlessDBFeature("has_make_line_method") def test_make_line(self): "Testing the `make_line` GeoQuerySet method." # Ensuring that a `TypeError` is raised on models without PointFields. @@ -610,8 +594,7 @@ class GeoQuerySetTest(TestCase): else: self.assertEqual(1, c.num_geom) - @no_mysql - @no_spatialite # SpatiaLite can only count vertices in LineStrings + @skipUnlessDBFeature("supports_num_points_poly") def test_num_points(self): "Testing the `num_points` GeoQuerySet method." for c in Country.objects.num_points(): @@ -647,8 +630,7 @@ class GeoQuerySetTest(TestCase): tol = 0.000000001 self.assertEqual(True, ref[c.name].equals_exact(c.point_on_surface, tol)) - @no_mysql - @no_spatialite + @skipUnlessDBFeature("has_reverse_method") def test_reverse_geom(self): "Testing GeoQuerySet.reverse_geom()." coords = [(-95.363151, 29.763374), (-95.448601, 29.713803)] @@ -673,9 +655,7 @@ class GeoQuerySetTest(TestCase): self.assertAlmostEqual(c1[0] * xfac, c2[0], tol) self.assertAlmostEqual(c1[1] * yfac, c2[1], tol) - @no_mysql - @no_oracle - @no_spatialite + @skipUnlessDBFeature("has_snap_to_grid_method") def test_snap_to_grid(self): "Testing GeoQuerySet.snap_to_grid()." # Let's try and break snap_to_grid() with bad combinations of arguments. diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 821d53f8a59..410216697bc 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.contrib.gis.geos import HAS_GEOS -from django.contrib.gis.tests.utils import mysql, no_mysql, no_oracle, no_spatialite +from django.contrib.gis.tests.utils import mysql, no_mysql, no_oracle from django.test import TestCase, skipUnlessDBFeature if HAS_GEOS: @@ -60,8 +60,7 @@ class RelatedGeoModelTest(TestCase): qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point')) check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) - @no_mysql - @no_spatialite + @skipUnlessDBFeature("supports_extent_aggr") def test04a_related_extent_aggregate(self): "Testing the `extent` GeoQuerySet aggregates on related geographic models." # This combines the Extent and Union aggregates into one query @@ -265,9 +264,7 @@ class RelatedGeoModelTest(TestCase): # Should be `None`, and not a 'dummy' model. self.assertEqual(None, b.author) - @no_mysql - @no_oracle - @no_spatialite + @skipUnlessDBFeature("supports_collect_aggr") def test14_collect(self): "Testing the `collect` GeoQuerySet method and `Collect` aggregate." # Reference query: diff --git a/django/contrib/gis/tests/utils.py b/django/contrib/gis/tests/utils.py index 29e17e71131..614dbe808d9 100644 --- a/django/contrib/gis/tests/utils.py +++ b/django/contrib/gis/tests/utils.py @@ -29,9 +29,6 @@ def no_mysql(func): return no_backend(func, 'mysql') -def no_spatialite(func): - return no_backend(func, 'spatialite') - # Shortcut booleans to omit only portions of tests. _default_db = settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].rsplit('.')[-1] oracle = _default_db == 'oracle' diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index c9e9f1fc284..6958135f885 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -270,11 +270,11 @@ Method PostGIS Oracle SpatiaLite ==================================== ======= ====== ========== :meth:`GeoQuerySet.area` X X X :meth:`GeoQuerySet.centroid` X X X -:meth:`GeoQuerySet.collect` X +:meth:`GeoQuerySet.collect` X (from v3.0) :meth:`GeoQuerySet.difference` X X X :meth:`GeoQuerySet.distance` X X X :meth:`GeoQuerySet.envelope` X X -:meth:`GeoQuerySet.extent` X X +:meth:`GeoQuerySet.extent` X X (from v3.0) :meth:`GeoQuerySet.extent3d` X :meth:`GeoQuerySet.force_rhr` X :meth:`GeoQuerySet.geohash` X diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 935893265bc..e5cecc230c0 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -83,6 +83,9 @@ Minor features * Compatibility shims for ``SpatialRefSys`` and ``GeometryColumns`` changed in Django 1.2 have been removed. +* The Spatialite backend now supports ``Collect`` and ``Extent`` aggregates + when the database version is 3.0 or later. + :mod:`django.contrib.messages` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^