Author: jbronn
Date: 2007-08-12 20:33:26 -0500 (Sun, 12 Aug 2007)
New Revision: 5881

Modified:
   django/branches/gis/django/contrib/gis/db/backend/__init__.py
   django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py
   django/branches/gis/django/contrib/gis/db/backend/postgis/field.py
   django/branches/gis/django/contrib/gis/db/backend/postgis/management.py
   django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
   django/branches/gis/django/contrib/gis/db/models/manager.py
   django/branches/gis/django/contrib/gis/db/models/query.py
   django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
Log:
gis: added transform() manager method; _get_sql_clause() moved to GeoQuerySet; 
cleaned up PostGIS backend for 1.3.0; added tests for 'equals' and 'relate' 
lookups.


Modified: django/branches/gis/django/contrib/gis/db/backend/__init__.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/__init__.py       
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/__init__.py       
2007-08-13 01:33:26 UTC (rev 5881)
@@ -18,10 +18,10 @@
 
 if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
     # PostGIS is the spatial database, getting the rquired modules, renaming 
as necessary.
-    from postgis import \
+    from django.contrib.gis.db.backend.postgis import \
         PostGISField as GeoBackendField, \
         POSTGIS_TERMS as GIS_TERMS, \
-        create_spatial_db, get_geo_where_clause
+        create_spatial_db, geo_quotename, get_geo_where_clause
 else:
     raise NotImplementedError, 'No Geographic Backend exists for %s' % 
settings.DATABASE_NAME
 
@@ -249,16 +249,36 @@
         # If the field is a geometry field, then the WHERE clause will need to 
be obtained
         # with the get_geo_where_clause()
         if hasattr(field, '_geom'):
+            # Do we have multiple arguments, e.g., ST_Relate, ST_DWithin 
lookup types
+            #  need more than argument.
+            multiple_args = isinstance(value, tuple)
+
             # Getting the geographic where clause.
             gwc = get_geo_where_clause(lookup_type, current_table + '.', 
column, value)
 
             # Getting the geographic parameters from the field.
-            geo_params = field.get_db_prep_lookup(lookup_type, value)
+            if multiple_args:
+                geo_params = field.get_db_prep_lookup(lookup_type, value[0])
+            else:
+                geo_params = field.get_db_prep_lookup(lookup_type, value)
      
             # If a dictionary was passed back from the field modify the where 
clause.
-            if isinstance(geo_params, dict):
-                gwc = gwc % geo_params['where']
-                geo_params = geo_params['params']
+            param_dict = isinstance(geo_params, dict)
+            if param_dict:
+                subst_list = geo_params['where']
+                if multiple_args: subst_list += map(geo_quotename, value[1:])
+                geo_params = geo_params['params']  
+                gwc = gwc % tuple(subst_list)
+            elif multiple_args:
+                # Modify the where clause if we have multiple arguments -- the 
+                #  first substitution will be for another placeholder (for the 
+                #  geometry) since it is already apart of geo_params.
+                subst_list = ['%s']
+                subst_list += map(geo_quotename, value[1:])
+                gwc = gwc % tuple(subst_list)
+                
+            # Finally, appending onto the WHERE clause, and extending with any 
+            #  additional parameters.
             where.append(gwc)
             params.extend(geo_params)
         else:

Modified: django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py       
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py       
2007-08-13 01:33:26 UTC (rev 5881)
@@ -1,11 +1,12 @@
 """
   The PostGIS spatial database backend module.
 """
-from query import \
-    get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
+from django.contrib.gis.db.backend.postgis.query import \
+    get_geo_where_clause, geo_quotename, \
+    GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
     MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
-from creation import create_spatial_db
-from field import PostGISField
+from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
+from django.contrib.gis.db.backend.postgis.field import PostGISField
 
 # Whether PostGIS has AsKML() support.
 if MAJOR_VERSION == 1:

Modified: django/branches/gis/django/contrib/gis/db/backend/postgis/field.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/field.py  
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/field.py  
2007-08-13 01:33:26 UTC (rev 5881)
@@ -1,7 +1,7 @@
 from django.db.models.fields import Field # Django base Field class
 from django.contrib.gis.geos import GEOSGeometry, GEOSException 
+from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, 
geo_quotename as quotename
 from types import StringType
-from query import POSTGIS_TERMS, quotename
 
 class PostGISField(Field):
     def _add_geom(self, style, db_table):
@@ -66,16 +66,18 @@
         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."
+        """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)",
+                    # 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' : ["ST_Transform(%s,%s)"],
                             'params' : [value, self._srid]
                             }
                 else:
@@ -102,6 +104,6 @@
         "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
+            return 'ST_Transform(%%s, %s)' % self._srid
         else:
             return '%s'

Modified: 
django/branches/gis/django/contrib/gis/db/backend/postgis/management.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/management.py     
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/management.py     
2007-08-13 01:33:26 UTC (rev 5881)
@@ -3,6 +3,7 @@
 
   See PostGIS docs at Ch. 6.2.1 for more information on these functions.
 """
+import re
 
 def _get_postgis_func(func):
     "Helper routine for calling PostGIS functions and returning their result."
@@ -13,6 +14,7 @@
     cursor.close()
     return row[0]
 
+### PostGIS management functions ###
 def postgis_geos_version():
     "Returns the version of the GEOS library used with PostGIS."
     return _get_postgis_func('postgis_geos_version')
@@ -33,4 +35,22 @@
     "Returns PostGIS version number and compile-time options."
     return _get_postgis_func('postgis_full_version')
 
+### Routines for parsing output of management functions. ###
+version_regex = re.compile('^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
+def postgis_version_tuple():
+    "Returns the PostGIS version as a tuple."
 
+    # Getting the PostGIS version
+    version = postgis_lib_version()
+    m = version_regex.match(version)
+    if m:
+        major = int(m.group('major'))
+        minor1 = int(m.group('minor1'))
+        minor2 = int(m.group('minor2'))
+    else:
+        raise Exception, 'Could not parse PostGIS version string: %s' % version
+
+    return (version, major, minor1, minor2)
+
+
+    

Modified: django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/backend/postgis/query.py  
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/backend/postgis/query.py  
2007-08-13 01:33:26 UTC (rev 5881)
@@ -3,98 +3,105 @@
   routine for PostGIS.
 """
 from django.db import backend
-from management import postgis_lib_version
+from django.contrib.gis.db.backend.postgis.management import 
postgis_version_tuple
+from types import StringType, UnicodeType
 
-# Getting the PostGIS version
-POSTGIS_VERSION = postgis_lib_version()
-MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, 
POSTGIS_VERSION.split('.'))
+# Getting the PostGIS version information
+POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = 
postgis_version_tuple()
 
 # 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:
+#  TODO: Confirm tests with PostGIS versions 1.1.x -- should work.  
+#        Versions <= 1.0.x do not use GEOS C API, and will not be supported.
+if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # 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.
+    # The "~" operator returns true if A's bounding box completely contains
+    #  by 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.
+    # The "&&" operator returns true if A's bounding box overlaps
+    #  B's bounding box.
     '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').
-#
+# Versions of PostGIS >= 1.2.2 changed their naming convention to be
+#  'SQL-MM-centric' to conform with the ISO standard. Practically, this 
+#  means that 'ST_' is prefixes geometry function names.
+if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or 
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))):
+    GEOM_FUNC_PREFIX = 'ST_'
+else:
+    GEOM_FUNC_PREFIX = ''
+
+# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
+#  first before calling the more computationally expensive GEOS routines 
(called
+#  "inline index magic"):
+#    'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
+#    'covers'.
 POSTGIS_GEOMETRY_FUNCTIONS = {
-    'equals' : '%sEquals',
-    'disjoint' : '%sDisjoint',
-    'touches' : '%sTouches',
-    'crosses' : '%sCrosses',
-    'within' : '%sWithin',
-    'overlaps' : '%sOverlaps',
-    'contains' : '%sContains',
-    'intersects' : '%sIntersects',
-    'relate' : ('%sRelate', str),
+    'equals' : 'Equals',
+    'disjoint' : 'Disjoint',
+    'touches' : 'Touches',
+    'crosses' : 'Crosses',
+    'within' : 'Within',
+    'overlaps' : 'Overlaps',
+    'contains' : 'Contains',
+    'intersects' : 'Intersects',
+    'relate' : ('Relate', 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.
+if GEOM_FUNC_PREFIX == 'ST_':
+    # Adding the GEOM_FUNC_PREFIX to the lookup functions.
+    for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items():
+        if isinstance(func, tuple):
+            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], 
func[1])
+        else:
+            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func
+
+    # 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',
+        {'dwithin' : ('ST_DWithin', float),
+         'coveredby' : 'ST_CoveredBy',
+         'covers' : 'ST_Covers',
          }
         )
-    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
@@ -102,6 +109,14 @@
 POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 
'isnull')
 POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
 
+### PostGIS-specific Methods ###
+def get_geom_func(lookup_type):
+    func_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
+    if isinstance(func_info, tuple):
+        return func_info[0]
+    else:
+        return func_info
+
 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('.'):
@@ -116,12 +131,41 @@
 
     # 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)
+        lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
     except KeyError:
         pass
+    else:
+        # Lookup types that are tuples take tuple arguments, e.g., 'relate' 
and 
+        #  'dwithin' lookup types.
+        if isinstance(lookup_info, tuple):
+            # First element of tuple is lookup type, second element is the type
+            #  of the expected argument (e.g., str, float)
+            func, arg_type = lookup_info
+
+            # Ensuring that a tuple _value_ was passed in from the user
+            if not isinstance(value, tuple) or len(value) != 2: 
+                raise TypeError, '2-element tuple required for %s lookup 
type.' % lookup_type
+            
+            # Ensuring the argument type matches what we expect.
+            if not isinstance(value[1], arg_type):
+                raise TypeError, 'Argument type should be %s, got %s instead.' 
% (arg_type, type(value[1]))
+            
+            return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
+        else:
+            # Returning the SQL necessary for the geometry function call. For 
example: 
+            #  ST_Contains("geoapp_country"."poly", ST_GeomFromText(..))
+            return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name)
     
     # 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)
+
+def geo_quotename(value, dbl=False):
+    "Returns the quotation used for PostGIS on a given value (uses single 
quotes by default)."
+    if isinstance(value, (StringType, UnicodeType)):
+        if dbl: return '"%s"' % value
+        else: return "'%s'" % value
+    else:
+        return str(value)

Modified: django/branches/gis/django/contrib/gis/db/models/manager.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/models/manager.py 2007-08-13 
01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/models/manager.py 2007-08-13 
01:33:26 UTC (rev 5881)
@@ -9,3 +9,6 @@
 
     def kml(self, field_name, **kwargs):
         return self.get_query_set().kml(field_name, **kwargs)
+
+    def transform(self, field_name, **kwargs):
+        return self.get_query_set().transform(field_name, **kwargs)

Modified: django/branches/gis/django/contrib/gis/db/models/query.py
===================================================================
--- django/branches/gis/django/contrib/gis/db/models/query.py   2007-08-13 
01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/db/models/query.py   2007-08-13 
01:33:26 UTC (rev 5881)
@@ -1,8 +1,9 @@
 import operator
 from django.core.exceptions import ImproperlyConfigured
 from django.db import backend
-from django.db.models.query import Q, QuerySet
+from django.db.models.query import Q, QuerySet, handle_legacy_orderlist, 
quote_only_if_word
 from django.db.models.fields import FieldDoesNotExist
+from django.utils.datastructures import SortedDict
 from django.contrib.gis.db.models.fields import GeometryField
 from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends 
on the spatial database backend.
 
@@ -16,12 +17,16 @@
 class GeoQuerySet(QuerySet):
     "Geographical-enabled QuerySet object."
 
+    #### Overloaded QuerySet Routines ####
     def __init__(self, model=None):
         super(GeoQuerySet, self).__init__(model=model)
 
         # We only want to use the GeoQ object for our queries
         self._filters = GeoQ()
 
+        # For replacement fields in the SELECT.
+        self._custom_select = {}
+
     def _filter_or_exclude(self, mapper, *args, **kwargs):
         # mapper is a callable used to transform Q objects,
         # or None for identity transform
@@ -33,11 +38,113 @@
 
         clone = self._clone()
         if len(kwargs) > 0:
-            clone._filters = clone._filters & mapper(GeoQ(**kwargs)) # Using 
the GeoQ object for our filters instead
+            # Using the GeoQ object for our filters instead
+            clone._filters = clone._filters & mapper(GeoQ(**kwargs))
         if len(args) > 0:
             clone._filters = clone._filters & reduce(operator.and_, 
map(mapper, args))
         return clone
 
+    def _get_sql_clause(self):
+        opts = self.model._meta
+
+        # Construct the fundamental parts of the query: SELECT X FROM Y WHERE 
Z.
+        select = []
+
+        # This is the only component of this routine that is customized for 
the 
+        #  GeoQuerySet. Specifically, this allows operations to be done on 
fields 
+        #  in the SELECT, overriding their values -- this is different from 
using 
+        #  QuerySet.extra(select=foo) because extra() adds an  an _additional_ 
+        #  field to be selected.  Used in returning transformed geometries.
+        for f in opts.fields:
+            if f.column in self._custom_select: 
select.append(self._custom_select[f.column])
+            else: select.append(self._field_column(f))
+
+        tables = [quote_only_if_word(t) for t in self._tables]
+        joins = SortedDict()
+        where = self._where[:]
+        params = self._params[:]
+
+        # Convert self._filters into SQL.
+        joins2, where2, params2 = self._filters.get_sql(opts)
+        joins.update(joins2)
+        where.extend(where2)
+        params.extend(params2)
+
+        # Add additional tables and WHERE clauses based on select_related.
+        if self._select_related:
+            fill_table_cache(opts, select, tables, where,
+                             old_prefix=opts.db_table,
+                             cache_tables_seen=[opts.db_table],
+                             max_depth=self._max_related_depth)
+
+        # Add any additional SELECTs.
+        if self._select:
+            select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), 
backend.quote_name(s[0])) for s in self._select.items()])
+
+        # Start composing the body of the SQL statement.
+        sql = [" FROM", backend.quote_name(opts.db_table)]
+
+        # Compose the join dictionary into SQL describing the joins.
+        if joins:
+            sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, 
alias, condition)
+                            for (alias, (table, join_type, condition)) in 
joins.items()]))
+
+        # Compose the tables clause into SQL.
+        if tables:
+            sql.append(", " + ", ".join(tables))
+
+        # Compose the where clause into SQL.
+        if where:
+            sql.append(where and "WHERE " + " AND ".join(where))
+
+        # ORDER BY clause
+        order_by = []
+        if self._order_by is not None:
+            ordering_to_use = self._order_by
+        else:
+            ordering_to_use = opts.ordering
+        for f in handle_legacy_orderlist(ordering_to_use):
+            if f == '?': # Special case.
+                order_by.append(backend.get_random_function_sql())
+            else:
+                if f.startswith('-'):
+                    col_name = f[1:]
+                    order = "DESC"
+                else:
+                    col_name = f
+                    order = "ASC"
+                if "." in col_name:
+                    table_prefix, col_name = col_name.split('.', 1)
+                    table_prefix = backend.quote_name(table_prefix) + '.'
+                else:
+                    # Use the database table as a column prefix if it wasn't 
given,
+                    # and if the requested column isn't a custom SELECT.
+                    if "." not in col_name and col_name not in (self._select 
or ()):
+                        table_prefix = backend.quote_name(opts.db_table) + '.'
+                    else:
+                        table_prefix = ''
+                order_by.append('%s%s %s' % (table_prefix, 
backend.quote_name(orderfield2column(col_name, opts)), order))
+        if order_by:
+            sql.append("ORDER BY " + ", ".join(order_by))
+
+        # LIMIT and OFFSET clauses
+        if self._limit is not None:
+            sql.append("%s " % backend.get_limit_offset_sql(self._limit, 
self._offset))
+        else:
+            assert self._offset is None, "'offset' is not allowed without 
'limit'"
+
+        return select, " ".join(sql), params
+
+    def _clone(self, klass=None, **kwargs):
+        c = super(GeoQuerySet, self)._clone(klass, **kwargs)
+        c._custom_select = self._custom_select
+        return c
+
+    #### Methods specific to the GeoQuerySet ####
+    def _field_column(self, field):
+        return "%s.%s" % (backend.quote_name(self.model._meta.db_table),
+                          backend.quote_name(field.column))
+    
     def kml(self, field_name, precision=8):
         """Returns KML representation of the given field name in a `kml` 
         attribute on each element of the QuerySet."""
@@ -51,8 +158,23 @@
         field = self.model._meta.get_field(field_name)
         if not isinstance(field, GeometryField):
             raise TypeError, 'KML output only available on GeometryField 
fields.'
-        field_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table),
-                               backend.quote_name(field.column))
+        field_col = self._field_column(field)
         
         # Adding the AsKML function call to the SELECT part of the SQL.
         return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, 
precision)})
+
+    def transform(self, field_name, srid=4326):
+        """Transforms the given geometry field to the given SRID.  If no SRID 
is
+        provided, the transformation will default to using 4326 (WGS84)."""
+        field = self.model._meta.get_field(field_name)
+        if not isinstance(field, GeometryField):
+            raise TypeError, 'ST_Transform() only available for GeometryField 
fields.'
+
+        # Setting the key for the field's column with the custom SELECT SQL to 
+        #  override the geometry column returned from the database.
+        self._custom_select[field.column] = \
+            '(ST_Transform(%s, %s)) AS %s' % (self._field_column(field), srid, 
+                                              backend.quote_name(field.column))
+        return self._clone()
+
+    

Modified: django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
===================================================================
--- django/branches/gis/django/contrib/gis/tests/geoapp/tests.py        
2007-08-13 01:17:19 UTC (rev 5880)
+++ django/branches/gis/django/contrib/gis/tests/geoapp/tests.py        
2007-08-13 01:33:26 UTC (rev 5881)
@@ -85,6 +85,23 @@
         ptown = City.objects.kml('point', precision=9).get(name='Pueblo')
         
self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>',
 ptown.kml)
 
+    def test04_transform(self):
+        "Testing the transform() queryset method."
+
+        # Pre-transformed points for Houston and Pueblo.
+        htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
+        ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
+
+        # Asserting the result of the transform operation with the values in
+        #  the pre-transformed points.
+        h = City.objects.transform('point', 
srid=htown.srid).get(name='Houston')
+        self.assertAlmostEqual(htown.x, h.point.x, 8)
+        self.assertAlmostEqual(htown.y, h.point.y, 8)
+
+        p = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
+        self.assertAlmostEqual(ptown.x, p.point.x, 8)
+        self.assertAlmostEqual(ptown.y, p.point.y, 8)
+
     def test10_contains_contained(self):
         "Testing the 'contained' and 'contains' lookup types."
 
@@ -200,6 +217,45 @@
         self.assertEqual(2, len(qs))
         for c in qs: self.assertEqual(True, c.name in cities)
 
+    def test14_equals(self):
+        "Testing the 'same_as' and 'equals' lookup types."
+        pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
+        c1 = City.objects.get(point=pnt)
+        c2 = City.objects.get(point__same_as=pnt)
+        c3 = City.objects.get(point__equals=pnt)
+        for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
+
+    def test15_relate(self):
+        "Testing the 'relate' lookup type."
+        # To make things more interesting, we will have our Texas reference 
point in 
+        #  different SRIDs.
+        pnt1 = fromstr('POINT (649287.0363174345111474 
4177429.4494686722755432)', srid=2847)
+        pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326)
+
+        # Testing bad argument tuples that should return a TypeError
+        bad_args = [(pnt1, 0), (pnt2, 'T*T***FF*', 0), (23, 'foo')]
+        for args in bad_args:
+            try:
+                qs = Country.objects.filter(mpoly__relate=args)
+                cnt = qs.count()
+            except TypeError:
+                pass
+            else:
+                self.fail('Expected a TypeError')
+
+        # 'T*T***FF*' => Contains()
+        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 
'T*T***FF*')).name)
+        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 
'T*T***FF*')).name)
+
+        # 'T*F**F***' => Within()
+        ks = State.objects.get(name='Kansas')
+        self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 
'T*F**F***')).name)
+
+        # 'T********' => Intersects()
+        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 
'T********')).name)
+        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 
'T********')).name)
+        self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 
'T********')).name)
+
 def suite():
     s = unittest.TestSuite()
     s.addTest(unittest.makeSuite(GeoModelTest))


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to