From ffcc38ebe819fe8f46dd4a0941d202fcde5adb1d Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sun, 29 Jul 2007 19:58:24 +0000 Subject: [PATCH] gis: created backend module, that will (in the future) allow for support of different spatial databases; improved postgis-specific code in preparation for PostGIS 1.2.2 (and beyond). git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@5776 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../postgis.py => backend/__init__.py} | 109 ++++----------- .../gis/db/backend/postgis/__init__.py | 7 + .../backend/postgis/creation.py} | 0 .../contrib/gis/db/backend/postgis/field.py | 107 +++++++++++++++ .../gis/db/backend/postgis/management.py | 36 +++++ .../contrib/gis/db/backend/postgis/query.py | 127 ++++++++++++++++++ .../contrib/gis/db/models/fields/__init__.py | 111 +-------------- django/contrib/gis/db/models/query.py | 2 +- django/contrib/gis/tests/__init__.py | 2 +- django/contrib/gis/tests/geoapp/tests.py | 2 +- 10 files changed, 305 insertions(+), 198 deletions(-) rename django/contrib/gis/db/{models/postgis.py => backend/__init__.py} (73%) create mode 100644 django/contrib/gis/db/backend/postgis/__init__.py rename django/contrib/gis/{utils/spatial_db.py => db/backend/postgis/creation.py} (100%) create mode 100644 django/contrib/gis/db/backend/postgis/field.py create mode 100644 django/contrib/gis/db/backend/postgis/management.py create mode 100644 django/contrib/gis/db/backend/postgis/query.py diff --git a/django/contrib/gis/db/models/postgis.py b/django/contrib/gis/db/backend/__init__.py similarity index 73% rename from django/contrib/gis/db/models/postgis.py rename to django/contrib/gis/db/backend/__init__.py index fe17b12499..afda586dea 100644 --- a/django/contrib/gis/db/models/postgis.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -1,92 +1,29 @@ -# This module is meant to re-define the helper routines used by the -# django.db.models.query objects to be customized for PostGIS. +""" + This module provides the backend for spatial SQL construction with Django. + + Specifically, this module will import the correct routines and modules + needed for GeoDjango + + (1) GeoBackEndField, a base class needed for GeometryField. + (2) The parse_lookup() function, used for spatial SQL construction by + the GeoQuerySet. + + Currently only PostGIS is supported, but someday backends will be aded for + additional spatial databases. +""" +from django.conf import settings from django.db import backend from django.db.models.query import LOOKUP_SEPARATOR, field_choices, find_field, FieldFound, QUERY_TERMS, get_where_clause from django.utils.datastructures import SortedDict -# PostGIS-specific operators. The commented descriptions of these -# operators come from Section 6.2.2 of the official PostGIS documentation. -POSTGIS_OPERATORS = { - # The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. - 'overlaps_left' : '&< %s', - # The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. - 'overlaps_right' : '&> %s', - # The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. - 'left' : '<< %s', - # The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box. - 'right' : '>> %s', - # The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box. - 'overlaps_below' : '&<| %s', - # The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box. - 'overlaps_above' : '|&> %s', - # The "<<|" operator returns true if A's bounding box is strictly below B's bounding box. - 'strictly_below' : '<<| %s', - # The "|>>" operator returns true if A's bounding box is strictly above B's bounding box. - 'strictly_above' : '|>> %s', - # The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if - # A and B are the same feature, vertex-by-vertex, the operator returns true. - 'same_as' : '~= %s', - 'exact' : '~= %s', - # The "@" operator returns true if A's bounding box is completely contained by B's bounding box. - 'contained' : '@ %s', - # The "~" operator returns true if A's bounding box completely contains B's bounding box. - 'bbcontains' : '~ %s', - # The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the - # operator returns true. - 'bboverlaps' : '&& %s', - } - -# PostGIS Geometry Functions -- most of these use GEOS. -POSTGIS_GEOMETRY_FUNCTIONS = { - #'distance' : 'Distance', -- doesn't work right now. - 'equals' : 'Equals', - 'disjoint' : 'Disjoint', - 'touches' : 'Touches', - 'crosses' : 'Crosses', - 'within' : 'Within', - 'overlaps' : 'Overlaps', - 'contains' : 'Contains', - 'intersects' : 'Intersects', - 'relate' : 'Relate', - } - -# Any other lookup types that do not require a mapping. -MISC_TERMS = ['isnull'] - -# The quotation used for postgis (uses single quotes). -def quotename(value, dbl=False): - if dbl: return '"%s"' % value - else: return "'%s'" % value - -# These are the PostGIS-customized QUERY_TERMS, combines both the operators -# and the geometry functions. -POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first -POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometry Functions -POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') -POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable - -def get_geo_where_clause(lookup_type, table_prefix, field_name, value): - if table_prefix.endswith('.'): - table_prefix = backend.quote_name(table_prefix[:-1])+'.' - field_name = backend.quote_name(field_name) - - # See if a PostGIS operator matches the lookup type first - try: - return '%s%s %s' % (table_prefix, field_name, (POSTGIS_OPERATORS[lookup_type] % '%s')) - except KeyError: - pass - - # See if a PostGIS Geometry function matches the lookup type next - try: - return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name) - except KeyError: - pass - - # For any other lookup type - if lookup_type == 'isnull': - return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) - - raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) +if settings.DATABASE_ENGINE == 'postgresql_psycopg2': + # PostGIS is the spatial database, getting the rquired modules, renaming as necessary. + from postgis import \ + PostGISField as GeoBackendField, \ + POSTGIS_TERMS as GIS_TERMS, \ + create_spatial_db, get_geo_where_clause +else: + raise NotImplementedError, 'No Geographic Backend exists for %s' % settings.DATABASE_NAME #### query.py overloaded functions #### # parse_lookup() and lookup_inner() are modified from their django/db/models/query.py @@ -130,7 +67,7 @@ def parse_lookup(kwarg_items, opts): if lookup_type == 'pk': lookup_type = 'exact' path.append(None) - elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in POSTGIS_TERMS)): + elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in GIS_TERMS)): path.append(lookup_type) lookup_type = 'exact' diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py new file mode 100644 index 0000000000..0564ad7348 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -0,0 +1,7 @@ +""" + The PostGIS spatial database backend module. +""" +from query import get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS +from creation import create_spatial_db +from field import PostGISField + diff --git a/django/contrib/gis/utils/spatial_db.py b/django/contrib/gis/db/backend/postgis/creation.py similarity index 100% rename from django/contrib/gis/utils/spatial_db.py rename to django/contrib/gis/db/backend/postgis/creation.py diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py new file mode 100644 index 0000000000..09bc4e642b --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -0,0 +1,107 @@ +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.geos import GEOSGeometry, GEOSException +from types import StringType +from query import POSTGIS_TERMS, quotename + +class PostGISField(Field): + def _add_geom(self, style, db_table): + """Constructs the addition of the geometry to the table using the + AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure. + + Takes the style object (provides syntax highlighting) and the + database table as parameters. + """ + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_TABLE('AddGeometryColumn') + '(' + \ + style.SQL_TABLE(quotename(db_table)) + ', ' + \ + style.SQL_FIELD(quotename(self.column)) + ', ' + \ + style.SQL_FIELD(str(self._srid)) + ', ' + \ + style.SQL_COLTYPE(quotename(self._geom)) + ', ' + \ + style.SQL_KEYWORD(str(self._dim)) + ');' + + if not self.null: + # Add a NOT NULL constraint to the field + sql += '\n' + \ + style.SQL_KEYWORD('ALTER TABLE ') + \ + style.SQL_TABLE(quotename(db_table, dbl=True)) + \ + style.SQL_KEYWORD(' ALTER ') + \ + style.SQL_FIELD(quotename(self.column, dbl=True)) + \ + style.SQL_KEYWORD(' SET NOT NULL') + ';' + return sql + + def _geom_index(self, style, db_table, + index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): + "Creates a GiST index for this geometry field." + sql = style.SQL_KEYWORD('CREATE INDEX ') + \ + style.SQL_TABLE(quotename('%s_%s_id' % (db_table, self.column), dbl=True)) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(quotename(db_table, dbl=True)) + \ + style.SQL_KEYWORD(' USING ') + \ + style.SQL_COLTYPE(index_type) + ' ( ' + \ + style.SQL_FIELD(quotename(self.column, dbl=True)) + ' ' + \ + style.SQL_KEYWORD(index_opts) + ' );' + return sql + + def _post_create_sql(self, style, db_table): + """Returns SQL that will be executed after the model has been + created. Geometry columns must be added after creation with the + PostGIS AddGeometryColumn() function.""" + + # Getting the AddGeometryColumn() SQL necessary to create a PostGIS + # geometry field. + post_sql = self._add_geom(style, db_table) + + # If the user wants to index this data, then get the indexing SQL as well. + if self._index: + return '%s\n%s' % (post_sql, self._geom_index(style, db_table)) + else: + return post_sql + + def _post_delete_sql(self, style, db_table): + "Drops the geometry column." + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ + style.SQL_TABLE(quotename(db_table)) + ', ' + \ + style.SQL_FIELD(quotename(self.column)) + ');' + return sql + + def get_db_prep_lookup(self, lookup_type, value): + "Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value." + if lookup_type in POSTGIS_TERMS: + if lookup_type == 'isnull': return [value] # special case for NULL geometries. + if not bool(value): return [None] # If invalid value passed in. + if isinstance(value, GEOSGeometry): + # GEOSGeometry instance passed in. + if value.srid != self._srid: + # Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key + # to the where parameters, since we need to transform the geometry in the query. + return {'where' : "Transform(%s,%s)", + 'params' : [value, self._srid] + } + else: + # Just return the GEOSGeometry, it has its own psycopg2 adaptor. + return [value] + elif isinstance(value, StringType): + # String instance passed in, assuming WKT. + # TODO: Any validation needed here to prevent SQL injection? + return ["SRID=%d;%s" % (self._srid, value)] + else: + raise TypeError("Invalid type (%s) used for field lookup value." % str(type(value))) + else: + raise TypeError("Field has invalid lookup: %s" % lookup_type) + + def get_db_prep_save(self, value): + "Prepares the value for saving in the database." + if not bool(value): return None + if isinstance(value, GEOSGeometry): + return value + else: + return ("SRID=%d;%s" % (self._srid, wkt)) + + def get_placeholder(self, value): + "Provides a proper substitution value for " + if isinstance(value, GEOSGeometry) and value.srid != self._srid: + # Adding Transform() to the SQL placeholder. + return 'Transform(%%s, %s)' % self._srid + else: + return '%s' diff --git a/django/contrib/gis/db/backend/postgis/management.py b/django/contrib/gis/db/backend/postgis/management.py new file mode 100644 index 0000000000..33ff44066f --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/management.py @@ -0,0 +1,36 @@ +""" + This utility module is for obtaining information about the PostGIS installation. + + See PostGIS docs at Ch. 6.2.1 for more information on these functions. +""" + +def _get_postgis_func(func): + "Helper routine for calling PostGIS functions and returning their result." + from django.db import connection + cursor = connection.cursor() + cursor.execute('SELECT %s()' % func) + row = cursor.fetchone() + cursor.close() + return row[0] + +def postgis_geos_version(): + "Returns the version of the GEOS library used with PostGIS." + return _get_postgis_func('postgis_geos_version') + +def postgis_lib_version(): + "Returns the version number of the PostGIS library used with PostgreSQL." + return _get_postgis_func('postgis_lib_version') + +def postgis_proj_version(): + "Returns the version of the PROJ.4 library used with PostGIS." + return _get_postgis_func('postgis_proj_version') + +def postgis_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_version') + +def postgis_full_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_full_version') + + diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py new file mode 100644 index 0000000000..3e80d285f8 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -0,0 +1,127 @@ +""" + This module contains the spatial lookup types, and the get_geo_where_clause() + routine for PostGIS. +""" +from django.db import backend +from management import postgis_lib_version + +# Getting the PostGIS version +POSTGIS_VERSION = postgis_lib_version() +MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, POSTGIS_VERSION.split('.')) + +# The supported PostGIS versions. +# TODO: Confirm tests with PostGIS versions 1.1.x -- should work. Versions <= 1.0.x didn't use GEOS C API. +if MAJOR_VERSION != 1 or MINOR_VERSION1 <= 1: + raise Exception, 'PostGIS version %s not supported.' % POSTGIS_VERSION + +# PostGIS-specific operators. The commented descriptions of these +# operators come from Section 6.2.2 of the official PostGIS documentation. +POSTGIS_OPERATORS = { + # The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. + 'overlaps_left' : '&<', + # The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. + 'overlaps_right' : '&>', + # The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. + 'left' : '<<', + # The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box. + 'right' : '>>', + # The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box. + 'overlaps_below' : '&<|', + # The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box. + 'overlaps_above' : '|&>', + # The "<<|" operator returns true if A's bounding box is strictly below B's bounding box. + 'strictly_below' : '<<|', + # The "|>>" operator returns true if A's bounding box is strictly above B's bounding box. + 'strictly_above' : '|>>', + # The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if + # A and B are the same feature, vertex-by-vertex, the operator returns true. + 'same_as' : '~=', + 'exact' : '~=', + # The "@" operator returns true if A's bounding box is completely contained by B's bounding box. + 'contained' : '@', + # The "~" operator returns true if A's bounding box completely contains B's bounding box. + 'bbcontains' : '~', + # The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the + # operator returns true. + 'bboverlaps' : '&&', + } + +# PostGIS Geometry Relationship Functions -- most of these use GEOS. +# +# For PostGIS >= 1.2.2 these routines will do a bounding box query first before calling +# the more expensive GEOS routines (called 'inline index magic'). +# +POSTGIS_GEOMETRY_FUNCTIONS = { + 'equals' : '%sEquals', + 'disjoint' : '%sDisjoint', + 'touches' : '%sTouches', + 'crosses' : '%sCrosses', + 'within' : '%sWithin', + 'overlaps' : '%sOverlaps', + 'contains' : '%sContains', + 'intersects' : '%sIntersects', + 'relate' : ('%sRelate', str), + } + +# Versions of PostGIS >= 1.2.2 changed their naming convention to be 'SQL-MM-centric'. +# Practically, this means that 'ST_' is appended to geometry function names. +if MINOR_VERSION1 >= 2 and MINOR_VERSION2 >= 2: + # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2. + POSTGIS_GEOMETRY_FUNCTIONS.update( + {'dwithin' : ('%sDWithin', float), + 'coveredby' : '%sCoveredBy', + 'covers' : '%sCovers', + } + ) + GEOM_FUNC_PREFIX = 'ST_' +else: + GEOM_FUNC_PREFIX = '' + +# Updating with the geometry function prefix. +for k, v in POSTGIS_GEOMETRY_FUNCTIONS.items(): + if isinstance(v, tuple): + v = list(v) + v[0] = v[0] % GEOM_FUNC_PREFIX + v = tuple(v) + else: + v = v % GEOM_FUNC_PREFIX + POSTGIS_GEOMETRY_FUNCTIONS[k] = v + +# Any other lookup types that do not require a mapping. +MISC_TERMS = ['isnull'] + +# The quotation used for postgis (uses single quotes). +def quotename(value, dbl=False): + if dbl: return '"%s"' % value + else: return "'%s'" % value + +# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types +# allowed for geographic queries. +POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first +POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometry Functions +POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') +POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable + +def get_geo_where_clause(lookup_type, table_prefix, field_name, value): + "Returns the SQL WHERE clause for use in PostGIS SQL construction." + if table_prefix.endswith('.'): + table_prefix = backend.quote_name(table_prefix[:-1])+'.' + field_name = backend.quote_name(field_name) + + # See if a PostGIS operator matches the lookup type first + try: + return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type]) + except KeyError: + pass + + # See if a PostGIS Geometry function matches the lookup type next + try: + return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name) + except KeyError: + pass + + # Handling 'isnull' lookup type + if lookup_type == 'isnull': + return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) + + raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index d27208dffe..e004d3bbea 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,15 +1,10 @@ -# The Django base Field class. -from django.db.models.fields import Field +from django.contrib.gis.db.backend import GeoBackendField # depends on the spatial database backend. from django.contrib.gis.db.models.proxy import GeometryProxy -from django.contrib.gis.db.models.postgis import POSTGIS_TERMS, quotename from django.contrib.gis.oldforms import WKTField from django.utils.functional import curry -from django.contrib.gis.geos import GEOSGeometry, GEOSException -from types import StringType #TODO: Flesh out widgets. - -class GeometryField(Field): +class GeometryField(GeoBackendField): "The base GIS field -- maps to the OpenGIS Specification Geometry type." # The OpenGIS Geometry name. @@ -33,67 +28,6 @@ class GeometryField(Field): self._dim = dim super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function - def _add_geom(self, style, db_table): - """Constructs the addition of the geometry to the table using the - AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure. - - Takes the style object (provides syntax highlighting) and the - database table as parameters. - """ - sql = style.SQL_KEYWORD('SELECT ') + \ - style.SQL_TABLE('AddGeometryColumn') + '(' + \ - style.SQL_TABLE(quotename(db_table)) + ', ' + \ - style.SQL_FIELD(quotename(self.column)) + ', ' + \ - style.SQL_FIELD(str(self._srid)) + ', ' + \ - style.SQL_COLTYPE(quotename(self._geom)) + ', ' + \ - style.SQL_KEYWORD(str(self._dim)) + ');' - - if not self.null: - # Add a NOT NULL constraint to the field - sql += '\n' + \ - style.SQL_KEYWORD('ALTER TABLE ') + \ - style.SQL_TABLE(quotename(db_table, dbl=True)) + \ - style.SQL_KEYWORD(' ALTER ') + \ - style.SQL_FIELD(quotename(self.column, dbl=True)) + \ - style.SQL_KEYWORD(' SET NOT NULL') + ';' - return sql - - def _geom_index(self, style, db_table, - index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): - "Creates a GiST index for this geometry field." - sql = style.SQL_KEYWORD('CREATE INDEX ') + \ - style.SQL_TABLE(quotename('%s_%s_id' % (db_table, self.column), dbl=True)) + \ - style.SQL_KEYWORD(' ON ') + \ - style.SQL_TABLE(quotename(db_table, dbl=True)) + \ - style.SQL_KEYWORD(' USING ') + \ - style.SQL_COLTYPE(index_type) + ' ( ' + \ - style.SQL_FIELD(quotename(self.column, dbl=True)) + ' ' + \ - style.SQL_KEYWORD(index_opts) + ' );' - return sql - - def _post_create_sql(self, style, db_table): - """Returns SQL that will be executed after the model has been - created. Geometry columns must be added after creation with the - PostGIS AddGeometryColumn() function.""" - - # Getting the AddGeometryColumn() SQL necessary to create a PostGIS - # geometry field. - post_sql = self._add_geom(style, db_table) - - # If the user wants to index this data, then get the indexing SQL as well. - if self._index: - return '%s\n%s' % (post_sql, self._geom_index(style, db_table)) - else: - return post_sql - - def _post_delete_sql(self, style, db_table): - "Drops the geometry column." - sql = style.SQL_KEYWORD('SELECT ') + \ - style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ - style.SQL_TABLE(quotename(db_table)) + ', ' + \ - style.SQL_FIELD(quotename(self.column)) + ');' - return sql - def contribute_to_class(self, cls, name): super(GeometryField, self).contribute_to_class(cls, name) @@ -112,47 +46,6 @@ class GeometryField(Field): def get_internal_type(self): return "NoField" - def get_db_prep_lookup(self, lookup_type, value): - "Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value." - if lookup_type in POSTGIS_TERMS: - if lookup_type == 'isnull': return [value] # special case for NULL geometries. - if not bool(value): return [None] # If invalid value passed in. - if isinstance(value, GEOSGeometry): - # GEOSGeometry instance passed in. - if value.srid != self._srid: - # Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key - # to the where parameters, since we need to transform the geometry in the query. - return {'where' : "Transform(%s,%s)", - 'params' : [value, self._srid] - } - else: - # Just return the GEOSGeometry, it has its own psycopg2 adaptor. - return [value] - elif isinstance(value, StringType): - # String instance passed in, assuming WKT. - # TODO: Any validation needed here to prevent SQL injection? - return ["SRID=%d;%s" % (self._srid, value)] - else: - raise TypeError("Invalid type (%s) used for field lookup value." % str(type(value))) - else: - raise TypeError("Field has invalid lookup: %s" % lookup_type) - - def get_db_prep_save(self, value): - "Prepares the value for saving in the database." - if not bool(value): return None - if isinstance(value, GEOSGeometry): - return value - else: - return ("SRID=%d;%s" % (self._srid, wkt)) - - def get_placeholder(self, value): - "Provides a proper substitution value for " - if isinstance(value, GEOSGeometry) and value.srid != self._srid: - # Adding Transform() to the SQL placeholder. - return 'Transform(%%s, %s)' % self._srid - else: - return '%s' - def get_manipulator_field_objs(self): "Using the WKTField (defined above) to be our manipulator." return [WKTField] diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 6619e54c9a..dab1f04eb6 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,7 +1,7 @@ from django.db.models.query import Q, QuerySet from django.db import backend from django.contrib.gis.db.models.fields import GeometryField -from django.contrib.gis.db.models.postgis import parse_lookup +from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend. from django.db.models.fields import FieldDoesNotExist import operator diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index fb4167fe22..fc9bd6cb03 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,6 +1,6 @@ from copy import copy from unittest import TestSuite, TextTestRunner -from django.contrib.gis.utils import create_spatial_db +from django.contrib.gis.db.backend import create_spatial_db from django.db import connection from django.test.utils import destroy_test_db diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 50a21aa227..dcc5e681ff 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -91,7 +91,7 @@ class GeoModelTest(unittest.TestCase): nmi.save() def test005_left_right(self): - "Testing the left ('<<') right ('>>') operators." + "Testing the 'left' and 'right' lookup types." # Left: A << B => true if xmax(A) < xmin(B) # Right: A >> B => true if xmin(A) > xmax(B)