django-gisserver 1.5.0__py3-none-any.whl → 2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/crs.py +401 -0
- gisserver/db.py +126 -51
- gisserver/exceptions.py +132 -4
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
- gisserver/extensions/queries.py +266 -0
- gisserver/features.py +253 -181
- gisserver/geometries.py +64 -311
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +311 -0
- gisserver/operations/base.py +130 -312
- gisserver/operations/wfs20.py +399 -375
- gisserver/output/__init__.py +14 -49
- gisserver/output/base.py +198 -144
- gisserver/output/csv.py +78 -75
- gisserver/output/geojson.py +37 -37
- gisserver/output/gml32.py +287 -259
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +73 -61
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +81 -169
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +426 -0
- gisserver/parsers/fes20/__init__.py +89 -31
- gisserver/parsers/fes20/expressions.py +172 -58
- gisserver/parsers/fes20/filters.py +116 -45
- gisserver/parsers/fes20/identifiers.py +66 -28
- gisserver/parsers/fes20/lookups.py +146 -0
- gisserver/parsers/fes20/operators.py +417 -161
- gisserver/parsers/fes20/sorting.py +113 -34
- gisserver/parsers/gml/__init__.py +17 -25
- gisserver/parsers/gml/base.py +36 -15
- gisserver/parsers/gml/geometries.py +105 -44
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +198 -0
- gisserver/parsers/ows/requests.py +160 -0
- gisserver/parsers/query.py +179 -0
- gisserver/parsers/values.py +87 -4
- gisserver/parsers/wfs20/__init__.py +39 -0
- gisserver/parsers/wfs20/adhoc.py +253 -0
- gisserver/parsers/wfs20/base.py +148 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +483 -0
- gisserver/parsers/wfs20/stored.py +193 -0
- gisserver/parsers/xml.py +261 -0
- gisserver/projection.py +367 -0
- gisserver/static/gisserver/index.css +20 -4
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +3 -3
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +445 -313
- gisserver/views.py +227 -62
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
"""These classes map to the FES 2.0 specification for expressions.
|
|
2
2
|
The class names are identical to those in the FES spec.
|
|
3
|
+
|
|
4
|
+
Inheritance structure:
|
|
5
|
+
|
|
6
|
+
* :class:`Expression`
|
|
7
|
+
|
|
8
|
+
* :class:`Function` for ``<fes:Function>``.
|
|
9
|
+
* :class:`Literal` for ``<fes:Literal>``.
|
|
10
|
+
* :class:`ValueReference` for ``<fes:ValueReference>``.
|
|
11
|
+
* :class:`BinaryOperator` for FES 1.0 compatibility.
|
|
12
|
+
|
|
13
|
+
The :class:`BinaryOperator` is included as expression
|
|
14
|
+
to handle FES 1.0 :class:`BinaryOperatorType` arithmetic tags:
|
|
15
|
+
``<fes:Add>``, ``<fes:Sub>``, ``<fes:Mul>``, ``<fes:Div>``.
|
|
3
16
|
"""
|
|
4
17
|
|
|
5
18
|
from __future__ import annotations
|
|
6
19
|
|
|
7
20
|
import operator
|
|
8
|
-
from dataclasses import dataclass
|
|
21
|
+
from dataclasses import dataclass, field
|
|
9
22
|
from datetime import date, datetime
|
|
10
23
|
from decimal import Decimal as D
|
|
11
24
|
from functools import cached_property
|
|
12
25
|
from typing import Union
|
|
13
|
-
from xml.etree.ElementTree import Element
|
|
14
26
|
|
|
15
|
-
from django.contrib.gis
|
|
27
|
+
from django.contrib.gis import geos
|
|
28
|
+
from django.contrib.gis.db import models as gis_models
|
|
29
|
+
from django.core.exceptions import ValidationError
|
|
16
30
|
from django.db import models
|
|
17
|
-
from django.db.models import
|
|
31
|
+
from django.db.models import Q, Value
|
|
18
32
|
from django.db.models.expressions import Combinable
|
|
19
33
|
|
|
20
34
|
from gisserver.exceptions import ExternalParsingError
|
|
21
|
-
from gisserver.
|
|
22
|
-
from gisserver.parsers.
|
|
35
|
+
from gisserver.extensions.functions import function_registry
|
|
36
|
+
from gisserver.parsers.ast import (
|
|
37
|
+
AstNode,
|
|
38
|
+
TagNameEnum,
|
|
39
|
+
expect_no_children,
|
|
40
|
+
expect_tag,
|
|
41
|
+
tag_registry,
|
|
42
|
+
)
|
|
23
43
|
from gisserver.parsers.gml import (
|
|
24
44
|
GM_Envelope,
|
|
25
45
|
GM_Object,
|
|
@@ -27,12 +47,12 @@ from gisserver.parsers.gml import (
|
|
|
27
47
|
is_gml_element,
|
|
28
48
|
parse_gml_node,
|
|
29
49
|
)
|
|
30
|
-
from gisserver.parsers.
|
|
50
|
+
from gisserver.parsers.query import RhsTypes
|
|
31
51
|
from gisserver.parsers.values import auto_cast
|
|
32
|
-
from gisserver.
|
|
52
|
+
from gisserver.parsers.xml import NSElement, parse_qname, xmlns
|
|
53
|
+
from gisserver.types import XPathMatch, XsdTypes
|
|
33
54
|
|
|
34
55
|
NoneType = type(None)
|
|
35
|
-
RhsTypes = Union[Combinable, Func, Q, GEOSGeometry, bool, int, str, date, datetime, tuple]
|
|
36
56
|
ParsedValue = Union[int, str, date, D, datetime, GM_Object, GM_Envelope, TM_Object, NoneType]
|
|
37
57
|
|
|
38
58
|
OUTPUT_FIELDS = {
|
|
@@ -43,6 +63,15 @@ OUTPUT_FIELDS = {
|
|
|
43
63
|
datetime: models.DateTimeField(),
|
|
44
64
|
float: models.FloatField(),
|
|
45
65
|
D: models.DecimalField(),
|
|
66
|
+
geos.GEOSGeometry: gis_models.GeometryField(),
|
|
67
|
+
geos.Point: gis_models.PointField(),
|
|
68
|
+
geos.LineString: gis_models.LineStringField(),
|
|
69
|
+
geos.LinearRing: gis_models.LineStringField(),
|
|
70
|
+
geos.Polygon: gis_models.PolygonField(),
|
|
71
|
+
geos.MultiPoint: gis_models.MultiPointField(),
|
|
72
|
+
geos.MultiPolygon: gis_models.MultiPolygonField(),
|
|
73
|
+
geos.MultiLineString: gis_models.MultiLineStringField(),
|
|
74
|
+
geos.GeometryCollection: gis_models.GeometryCollectionField(),
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
|
|
@@ -59,10 +88,20 @@ class BinaryOperatorType(TagNameEnum):
|
|
|
59
88
|
Div = operator.truediv
|
|
60
89
|
|
|
61
90
|
|
|
62
|
-
class Expression(
|
|
63
|
-
"""Abstract base class, as defined by FES spec.
|
|
91
|
+
class Expression(AstNode):
|
|
92
|
+
"""Abstract base class, as defined by FES spec.
|
|
93
|
+
|
|
94
|
+
The FES spec defines the following subclasses:
|
|
95
|
+
|
|
96
|
+
* :class:`ValueReference` (pointing to a field name)
|
|
97
|
+
* :class:`Literal` (a scalar value)
|
|
98
|
+
* :class:`Function` (a transformation for a value/field)
|
|
99
|
+
|
|
100
|
+
When code uses ``Expression.child_from_xml(element)``, the AST logic will
|
|
101
|
+
initialize the correct subclass for those elements.
|
|
102
|
+
"""
|
|
64
103
|
|
|
65
|
-
xml_ns =
|
|
104
|
+
xml_ns = xmlns.fes20
|
|
66
105
|
|
|
67
106
|
def build_lhs(self, compiler) -> str:
|
|
68
107
|
"""Get the expression as the left-hand-side of the equation.
|
|
@@ -86,7 +125,21 @@ class Expression(BaseNode):
|
|
|
86
125
|
@dataclass(repr=False)
|
|
87
126
|
@tag_registry.register("Literal")
|
|
88
127
|
class Literal(Expression):
|
|
89
|
-
"""The
|
|
128
|
+
"""The ``<fes:Literal>`` element that holds a literal value.
|
|
129
|
+
|
|
130
|
+
This can be a string value, possibly annotated with a type::
|
|
131
|
+
|
|
132
|
+
<fes:Literal type="xs:boolean">true</fes:Literal>
|
|
133
|
+
|
|
134
|
+
Following the spec, the value may also contain a complete geometry::
|
|
135
|
+
|
|
136
|
+
<fes:Literal>
|
|
137
|
+
<gml:Envelope xmlns:gml="http://www.opengis.net/gml/3.2" srsName="urn:ogc:def:crs:EPSG::4326">
|
|
138
|
+
<gml:lowerCorner>5.7 53.1</gml:lowerCorner>
|
|
139
|
+
<gml:upperCorner>6.1 53.5</gml:upperCorner>
|
|
140
|
+
</gml:Envelope>
|
|
141
|
+
</fes:Literal>
|
|
142
|
+
"""
|
|
90
143
|
|
|
91
144
|
# The XSD definition even defines a sequence of xsd:any as possible member!
|
|
92
145
|
raw_value: NoneType | str | GM_Object | GM_Envelope | TM_Object
|
|
@@ -100,7 +153,9 @@ class Literal(Expression):
|
|
|
100
153
|
|
|
101
154
|
@cached_property
|
|
102
155
|
def value(self) -> ParsedValue: # officially <xsd:any>
|
|
103
|
-
"""Access the value of the element, cast to the appropriate data type.
|
|
156
|
+
"""Access the value of the element, cast to the appropriate data type.
|
|
157
|
+
:raises ExternalParsingError: When the value can't be converted to the proper type.
|
|
158
|
+
"""
|
|
104
159
|
if not isinstance(self.raw_value, str):
|
|
105
160
|
return self.raw_value # GML element or None
|
|
106
161
|
elif self.type:
|
|
@@ -112,19 +167,20 @@ class Literal(Expression):
|
|
|
112
167
|
|
|
113
168
|
@cached_property
|
|
114
169
|
def type(self) -> XsdTypes | None:
|
|
170
|
+
"""Tell which datatype the literal holds.
|
|
171
|
+
This returns the type="..." value of the element.
|
|
172
|
+
"""
|
|
115
173
|
if not self.raw_type:
|
|
116
174
|
return None
|
|
117
175
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# No idea what XMLSchema was prefixed as (could be ns0 instead of xs:)
|
|
123
|
-
return XsdTypes(localname)
|
|
176
|
+
# The raw type is already translated into a fully qualified XML name,
|
|
177
|
+
# which also allows defining types as "ns0:string", "xs:string" or "xsd:string".
|
|
178
|
+
# These are directly matched to the XsdTypes enum value.
|
|
179
|
+
return XsdTypes(self.raw_type)
|
|
124
180
|
|
|
125
181
|
@classmethod
|
|
126
|
-
@expect_tag(
|
|
127
|
-
def from_xml(cls, element:
|
|
182
|
+
@expect_tag(xmlns.fes20, "Literal")
|
|
183
|
+
def from_xml(cls, element: NSElement):
|
|
128
184
|
children = len(element)
|
|
129
185
|
if not children:
|
|
130
186
|
# Common case: value is raw text
|
|
@@ -137,7 +193,10 @@ class Literal(Expression):
|
|
|
137
193
|
f"Unsupported child element for <Literal> element: {element[0].tag}."
|
|
138
194
|
)
|
|
139
195
|
|
|
140
|
-
return cls(
|
|
196
|
+
return cls(
|
|
197
|
+
raw_value=raw_value,
|
|
198
|
+
raw_type=parse_qname(element.attrib.get("type"), element.ns_aliases),
|
|
199
|
+
)
|
|
141
200
|
|
|
142
201
|
def build_lhs(self, compiler) -> str:
|
|
143
202
|
"""Alias the value when it's used in the left-hand-side.
|
|
@@ -166,14 +225,23 @@ class Literal(Expression):
|
|
|
166
225
|
@tag_registry.register("ValueReference")
|
|
167
226
|
@tag_registry.register("PropertyName", hidden=True) # FES 1.0 name that old clients still use.
|
|
168
227
|
class ValueReference(Expression):
|
|
169
|
-
"""The
|
|
228
|
+
"""The ``<fes:ValueReference>`` element that holds an XPath string.
|
|
170
229
|
In the fes XSD, this is declared as a subclass of xsd:string.
|
|
171
230
|
|
|
231
|
+
This parses the syntax like::
|
|
232
|
+
|
|
233
|
+
<fes:ValueReference>field-name</fes:ValueReference>
|
|
234
|
+
<fes:ValueReference>path/to/field-name</fes:ValueReference>
|
|
235
|
+
<fes:ValueReference>collection[@attr=value]/field-name</fes:ValueReference>
|
|
236
|
+
|
|
172
237
|
The old WFS1/FES1 "PropertyName" is allowed as an alias.
|
|
173
238
|
Various clients still send this, and mapserver/geoserver support this.
|
|
174
239
|
"""
|
|
175
240
|
|
|
241
|
+
#: The XPath value
|
|
176
242
|
xpath: str
|
|
243
|
+
#: The known namespaces aliases at this point in the XML tree
|
|
244
|
+
xpath_ns_aliases: dict[str, str] | None = field(compare=False, default=None)
|
|
177
245
|
|
|
178
246
|
def __str__(self):
|
|
179
247
|
return self.xpath
|
|
@@ -182,53 +250,56 @@ class ValueReference(Expression):
|
|
|
182
250
|
return f"ValueReference({self.xpath!r})"
|
|
183
251
|
|
|
184
252
|
@classmethod
|
|
185
|
-
@expect_tag(
|
|
186
|
-
|
|
187
|
-
|
|
253
|
+
@expect_tag(xmlns.fes20, "ValueReference", "PropertyName")
|
|
254
|
+
@expect_no_children
|
|
255
|
+
def from_xml(cls, element: NSElement):
|
|
256
|
+
return cls(xpath=element.text, xpath_ns_aliases=element.ns_aliases)
|
|
188
257
|
|
|
189
258
|
def build_lhs(self, compiler) -> str:
|
|
190
|
-
"""
|
|
191
|
-
|
|
259
|
+
"""Use the field name in a left-hand-side expression."""
|
|
260
|
+
# Optimized from base class: fields don't need an alias lookup through annotations.
|
|
261
|
+
match = self.parse_xpath(compiler.feature_types)
|
|
192
262
|
return match.build_lhs(compiler)
|
|
193
263
|
|
|
194
264
|
def build_rhs(self, compiler) -> RhsTypes:
|
|
195
|
-
"""
|
|
196
|
-
|
|
265
|
+
"""Use the field name in a right-hand expression.
|
|
266
|
+
This generates an F-expression for the ORM."""
|
|
267
|
+
match = self.parse_xpath(compiler.feature_types)
|
|
197
268
|
return match.build_rhs(compiler)
|
|
198
269
|
|
|
199
|
-
def parse_xpath(self,
|
|
270
|
+
def parse_xpath(self, feature_types: list) -> XPathMatch:
|
|
200
271
|
"""Convert the XPath into the required ORM query elements."""
|
|
201
|
-
|
|
202
|
-
# Can resolve against XSD paths, find the correct DB field name
|
|
203
|
-
return feature_type.resolve_element(self.xpath)
|
|
204
|
-
else:
|
|
205
|
-
# Only used by unit testing (when feature_type is not given).
|
|
206
|
-
parts = [word.strip() for word in self.xpath.split("/")]
|
|
207
|
-
return ORMPath(orm_path="__".join(parts), orm_filters=None)
|
|
208
|
-
|
|
209
|
-
@cached_property
|
|
210
|
-
def element_name(self):
|
|
211
|
-
"""Tell which element this reference points to."""
|
|
212
|
-
return self.xpath.rpartition("/")[2]
|
|
272
|
+
return feature_types[0].resolve_element(self.xpath, self.xpath_ns_aliases)
|
|
213
273
|
|
|
214
274
|
|
|
215
275
|
@dataclass
|
|
216
276
|
@tag_registry.register("Function")
|
|
217
277
|
class Function(Expression):
|
|
218
|
-
"""The
|
|
278
|
+
"""The ``<fes:Function name="...">`` element.
|
|
279
|
+
|
|
280
|
+
This parses the syntax such as::
|
|
281
|
+
|
|
282
|
+
<fes:Function name="Add">
|
|
283
|
+
<fes:ValueReference>field-name</fes:ValueReference>
|
|
284
|
+
<fes:Literal>2</fes:Literal>
|
|
285
|
+
</fes:Function>
|
|
286
|
+
|
|
287
|
+
Each argument of the function can be another :class:`Expression`,
|
|
288
|
+
such as a :class:`Function`, :class:`ValueReference` or :class:`Literal`.
|
|
289
|
+
"""
|
|
219
290
|
|
|
220
291
|
name: str # scoped name
|
|
221
292
|
arguments: list[Expression] # xsd:element ref="fes20:expression"
|
|
222
293
|
|
|
223
294
|
@classmethod
|
|
224
|
-
@expect_tag(
|
|
225
|
-
def from_xml(cls, element:
|
|
295
|
+
@expect_tag(xmlns.fes20, "Function")
|
|
296
|
+
def from_xml(cls, element: NSElement):
|
|
226
297
|
return cls(
|
|
227
|
-
name=
|
|
228
|
-
arguments=[Expression.
|
|
298
|
+
name=element.get_str_attribute("name"),
|
|
299
|
+
arguments=[Expression.child_from_xml(child) for child in element],
|
|
229
300
|
)
|
|
230
301
|
|
|
231
|
-
def build_rhs(self, compiler) ->
|
|
302
|
+
def build_rhs(self, compiler) -> models.Func:
|
|
232
303
|
"""Build the SQL function object"""
|
|
233
304
|
db_function = function_registry.resolve_function(self.name)
|
|
234
305
|
args = [arg.build_rhs(compiler) for arg in self.arguments]
|
|
@@ -236,31 +307,74 @@ class Function(Expression):
|
|
|
236
307
|
|
|
237
308
|
|
|
238
309
|
@dataclass
|
|
239
|
-
@tag_registry.
|
|
310
|
+
@tag_registry.register(BinaryOperatorType)
|
|
240
311
|
class BinaryOperator(Expression):
|
|
241
312
|
"""Support for FES 1.0 arithmetic operators.
|
|
242
313
|
|
|
314
|
+
This parses a syntax like::
|
|
315
|
+
|
|
316
|
+
<fes:Add>
|
|
317
|
+
<fes:ValueReference>field-name</fes:ValueReference>
|
|
318
|
+
<fes:Literal>2</fes:Literal>
|
|
319
|
+
</fes:Add>
|
|
320
|
+
|
|
321
|
+
The operator can be a ``<fes:Add>``, ``<fes:Sub>``, ``<fes:Mul>``, ``<fes:Div>``.
|
|
322
|
+
|
|
243
323
|
These are no longer part of the FES 2.0 spec, but clients (like QGis)
|
|
244
|
-
still assume the server
|
|
324
|
+
still assume the server use these. Hence, these need to be included.
|
|
245
325
|
"""
|
|
246
326
|
|
|
247
327
|
_operatorType: BinaryOperatorType
|
|
248
328
|
expression: tuple[Expression, Expression]
|
|
249
329
|
|
|
250
330
|
@classmethod
|
|
251
|
-
def from_xml(cls, element:
|
|
331
|
+
def from_xml(cls, element: NSElement):
|
|
252
332
|
return cls(
|
|
253
333
|
_operatorType=BinaryOperatorType.from_xml(element),
|
|
254
334
|
expression=(
|
|
255
|
-
Expression.
|
|
256
|
-
Expression.
|
|
335
|
+
Expression.child_from_xml(element[0]),
|
|
336
|
+
Expression.child_from_xml(element[1]),
|
|
257
337
|
),
|
|
258
338
|
)
|
|
259
339
|
|
|
260
340
|
def build_rhs(self, compiler) -> RhsTypes:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
341
|
+
self.validate_arithmetic(compiler, self.expression[0], self.expression[1])
|
|
342
|
+
value1 = self.expression[0].build_rhs(compiler)
|
|
343
|
+
value2 = self.expression[1].build_rhs(compiler)
|
|
344
|
+
|
|
345
|
+
# Func() + 2 or 2 + Func() are automatically handled,
|
|
346
|
+
# but Q(..) + 2 or 2 + Q(..) aren't.
|
|
347
|
+
# When both are values, a direct calculation takes place in Python.
|
|
348
|
+
if isinstance(value1, Q):
|
|
349
|
+
value2 = _make_combinable(value2)
|
|
350
|
+
if isinstance(value2, Q):
|
|
351
|
+
value1 = _make_combinable(value1)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
# This calls somthing like: operator.add(F("field"), 2) or operator.add(1, 2)
|
|
355
|
+
return self._operatorType.value(value1, value2)
|
|
356
|
+
except (TypeError, ValueError, ArithmeticError) as e:
|
|
357
|
+
raise ValidationError(
|
|
358
|
+
f"Invalid data for the 'fes:{self._operatorType.name}' element: {value1} {value2}"
|
|
359
|
+
) from e
|
|
360
|
+
|
|
361
|
+
def validate_arithmetic(self, compiler, lhs: Expression, rhs: Expression):
|
|
362
|
+
"""Check whether values support arithmetic operators."""
|
|
363
|
+
if isinstance(lhs, Literal) and isinstance(rhs, ValueReference):
|
|
364
|
+
lhs, rhs = rhs, lhs
|
|
365
|
+
|
|
366
|
+
if isinstance(lhs, ValueReference):
|
|
367
|
+
xsd_element = lhs.parse_xpath(compiler.feature_types).child
|
|
368
|
+
if isinstance(rhs, Literal):
|
|
369
|
+
# Since the element is resolved, inform the Literal how to parse the value.
|
|
370
|
+
# This avoids various validation errors along the path.
|
|
371
|
+
rhs.bind_type(xsd_element.type)
|
|
372
|
+
|
|
373
|
+
# Validate the expressions against each other
|
|
374
|
+
# This raises an ValidationError when values can't be converted
|
|
375
|
+
xsd_element.to_python(rhs.raw_value)
|
|
376
|
+
|
|
377
|
+
return None
|
|
264
378
|
|
|
265
379
|
|
|
266
380
|
def _make_combinable(value) -> Combinable | Q:
|
|
@@ -1,38 +1,115 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import AnyStr, ClassVar, Union
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from django.db.models import Q
|
|
7
7
|
|
|
8
|
-
from gisserver.exceptions import
|
|
9
|
-
from gisserver.parsers.
|
|
10
|
-
from gisserver.parsers.
|
|
11
|
-
from gisserver.
|
|
8
|
+
from gisserver.exceptions import InvalidParameterValue
|
|
9
|
+
from gisserver.parsers.ast import AstNode, expect_tag, tag_registry
|
|
10
|
+
from gisserver.parsers.gml import GEOSGMLGeometry
|
|
11
|
+
from gisserver.parsers.ows import KVPRequest
|
|
12
|
+
from gisserver.parsers.query import CompiledQuery
|
|
13
|
+
from gisserver.parsers.xml import NSElement, parse_xml_from_string, xmlns
|
|
12
14
|
|
|
13
|
-
from . import expressions, identifiers, operators
|
|
15
|
+
from . import expressions, identifiers, operators
|
|
14
16
|
|
|
17
|
+
#: The FES element group that can be used as body for the :class:`Filter` element.
|
|
15
18
|
FilterPredicates = Union[expressions.Function, operators.Operator]
|
|
16
19
|
|
|
20
|
+
# Fully qualified tag names
|
|
21
|
+
FES_RESOURCE_ID = xmlns.fes20.qname("ResourceId")
|
|
17
22
|
|
|
18
|
-
class Filter:
|
|
19
|
-
"""The <fes:Filter> element.
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
@dataclass
|
|
25
|
+
@tag_registry.register("Filter", xmlns.fes20)
|
|
26
|
+
class Filter(AstNode):
|
|
27
|
+
"""The ``<fes:Filter>`` element.
|
|
28
|
+
|
|
29
|
+
This parses and handles the syntax::
|
|
30
|
+
|
|
31
|
+
<fes:Filter>
|
|
32
|
+
<fes:SomeOperator>
|
|
33
|
+
...
|
|
34
|
+
</fes:SomeOperator>
|
|
35
|
+
</fes:Filter>
|
|
36
|
+
|
|
37
|
+
The :meth:`build_query` will convert the parsed tree
|
|
38
|
+
into a format that can build a Django ORM QuerySet.
|
|
39
|
+
|
|
40
|
+
.. seealso:: https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/filter_xsd.html#Filter
|
|
22
41
|
"""
|
|
23
42
|
|
|
24
|
-
query_language = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
|
|
43
|
+
query_language: ClassVar[str] = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
|
|
25
44
|
|
|
45
|
+
#: The filter predicate (body)
|
|
26
46
|
predicate: FilterPredicates
|
|
27
|
-
source: AnyStr | None
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
self.predicate = predicate
|
|
31
|
-
self.source = source
|
|
48
|
+
source: AnyStr | None = field(default=None, compare=False)
|
|
32
49
|
|
|
33
50
|
@classmethod
|
|
34
|
-
def
|
|
35
|
-
"""Parse
|
|
51
|
+
def from_kvp_request(cls, kvp: KVPRequest) -> Filter | None:
|
|
52
|
+
"""Parse the filter from the GET request."""
|
|
53
|
+
|
|
54
|
+
# Check filter language
|
|
55
|
+
filter_language = kvp.get_str("FILTER_LANGUAGE", default=cls.query_language)
|
|
56
|
+
if filter_language != cls.query_language:
|
|
57
|
+
raise InvalidParameterValue(
|
|
58
|
+
f"Invalid value for filterLanguage: {filter_language}", locator="filterLanguage"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Parse filter
|
|
62
|
+
filter = kvp.get_custom(
|
|
63
|
+
"filter", default=None, parser=lambda x: cls.from_string(x, kvp.ns_aliases)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Parse alternatives to filter
|
|
67
|
+
resource_ids = [
|
|
68
|
+
identifiers.ResourceId.from_string(rid, kvp.ns_aliases)
|
|
69
|
+
for rid in kvp.get_list("resourceID", default=[])
|
|
70
|
+
]
|
|
71
|
+
bbox = kvp.get_custom("bbox", default=None, parser=GEOSGMLGeometry.from_bbox)
|
|
72
|
+
|
|
73
|
+
# Make sure the various query options are not mixed.
|
|
74
|
+
cls.validate_kvp_exclusions(filter, bbox, resource_ids)
|
|
75
|
+
|
|
76
|
+
if filter is None:
|
|
77
|
+
# See if the other KVP parameters still provide a basic filter.
|
|
78
|
+
# Instead of implementing these parameters separately in the AdhocQueryExpression,
|
|
79
|
+
# they are implemented by constructing the filter AST internally.
|
|
80
|
+
if resource_ids:
|
|
81
|
+
filter = Filter(predicate=operators.IdOperator(resource_ids))
|
|
82
|
+
elif bbox is not None:
|
|
83
|
+
filter = Filter(
|
|
84
|
+
predicate=operators.BinarySpatialOperator(
|
|
85
|
+
operatorType=operators.SpatialOperatorName.BBOX,
|
|
86
|
+
operand1=None,
|
|
87
|
+
operand2=bbox,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return filter
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def validate_kvp_exclusions(
|
|
95
|
+
cls, filter: Filter | None, bbox: GEOSGMLGeometry | None, resource_ids: list
|
|
96
|
+
):
|
|
97
|
+
"""Validate mutually exclusive parameters"""
|
|
98
|
+
if filter is not None and (bbox is not None or resource_ids):
|
|
99
|
+
raise InvalidParameterValue(
|
|
100
|
+
"The FILTER parameter is mutually exclusive with BBOX and RESOURCEID",
|
|
101
|
+
locator="filter",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if resource_ids and (bbox is not None or filter is not None):
|
|
105
|
+
raise InvalidParameterValue(
|
|
106
|
+
"The RESOURCEID parameter is mutually exclusive with BBOX and FILTER",
|
|
107
|
+
locator="resourceId",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_string(cls, text: AnyStr, ns_aliases: dict[str, str] | None = None) -> Filter:
|
|
112
|
+
"""Parse an XML ``<fes:Filter>`` string.
|
|
36
113
|
|
|
37
114
|
This uses defusedxml by default, to avoid various XML injection attacks.
|
|
38
115
|
|
|
@@ -47,51 +124,45 @@ class Filter:
|
|
|
47
124
|
if "xmlns" not in first_tag and (
|
|
48
125
|
first_tag == "<Filter" or first_tag.startswith("<Filter ")
|
|
49
126
|
):
|
|
50
|
-
text = f'{first_tag} xmlns="{
|
|
127
|
+
text = f'{first_tag} xmlns="{xmlns.fes20}" xmlns:gml="{xmlns.gml32}"{text[end_first:]}'
|
|
51
128
|
|
|
52
|
-
|
|
53
|
-
root_element = fromstring(text)
|
|
54
|
-
except ParseError as e:
|
|
55
|
-
# Offer consistent results for callers to check for invalid data.
|
|
56
|
-
raise ExternalParsingError(str(e)) from e
|
|
129
|
+
root_element = parse_xml_from_string(text, extra_ns_aliases=ns_aliases)
|
|
57
130
|
return Filter.from_xml(root_element, source=text)
|
|
58
131
|
|
|
59
132
|
@classmethod
|
|
60
|
-
@expect_tag(
|
|
61
|
-
def from_xml(cls, element:
|
|
133
|
+
@expect_tag(xmlns.fes20, "Filter")
|
|
134
|
+
def from_xml(cls, element: NSElement, source: AnyStr | None = None) -> Filter:
|
|
62
135
|
"""Parse the <fes20:Filter> element."""
|
|
63
|
-
if len(element) > 1 or element[0].tag ==
|
|
136
|
+
if len(element) > 1 or element[0].tag == FES_RESOURCE_ID:
|
|
64
137
|
# fes20:ResourceId is the only element that may appear multiple times.
|
|
138
|
+
# Wrap it in an IdOperator so this class can have a single element as predicate.
|
|
65
139
|
return Filter(
|
|
66
140
|
predicate=operators.IdOperator(
|
|
67
|
-
[identifiers.Id.
|
|
141
|
+
[identifiers.Id.child_from_xml(child) for child in element]
|
|
68
142
|
),
|
|
69
143
|
source=source,
|
|
70
144
|
)
|
|
71
145
|
else:
|
|
72
146
|
return Filter(
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
# Can be Function or Operator (e.g. BinaryComparisonOperator),
|
|
148
|
+
# but not Literal or ValueReference.
|
|
149
|
+
predicate=tag_registry.node_from_xml(
|
|
150
|
+
element[0], allowed_types=FilterPredicates.__args__
|
|
75
151
|
),
|
|
76
152
|
source=source,
|
|
77
153
|
)
|
|
78
154
|
|
|
79
|
-
def
|
|
155
|
+
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
80
156
|
"""Collect the data to perform a Django ORM query."""
|
|
81
|
-
compiler = query.CompiledQuery(feature_type=feature_type, using=using)
|
|
82
|
-
|
|
83
157
|
# Function, Operator, IdList
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
compiler.add_lookups(q_object)
|
|
87
|
-
|
|
88
|
-
return compiler
|
|
158
|
+
# The operators may add the logic themselves, or return a Q object.
|
|
159
|
+
return self.predicate.build_query(compiler)
|
|
89
160
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if isinstance(
|
|
95
|
-
return self.predicate
|
|
161
|
+
def get_resource_id_types(self) -> list[str] | None:
|
|
162
|
+
"""When the filter predicate consists of ``<fes:ResourceId>`` elements, return those.
|
|
163
|
+
This can return an empty list in case a ``<fes:ResourceId>`` object doesn't define a type.
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(self.predicate, operators.IdOperator):
|
|
166
|
+
return self.predicate.get_type_names()
|
|
96
167
|
else:
|
|
97
|
-
return
|
|
168
|
+
return None
|