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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Handle adhoc-query objects.
|
|
2
|
+
|
|
3
|
+
The adhoc query is based on incoming request parameters,
|
|
4
|
+
such as the "FILTER", "BBOX" and "RESOURCEID" parameters.
|
|
5
|
+
|
|
6
|
+
These definitions follow the WFS spec.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from functools import cached_property
|
|
14
|
+
|
|
15
|
+
from django.db.models import Q
|
|
16
|
+
|
|
17
|
+
from gisserver.crs import CRS
|
|
18
|
+
from gisserver.exceptions import (
|
|
19
|
+
InvalidParameterValue,
|
|
20
|
+
MissingParameterValue,
|
|
21
|
+
OperationNotSupported,
|
|
22
|
+
)
|
|
23
|
+
from gisserver.parsers import fes20
|
|
24
|
+
from gisserver.parsers.ast import tag_registry
|
|
25
|
+
from gisserver.parsers.ows import KVPRequest
|
|
26
|
+
from gisserver.parsers.query import CompiledQuery
|
|
27
|
+
from gisserver.parsers.xml import NSElement, xmlns
|
|
28
|
+
from gisserver.projection import FeatureProjection
|
|
29
|
+
|
|
30
|
+
from .base import QueryExpression
|
|
31
|
+
from .projection import PropertyName
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
ADHOC_QUERY_ELEMENTS = (PropertyName, fes20.Filter, fes20.SortBy)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
@tag_registry.register("Query", xmlns.wfs)
|
|
40
|
+
class AdhocQuery(QueryExpression):
|
|
41
|
+
"""The Ad hoc query expression parameters.
|
|
42
|
+
|
|
43
|
+
This parses and handles the syntax::
|
|
44
|
+
|
|
45
|
+
<wfs:Query typeNames="...">
|
|
46
|
+
<wfs:PropertyName>...</wfs:PropertyName>
|
|
47
|
+
<fes:Filter>...</fes:Filter>
|
|
48
|
+
<fes:SortBy>...</fes:SortBy>
|
|
49
|
+
</wfs:Query>
|
|
50
|
+
|
|
51
|
+
And supports the KVP syntax::
|
|
52
|
+
|
|
53
|
+
?SERVICE=WFS&...&TYPENAMES=ns:myType&FILTER=...&SORTBY=...&SRSNAME=...&PROPERTYNAME=...
|
|
54
|
+
?SERVICE=WFS&...&TYPENAMES=ns:myType&BBOX=...&SORTBY=...&SRSNAME=...&PROPERTYNAME=...
|
|
55
|
+
?SERVICE=WFS&...&TYPENAMES=ns:myType&RESOURCEID=...
|
|
56
|
+
|
|
57
|
+
This represents all dynamic queries received as request (hence "adhoc"),
|
|
58
|
+
such as the "FILTER" and "BBOX" arguments from an HTTP GET.
|
|
59
|
+
|
|
60
|
+
The WFS Spec has 3 class levels for this:
|
|
61
|
+
|
|
62
|
+
- AdhocQueryExpression (types, projection, selection, sorting)
|
|
63
|
+
- Query (adds srsName, featureVersion)
|
|
64
|
+
- StoredQuery (adds storedQueryID)
|
|
65
|
+
|
|
66
|
+
For KVP requests, this class seems almost identifical to the provided parameters.
|
|
67
|
+
However, the KVP format allows to provide parameter lists,
|
|
68
|
+
to perform support multiple queries in a single request!
|
|
69
|
+
|
|
70
|
+
.. seealso::
|
|
71
|
+
https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/query_xsd.html#AbstractAdhocQueryExpressionType
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Tag attributes (implements ``fes:AbstractAdhocQueryExpression``)
|
|
75
|
+
# WFS allows multiple names to construct JOIN queries.
|
|
76
|
+
# See https://docs.ogc.org/is/09-025r2/09-025r2.html#107
|
|
77
|
+
# and https://docs.ogc.org/is/09-025r2/09-025r2.html#190
|
|
78
|
+
|
|
79
|
+
#: The 'typeNames' value if the request provided them. use :meth:`get_type_names` instead.
|
|
80
|
+
typeNames: list[str]
|
|
81
|
+
#: Aliases for typeNames are used for joining the same table twice. (JOIN statements are not supported yet).
|
|
82
|
+
aliases: list[str] | None = None
|
|
83
|
+
#: For XML POST requests, this handle value is returned in the ``<ows:Exception>``.
|
|
84
|
+
handle: str = ""
|
|
85
|
+
|
|
86
|
+
# part of the <wfs:Query> tag attributes:
|
|
87
|
+
#: The Coordinate Reference System to render the tag in
|
|
88
|
+
srsName: CRS | None = None
|
|
89
|
+
|
|
90
|
+
#: Projection clause (implements ``fes:AbstractProjectionClause``)
|
|
91
|
+
property_names: list[PropertyName] | None = None
|
|
92
|
+
|
|
93
|
+
#: Selection clause (implements ``fes:AbstractSelectionClause``).
|
|
94
|
+
#: - for XML POST this is encoded in a <fes:Filter> tag.
|
|
95
|
+
#: - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
|
|
96
|
+
filter: fes20.Filter | None = None
|
|
97
|
+
|
|
98
|
+
#: Sorting Clause (implements ``fes:AbstractSortingClause``)
|
|
99
|
+
sortBy: fes20.SortBy | None = None
|
|
100
|
+
|
|
101
|
+
def __post_init__(self):
|
|
102
|
+
if len(self.typeNames) > 1:
|
|
103
|
+
raise OperationNotSupported("Join queries are not supported", locator="typeNames")
|
|
104
|
+
if self.aliases:
|
|
105
|
+
raise OperationNotSupported("Join queries are not supported", locator="aliases")
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_xml(cls, element: NSElement) -> AdhocQuery:
|
|
109
|
+
"""Parse the XML element of the Query tag."""
|
|
110
|
+
type_names = [
|
|
111
|
+
element.parse_qname(qname)
|
|
112
|
+
for qname in element.get_str_attribute("typeNames").split(" ")
|
|
113
|
+
]
|
|
114
|
+
aliases = element.attrib.get("aliases", None)
|
|
115
|
+
srsName = element.attrib.get("srsName", None)
|
|
116
|
+
property_names = []
|
|
117
|
+
filter = None
|
|
118
|
+
sortBy = None
|
|
119
|
+
|
|
120
|
+
for child in element:
|
|
121
|
+
# The FES XSD dictates the element ordering, but this is ignored here.
|
|
122
|
+
node = tag_registry.node_from_xml(child, allowed_types=ADHOC_QUERY_ELEMENTS)
|
|
123
|
+
if isinstance(node, PropertyName):
|
|
124
|
+
property_names.append(node)
|
|
125
|
+
elif isinstance(node, fes20.Filter):
|
|
126
|
+
filter = node
|
|
127
|
+
elif isinstance(node, fes20.SortBy):
|
|
128
|
+
sortBy = node
|
|
129
|
+
else:
|
|
130
|
+
raise NotImplementedError(
|
|
131
|
+
f"Parsing {node.__class__} not handled in AdhocQuery.from_xml()"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return AdhocQuery(
|
|
135
|
+
typeNames=type_names,
|
|
136
|
+
aliases=aliases.split(" ") if aliases is not None else None,
|
|
137
|
+
handle=element.attrib.get("handle", ""),
|
|
138
|
+
property_names=property_names or None,
|
|
139
|
+
filter=filter,
|
|
140
|
+
sortBy=sortBy,
|
|
141
|
+
srsName=CRS.from_string(srsName) if srsName else None,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_kvp_request(cls, kvp: KVPRequest):
|
|
146
|
+
"""Build this object from an HTTP GET (key-value-pair) request.
|
|
147
|
+
|
|
148
|
+
Note the caller should have split the KVP request parameter lists.
|
|
149
|
+
This class only handles a single parameter pair.
|
|
150
|
+
"""
|
|
151
|
+
# Parse attributes
|
|
152
|
+
typeNames = [
|
|
153
|
+
kvp.parse_qname(qname)
|
|
154
|
+
for qname in kvp.get_list("typeNames", alias="TYPENAME", default=[])
|
|
155
|
+
]
|
|
156
|
+
aliases = kvp.get_list("aliases", default=None)
|
|
157
|
+
srsName = kvp.get_custom("srsName", default=None, parser=CRS.from_string)
|
|
158
|
+
|
|
159
|
+
# KVP requests may omit the typenames if RESOURCEID=... is given.
|
|
160
|
+
if not typeNames and "RESOURCEID" not in kvp:
|
|
161
|
+
raise MissingParameterValue("Empty TYPENAMES parameter", locator="typeNames")
|
|
162
|
+
|
|
163
|
+
# Parse elements
|
|
164
|
+
filter = fes20.Filter.from_kvp_request(kvp)
|
|
165
|
+
sort_by = fes20.SortBy.from_kvp_request(kvp)
|
|
166
|
+
|
|
167
|
+
# Parse projection
|
|
168
|
+
property_names = None
|
|
169
|
+
if "PROPERTYNAME" in kvp:
|
|
170
|
+
names = kvp.get_list("propertyName", default=[])
|
|
171
|
+
# Check for WFS 1.x syntax of ?PROPERTYNAME=*
|
|
172
|
+
if names != ["*"]:
|
|
173
|
+
property_names = [
|
|
174
|
+
PropertyName(xpath=name, xpath_ns_aliases=kvp.ns_aliases) for name in names
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
return AdhocQuery(
|
|
178
|
+
# Attributes
|
|
179
|
+
typeNames=typeNames,
|
|
180
|
+
aliases=aliases,
|
|
181
|
+
srsName=srsName,
|
|
182
|
+
# Elements
|
|
183
|
+
property_names=property_names,
|
|
184
|
+
filter=filter,
|
|
185
|
+
sortBy=sort_by,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@cached_property
|
|
189
|
+
def query_locator(self):
|
|
190
|
+
"""Overrides the 'query_locator' attribute, so the 'locator' argument is correctly set."""
|
|
191
|
+
if self.filter is not None and self.filter.get_resource_id_types():
|
|
192
|
+
return "resourceId"
|
|
193
|
+
else:
|
|
194
|
+
return "filter"
|
|
195
|
+
|
|
196
|
+
def get_type_names(self) -> list[str]:
|
|
197
|
+
"""Tell which type names this query uses.
|
|
198
|
+
Multiple values means a JOIN is made (not supported yet).
|
|
199
|
+
"""
|
|
200
|
+
if not self.typeNames and self.filter is not None:
|
|
201
|
+
# Also make the behavior consistent, always supply the type name.
|
|
202
|
+
return self.filter.get_resource_id_types() or []
|
|
203
|
+
else:
|
|
204
|
+
return self.typeNames
|
|
205
|
+
|
|
206
|
+
def get_projection(self) -> FeatureProjection:
|
|
207
|
+
"""Tell how the ``<wfs:Query>`` element should be displayed."""
|
|
208
|
+
return FeatureProjection(
|
|
209
|
+
self.feature_types,
|
|
210
|
+
self.property_names,
|
|
211
|
+
self.value_reference,
|
|
212
|
+
output_crs=self.srsName,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def bind(self, *args, **kwargs):
|
|
216
|
+
"""Override to make sure the 'locator' points to the actual object that defined the type."""
|
|
217
|
+
try:
|
|
218
|
+
super().bind(*args, **kwargs)
|
|
219
|
+
except InvalidParameterValue as e:
|
|
220
|
+
if not self.typeNames:
|
|
221
|
+
e.locator = "resourceId"
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
# Validate the srsName too
|
|
225
|
+
if self.srsName is not None:
|
|
226
|
+
self.srsName = self.feature_types[0].resolve_crs(self.srsName, locator="srsName")
|
|
227
|
+
|
|
228
|
+
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
229
|
+
"""Apply our collected filter data to the compiler."""
|
|
230
|
+
# Add the sorting
|
|
231
|
+
if self.sortBy is not None:
|
|
232
|
+
self.sortBy.build_ordering(compiler)
|
|
233
|
+
|
|
234
|
+
if self.filter is not None:
|
|
235
|
+
# Generate the internal query object from the <fes:Filter>,
|
|
236
|
+
# this can return a Q object.
|
|
237
|
+
return self.filter.build_query(compiler)
|
|
238
|
+
else:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def as_kvp(self) -> dict:
|
|
242
|
+
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
243
|
+
params = super().as_kvp()
|
|
244
|
+
params["TYPENAMES"] = ",".join(self.typeNames)
|
|
245
|
+
if self.srsName is not None:
|
|
246
|
+
params["SRSNAME"] = str(self.srsName)
|
|
247
|
+
|
|
248
|
+
if self.filter is not None:
|
|
249
|
+
raise NotImplementedError() # not going to parse that, nor does mapserver
|
|
250
|
+
if self.sortBy is not None:
|
|
251
|
+
params["SORTBY"] = self.sortBy.as_kvp()
|
|
252
|
+
|
|
253
|
+
return params
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Entry point to handle queries.
|
|
2
|
+
|
|
3
|
+
WFS defines 2 query types:
|
|
4
|
+
- Adhoc queries are constructed directly from request parameters.
|
|
5
|
+
- Stored queries are defined first, and executed later.
|
|
6
|
+
|
|
7
|
+
Both use the FES (Filter Encoding Syntax) filtering logic internally.
|
|
8
|
+
|
|
9
|
+
The objects in this module closely follow the WFS spec.
|
|
10
|
+
By using the same type definitions, a lot of code logic follows naturally.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
from django.db.models import Q, QuerySet
|
|
18
|
+
|
|
19
|
+
from gisserver.exceptions import (
|
|
20
|
+
ExternalValueError,
|
|
21
|
+
InvalidParameterValue,
|
|
22
|
+
OperationNotSupported,
|
|
23
|
+
)
|
|
24
|
+
from gisserver.features import FeatureType
|
|
25
|
+
from gisserver.parsers import fes20, wfs20
|
|
26
|
+
from gisserver.parsers.ast import AstNode
|
|
27
|
+
from gisserver.parsers.query import CompiledQuery
|
|
28
|
+
from gisserver.projection import FeatureProjection
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class QueryExpression(AstNode):
|
|
32
|
+
"""WFS base class for all queries.
|
|
33
|
+
This object type is defined in the WFS spec (as ``<fes:AbstractQueryExpression>``).
|
|
34
|
+
|
|
35
|
+
The WFS server can initiate queries in multiple ways.
|
|
36
|
+
This class provides the common interface for all these query types;
|
|
37
|
+
whether the request provided "ad-hoc" parameters or called a stored procedure.
|
|
38
|
+
Each query type has its own way of generating the actual database statement to perform.
|
|
39
|
+
|
|
40
|
+
The subclasses can override the following logic:
|
|
41
|
+
|
|
42
|
+
* :meth:`get_type_names` defines which types this query applies to.
|
|
43
|
+
* :meth:`build_query` defines how to filter the queryset.
|
|
44
|
+
|
|
45
|
+
For full control, these methods can also be overwritten instead:
|
|
46
|
+
|
|
47
|
+
* :meth:`get_queryset` defines the full results.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
#: Configuration for the 'locator' argument in exceptions
|
|
51
|
+
query_locator: ClassVar[str] = None
|
|
52
|
+
|
|
53
|
+
# QueryExpression
|
|
54
|
+
#: The 'handle' that will be returned in exceptions.
|
|
55
|
+
handle: str = ""
|
|
56
|
+
|
|
57
|
+
#: Projection parameters (overwritten by subclasses)
|
|
58
|
+
#: In the WFS spec, this is only part of the operation/presentation.
|
|
59
|
+
#: For Django, we'd like to make this part of the query too.
|
|
60
|
+
property_names: list[wfs20.PropertyName] | None = None # PropertyName
|
|
61
|
+
|
|
62
|
+
#: The valueReference for the GetPropertyValue call, provided here for extra ORM filtering.
|
|
63
|
+
value_reference: fes20.ValueReference | None = None
|
|
64
|
+
|
|
65
|
+
def bind(
|
|
66
|
+
self,
|
|
67
|
+
feature_types: list[FeatureType],
|
|
68
|
+
value_reference: fes20.ValueReference | None = None,
|
|
69
|
+
):
|
|
70
|
+
"""Bind the query to context outside its tag definition.
|
|
71
|
+
|
|
72
|
+
:param feature_types: The corresponding feature types for :meth:`get_type_names`.
|
|
73
|
+
:param value_reference: Which field is returned (by ``GetPropertyValue``)
|
|
74
|
+
"""
|
|
75
|
+
# Store the resolved feature types
|
|
76
|
+
self.feature_types = feature_types
|
|
77
|
+
|
|
78
|
+
# for GetPropertyValue
|
|
79
|
+
if value_reference is not None:
|
|
80
|
+
self.value_reference = value_reference
|
|
81
|
+
|
|
82
|
+
def get_queryset(self) -> QuerySet:
|
|
83
|
+
"""Generate the queryset for the specific feature type.
|
|
84
|
+
|
|
85
|
+
This method can be overwritten in subclasses to define the returned data.
|
|
86
|
+
However, consider overwriting :meth:`build_query` instead of simple data.
|
|
87
|
+
"""
|
|
88
|
+
if len(self.feature_types) > 1:
|
|
89
|
+
raise OperationNotSupported("Join queries are not supported", locator="typeNames")
|
|
90
|
+
|
|
91
|
+
# To apply the filters, an internal CompiledQuery object is created.
|
|
92
|
+
# This collects all steps, to create the final QuerySet object.
|
|
93
|
+
# The build_query() method may return an Q object, or fill the compiler itself.
|
|
94
|
+
compiler = CompiledQuery(self.feature_types)
|
|
95
|
+
q_object = self.build_query(compiler)
|
|
96
|
+
if q_object is not None:
|
|
97
|
+
compiler.add_lookups(q_object)
|
|
98
|
+
|
|
99
|
+
# While property names are projection, this hook should
|
|
100
|
+
# make it possible to perform extra query adjustments for complex expressions.
|
|
101
|
+
if self.property_names:
|
|
102
|
+
for property_name in self.property_names:
|
|
103
|
+
property_name.decorate_query(compiler)
|
|
104
|
+
|
|
105
|
+
if self.value_reference is not None:
|
|
106
|
+
try:
|
|
107
|
+
self.value_reference.parse_xpath(self.feature_types)
|
|
108
|
+
except ExternalValueError as e:
|
|
109
|
+
raise InvalidParameterValue(
|
|
110
|
+
f"Field '{self.value_reference.xpath}' does not exist.",
|
|
111
|
+
locator="valueReference",
|
|
112
|
+
) from e
|
|
113
|
+
|
|
114
|
+
# For GetPropertyValue, adjust the query so only that value is requested.
|
|
115
|
+
# This makes sure XPath attribute selectors are already handled by the
|
|
116
|
+
# database query, instead of being a presentation-layer handling.
|
|
117
|
+
# This supports cases like: ``addresses/Address[street="Oxfordstrasse"]/number``
|
|
118
|
+
field = self.value_reference.build_rhs(compiler)
|
|
119
|
+
|
|
120
|
+
queryset = compiler.get_queryset()
|
|
121
|
+
return queryset.values("pk", member=field)
|
|
122
|
+
else:
|
|
123
|
+
return compiler.get_queryset()
|
|
124
|
+
|
|
125
|
+
def get_type_names(self) -> list[str]:
|
|
126
|
+
"""Tell which type names this query applies to.
|
|
127
|
+
Multiple values means a JOIN is made (not supported yet).
|
|
128
|
+
|
|
129
|
+
This method needs to be defined in subclasses.
|
|
130
|
+
"""
|
|
131
|
+
raise NotImplementedError(
|
|
132
|
+
f"{self.__class__.__name__}.get_type_names() should be implemented."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def get_projection(self) -> FeatureProjection:
|
|
136
|
+
"""Tell how the query should be displayed."""
|
|
137
|
+
raise NotImplementedError()
|
|
138
|
+
|
|
139
|
+
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
140
|
+
"""Define the compiled query that filters the queryset."""
|
|
141
|
+
raise NotImplementedError()
|
|
142
|
+
|
|
143
|
+
def as_kvp(self) -> dict:
|
|
144
|
+
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
145
|
+
params = {}
|
|
146
|
+
if self.property_names:
|
|
147
|
+
params["PROPERTYNAME"] = ",".join(p.xpath for p in self.property_names)
|
|
148
|
+
return params
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Projection tag parsing for WFS 2.0"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from gisserver.exceptions import (
|
|
9
|
+
InvalidParameterValue,
|
|
10
|
+
OperationParsingFailed,
|
|
11
|
+
OperationProcessingFailed,
|
|
12
|
+
)
|
|
13
|
+
from gisserver.parsers.ast import AstNode, tag_registry
|
|
14
|
+
from gisserver.parsers.query import CompiledQuery
|
|
15
|
+
from gisserver.parsers.xml import NSElement, xmlns
|
|
16
|
+
from gisserver.types import XPathMatch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResolveValue(Enum):
|
|
20
|
+
"""The ``wfs:ResolveValueType`` enum, used by :class:`StandardResolveParameters`."""
|
|
21
|
+
|
|
22
|
+
local = "local"
|
|
23
|
+
remote = "remote"
|
|
24
|
+
all = "all"
|
|
25
|
+
none = "none"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _missing_(cls, value):
|
|
29
|
+
raise OperationParsingFailed(f"Invalid resolve value: {value}", locator="resolve")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
@tag_registry.register("PropertyName", xmlns.wfs)
|
|
34
|
+
class PropertyName(AstNode):
|
|
35
|
+
"""The ``<wfs:PropertyName>`` element in the projection clause.
|
|
36
|
+
|
|
37
|
+
This parses and handles the syntax::
|
|
38
|
+
|
|
39
|
+
<wfs:PropertyName>ns0:tagname</wfs:PropertyName>
|
|
40
|
+
|
|
41
|
+
More advanced syntax is not supported, such as::
|
|
42
|
+
|
|
43
|
+
<wfs:PropertyName resolve="all" resolvePath="valueOf(relatedTo)">valueOf(registerEntry)</wfs:PropertyName>
|
|
44
|
+
|
|
45
|
+
Note this element exists in the WFS namespace, not the FES namespace!
|
|
46
|
+
The ``<fes:PropertyName>`` is an old FES 1.x element that has been replaced by ``<fes:ValueReference>``.
|
|
47
|
+
The old ``<fes:PropertyName>`` is still supported as an alias by this server.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
xpath: str # Officially only a 'QName'
|
|
51
|
+
xpath_ns_aliases: dict[str, str]
|
|
52
|
+
|
|
53
|
+
# Unused, yet included for completeness.
|
|
54
|
+
# These are available in the XML syntax, and override
|
|
55
|
+
# the default given in the <wfs:GetFeature> element.
|
|
56
|
+
resolve: ResolveValue = ResolveValue.none
|
|
57
|
+
resolve_depth: int | None = None
|
|
58
|
+
resolve_path: str | None = None
|
|
59
|
+
resolve_timeout: int = 300
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_xml(cls, element: NSElement):
|
|
63
|
+
"""Parse the XML tag."""
|
|
64
|
+
depth = parse_resolve_depth(element.attrib.get("resolveDepth", None))
|
|
65
|
+
return cls(
|
|
66
|
+
xpath=element.text,
|
|
67
|
+
resolve=ResolveValue[element.attrib.get("resolve", "none")],
|
|
68
|
+
resolve_depth=depth,
|
|
69
|
+
resolve_path=element.attrib.get("resolvePath"),
|
|
70
|
+
resolve_timeout=element.get_int_attribute("resolveTimeout", 300),
|
|
71
|
+
xpath_ns_aliases=element.ns_aliases,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def parse_xpath(self, feature_types: list) -> XPathMatch:
|
|
75
|
+
"""Convert the XPath into the required ORM query elements."""
|
|
76
|
+
if self.resolve_path:
|
|
77
|
+
raise OperationProcessingFailed("resolvePath is not supported", locator="propertyName")
|
|
78
|
+
if "valueOf(" in self.xpath or (self.resolve_path and "valueOf(" in self.resolve_path):
|
|
79
|
+
raise OperationProcessingFailed(
|
|
80
|
+
"valueOf(..) syntax is not supported", locator="propertyName"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Can resolve against XSD paths, find the correct DB field name
|
|
84
|
+
return feature_types[0].resolve_element(self.xpath, self.xpath_ns_aliases)
|
|
85
|
+
|
|
86
|
+
def decorate_query(self, compiler: CompiledQuery):
|
|
87
|
+
"""Update the low-level query based on this property projection."""
|
|
88
|
+
# This will also validate the name because it resolves the ORM path.
|
|
89
|
+
# The actual limiting of fields happens inside the decorate_queryset() of the renderer.
|
|
90
|
+
xpath_match = self.parse_xpath(compiler.feature_types)
|
|
91
|
+
if xpath_match.orm_filters:
|
|
92
|
+
# Apply any xpath [attr=value] lookups.
|
|
93
|
+
compiler.add_extra_lookup(xpath_match.orm_filters)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_resolve_depth(depth: str | None) -> int | None:
|
|
97
|
+
"""Parse the resolve depth value."""
|
|
98
|
+
try:
|
|
99
|
+
return None if depth is None or depth == "*" else int(depth)
|
|
100
|
+
except (ValueError, TypeError) as e:
|
|
101
|
+
raise InvalidParameterValue(
|
|
102
|
+
"ResolveDepth must be an integer or '*'", locator="resolveDepth"
|
|
103
|
+
) from e
|