|
1 from django.db.models.fields import Field, FieldDoesNotExist |
|
2 from django.db.models.sql.constants import LOOKUP_SEP |
|
3 from django.db.models.sql.expressions import SQLEvaluator |
|
4 from django.db.models.sql.where import Constraint, WhereNode |
|
5 from django.contrib.gis.db.models.fields import GeometryField |
|
6 |
|
7 class GeoConstraint(Constraint): |
|
8 """ |
|
9 This subclass overrides `process` to better handle geographic SQL |
|
10 construction. |
|
11 """ |
|
12 def __init__(self, init_constraint): |
|
13 self.alias = init_constraint.alias |
|
14 self.col = init_constraint.col |
|
15 self.field = init_constraint.field |
|
16 |
|
17 def process(self, lookup_type, value, connection): |
|
18 if isinstance(value, SQLEvaluator): |
|
19 # Make sure the F Expression destination field exists, and |
|
20 # set an `srid` attribute with the same as that of the |
|
21 # destination. |
|
22 geo_fld = GeoWhereNode._check_geo_field(value.opts, value.expression.name) |
|
23 if not geo_fld: |
|
24 raise ValueError('No geographic field found in expression.') |
|
25 value.srid = geo_fld.srid |
|
26 db_type = self.field.db_type(connection=connection) |
|
27 params = self.field.get_db_prep_lookup(lookup_type, value, connection=connection) |
|
28 return (self.alias, self.col, db_type), params |
|
29 |
|
30 class GeoWhereNode(WhereNode): |
|
31 """ |
|
32 Used to represent the SQL where-clause for spatial databases -- |
|
33 these are tied to the GeoQuery class that created it. |
|
34 """ |
|
35 def add(self, data, connector): |
|
36 if isinstance(data, (list, tuple)): |
|
37 obj, lookup_type, value = data |
|
38 if ( isinstance(obj, Constraint) and |
|
39 isinstance(obj.field, GeometryField) ): |
|
40 data = (GeoConstraint(obj), lookup_type, value) |
|
41 super(GeoWhereNode, self).add(data, connector) |
|
42 |
|
43 def make_atom(self, child, qn, connection): |
|
44 lvalue, lookup_type, value_annot, params_or_value = child |
|
45 if isinstance(lvalue, GeoConstraint): |
|
46 data, params = lvalue.process(lookup_type, params_or_value, connection) |
|
47 spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn) |
|
48 return spatial_sql, params |
|
49 else: |
|
50 return super(GeoWhereNode, self).make_atom(child, qn, connection) |
|
51 |
|
52 @classmethod |
|
53 def _check_geo_field(cls, opts, lookup): |
|
54 """ |
|
55 Utility for checking the given lookup with the given model options. |
|
56 The lookup is a string either specifying the geographic field, e.g. |
|
57 'point, 'the_geom', or a related lookup on a geographic field like |
|
58 'address__point'. |
|
59 |
|
60 If a GeometryField exists according to the given lookup on the model |
|
61 options, it will be returned. Otherwise returns None. |
|
62 """ |
|
63 # This takes into account the situation where the lookup is a |
|
64 # lookup to a related geographic field, e.g., 'address__point'. |
|
65 field_list = lookup.split(LOOKUP_SEP) |
|
66 |
|
67 # Reversing so list operates like a queue of related lookups, |
|
68 # and popping the top lookup. |
|
69 field_list.reverse() |
|
70 fld_name = field_list.pop() |
|
71 |
|
72 try: |
|
73 geo_fld = opts.get_field(fld_name) |
|
74 # If the field list is still around, then it means that the |
|
75 # lookup was for a geometry field across a relationship -- |
|
76 # thus we keep on getting the related model options and the |
|
77 # model field associated with the next field in the list |
|
78 # until there's no more left. |
|
79 while len(field_list): |
|
80 opts = geo_fld.rel.to._meta |
|
81 geo_fld = opts.get_field(field_list.pop()) |
|
82 except (FieldDoesNotExist, AttributeError): |
|
83 return False |
|
84 |
|
85 # Finally, make sure we got a Geographic field and return. |
|
86 if isinstance(geo_fld, GeometryField): |
|
87 return geo_fld |
|
88 else: |
|
89 return False |