Replaced no_spatialite by connection features

Refs #22632. Thanks Tim Graham for the review.
This commit is contained in:
Claude Paroz 2014-08-19 18:47:23 +02:00
parent 46c7707e50
commit a7d964ab87
13 changed files with 105 additions and 63 deletions

View File

@ -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):
"""

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -65,17 +65,25 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
name = 'spatialite'
spatialite = True
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\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.

View File

@ -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.

View File

@ -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,11 +58,10 @@ class Truth(models.Model):
app_label = 'geoapp'
if not spatialite:
class Feature(NamedModel):
geom = models.GeometryField()
class MinusOneSRID(models.Model):
geom = models.PointField(srid=-1) # Minus one SRID.

View File

@ -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

View File

@ -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('<Point><coordinates>-104.609252,38.255001</coordinates></Point>', 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.

View File

@ -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:

View File

@ -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'

View File

@ -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

View File

@ -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`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^