From 05b4d2f67bc3e75d2d1aada844d6031cc126e48e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 16 Dec 2009 02:33:33 +0000 Subject: [PATCH] [soc2009/multidb] Fixed #11741 -- Updates to the spatial backends (e.g., re-enabled POSTGIS_VERSION setting); added geometry backend module. Patch from Justin Bronn. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11872 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/db/backends/base.py | 47 +++- .../gis/db/backends/mysql/operations.py | 6 +- django/contrib/gis/db/backends/oracle/base.py | 2 +- .../gis/db/backends/oracle/compiler.py | 24 +- .../gis/db/backends/oracle/operations.py | 66 ++++-- .../gis/db/backends/postgis/creation.py | 43 ++-- .../gis/db/backends/postgis/operations.py | 208 ++++++++++++++---- .../gis/db/backends/spatialite/operations.py | 60 +++-- django/contrib/gis/db/models/fields.py | 69 +++--- django/contrib/gis/db/models/manager.py | 6 +- django/contrib/gis/db/models/query.py | 10 +- django/contrib/gis/db/models/sql/query.py | 2 +- .../contrib/gis/db/models/sql/subqueries.py | 36 --- django/contrib/gis/db/models/sql/where.py | 6 +- django/contrib/gis/geometry/__init__.py | 9 - .../contrib/gis/geometry/backend/__init__.py | 21 ++ django/contrib/gis/geometry/backend/geos.py | 3 + django/contrib/gis/tests/relatedapp/tests.py | 2 +- 18 files changed, 428 insertions(+), 192 deletions(-) delete mode 100644 django/contrib/gis/db/models/sql/subqueries.py create mode 100644 django/contrib/gis/geometry/backend/__init__.py create mode 100644 django/contrib/gis/geometry/backend/geos.py diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index 72a45473f7..b977278b16 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -1,5 +1,6 @@ """ - +Base/mixin classes for the spatial backend database operations and the +`SpatialRefSys` model the backend. """ import re from django.conf import settings @@ -14,8 +15,9 @@ class BaseSpatialOperations(object): distance_functions = {} geometry_functions = {} geometry_operators = {} + geography_operators = {} + geography_functions = {} gis_terms = {} - limited_where = {} # Quick booleans for the type of this spatial backend, and # an attribute for the spatial database version tuple (if applicable) @@ -28,6 +30,9 @@ class BaseSpatialOperations(object): # How the geometry column should be selected. select = None + # Does the spatial database have a geography type? + geography = False + area = False centroid = False difference = False @@ -37,11 +42,13 @@ class BaseSpatialOperations(object): envelope = False force_rhr = False mem_size = False + bounding_circle = False num_geom = False num_points = False perimeter = False perimeter3d = False point_on_surface = False + polygonize = False scale = False snap_to_grid = False sym_difference = False @@ -67,11 +74,6 @@ class BaseSpatialOperations(object): from_text = False from_wkb = False - def geo_quote_name(self, name): - if isinstance(name, unicode): - name = name.encode('ascii') - return "'%s'" % name - # Default conversion functions for aggregates; will be overridden if implemented # for the spatial backend. def convert_extent(self, box): @@ -83,6 +85,37 @@ class BaseSpatialOperations(object): def convert_geom(self, geom_val, geom_field): raise NotImplementedError('Aggregate method not implemented for this spatial backend.') + # For quoting column values, rather than columns. + def geo_quote_name(self, name): + if isinstance(name, unicode): + name = name.encode('ascii') + return "'%s'" % name + + # GeometryField operations + def geo_db_type(self, f): + """ + Returns the database column type for the geometry field on + the spatial backend. + """ + raise NotImplementedError + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters for the given geometry field, + lookup value, and lookup type. + """ + raise NotImplementedError('Distance operations not available on this spatial backend.') + + def get_geom_placeholder(self, f, value): + """ + Returns the placeholder for the given geometry field with the given + value. Depending on the spatial backend, the placeholder may contain a + stored procedure call to the transformation function of the spatial + backend. + """ + raise NotImplementedError + + # Spatial SQL Construction def spatial_aggregate_sql(self, agg): raise NotImplementedError('Aggregate support not implemented for this spatial backend.') diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index c086543a23..658aebe04e 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -31,6 +31,9 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations): gis_terms = dict([(term, None) for term in geometry_functions.keys() + ['isnull']]) + def geo_db_type(self, f): + return f.geom_type + def get_geom_placeholder(self, value, srid): """ The placeholder here has to include MySQL's WKT constructor. Because @@ -43,8 +46,7 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations): placeholder = '%s(%%s)' % self.from_text return placeholder - def spatial_lookup_sql(self, lvalue, lookup_type, value, field): - qn = self.quote_name + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): alias, col, db_type = lvalue geo_col = '%s.%s' % (qn(alias), qn(col)) diff --git a/django/contrib/gis/db/backends/oracle/base.py b/django/contrib/gis/db/backends/oracle/base.py index 111167f258..b39cb555a2 100644 --- a/django/contrib/gis/db/backends/oracle/base.py +++ b/django/contrib/gis/db/backends/oracle/base.py @@ -7,4 +7,4 @@ class DatabaseWrapper(OracleDatabaseWrapper): def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) self.creation = OracleCreation(self) - self.ops = OracleOperations() + self.ops = OracleOperations(self) diff --git a/django/contrib/gis/db/backends/oracle/compiler.py b/django/contrib/gis/db/backends/oracle/compiler.py index 98da0163ba..f0eb5cad00 100644 --- a/django/contrib/gis/db/backends/oracle/compiler.py +++ b/django/contrib/gis/db/backends/oracle/compiler.py @@ -7,7 +7,29 @@ class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler): pass class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler): - pass + def placeholder(self, field, val): + if field is None: + # A field value of None means the value is raw. + return val + elif hasattr(field, 'get_placeholder'): + # Some fields (e.g. geo fields) need special munging before + # they can be inserted. + ph = field.get_placeholder(val, self.connection) + if ph == 'NULL': + # If the placeholder returned is 'NULL', then we need to + # to remove None from the Query parameters. Specifically, + # cx_Oracle will assume a CHAR type when a placeholder ('%s') + # is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use + # 'NULL' for the value, and remove None from the query params. + # See also #10888. + param_idx = self.query.columns.index(field.column) + params = list(self.query.params) + params.pop(param_idx) + self.query.params = tuple(params) + return ph + else: + # Return the common case for the placeholder + return '%s' class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler): pass diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index eba1fd5ca4..4599904c63 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -14,7 +14,7 @@ from django.db.backends.oracle.base import DatabaseOperations from django.contrib.gis.db.backends.base import BaseSpatialOperations from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter from django.contrib.gis.db.backends.util import SpatialFunction -from django.contrib.gis.geometry import Geometry +from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance class SDOOperation(SpatialFunction): @@ -91,7 +91,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): sym_difference = 'SDO_GEOM.SDO_XOR' transform = 'SDO_CS.TRANSFORM' union = 'SDO_GEOM.SDO_UNION' - unionagg = 'SDO_AGGR_UNION' + unionagg = 'SDO_AGGR_UNION' # We want to get SDO Geometries as WKT because it is much easier to # instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings. @@ -128,6 +128,10 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): gis_terms += geometry_functions.keys() gis_terms = dict([(term, None) for term in gis_terms]) + def __init__(self, connection): + super(OracleOperations, self).__init__() + self.connection = connection + def convert_extent(self, clob): if clob: # Generally, Oracle returns a polygon for the extent -- however, @@ -156,7 +160,40 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): else: return None - def get_geom_placeholder(self, value, srid): + def geo_db_type(self, f): + """ + Returns the geometry database type for Oracle. Unlike other spatial + backends, no stored procedure is necessary and it's the same for all + geometry types. + """ + return 'MDSYS.SDO_GEOMETRY' + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters given the value and the lookup type. + On Oracle, geometry columns with a geodetic coordinate system behave + implicitly like a geography column, and thus meters will be used as + the distance parameter on them. + """ + if not value: + return [] + value = value[0] + if isinstance(value, Distance): + if f.geodetic(self.connection): + dist_param = value.m + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + dist_param = value + + # dwithin lookups on oracle require a special string parameter + # that starts with "distance=". + if lookup_type == 'dwithin': + dist_param = 'distance=%s' % dist_param + + return [dist_param] + + def get_geom_placeholder(self, f, value): """ Provides a proper substitution value for Geometries that are not in the SRID of the field. Specifically, this routine will substitute in the @@ -165,26 +202,25 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): if value is None: return 'NULL' - def transform_value(value, srid): - return value.srid != srid + def transform_value(val, srid): + return val.srid != srid if hasattr(value, 'expression'): - if transform_value(value, srid): - placeholder = '%s(%%s, %s)' % (self.transform, srid) + if transform_value(value, f.srid): + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) else: placeholder = '%s' # No geometry value used for F expression, substitue in # the column name instead. return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) else: - if transform_value(value, srid): - return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, srid) + if transform_value(value, f.srid): + return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid) else: - return 'SDO_GEOMETRY(%%s, %s)' % srid + return 'SDO_GEOMETRY(%%s, %s)' % f.srid - def spatial_lookup_sql(self, lvalue, lookup_type, value, field): + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): "Returns the SQL WHERE clause for use in Oracle spatial SQL construction." - qn = self.quote_name alias, col, db_type = lvalue # Getting the quoted table name as `geo_col`. @@ -214,15 +250,15 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): if lookup_type == 'relate': # The SDORelate class handles construction for these queries, # and verifies the mask argument. - return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(geom, field.srid)) + return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(field, geom)) else: # Otherwise, just call the `as_sql` method on the SDOOperation instance. - return sdo_op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid)) + return sdo_op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) else: # Lookup info is a SDOOperation instance, whose `as_sql` method returns # the SQL necessary for the geometry function call. For example: # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' - return lookup_info.as_sql(geo_col, self.get_geom_placeholder(value, field.srid)) + return lookup_info.as_sql(geo_col, self.get_geom_placeholder(field, value)) elif lookup_type == 'isnull': # Handling 'isnull' lookup type return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py index 9afb5e2254..e14c79258d 100644 --- a/django/contrib/gis/db/backends/postgis/creation.py +++ b/django/contrib/gis/db/backends/postgis/creation.py @@ -16,32 +16,43 @@ class PostGISCreation(DatabaseCreation): qn = self.connection.ops.quote_name db_table = model._meta.db_table - output.append(style.SQL_KEYWORD('SELECT ') + - style.SQL_TABLE('AddGeometryColumn') + '(' + - style.SQL_TABLE(gqn(db_table)) + ', ' + - style.SQL_FIELD(gqn(f.column)) + ', ' + - style.SQL_FIELD(str(f.srid)) + ', ' + - style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' + - style.SQL_KEYWORD(str(f.dim)) + ');') + if f.geography: + # Geogrophy columns are created normally. + pass + else: + # Geometry columns are created by `AddGeometryColumn` + # stored procedure. + output.append(style.SQL_KEYWORD('SELECT ') + + style.SQL_TABLE('AddGeometryColumn') + '(' + + style.SQL_TABLE(gqn(db_table)) + ', ' + + style.SQL_FIELD(gqn(f.column)) + ', ' + + style.SQL_FIELD(str(f.srid)) + ', ' + + style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' + + style.SQL_KEYWORD(str(f.dim)) + ');') - if not f.null: - # Add a NOT NULL constraint to the field - output.append(style.SQL_KEYWORD('ALTER TABLE ') + - style.SQL_TABLE(qn(db_table)) + - style.SQL_KEYWORD(' ALTER ') + - style.SQL_FIELD(qn(f.column)) + - style.SQL_KEYWORD(' SET NOT NULL') + ';') + if not f.null: + # Add a NOT NULL constraint to the field + output.append(style.SQL_KEYWORD('ALTER TABLE ') + + style.SQL_TABLE(qn(db_table)) + + style.SQL_KEYWORD(' ALTER ') + + style.SQL_FIELD(qn(f.column)) + + style.SQL_KEYWORD(' SET NOT NULL') + ';') if f.spatial_index: + # Spatial indexes created the same way for both Geometry and + # Geography columns + if f.geography: + index_opts = '' + else: + index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts) output.append(style.SQL_KEYWORD('CREATE INDEX ') + style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) + style.SQL_KEYWORD(' ON ') + style.SQL_TABLE(qn(db_table)) + style.SQL_KEYWORD(' USING ') + style.SQL_COLTYPE(self.geom_index_type) + ' ( ' + - style.SQL_FIELD(qn(f.column)) + ' ' + - style.SQL_KEYWORD(self.geom_index_opts) + ' );') + style.SQL_FIELD(qn(f.column)) + index_opts + ' );') return output def sql_table_creation_suffix(self): diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 4cc0aec256..7b65404d1d 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -1,12 +1,15 @@ import re from decimal import Decimal -from django.db.backends.postgresql.operations import DatabaseOperations +from django.conf import settings from django.contrib.gis.db.backends.base import BaseSpatialOperations from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter -from django.contrib.gis.geometry import Geometry +from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance +from django.core.exceptions import ImproperlyConfigured +from django.db.backends.postgresql.operations import DatabaseOperations +from django.db.backends.postgresql_psycopg2.base import Database #### Classes used in constructing PostGIS spatial SQL #### class PostGISOperator(SpatialOperation): @@ -68,23 +71,48 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): super(PostGISOperations, self).__init__(connection) # Trying to get the PostGIS version because the function - # signatures will depend on the version used. + # signatures will depend on the version used. The cost + # here is a database query to determine the version, which + # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple + # comprising user-supplied values for the major, minor, and + # subminor revision of PostGIS. try: - vtup = self.postgis_version_tuple() - version = vtup[1:] + if hasattr(settings, 'POSTGIS_VERSION'): + vtup = settings.POSTGIS_VERSION + if len(vtup) == 3: + # The user-supplied PostGIS version. + version = vtup + else: + # This was the old documented way, but it's stupid to + # include the string. + version = vtup[1:4] + else: + vtup = self.postgis_version_tuple() + version = vtup[1:] + + # Getting the prefix -- even though we don't officially support + # PostGIS 1.2 anymore, keeping it anyway in case a prefix change + # for something else is necessary. if version >= (1, 2, 2): prefix = 'ST_' else: prefix = '' + self.geom_func_prefix = prefix self.spatial_version = version + except Database.ProgrammingError: + raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". ' + 'GeoDjango requires at least PostGIS version 1.3. ' + 'Was the database created from a spatial database ' + 'template?' % self.connection.settings_dict['NAME'] + ) except Exception, e: - # TODO: Plain raising right now. + # TODO: Raise helpful exceptions as they become known. raise # PostGIS-specific operators. The commented descriptions of these # operators come from Section 7.6 of the PostGIS 1.4 documentation. - self.spatial_operators = { + self.geometry_operators = { # The "&<" operator returns true if A's bounding box overlaps or # is to the left of B's bounding box. 'overlaps_left' : PostGISOperator('&<'), @@ -166,19 +194,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): # Adding the distance functions to the geometries lookup. self.geometry_functions.update(self.distance_functions) - # ST_ContainsProperly and GeoHash serialization added in 1.4. - if version >= (1, 4, 0): - GEOHASH = 'ST_GeoHash' - self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly') - else: - GEOHASH = False - - # Creating a dictionary lookup of all GIS terms for PostGIS. - gis_terms = ['isnull'] - gis_terms += self.spatial_operators.keys() - gis_terms += self.geometry_functions.keys() - self.gis_terms = dict([(term, None) for term in gis_terms]) - # The union aggregate and topology operation use the same signature # in versions 1.3+. if version < (1, 3, 0): @@ -194,7 +209,40 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): else: GEOJSON = prefix + 'AsGeoJson' + # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4. + if version >= (1, 4, 0): + GEOHASH = 'ST_GeoHash' + MAKELINE = 'ST_MakeLine' + BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle' + self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly') + else: + GEOHASH, MAKELINE, BOUNDINGCIRCLE = False, False, False + + # Geography type support added in 1.5. + if version >= (1, 5, 0): + self.geography = True + # Only a subset of the operators and functions are available + # for the geography type. + self.geography_functions = self.distance_functions.copy() + self.geography_functions.update({ + 'coveredby' : self.geometry_functions['coveredby'], + 'covers' : self.geometry_functions['covers'], + 'intersects' : self.geometry_functions['intersects'], + }) + self.geography_operators = { + 'bboverlaps' : PostGISOperator('&&'), + 'exact' : PostGISOperator('~='), + 'same_as' : PostGISOperator('~='), + } + + # Creating a dictionary lookup of all GIS terms for PostGIS. + gis_terms = ['isnull'] + gis_terms += self.geometry_operators.keys() + gis_terms += self.geometry_functions.keys() + self.gis_terms = dict([(term, None) for term in gis_terms]) + self.area = prefix + 'Area' + self.bounding_circle = BOUNDINGCIRCLE self.centroid = prefix + 'Centroid' self.collect = prefix + 'Collect' self.difference = prefix + 'Difference' @@ -212,13 +260,14 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.length = prefix + 'Length' self.length3d = prefix + 'Length3D' self.length_spheroid = prefix + 'length_spheroid' - self.makeline = prefix + 'MakeLine' + self.makeline = MAKELINE self.mem_size = prefix + 'mem_size' self.num_geom = prefix + 'NumGeometries' self.num_points =prefix + 'npoints' self.perimeter = prefix + 'Perimeter' self.perimeter3d = prefix + 'Perimeter3D' self.point_on_surface = prefix + 'PointOnSurface' + self.polygonize = prefix + 'Polygonize' self.scale = prefix + 'Scale' self.snap_to_grid = prefix + 'SnapToGrid' self.svg = prefix + 'AsSVG' @@ -237,16 +286,22 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): return agg_name in self.valid_aggregates def convert_extent(self, box): - # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; - # parsing out and returning as a 4-tuple. + """ + Returns a 4-tuple extent for the `Extent` aggregate by converting + the bounding box text returned by PostGIS (`box` argument), for + example: "BOX(-90.0 30.0, -85.0 40.0)". + """ ll, ur = box[4:-1].split(',') xmin, ymin = map(float, ll.split()) xmax, ymax = map(float, ur.split()) return (xmin, ymin, xmax, ymax) def convert_extent3d(self, box3d): - # Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)"; - # parsing out and returning as a 4-tuple. + """ + Returns a 6-tuple extent for the `Extent3D` aggregate by converting + the 3d bounding-box text returnded by PostGIS (`box3d` argument), for + example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)". + """ ll, ur = box3d[6:-1].split(',') xmin, ymin, zmin = map(float, ll.split()) xmax, ymax, zmax = map(float, ur.split()) @@ -261,17 +316,78 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): else: return None - def get_geom_placeholder(self, value, srid): + def geo_db_type(self, f): + """ + Return the database field type for the given geometry field. + Typically this is `None` because geometry columns are added via + the `AddGeometryColumn` stored procedure, unless the field + has been specified to be of geography type instead. + """ + if f.geography: + if not self.geography: + raise NotImplementedError('PostGIS 1.5 required for geography column support.') + + if f.srid != 4326: + raise NotImplementedError('PostGIS 1.5 supports geography columns ' + 'only with an SRID of 4326.') + + return 'geography(%s,%d)'% (f.geom_type, f.srid) + else: + return None + + def get_distance(self, f, dist_val, lookup_type): + """ + Retrieve the distance parameters for the given geometry field, + distance lookup value, and the distance lookup type. + + This is the most complex implementation of the spatial backends due to + what is supported on geodetic geometry columns vs. what's available on + projected geometry columns. In addition, it has to take into account + the newly introduced geography column type introudced in PostGIS 1.5. + """ + # Getting the distance parameter and any options. + if len(dist_val) == 1: + value, option = dist_val[0], None + else: + value, option = dist_val + + # Shorthand boolean flags. + geodetic = f.geodetic(self.connection) + geography = f.geography and self.geography + + if isinstance(value, Distance): + if geography: + dist_param = value.m + elif geodetic: + if lookup_type == 'dwithin': + raise ValueError('Only numeric values of degree units are ' + 'allowed on geographic DWithin queries.') + dist_param = value.m + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + # Assuming the distance is in the units of the field. + dist_param = value + + if (not geography and geodetic and lookup_type != 'dwithin' + and option == 'spheroid'): + # using distance_spheroid requires the spheroid of the field as + # a parameter. + return [f._spheroid, dist_param] + else: + return [dist_param] + + def get_geom_placeholder(self, f, value): """ Provides a proper substitution value for Geometries that are not in the SRID of the field. Specifically, this routine will substitute in the ST_Transform() function call. """ - if value is None or value.srid == srid: + if value is None or value.srid == f.srid: placeholder = '%s' else: # Adding Transform() to the SQL placeholder. - placeholder = '%s(%%s, %s)' % (self.transform, srid) + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) if hasattr(value, 'expression'): # If this is an F expression, then we don't really want @@ -290,7 +406,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): cursor.execute('SELECT %s()' % func) row = cursor.fetchone() except: - # TODO: raise helpful exception here. + # Responsibility of callers to perform error handling. raise finally: cursor.close() @@ -334,32 +450,42 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): return (version, major, minor1, minor2) - def num_params(self, lookup_type, val): - def exactly_two(val): return val == 2 - def two_to_three(val): return val >= 2 and val <=3 + def num_params(self, lookup_type, num_param): + """ + Helper routine that returns a boolean indicating whether the number of + parameters is correct for the lookup type. + """ + def exactly_two(np): return np == 2 + def two_to_three(np): return np >= 2 and np <=3 if (lookup_type in self.distance_functions and lookup_type != 'dwithin'): - return two_to_three(val) + return two_to_three(num_param) else: - return exactly_two(val) + return exactly_two(num_param) - def spatial_lookup_sql(self, lvalue, lookup_type, value, field): + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): """ Constructs spatial SQL from the given lookup value tuple a (alias, col, db_type), the lookup type string, lookup value, and the geometry field. """ - qn = self.quote_name alias, col, db_type = lvalue # Getting the quoted geometry column. geo_col = '%s.%s' % (qn(alias), qn(col)) - if lookup_type in self.spatial_operators: + if lookup_type in self.geometry_operators: + if field.geography and not lookup_type in self.geography_operators: + raise ValueError('PostGIS geography does not support the ' + '"%s" lookup.' % lookup_type) # Handling a PostGIS operator. - op = self.spatial_operators[lookup_type] - return op.as_sql(geo_col, self.get_geom_placeholder(value, field.srid)) + op = self.geometry_operators[lookup_type] + return op.as_sql(geo_col, self.get_geom_placeholder(field, value)) elif lookup_type in self.geometry_functions: + if field.geography and not lookup_type in self.geography_functions: + raise ValueError('PostGIS geography type does not support the ' + '"%s" lookup.' % lookup_type) + # See if a PostGIS geometry function matches the lookup type. tmp = self.geometry_functions[lookup_type] @@ -392,7 +518,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): if lookup_type == 'relate': op = op(self.geom_func_prefix, value[1]) elif lookup_type in self.distance_functions and lookup_type != 'dwithin': - if field.geodetic(self.connection): + if not field.geography and field.geodetic(self.connection): # Geodetic distances are only availble from Points to PointFields. if field.geom_type != 'POINT': raise ValueError('PostGIS spherical operations are only valid on PointFields.') @@ -412,7 +538,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): geom = value # Calling the `as_sql` function on the operation instance. - return op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid)) + return op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) elif lookup_type == 'isnull': # Handling 'isnull' lookup type diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index d3542ae82c..7b4e4364ad 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -4,7 +4,7 @@ from decimal import Decimal from django.contrib.gis.db.backends.base import BaseSpatialOperations from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter -from django.contrib.gis.geometry import Geometry +from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance from django.core.exceptions import ImproperlyConfigured from django.db.backends.sqlite3.base import DatabaseOperations @@ -119,11 +119,17 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): try: vtup = self.spatialite_version_tuple() version = vtup[1:] - self.spatial_version = version if version < (2, 3, 1): - raise Exception('GeoDjango only supports SpatiaLite versions 2.3.1+') - except Exception, e: + raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions ' + '2.3.1 and above') + self.spatial_version = version + except ImproperlyConfigured: raise + except Exception, msg: + raise ImproperlyConfigured('Cannot determine the SpatiaLite version for the "%s" ' + 'database (error was "%s"). Was the SpatiaLite initialization ' + 'SQL loaded on this database?' % + (self.connection.settings_dict['NAME'], msg)) # Creating the GIS terms dictionary. gis_terms = ['isnull'] @@ -147,7 +153,36 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): else: return None - def get_geom_placeholder(self, value, srid): + def geo_db_type(self, f): + """ + Returns None because geometry columnas are added via the + `AddGeometryColumn` stored procedure on SpatiaLite. + """ + return None + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters for the given geometry field, + lookup value, and lookup type. SpatiaLite only supports regular + cartesian-based queries (no spheroid/sphere calculations for point + geometries like PostGIS). + """ + if not value: + return [] + value = value[0] + if isinstance(value, Distance): + if f.geodetic(self.connection): + raise ValueError('SpatiaLite does not support distance queries on ' + 'geometry fields with a geodetic coordinate system. ' + 'Distance objects; use a numeric value of your ' + 'distance in degrees instead.') + else: + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + dist_param = value + return [dist_param] + + def get_geom_placeholder(self, f, value): """ Provides a proper substitution value for Geometries that are not in the SRID of the field. Specifically, this routine will substitute in the @@ -156,19 +191,19 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): def transform_value(value, srid): return not (value is None or value.srid == srid) if hasattr(value, 'expression'): - if transform_value(value, srid): - placeholder = '%s(%%s, %s)' % (self.transform, srid) + if transform_value(value, f.srid): + placeholder = '%s(%%s, %s)' % (self.transform, f.srid) else: placeholder = '%s' # No geometry value used for F expression, substitue in # the column name instead. return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) else: - if transform_value(value, srid): + if transform_value(value, f.srid): # Adding Transform() to the SQL placeholder. - return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, srid) + return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, f.srid) else: - return '%s(%%s,%s)' % (self.from_text, srid) + return '%s(%%s,%s)' % (self.from_text, f.srid) def _get_spatialite_func(self, func): """ @@ -229,13 +264,12 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): sql_function = getattr(self, agg_name) return sql_template, sql_function - def spatial_lookup_sql(self, lvalue, lookup_type, value, field): + def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn): """ Returns the SpatiaLite-specific SQL for the given lookup value [a tuple of (alias, column, db_type)], lookup type, lookup value, and the model field. """ - qn = self.quote_name alias, col, db_type = lvalue # Getting the quoted field as `geo_col`. @@ -278,7 +312,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): op = tmp geom = value # Calling the `as_sql` function on the operation instance. - return op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid)) + return op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) elif lookup_type == 'isnull': # Handling 'isnull' lookup type return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 729b0cf354..0a4c46d4ee 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -1,7 +1,7 @@ from django.db.models.fields import Field from django.contrib.gis import forms from django.contrib.gis.db.models.proxy import GeometryProxy -from django.contrib.gis.geometry import Geometry, GeometryException +from django.contrib.gis.geometry.backend import Geometry, GeometryException from django.contrib.gis.measure import Distance from django.db.models.sql.expressions import SQLEvaluator @@ -40,8 +40,8 @@ def get_srid_info(srid, connection): return _srid_cache[name][srid] -class GeometryField(SpatialBackend.Field): - """The base GIS field -- maps to the OpenGIS Specification Geometry type.""" +class GeometryField(Field): + "The base GIS field -- maps to the OpenGIS Specification Geometry type." # The OpenGIS Geometry name. geom_type = 'GEOMETRY' @@ -50,7 +50,7 @@ class GeometryField(SpatialBackend.Field): geodetic_units = ('Decimal Degree', 'degree') def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, - **kwargs): + geography=False, **kwargs): """ The initialization function for geometry fields. Takes the following as keyword arguments: @@ -67,8 +67,14 @@ class GeometryField(SpatialBackend.Field): dim: The number of dimensions for this geometry. Defaults to 2. - Oracle-specific keywords: - extent, tolerance. + extent: + Customize the extent, in a 4-tuple of WGS 84 coordinates, for the + geometry field entry in the `USER_SDO_GEOM_METADATA` table. Defaults + to (-180.0, -90.0, 180.0, 90.0). + + tolerance: + Define the tolerance, in meters, to use for the geometry field + entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05. """ # Setting the index flag with the value of the `spatial_index` keyword. @@ -85,6 +91,9 @@ class GeometryField(SpatialBackend.Field): # first parameter, so this works like normal fields. kwargs['verbose_name'] = verbose_name + # Is this a geography rather than a geometry column? + self.geography = geography + # Oracle-specific private attributes for creating the entrie in # `USER_SDO_GEOM_METADATA` self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0)) @@ -121,17 +130,13 @@ class GeometryField(SpatialBackend.Field): """ return self.units_name(connection) in self.geodetic_units - def get_distance(self, dist_val, lookup_type, connection): + def get_distance(self, value, lookup_type, connection): """ Returns a distance number in units of the field. For example, if `D(km=1)` was passed in and the units of the field were in meters, then 1000 would be returned. """ - # Getting the distance parameter and any options. - if len(dist_val) == 1: - dist, option = dist_val[0], None - else: - dist, option = dist_val + return connection.ops.get_distance(self, value, lookup_type) if isinstance(dist, Distance): if self.geodetic(connection): @@ -149,7 +154,7 @@ class GeometryField(SpatialBackend.Field): if connection.ops.oracle and lookup_type == 'dwithin': dist_param = 'distance=%s' % dist_param - + if connection.ops.postgis and self.geodetic(connection) and lookup_type != 'dwithin' and option == 'spheroid': # On PostGIS, by default `ST_distance_sphere` is used; but if the # accuracy of `ST_distance_spheroid` is needed than the spheroid @@ -179,11 +184,11 @@ class GeometryField(SpatialBackend.Field): # from the given string input. if isinstance(geom, Geometry): pass - elif isinstance(geom, basestring): + elif isinstance(geom, basestring) or hasattr(geom, '__geo_interface__'): try: geom = Geometry(geom) except GeometryException: - raise ValueError('Could not create geometry from lookup value: %s' % str(value)) + raise ValueError('Could not create geometry from lookup value.') else: raise ValueError('Cannot use parameter of `%s` type as lookup parameter.' % type(value)) @@ -217,17 +222,7 @@ class GeometryField(SpatialBackend.Field): setattr(cls, self.attname, GeometryProxy(Geometry, self)) def db_type(self, connection): - if (connection.ops.postgis or - connection.ops.spatialite): - # Geometry columns on these spatial backends are initialized via - # the `AddGeometryColumn` stored procedure. - return None - elif connection.ops.mysql: - return self.geom_type - elif connection.ops.oracle: - return 'MDSYS.SDO_GEOMETRY' - else: - raise NotImplementedError + return connection.ops.geo_db_type(self) def formfield(self, **kwargs): defaults = {'form_class' : forms.GeometryField, @@ -240,7 +235,11 @@ class GeometryField(SpatialBackend.Field): def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): """ - XXX: Document me. + Prepare for the database lookup, and return any spatial parameters + necessary for the query. This includes wrapping any geometry + parameters with a backend-specific adapter and formatting any distance + parameters into the correct units for the coordinate system of the + field. """ if lookup_type in connection.ops.gis_terms: # special case for isnull lookup @@ -254,8 +253,6 @@ class GeometryField(SpatialBackend.Field): if lookup_type in connection.ops.distance_functions: # Getting the distance parameter in the units of the field. params += self.get_distance(value[1:], lookup_type, connection) - elif lookup_type in connection.ops.limited_where: - pass else: params += value[1:] elif isinstance(value, SQLEvaluator): @@ -281,33 +278,31 @@ class GeometryField(SpatialBackend.Field): return connection.ops.Adapter(self.get_prep_value(value)) def get_placeholder(self, value, connection): - return connection.ops.get_geom_placeholder(value, self.srid) + """ + Returns the placeholder for the geometry column for the + given value. + """ + return connection.ops.get_geom_placeholder(self, value) # The OpenGIS Geometry Type Fields class PointField(GeometryField): - """Point""" geom_type = 'POINT' class LineStringField(GeometryField): - """Line string""" geom_type = 'LINESTRING' class PolygonField(GeometryField): - """Polygon""" geom_type = 'POLYGON' class MultiPointField(GeometryField): - """Multi-point""" geom_type = 'MULTIPOINT' class MultiLineStringField(GeometryField): - """Multi-line string""" geom_type = 'MULTILINESTRING' class MultiPolygonField(GeometryField): - """Multi polygon""" geom_type = 'MULTIPOLYGON' class GeometryCollectionField(GeometryField): - """Geometry collection""" geom_type = 'GEOMETRYCOLLECTION' + diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index d3d7f6be97..b39d652d0d 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -1,6 +1,5 @@ from django.db.models.manager import Manager from django.contrib.gis.db.models.query import GeoQuerySet -from django.contrib.gis.db.models.sql.subqueries import insert_query class GeoManager(Manager): "Overrides Manager to return Geographic QuerySets." @@ -54,7 +53,7 @@ class GeoManager(Manager): def make_line(self, *args, **kwargs): return self.get_query_set().make_line(*args, **kwargs) - + def mem_size(self, *args, **kwargs): return self.get_query_set().mem_size(*args, **kwargs) @@ -93,6 +92,3 @@ class GeoManager(Manager): def unionagg(self, *args, **kwargs): return self.get_query_set().unionagg(*args, **kwargs) - - def _insert(self, values, **kwargs): - return insert_query(self.model, values, **kwargs) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 9456d66713..8f25c13e89 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -4,7 +4,7 @@ from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQueryS from django.contrib.gis.db.models import aggregates from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode -from django.contrib.gis.geometry import Geometry +from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Area, Distance class GeoQuerySet(QuerySet): @@ -542,6 +542,7 @@ class GeoQuerySet(QuerySet): # units of the geometry field. connection = connections[self.db] geodetic = geo_field.geodetic(connection) + geography = geo_field.geography if geodetic: dist_att = 'm' @@ -569,7 +570,8 @@ class GeoQuerySet(QuerySet): # keyword or when calculating the length of geodetic field, make # sure the 'spheroid' distance setting string is passed in so we # get the correct spatial stored procedure. - if spheroid or (backend.postgis and geodetic and length): + if spheroid or (backend.postgis and geodetic and + (not geography) and length): lookup_params.append('spheroid') lookup_params = geo_field.get_prep_value(lookup_params) params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection) @@ -625,7 +627,7 @@ class GeoQuerySet(QuerySet): # `transform()` was not used on this GeoQuerySet. procedure_fmt = '%(geo_col)s,%(geom)s' - if geodetic: + if not geography and geodetic: # Spherical distance calculation is needed (because the geographic # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() # procedures may only do queries from point columns to point geometries @@ -644,7 +646,7 @@ class GeoQuerySet(QuerySet): procedure_args.update({'function' : backend.distance_sphere}) elif length or perimeter: procedure_fmt = '%(geo_col)s' - if geodetic and length: + if not geography and geodetic and length: # There's no `length_sphere`, and `length_spheroid` also # works on 3D geometries. procedure_fmt += ",'%(spheroid)s'" diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index ed3ca9d9ff..c7341db757 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -5,7 +5,7 @@ from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.models.sql import aggregates as gis_aggregates from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField from django.contrib.gis.db.models.sql.where import GeoWhereNode -from django.contrib.gis.geometry import Geometry +from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Area, Distance diff --git a/django/contrib/gis/db/models/sql/subqueries.py b/django/contrib/gis/db/models/sql/subqueries.py deleted file mode 100644 index 21185a2967..0000000000 --- a/django/contrib/gis/db/models/sql/subqueries.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.db import connections -from django.db.models.sql.subqueries import InsertQuery - -class GeoInsertQuery(InsertQuery): - def insert_values(self, insert_values, raw_values=False): - """ - Set up the insert query from the 'insert_values' dictionary. The - dictionary gives the model field names and their target values. - - If 'raw_values' is True, the values in the 'insert_values' dictionary - are inserted directly into the query, rather than passed as SQL - parameters. This provides a way to insert NULL and DEFAULT keywords - into the query, for example. - """ - placeholders, values = [], [] - for field, val in insert_values: - placeholders.append((field, val)) - self.columns.append(field.column) - - if not placeholders[-1] == 'NULL': - values.append(val) - if raw_values: - self.values.extend([(None, v) for v in values]) - else: - self.params += tuple(values) - self.values.extend(placeholders) - -def insert_query(model, values, return_id=False, raw_values=False, using=None): - """ - Inserts a new record for the given model. This provides an interface to - the InsertQuery class and is how Model.save() is implemented. It is not - part of the public API. - """ - query = GeoInsertQuery(model) - query.insert_values(values, raw_values) - return query.get_compiler(using=using).execute_sql(return_id) diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index ba93c951be..17c210bafb 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -44,7 +44,7 @@ class GeoWhereNode(WhereNode): lvalue, lookup_type, value_annot, params_or_value = child if isinstance(lvalue, GeoConstraint): data, params = lvalue.process(lookup_type, params_or_value, connection) - spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field) + spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn) return spatial_sql, params else: return super(GeoWhereNode, self).make_atom(child, qn, connection) @@ -52,7 +52,7 @@ class GeoWhereNode(WhereNode): @classmethod def _check_geo_field(cls, opts, lookup): """ - Utility for checking the given lookup with the given model options. + Utility for checking the given lookup with the given model options. The lookup is a string either specifying the geographic field, e.g. 'point, 'the_geom', or a related lookup on a geographic field like 'address__point'. @@ -74,7 +74,7 @@ class GeoWhereNode(WhereNode): # If the field list is still around, then it means that the # lookup was for a geometry field across a relationship -- # thus we keep on getting the related model options and the - # model field associated with the next field in the list + # model field associated with the next field in the list # until there's no more left. while len(field_list): opts = geo_fld.rel.to._meta diff --git a/django/contrib/gis/geometry/__init__.py b/django/contrib/gis/geometry/__init__.py index 31bed79ece..e69de29bb2 100644 --- a/django/contrib/gis/geometry/__init__.py +++ b/django/contrib/gis/geometry/__init__.py @@ -1,9 +0,0 @@ -from django.conf import settings - -__all__ = ['Geometry', 'GeometryException'] - -from django.contrib.gis.geos import GEOSGeometry, GEOSException - -Geometry = GEOSGeometry -GeometryException = GEOSException - diff --git a/django/contrib/gis/geometry/backend/__init__.py b/django/contrib/gis/geometry/backend/__init__.py new file mode 100644 index 0000000000..d79a5563ed --- /dev/null +++ b/django/contrib/gis/geometry/backend/__init__.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.importlib import import_module + +geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos') + +try: + module = import_module('.%s' % geom_backend, 'django.contrib.gis.geometry.backend') +except ImportError, e: + try: + module = import_module(geom_backend) + except ImportError, e_user: + raise ImproperlyConfigured('Could not import user-defined GEOMETRY_BACKEND ' + '"%s".' % geom_backend) + +try: + Geometry = module.Geometry + GeometryException = module.GeometryException +except AttributeError: + raise ImproperlyConfigured('Cannot import Geometry from the "%s" ' + 'geometry backend.' % geom_backend) diff --git a/django/contrib/gis/geometry/backend/geos.py b/django/contrib/gis/geometry/backend/geos.py new file mode 100644 index 0000000000..a1ac096b63 --- /dev/null +++ b/django/contrib/gis/geometry/backend/geos.py @@ -0,0 +1,3 @@ +from django.contrib.gis.geos import \ + GEOSGeometry as Geometry, \ + GEOSException as GeometryException diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index b619c0beda..8ebe871e6d 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,7 +1,7 @@ import os, unittest from django.contrib.gis.geos import * from django.contrib.gis.db.models import Collect, Count, Extent, F, Union -from django.contrib.gis.geometry import Geometry +from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.tests.utils import mysql, oracle, postgis, spatialite, no_mysql, no_oracle, no_spatialite from django.conf import settings from models import City, Location, DirectoryEntry, Parcel, Book, Author