diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index 20d81d5de4..09d8452212 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -16,6 +16,9 @@ class BaseSpatialFeatures(object): # Does the database contain a SpatialRefSys model to store SRID information? has_spatialrefsys_table = True + # Reference implementation of 3D functions is: + # http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions + supports_3d_functions = False # Does the database support SRID transform operations? supports_transform = True # Do geometric relationship operations operate on real shapes (or only on bounding boxes)? @@ -29,24 +32,34 @@ class BaseSpatialFeatures(object): # The following properties indicate if the database backend support # certain lookups (dwithin, left and right, relate, ...) + supports_distances_lookups = True supports_left_right_lookups = False @property - def supports_relate_lookup(self): - return 'relate' in self.connection.ops.geometry_functions + def supports_bbcontains_lookup(self): + return 'bbcontains' in self.connection.ops.gis_terms @property - def has_dwithin_lookup(self): + def supports_contained_lookup(self): + return 'contained' in self.connection.ops.gis_terms + + @property + def supports_dwithin_lookup(self): return 'dwithin' in self.connection.ops.distance_functions + @property + def supports_relate_lookup(self): + return 'relate' in self.connection.ops.gis_terms + # For each of those methods, the class will have a property named # `has__method` (defined in __init__) which accesses connection.ops # to determine GIS method availability. geoqueryset_methods = ( - 'centroid', 'difference', 'envelope', 'force_rhr', 'geohash', 'gml', - 'intersection', 'kml', 'num_geom', 'perimeter', 'point_on_surface', - 'reverse', 'scale', 'snap_to_grid', 'svg', 'sym_difference', - 'transform', 'translate', 'union', 'unionagg', + 'area', 'centroid', 'difference', 'distance', 'distance_spheroid', + 'envelope', 'force_rhr', 'geohash', 'gml', 'intersection', 'kml', + 'length', 'num_geom', 'perimeter', 'point_on_surface', 'reverse', + 'scale', 'snap_to_grid', 'svg', 'sym_difference', 'transform', + 'translate', 'union', 'unionagg', ) # Specifies whether the Collect and Extent aggregates are supported by the database diff --git a/django/contrib/gis/db/backends/mysql/base.py b/django/contrib/gis/db/backends/mysql/base.py index 47e8177da8..769d2fa609 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_distances_lookups = False supports_transform = False supports_real_shape_operations = False supports_null_geometries = False diff --git a/django/contrib/gis/db/backends/postgis/base.py b/django/contrib/gis/db/backends/postgis/base.py index cb2cbe53aa..7a2a374f10 100644 --- a/django/contrib/gis/db/backends/postgis/base.py +++ b/django/contrib/gis/db/backends/postgis/base.py @@ -11,6 +11,7 @@ from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures): + supports_3d_functions = True supports_left_right_lookups = True diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index a9a7bb7f6f..eb1d9fdeb4 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -631,8 +631,8 @@ class GeoQuerySet(QuerySet): u, unit_name, s = get_srid_info(self.query.transformed_srid, connection) geodetic = unit_name.lower() in geo_field.geodetic_units - if backend.spatialite and geodetic: - raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.') + if geodetic and not connection.features.supports_distance_geodetic: + raise ValueError('This database does not support linear distance calculations on geodetic coordinate systems.') if distance: if self.query.transformed_srid: @@ -690,8 +690,8 @@ class GeoQuerySet(QuerySet): # works on 3D geometries. procedure_fmt += ",'%(spheroid)s'" procedure_args.update({'function': backend.length_spheroid, 'spheroid': params[1]}) - elif geom_3d and backend.postgis: - # Use 3D variants of perimeter and length routines on PostGIS. + elif geom_3d and connection.features.supports_3d_functions: + # Use 3D variants of perimeter and length routines on supported backends. if perimeter: procedure_args.update({'function': backend.perimeter3d}) elif length: diff --git a/django/contrib/gis/tests/distapp/models.py b/django/contrib/gis/tests/distapp/models.py index 4d95b219d7..87694a424f 100644 --- a/django/contrib/gis/tests/distapp/models.py +++ b/django/contrib/gis/tests/distapp/models.py @@ -1,4 +1,5 @@ from django.contrib.gis.db import models +from django.contrib.gis.tests.utils import gisfield_may_be_null from django.utils.encoding import python_2_unicode_compatible @@ -38,7 +39,7 @@ class CensusZipcode(NamedModel): class SouthTexasZipcode(NamedModel): "Model for a few South Texas ZIP codes." - poly = models.PolygonField(srid=32140, null=True) + poly = models.PolygonField(srid=32140, null=gisfield_may_be_null) class Interstate(NamedModel): diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index 348fe6f6cc..00b9ee1720 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -6,9 +6,7 @@ from django.db import connection 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 -) +from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle from django.test import TestCase, skipUnlessDBFeature if HAS_GEOS: @@ -18,8 +16,6 @@ if HAS_GEOS: SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode) -@skipUnless(HAS_GEOS and not mysql, - "GEOS and spatial db (not mysql) are required.") @skipUnlessDBFeature("gis_enabled") class DistanceTest(TestCase): fixtures = ['initial'] @@ -50,7 +46,7 @@ class DistanceTest(TestCase): self.assertEqual(1, Interstate.objects.count()) self.assertEqual(1, SouthTexasInterstate.objects.count()) - @skipUnlessDBFeature("has_dwithin_lookup") + @skipUnlessDBFeature("supports_dwithin_lookup") def test_dwithin(self): """ Test the `dwithin` lookup type. @@ -99,6 +95,7 @@ class DistanceTest(TestCase): else: self.assertListEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist)))) + @skipUnlessDBFeature("has_distance_method") def test_distance_projected(self): """ Test the `distance` GeoQuerySet method on projected coordinate systems. @@ -139,7 +136,7 @@ class DistanceTest(TestCase): self.assertAlmostEqual(m_distances[i], c.distance.m, tol) self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) - @skipUnlessDBFeature("supports_distance_geodetic") + @skipUnlessDBFeature("has_distance_method", "supports_distance_geodetic") def test_distance_geodetic(self): """ Test the `distance` GeoQuerySet method on geodetic coordinate systems. @@ -149,16 +146,16 @@ class DistanceTest(TestCase): # Testing geodetic distance calculation with a non-point geometry # (a LineString of Wollongong and Shellharbour coords). ls = LineString(((150.902, -34.4245), (150.87, -34.5789))) - if oracle or postgis: - # Reference query: - # SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326)) FROM distapp_australiacity ORDER BY name; - distances = [1120954.92533513, 140575.720018241, 640396.662906304, - 60580.9693849269, 972807.955955075, 568451.8357838, - 40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0] - qs = AustraliaCity.objects.distance(ls).order_by('name') - for city, distance in zip(qs, distances): - # Testing equivalence to within a meter. - self.assertAlmostEqual(distance, city.distance.m, 0) + + # Reference query: + # SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326)) FROM distapp_australiacity ORDER BY name; + distances = [1120954.92533513, 140575.720018241, 640396.662906304, + 60580.9693849269, 972807.955955075, 568451.8357838, + 40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0] + qs = AustraliaCity.objects.distance(ls).order_by('name') + for city, distance in zip(qs, distances): + # Testing equivalence to within a meter. + self.assertAlmostEqual(distance, city.distance.m, 0) # Got the reference distances using the raw SQL statements: # SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11)); @@ -197,6 +194,7 @@ class DistanceTest(TestCase): self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol) @no_oracle # Oracle already handles geographic distance calculation. + @skipUnlessDBFeature("has_distance_method") def test_distance_transform(self): """ Test the `distance` GeoQuerySet method used with `transform` on a geographic field. @@ -226,6 +224,7 @@ class DistanceTest(TestCase): for i, z in enumerate(qs): self.assertAlmostEqual(z.distance.m, dists_m[i], 5) + @skipUnlessDBFeature("supports_distances_lookups") def test_distance_lookups(self): """ Test the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types. @@ -255,6 +254,7 @@ class DistanceTest(TestCase): qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300))) self.assertEqual(['77002', '77025', '77401'], self.get_names(qs)) + @skipUnlessDBFeature("supports_distances_lookups", "supports_distance_geodetic") def test_geodetic_distance_lookups(self): """ Test distance lookups on geodetic coordinate systems. @@ -264,23 +264,11 @@ class DistanceTest(TestCase): line = GEOSGeometry('LINESTRING(144.9630 -37.8143,151.2607 -33.8870)', 4326) dist_qs = AustraliaCity.objects.filter(point__distance_lte=(line, D(km=100))) - if oracle or postgis: - # Oracle and PostGIS can do distance lookups on arbitrary geometries. - self.assertEqual(9, dist_qs.count()) - self.assertEqual(['Batemans Bay', 'Canberra', 'Hillsdale', - 'Melbourne', 'Mittagong', 'Shellharbour', - 'Sydney', 'Thirroul', 'Wollongong'], - self.get_names(dist_qs)) - else: - # spatialite only allow geodetic distance queries (utilizing - # ST_Distance_Sphere/ST_Distance_Spheroid) from Points to PointFields - # on geometry columns. - self.assertRaises(ValueError, dist_qs.count) - - # Ensured that a ValueError was raised, none of the rest of the test is - # support on this backend, so bail now. - if spatialite: - return + self.assertEqual(9, dist_qs.count()) + self.assertEqual(['Batemans Bay', 'Canberra', 'Hillsdale', + 'Melbourne', 'Mittagong', 'Shellharbour', + 'Sydney', 'Thirroul', 'Wollongong'], + self.get_names(dist_qs)) # Too many params (4 in this case) should raise a ValueError. self.assertRaises(ValueError, len, @@ -309,18 +297,18 @@ class DistanceTest(TestCase): # Geodetic distance lookup but telling GeoDjango to use `distance_spheroid` # instead (we should get the same results b/c accuracy variance won't matter # in this test case). - if postgis: + querysets = [qs1] + if connection.features.has_distance_spheroid_method: gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid')) gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid')) qs2 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq3 | gq4) - querysets = [qs1, qs2] - else: - querysets = [qs1] + querysets.append(qs2) for qs in querysets: cities = self.get_names(qs) self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul']) + @skipUnlessDBFeature("has_area_method") def test_area(self): """ Test the `area` GeoQuerySet method. @@ -333,6 +321,7 @@ class DistanceTest(TestCase): for i, z in enumerate(SouthTexasZipcode.objects.order_by('name').area()): self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol) + @skipUnlessDBFeature("has_length_method") def test_length(self): """ Test the `length` GeoQuerySet method. @@ -342,13 +331,13 @@ class DistanceTest(TestCase): len_m1 = 473504.769553813 len_m2 = 4617.668 - if spatialite: - # Does not support geodetic coordinate systems. - self.assertRaises(ValueError, Interstate.objects.length) - else: + if connection.features.supports_distance_geodetic: qs = Interstate.objects.length() tol = 2 if oracle else 3 self.assertAlmostEqual(len_m1, qs[0].length.m, tol) + else: + # Does not support geodetic coordinate systems. + self.assertRaises(ValueError, Interstate.objects.length) # Now doing length on a projected coordinate system. i10 = SouthTexasInterstate.objects.length().get(name='I-10') @@ -370,6 +359,7 @@ class DistanceTest(TestCase): for i, c in enumerate(SouthTexasCity.objects.perimeter(model_att='perim')): self.assertEqual(0, c.perim.m) + @skipUnlessDBFeature("has_area_method", "has_distance_method") def test_measurement_null_fields(self): """ Test the measurement GeoQuerySet methods on fields with NULL values. diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index 130124c00c..b57b130704 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -6,8 +6,7 @@ from unittest import skipUnless from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import HAS_GEOS -from django.contrib.gis.tests.utils import postgis -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from django.utils._os import upath if HAS_GEOS: @@ -62,7 +61,8 @@ bbox_data = ( ) -@skipUnless(HAS_GEOS and HAS_GDAL and postgis, "Geos, GDAL and postgis are required.") +@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.") +@skipUnlessDBFeature("gis_enabled", "supports_3d_functions") class Geo3DTest(TestCase): """ Only a subset of the PostGIS routines are 3D-enabled, and this TestCase @@ -70,7 +70,7 @@ class Geo3DTest(TestCase): available within GeoDjango. For more information, see the PostGIS docs on the routines that support 3D: - http://postgis.refractions.net/documentation/manual-1.5/ch08.html#PostGIS_3D_Functions + http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions """ def _load_interstate_data(self): diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py index 1e07e00d9d..415dd568ad 100644 --- a/django/contrib/gis/tests/geoapp/models.py +++ b/django/contrib/gis/tests/geoapp/models.py @@ -1,10 +1,7 @@ from django.contrib.gis.db import models -from django.contrib.gis.tests.utils import mysql +from django.contrib.gis.tests.utils import gisfield_may_be_null from django.utils.encoding import python_2_unicode_compatible -# MySQL spatial indices can't handle NULL geometries. -null_flag = not mysql - @python_2_unicode_compatible class NamedModel(models.Model): @@ -42,7 +39,7 @@ class PennsylvaniaCity(City): class State(NamedModel): - poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here. + poly = models.PolygonField(null=gisfield_may_be_null) # Allowing NULL geometries here. class Track(NamedModel): diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 87dd30a812..8fd912fb9e 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -2,12 +2,11 @@ from __future__ import unicode_literals import re import unittest -from unittest import skipUnless 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 mysql, oracle, postgis, spatialite +from django.contrib.gis.tests.utils import oracle, postgis, spatialite from django.test import TestCase, skipUnlessDBFeature from django.utils import six @@ -142,11 +141,12 @@ class GeoModelTest(TestCase): # If the GeometryField SRID is -1, then we shouldn't perform any # transformation if the SRID of the input geometry is different. - # SpatiaLite does not support missing SRID values. - if not spatialite: - m1 = MinusOneSRID(geom=Point(17, 23, srid=4326)) - m1.save() - self.assertEqual(-1, m1.geom.srid) + if spatialite and connection.ops.spatial_version < 3: + # SpatiaLite < 3 does not support missing SRID values. + return + m1 = MinusOneSRID(geom=Point(17, 23, srid=4326)) + m1.save() + self.assertEqual(-1, m1.geom.srid) def test_createnull(self): "Testing creating a model instance and the geometry being None" @@ -223,7 +223,7 @@ class GeoLookupTest(TestCase): # Seeing what cities are in Texas, should get Houston and Dallas, # and Oklahoma City because 'contained' only checks on the # _bounding box_ of the Geometries. - if not oracle: + if connection.features.supports_contained_lookup: qs = City.objects.filter(point__contained=texas.mpoly) self.assertEqual(3, qs.count()) cities = ['Houston', 'Dallas', 'Oklahoma City'] @@ -245,23 +245,22 @@ class GeoLookupTest(TestCase): self.assertEqual('New Zealand', nz.name) # Spatialite 2.3 thinks that Lawrence is in Puerto Rico (a NULL geometry). - if not spatialite: + if not (spatialite and connection.ops.spatial_version < 3): ks = State.objects.get(poly__contains=lawrence.point) self.assertEqual('Kansas', ks.name) # Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas) # are not contained in Texas or New Zealand. - self.assertEqual(0, len(Country.objects.filter(mpoly__contains=pueblo.point))) # Query w/GEOSGeometry object - self.assertEqual((mysql and 1) or 0, - len(Country.objects.filter(mpoly__contains=okcity.point.wkt))) # Qeury w/WKT + self.assertEqual(len(Country.objects.filter(mpoly__contains=pueblo.point)), 0) # Query w/GEOSGeometry object + self.assertEqual(len(Country.objects.filter(mpoly__contains=okcity.point.wkt)), + 0 if connection.features.supports_real_shape_operations else 1) # Query w/WKT # OK City is contained w/in bounding box of Texas. - if not oracle: + if connection.features.supports_bbcontains_lookup: qs = Country.objects.filter(mpoly__bbcontains=okcity.point) self.assertEqual(1, len(qs)) self.assertEqual('Texas', qs[0].name) - # Only PostGIS has `left` and `right` lookup types. @skipUnlessDBFeature("supports_left_right_lookups") def test_left_right_lookups(self): "Testing the 'left' and 'right' lookup types." @@ -409,10 +408,9 @@ class GeoQuerySetTest(TestCase): for s in qs: self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol)) - @skipUnlessDBFeature("has_difference_method") - @skipUnlessDBFeature("has_intersection_method") - @skipUnlessDBFeature("has_sym_difference_method") - @skipUnlessDBFeature("has_union_method") + @skipUnlessDBFeature( + "has_difference_method", "has_intersection_method", + "has_sym_difference_method", "has_union_method") def test_diff_intersection_union(self): "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods." geom = Point(5, 23) @@ -610,7 +608,7 @@ class GeoQuerySetTest(TestCase): 'Texas': fromstr('POINT (-103.002434 36.500397)', srid=4326), } - elif postgis or spatialite: + else: # Using GEOSGeometry to compute the reference point on surface values # -- since PostGIS also uses GEOS these should be the same. ref = {'New Zealand': Country.objects.get(name='New Zealand').mpoly.point_on_surface, diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py index 3d73e69867..59851c35a0 100644 --- a/django/contrib/gis/tests/geogapp/tests.py +++ b/django/contrib/gis/tests/geogapp/tests.py @@ -10,14 +10,14 @@ from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.measure import D from django.contrib.gis.tests.utils import postgis -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from django.utils._os import upath if HAS_GEOS: from .models import City, County, Zipcode -@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") +@skipUnlessDBFeature("gis_enabled") class GeographyTest(TestCase): fixtures = ['initial'] @@ -25,6 +25,7 @@ class GeographyTest(TestCase): "Ensure geography features loaded properly." self.assertEqual(8, City.objects.count()) + @skipUnlessDBFeature("supports_distances_lookups", "supports_distance_geodetic") def test02_distance_lookup(self): "Testing GeoQuerySet distance lookup support on non-point geography fields." z = Zipcode.objects.get(code='77002') @@ -39,12 +40,14 @@ class GeographyTest(TestCase): for cities in [cities1, cities2]: self.assertEqual(['Dallas', 'Houston', 'Oklahoma City'], cities) + @skipUnlessDBFeature("has_distance_method", "supports_distance_geodetic") def test03_distance_method(self): "Testing GeoQuerySet.distance() support on non-point geography fields." # `GeoQuerySet.distance` is not allowed geometry fields. htown = City.objects.get(name='Houston') Zipcode.objects.distance(htown.point) + @skipUnless(postgis, "This is a PostGIS-specific test") def test04_invalid_operators_functions(self): "Ensuring exceptions are raised for operators & functions invalid on geography fields." # Only a subset of the geometry functions & operator are available @@ -89,6 +92,7 @@ class GeographyTest(TestCase): self.assertEqual(name, c.name) self.assertEqual(state, c.state) + @skipUnlessDBFeature("has_area_method", "supports_distance_geodetic") def test06_geography_area(self): "Testing that Area calculations work on geography columns." # SELECT ST_Area(poly) FROM geogapp_zipcode WHERE code='77002'; diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py index 6a36d3efb7..9dece4372f 100644 --- a/django/contrib/gis/tests/inspectapp/tests.py +++ b/django/contrib/gis/tests/inspectapp/tests.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os +import re from unittest import skipUnless from django.core.management import call_command @@ -11,7 +12,7 @@ from django.contrib.gis.geometry.test_data import TEST_DATA from django.utils.six import StringIO if HAS_GDAL: - from django.contrib.gis.gdal import Driver + from django.contrib.gis.gdal import Driver, OGRException from django.contrib.gis.utils.ogrinspect import ogrinspect from .models import AllOGRFields @@ -77,23 +78,20 @@ class OGRInspectTest(TestCase): self.assertEqual(model_def, '\n'.join(expected)) def test_time_field(self): - # Only possible to test this on PostGIS at the moment. MySQL - # complains about permissions, and SpatiaLite/Oracle are - # insanely difficult to get support compiled in for in GDAL. - if not connections['default'].ops.postgis: - self.skipTest("This database does not support 'ogrinspect'ion") - # Getting the database identifier used by OGR, if None returned # GDAL does not have the support compiled in. ogr_db = get_ogr_db_string() if not ogr_db: - self.skipTest("Your GDAL installation does not support PostGIS databases") + self.skipTest("Unable to setup an OGR connection to your database") - # Writing shapefiles via GDAL currently does not support writing OGRTime - # fields, so we need to actually use a database - model_def = ogrinspect(ogr_db, 'Measurement', - layer_key=AllOGRFields._meta.db_table, - decimal=['f_decimal']) + try: + # Writing shapefiles via GDAL currently does not support writing OGRTime + # fields, so we need to actually use a database + model_def = ogrinspect(ogr_db, 'Measurement', + layer_key=AllOGRFields._meta.db_table, + decimal=['f_decimal']) + except OGRException: + self.skipTest("Unable to setup an OGR connection to your database") self.assertTrue(model_def.startswith( '# This is an auto-generated Django model module created by ogrinspect.\n' @@ -111,10 +109,9 @@ class OGRInspectTest(TestCase): self.assertIn(' f_char = models.CharField(max_length=10)', model_def) self.assertIn(' f_date = models.DateField()', model_def) - self.assertTrue(model_def.endswith( - ' geom = models.PolygonField()\n' - ' objects = models.GeoManager()' - )) + self.assertIsNotNone(re.search( + r' geom = models.PolygonField\(([^\)])*\)\n' # Some backends may have srid=-1 + r' objects = models.GeoManager\(\)', model_def)) def test_management_command(self): shp_file = os.path.join(TEST_DATA, 'cities', 'cities.shp') @@ -142,7 +139,7 @@ def get_ogr_db_string(): 'django.contrib.gis.db.backends.spatialite': ('SQLite', '%(db_name)s', '') } - drv_name, db_str, param_sep = drivers[db['ENGINE']] + drv_name, db_str, param_sep = drivers.get(db['ENGINE']) # Ensure that GDAL library has driver support for the database. try: diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 1e870c678a..5a0d15247e 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -8,8 +8,7 @@ import unittest from unittest import skipUnless from django.contrib.gis.gdal import HAS_GDAL -from django.contrib.gis.tests.utils import mysql -from django.db import router +from django.db import connection, router from django.conf import settings from django.test import TestCase, skipUnlessDBFeature from django.utils._os import upath @@ -151,7 +150,7 @@ class LayerMapTest(TestCase): # Unique may take tuple or string parameters. for arg in ('name', ('name', 'mpoly')): lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique=arg) - except: + except Exception: self.fail('No exception should be raised for proper use of keywords.') # Testing invalid params for the `unique` keyword. @@ -159,7 +158,7 @@ class LayerMapTest(TestCase): self.assertRaises(e, LayerMapping, County, co_shp, co_mapping, transform=False, unique=arg) # No source reference system defined in the shapefile, should raise an error. - if not mysql: + if connection.features.supports_transform: self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping) # Passing in invalid ForeignKey mapping parameters -- must be a dictionary diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 6b7de4a87a..67dd07b583 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals from django.contrib.gis.geos import HAS_GEOS -from django.contrib.gis.tests.utils import mysql, no_oracle +from django.contrib.gis.tests.utils import no_oracle +from django.db import connection from django.test import TestCase, skipUnlessDBFeature if HAS_GEOS: @@ -146,7 +147,7 @@ class RelatedGeoModelTest(TestCase): self.assertEqual(1, len(qs)) self.assertEqual('P2', qs[0].name) - if not mysql: + if connection.features.supports_transform: # This time center2 is in a different coordinate system and needs # to be wrapped in transformation SQL. qs = Parcel.objects.filter(center2__within=F('border1')) @@ -159,7 +160,7 @@ class RelatedGeoModelTest(TestCase): self.assertEqual(1, len(qs)) self.assertEqual('P1', qs[0].name) - if not mysql: + if connection.features.supports_transform: # This time the city column should be wrapped in transformation SQL. qs = Parcel.objects.filter(border2__contains=F('city__location__point')) self.assertEqual(1, len(qs)) diff --git a/django/contrib/gis/tests/utils.py b/django/contrib/gis/tests/utils.py index 52c1a6c74e..3a43f8852b 100644 --- a/django/contrib/gis/tests/utils.py +++ b/django/contrib/gis/tests/utils.py @@ -28,6 +28,9 @@ postgis = _default_db == 'postgis' mysql = _default_db == 'mysql' spatialite = _default_db == 'spatialite' +# MySQL spatial indices can't handle NULL geometries. +gisfield_may_be_null = not mysql + if oracle and 'gis' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE']: from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys as SpatialRefSys elif postgis: diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index ead2f170ac..b3765ef8da 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -103,10 +103,10 @@ class LayerMapping(object): # Getting the geometry column associated with the model (an # exception will be raised if there is no geometry column). - if self.spatial_backend.mysql: - transform = False - else: + if connections[self.using].features.supports_transform: self.geo_field = self.geometry_field() + else: + transform = False # Checking the source spatial reference system, and getting # the coordinate transformation object (unless the `transform`