1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

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
This commit is contained in:
Justin Bronn 2007-07-29 19:58:24 +00:00
parent 7a98b6128e
commit ffcc38ebe8
10 changed files with 305 additions and 198 deletions

View File

@ -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 import backend
from django.db.models.query import LOOKUP_SEPARATOR, field_choices, find_field, FieldFound, QUERY_TERMS, get_where_clause from django.db.models.query import LOOKUP_SEPARATOR, field_choices, find_field, FieldFound, QUERY_TERMS, get_where_clause
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
# PostGIS-specific operators. The commented descriptions of these if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# operators come from Section 6.2.2 of the official PostGIS documentation. # PostGIS is the spatial database, getting the rquired modules, renaming as necessary.
POSTGIS_OPERATORS = { from postgis import \
# The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. PostGISField as GeoBackendField, \
'overlaps_left' : '&< %s', POSTGIS_TERMS as GIS_TERMS, \
# The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. create_spatial_db, get_geo_where_clause
'overlaps_right' : '&> %s', else:
# The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. raise NotImplementedError, 'No Geographic Backend exists for %s' % settings.DATABASE_NAME
'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)
#### query.py overloaded functions #### #### query.py overloaded functions ####
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py # 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': if lookup_type == 'pk':
lookup_type = 'exact' lookup_type = 'exact'
path.append(None) 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) path.append(lookup_type)
lookup_type = 'exact' lookup_type = 'exact'

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,10 @@
# The Django base Field class. from django.contrib.gis.db.backend import GeoBackendField # depends on the spatial database backend.
from django.db.models.fields import Field
from django.contrib.gis.db.models.proxy import GeometryProxy 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.contrib.gis.oldforms import WKTField
from django.utils.functional import curry from django.utils.functional import curry
from django.contrib.gis.geos import GEOSGeometry, GEOSException
from types import StringType
#TODO: Flesh out widgets. #TODO: Flesh out widgets.
class GeometryField(GeoBackendField):
class GeometryField(Field):
"The base GIS field -- maps to the OpenGIS Specification Geometry type." "The base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name. # The OpenGIS Geometry name.
@ -33,67 +28,6 @@ class GeometryField(Field):
self._dim = dim self._dim = dim
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function 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): def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name) super(GeometryField, self).contribute_to_class(cls, name)
@ -112,47 +46,6 @@ class GeometryField(Field):
def get_internal_type(self): def get_internal_type(self):
return "NoField" 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): def get_manipulator_field_objs(self):
"Using the WKTField (defined above) to be our manipulator." "Using the WKTField (defined above) to be our manipulator."
return [WKTField] return [WKTField]

View File

@ -1,7 +1,7 @@
from django.db.models.query import Q, QuerySet from django.db.models.query import Q, QuerySet
from django.db import backend from django.db import backend
from django.contrib.gis.db.models.fields import GeometryField 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 from django.db.models.fields import FieldDoesNotExist
import operator import operator

View File

@ -1,6 +1,6 @@
from copy import copy from copy import copy
from unittest import TestSuite, TextTestRunner 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.db import connection
from django.test.utils import destroy_test_db from django.test.utils import destroy_test_db

View File

@ -91,7 +91,7 @@ class GeoModelTest(unittest.TestCase):
nmi.save() nmi.save()
def test005_left_right(self): 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) # Left: A << B => true if xmax(A) < xmin(B)
# Right: A >> B => true if xmin(A) > xmax(B) # Right: A >> B => true if xmin(A) > xmax(B)