Django

Code

Changeset 7512

Show
Ignore:
Timestamp:
05/01/08 13:17:50 (2 months ago)
Author:
jbronn
Message:

gis: Fixed #7126 (with tests); moved GeoQuery and GeoWhereNode into sql submodule; the GeoQuerySet.transform may now be used on geometry fields related via foreign key.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/gis/django/contrib/gis/db/backend/__init__.py

    r7482 r7512  
    33 
    44 Specifically, this module will import the correct routines and modules 
    5  needed for GeoDjango
     5 needed for GeoDjango to interface with the spatial database
    66  
     7 Some of the more important classes and routines from the spatial backend 
     8 include: 
     9 
    710 (1) `GeoBackEndField`, a base class needed for GeometryField. 
    8  (2) `GeoWhereNode`, a subclass of `WhereNode` used to contruct spatial SQL. 
    9  (3) `SpatialBackend`, a container object for information specific to the 
     11 (2) `get_geo_where_clause`, a routine used by `GeoWhereNode`. 
     12 (3) `GIS_TERMS`, a listing of all valid GeoDjango lookup types. 
     13 (4) `SpatialBackend`, a container object for information specific to the 
    1014     spatial backend. 
    1115""" 
    1216from django.conf import settings 
    1317from django.db.models.sql.query import QUERY_TERMS 
    14 from django.db.models.sql.where import WhereNode 
    1518from django.contrib.gis.db.backend.util import gqn 
    1619 
     
    6265    raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) 
    6366 
    64 class GeoWhereNode(WhereNode): 
    65     """ 
    66     The GeoWhereNode calls the `get_geo_where_clause` from the appropriate 
    67     spatial backend in order to construct correct spatial SQL. 
    68     """ 
    69     def make_atom(self, child, qn): 
    70         table_alias, name, field, lookup_type, value = child 
    71         if hasattr(field, '_geom'): 
    72             if lookup_type in GIS_TERMS: 
    73                 # Getting the geographic where clause; substitution parameters 
    74                 # will be populated in the GeoFieldSQL object returned by the 
    75                 # GeometryField. 
    76                 gwc = get_geo_where_clause(lookup_type, table_alias, field, value) 
    77                 where, params = field.get_db_prep_lookup(lookup_type, value) 
    78                 return gwc % tuple(where), params 
    79             else: 
    80                 raise TypeError('Invalid lookup type: %r' % lookup_type) 
    81         else: 
    82             # If not a GeometryField, call the `make_atom` from the  
    83             # base class. 
    84             return super(GeoWhereNode, self).make_atom(child, qn) 
    85  
    8667class SpatialBackend(object): 
    8768    "A container for properties of the SpatialBackend." 
     
    10788    limited_where = LIMITED_WHERE 
    10889 
     90    # Shortcut booleans. 
     91    mysql = SPATIAL_BACKEND == 'mysql' 
     92    oracle = SPATIAL_BACKEND == 'oracle' 
     93    postgis = SPATIAL_BACKEND == 'postgis' 
     94 
    10995    # Class for the backend field. 
    11096    Field = GeoBackendField 
  • django/branches/gis/django/contrib/gis/db/models/query.py

    r7482 r7512  
     1from itertools import izip 
    12from django.core.exceptions import ImproperlyConfigured 
    23from django.db import connection 
    34from django.db.models.query import sql, QuerySet, Q 
    4 from django.db.models.fields import FieldDoesNotExist 
    5 from django.contrib.gis.db.backend import gqn, GeoWhereNode, SpatialBackend, QUERY_TERMS 
     5 
     6from django.contrib.gis.db.backend import SpatialBackend 
    67from django.contrib.gis.db.models.fields import GeometryField, PointField 
     8from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode 
    79from django.contrib.gis.geos import GEOSGeometry, Point 
    8  
    9 # Aliases. 
    1010qn = connection.ops.quote_name 
    11 oracle = SpatialBackend.name == 'oracle' 
    12 postgis = SpatialBackend.name == 'postgis' 
    13  
    14 # All valid lookup terms. 
    15 ALL_TERMS = QUERY_TERMS.copy() 
    16 ALL_TERMS.update(dict((term, None) for term in SpatialBackend.gis_terms)) 
    1711 
    1812# For backwards-compatibility; Q object should work just fine 
    19 # using queryset-refactor. 
     13# after queryset-refactor. 
    2014class GeoQ(Q): pass 
    2115 
     
    2721    def as_sql(self, *args, **kwargs): 
    2822        return self.sql 
    29  
    30 # Getting the `Query` base class from the backend (needed specifically 
    31 # for Oracle backends). 
    32 Query = QuerySet().query.__class__ 
    33  
    34 class GeoQuery(Query): 
    35     "The Geographic Query, needed to construct spatial SQL." 
    36  
    37     # Overridding the valid query terms. 
    38     query_terms = ALL_TERMS 
    39  
    40     #### Methods overridden from the base Query class #### 
    41     def __init__(self, model, conn): 
    42         super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode) 
    43         # The following attributes are customized for the GeoQuerySet. 
    44         # The GeoWhereNode and SpatialBackend classes contain backend-specific 
    45         # routines and functions. 
    46         self.custom_select = {} 
    47         self.ewkt = None 
    48  
    49     def clone(self, *args, **kwargs): 
    50         obj = super(GeoQuery, self).clone(*args, **kwargs) 
    51         # Customized selection dictionary and EWKT flag have to be added to obj. 
    52         obj.custom_select = self.custom_select.copy() 
    53         obj.ewkt = self.ewkt 
    54         return obj 
    55  
    56     def get_default_columns(self, with_aliases=False, col_aliases=None): 
    57         """ 
    58         Computes the default columns for selecting every field in the base 
    59         model. 
    60  
    61         Returns a list of strings, quoted appropriately for use in SQL 
    62         directly, as well as a set of aliases used in the select statement. 
    63  
    64         This routine is overridden from Query to handle customized selection of  
    65         geometry columns. 
    66         """ 
    67         result = [] 
    68         table_alias = self.tables[0] 
    69         root_pk = self.model._meta.pk.column 
    70         seen = {None: table_alias} 
    71         qn = self.quote_name_unless_alias 
    72         qn2 = self.connection.ops.quote_name 
    73         aliases = set() 
    74         for field, model in self.model._meta.get_fields_with_model(): 
    75             try: 
    76                 alias = seen[model] 
    77             except KeyError: 
    78                 alias = self.join((table_alias, model._meta.db_table, 
    79                         root_pk, model._meta.pk.column)) 
    80                 seen[model] = alias 
    81  
    82             # This part of the function is customized for GeoQuerySet. We 
    83             # see if there was any custom selection specified in the 
    84             # dictionary, and set up the selection format appropriately. 
    85             sel_fmt = self.get_select_format(field) 
    86             if field.column in self.custom_select: 
    87                 field_sel = sel_fmt % self.custom_select[field.column] 
    88             else: 
    89                 field_sel = sel_fmt % self._field_column(field, alias) 
    90  
    91             if with_aliases and field.column in col_aliases: 
    92                 c_alias = 'Col%d' % len(col_aliases) 
    93                 result.append('%s AS %s' % (field_sel, c_alias)) 
    94                 col_aliases.add(c_alias) 
    95                 aliases.add(c_alias) 
    96             else: 
    97                 r = field_sel 
    98                 result.append(r) 
    99                 aliases.add(r) 
    100                 if with_aliases: 
    101                     col_aliases.add(field.column) 
    102         return result, aliases 
    103  
    104     #### Routines unique to GeoQuery #### 
    105     def get_select_format(self, fld): 
    106         """ 
    107         Returns the selection format string, depending on the requirements 
    108         of the spatial backend.  For example, Oracle and MySQL require custom 
    109         selection formats in order to retrieve geometries in OGC WKT. For all 
    110         other fields a simple '%s' format string is returned. 
    111         """ 
    112         if SpatialBackend.select and hasattr(fld, '_geom'): 
    113             # This allows operations to be done on fields in the SELECT, 
    114             # overriding their values -- used by the Oracle and MySQL 
    115             # spatial backends to get database values as WKT, and by the 
    116             # `transform` method. 
    117             sel_fmt = SpatialBackend.select 
    118  
    119             # Because WKT doesn't contain spatial reference information, 
    120             # the SRID is prefixed to the returned WKT to ensure that the 
    121             # transformed geometries have an SRID different than that of the 
    122             # field -- this is only used by `transform` for Oracle backends. 
    123             if self.ewkt and oracle: 
    124                 sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt) 
    125         else: 
    126             sel_fmt = '%s' 
    127         return sel_fmt 
    128  
    129     def _field_column(self, field, table_alias=None): 
    130         """ 
    131         Helper function that returns the database column for the given field. 
    132         The table and column are returned (quoted) in the proper format, e.g., 
    133         `"geoapp_city"."point"`. 
    134         """ 
    135         if table_alias is None: table_alias = self.model._meta.db_table 
    136         return "%s.%s" % (self.quote_name_unless_alias(table_alias), qn(field.column)) 
    137  
    138     def _geo_field(self, field_name=None): 
    139         """ 
    140         Returns the first Geometry field encountered; or specified via the 
    141         `field_name` keyword. 
    142         """ 
    143         for field in self.model._meta.fields: 
    144             if isinstance(field, GeometryField): 
    145                 fname = field.name 
    146                 if field_name: 
    147                     if field_name == field.name: return field 
    148                 else: 
    149                     return field 
    150         return False 
    15123 
    15224class GeoQuerySet(QuerySet): 
     
    18860        # `distance_lte` lookup type. 
    18961        where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0)) 
    190         if oracle: 
     62        if SpatialBackend.oracle: 
    19163            # The `tolerance` keyword may be used for Oracle; the tolerance is  
    19264            # in meters -- a default of 5 centimeters is used. 
     
    230102 
    231103        self.query.select = [GeomSQL(extent_sql)] 
     104        self.query.select_fields = [None] 
    232105        try: 
    233106            esql, params = self.query.as_sql() 
     
    268141        geo_col = self.query._field_column(geo_field) 
    269142 
    270         if oracle: 
     143        if SpatialBackend.oracle: 
    271144            gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)} 
    272         elif postgis: 
     145        elif SpatialBackend.postgis: 
    273146            # PostGIS AsGML() aggregate function parameter order depends on the  
    274147            # version -- uggh. 
     
    296169        if not geo_field: 
    297170            raise TypeError('KML output only available on GeometryFields.') 
     171 
    298172        geo_col = self.query._field_column(geo_field) 
    299173 
    300174        # Adding the AsKML function call to SELECT part of the SQL. 
    301175        return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)}) 
    302      
     176 
    303177    def transform(self, field_name=None, srid=4326): 
    304178        """ 
     
    321195        if not geo_field: 
    322196            raise TypeError('%s() only available for GeometryFields' % TRANSFORM) 
    323    
     197         
     198        # Getting the selection SQL for the given geograph 
     199        field_col = self._geocol_select(geo_field, field_name) 
     200 
    324201        # Why cascading substitutions? Because spatial backends like 
    325202        # Oracle and MySQL already require a function call to convert to text, thus 
    326203        # when there's also a transformation we need to cascade the substitutions. 
    327204        # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' 
    328         geo_col = self.query.custom_select.get(geo_field.column, self.query._field_column(geo_field)
     205        geo_col = self.query.custom_select.get(geo_field, field_col
    329206         
    330207        # Setting the key for the field's column with the custom SELECT SQL to 
    331208        # override the geometry column returned from the database. 
    332         if oracle: 
     209        if SpatialBackend.oracle: 
    333210            custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid) 
    334211            self.query.ewkt = srid 
    335212        else: 
    336             custom_sel = '(%s(%s, %s)) AS %s' % \ 
    337                          (TRANSFORM, geo_col, srid, qn(geo_field.column)) 
    338         self.query.custom_select[geo_field.column] = custom_sel 
     213            custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid) 
     214        self.query.custom_select[geo_field] = custom_sel 
    339215        return self._clone() 
    340216 
     
    358234        # Replacing the select with a call to the ST_Union stored procedure 
    359235        #  on the geographic field column. 
    360         if oracle: 
     236        if SpatialBackend.oracle: 
    361237            union_sql = '%s' % SpatialBackend.select 
    362238            union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance)) 
     
    366242        # Only want the union SQL to be selected. 
    367243        self.query.select = [GeomSQL(union_sql)] 
     244        self.query.select_fields = [GeometryField] 
    368245        try: 
    369246            usql, params = self.query.as_sql() 
     
    374251        cursor = connection.cursor() 
    375252        cursor.execute(usql, params) 
    376         if oracle: 
     253        if SpatialBackend.oracle: 
    377254            # On Oracle have to read out WKT from CLOB first. 
    378255            clob = cursor.fetchone()[0] 
     
    384261        if u: return GEOSGeometry(u) 
    385262        else: return None 
     263 
     264    # Private API utilities, subject to change. 
     265    def _geocol_select(self, geo_field, field_name): 
     266        """ 
     267        Helper routine for constructing the SQL to select the geographic 
     268        column.  Takes into account if the geographic field is in a 
     269        ForeignKey relation to the current model. 
     270        """ 
     271        # Is this operation going to be on a related geographic field? 
     272        if not geo_field in self.model._meta.fields: 
     273            # If so, it'll have to be added to the select related information 
     274            # (e.g., if 'location__point' was given as the field name). 
     275            self.query.add_select_related([field_name]) 
     276            self.query.pre_sql_setup() 
     277            rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] 
     278            return self.query._field_column(geo_field, rel_table) 
     279        else: 
     280            return self.query._field_column(geo_field) 
  • django/branches/gis/django/contrib/gis/tests/__init__.py

    r6918 r7512  
    1212 
    1313# Tests that require use of a spatial database (e.g., creation of models) 
    14 test_models = ['geoapp'
     14test_models = ['geoapp', 'relatedapp'
    1515 
    1616# Tests that do not require setting up and tearing down a spatial database.