django-gisserver 1.4.1__py3-none-any.whl → 2.0__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.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +63 -60
- gisserver/exceptions.py +47 -9
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +267 -240
- gisserver/geometries.py +34 -39
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +129 -305
- gisserver/operations/wfs20.py +428 -336
- gisserver/output/__init__.py +10 -48
- gisserver/output/base.py +198 -143
- gisserver/output/csv.py +81 -85
- gisserver/output/geojson.py +63 -72
- gisserver/output/gml32.py +310 -281
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +71 -30
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -154
- gisserver/output/xmlschema.py +86 -47
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +15 -11
- gisserver/parsers/fes20/expressions.py +89 -50
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +336 -128
- gisserver/parsers/fes20/sorting.py +107 -34
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +6 -3
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +11 -11
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +375 -258
- gisserver/views.py +206 -75
- django_gisserver-1.4.1.dist-info/RECORD +0 -53
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -275
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -34
- gisserver/queries/adhoc.py +0 -181
- gisserver/queries/base.py +0 -146
- gisserver/queries/stored.py +0 -205
- 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.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/features.py
CHANGED
|
@@ -17,23 +17,26 @@ For example, model relationships can be modelled to a different XML layout.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import html
|
|
20
|
-
import
|
|
21
|
-
|
|
20
|
+
import itertools
|
|
21
|
+
import logging
|
|
22
22
|
from dataclasses import dataclass
|
|
23
|
-
from functools import cached_property, lru_cache
|
|
24
|
-
from typing import Union
|
|
23
|
+
from functools import cached_property, lru_cache
|
|
24
|
+
from typing import TYPE_CHECKING, Literal, Union
|
|
25
25
|
|
|
26
|
-
from django.conf import settings
|
|
27
26
|
from django.contrib.gis.db import models as gis_models
|
|
28
27
|
from django.contrib.gis.db.models import Extent, GeometryField
|
|
29
28
|
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
|
30
29
|
from django.db import models
|
|
31
|
-
from django.
|
|
30
|
+
from django.http import HttpRequest
|
|
32
31
|
|
|
33
|
-
from gisserver
|
|
34
|
-
from gisserver.
|
|
32
|
+
from gisserver import conf
|
|
33
|
+
from gisserver.compat import ArrayField, GeneratedField
|
|
34
|
+
from gisserver.db import get_db_geometry_target
|
|
35
|
+
from gisserver.exceptions import ExternalValueError, InvalidParameterValue
|
|
35
36
|
from gisserver.geometries import CRS, WGS84, BoundingBox
|
|
37
|
+
from gisserver.parsers.xml import parse_qname, xmlns
|
|
36
38
|
from gisserver.types import (
|
|
39
|
+
GeometryXsdElement,
|
|
37
40
|
GmlBoundedByElement,
|
|
38
41
|
GmlIdAttribute,
|
|
39
42
|
GmlNameElement,
|
|
@@ -44,18 +47,10 @@ from gisserver.types import (
|
|
|
44
47
|
XsdTypes,
|
|
45
48
|
)
|
|
46
49
|
|
|
47
|
-
if
|
|
48
|
-
from
|
|
49
|
-
else:
|
|
50
|
-
ArrayField = None
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
from typing import Literal # Python 3.8
|
|
54
|
-
|
|
55
|
-
_all_ = Literal["__all__"]
|
|
56
|
-
except ImportError:
|
|
57
|
-
_all_ = str
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from gisserver.projection import FeatureRelation
|
|
58
52
|
|
|
53
|
+
_all_ = Literal["__all__"]
|
|
59
54
|
|
|
60
55
|
__all__ = [
|
|
61
56
|
"FeatureType",
|
|
@@ -65,6 +60,8 @@ __all__ = [
|
|
|
65
60
|
"get_basic_field_type",
|
|
66
61
|
]
|
|
67
62
|
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
68
65
|
XSD_TYPES = {
|
|
69
66
|
models.CharField: XsdTypes.string,
|
|
70
67
|
models.TextField: XsdTypes.string,
|
|
@@ -91,7 +88,7 @@ DEFAULT_XSD_TYPE = XsdTypes.anyType
|
|
|
91
88
|
|
|
92
89
|
|
|
93
90
|
def get_basic_field_type(
|
|
94
|
-
field_name: str, model_field: models.Field | ForeignObjectRel
|
|
91
|
+
field_name: str, model_field: models.Field | models.ForeignObjectRel
|
|
95
92
|
) -> XsdAnyType:
|
|
96
93
|
"""Determine the XSD field type for a Django field."""
|
|
97
94
|
if ArrayField is not None and isinstance(model_field, ArrayField):
|
|
@@ -99,6 +96,10 @@ def get_basic_field_type(
|
|
|
99
96
|
# The array notation is written as "is_many"
|
|
100
97
|
model_field = model_field.base_field
|
|
101
98
|
|
|
99
|
+
if GeneratedField is not None and isinstance(model_field, GeneratedField):
|
|
100
|
+
# Allow things like: models.GeneratedField(SomeFunction("geofield"), output_field=models.GeometryField())
|
|
101
|
+
model_field = model_field.output_field
|
|
102
|
+
|
|
102
103
|
try:
|
|
103
104
|
# Direct instance, quickly resolved!
|
|
104
105
|
return XSD_TYPES[model_field.__class__]
|
|
@@ -108,7 +109,7 @@ def get_basic_field_type(
|
|
|
108
109
|
if isinstance(model_field, models.ForeignKey):
|
|
109
110
|
# Don't let it query on the relation value yet
|
|
110
111
|
return get_basic_field_type(field_name, model_field.target_field)
|
|
111
|
-
elif isinstance(model_field, ForeignObjectRel):
|
|
112
|
+
elif isinstance(model_field, models.ForeignObjectRel):
|
|
112
113
|
# e.g. ManyToOneRel descriptor of a foreignkey_id field.
|
|
113
114
|
return get_basic_field_type(field_name, model_field.remote_field.target_field)
|
|
114
115
|
else:
|
|
@@ -125,7 +126,12 @@ def get_basic_field_type(
|
|
|
125
126
|
return DEFAULT_XSD_TYPE
|
|
126
127
|
|
|
127
128
|
|
|
128
|
-
def _get_model_fields(
|
|
129
|
+
def _get_model_fields(
|
|
130
|
+
model: type[models.Model],
|
|
131
|
+
fields: _all_ | list[str],
|
|
132
|
+
parent: ComplexFeatureField | None = None,
|
|
133
|
+
feature_type: FeatureType | None = None,
|
|
134
|
+
):
|
|
129
135
|
if fields == "__all__":
|
|
130
136
|
# All regular fields
|
|
131
137
|
# Relationships will not be expanded since it can expose so many other fields.
|
|
@@ -136,15 +142,16 @@ def _get_model_fields(model, fields, parent=None):
|
|
|
136
142
|
# .bind() is called directly by providing these arguments via init:
|
|
137
143
|
model=model,
|
|
138
144
|
parent=parent,
|
|
145
|
+
feature_type=feature_type,
|
|
139
146
|
)
|
|
140
147
|
for f in model._meta.get_fields()
|
|
141
|
-
if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey
|
|
148
|
+
if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey, OneToOneField
|
|
142
149
|
]
|
|
143
150
|
else:
|
|
144
151
|
# Only defined fields
|
|
145
152
|
fields = [f if isinstance(f, FeatureField) else FeatureField(f) for f in fields]
|
|
146
|
-
for field in fields:
|
|
147
|
-
field.bind(model, parent=parent)
|
|
153
|
+
for field in fields: # type: FeatureField
|
|
154
|
+
field.bind(model, parent=parent, feature_type=feature_type)
|
|
148
155
|
return fields
|
|
149
156
|
|
|
150
157
|
|
|
@@ -169,10 +176,10 @@ class FeatureField:
|
|
|
169
176
|
"""
|
|
170
177
|
|
|
171
178
|
#: Allow to override the XSD element type that this field will generate.
|
|
172
|
-
xsd_element_class: type[XsdElement] =
|
|
179
|
+
xsd_element_class: type[XsdElement] = None
|
|
173
180
|
|
|
174
181
|
model: type[models.Model] | None
|
|
175
|
-
model_field: models.Field | ForeignObjectRel | None
|
|
182
|
+
model_field: models.Field | models.ForeignObjectRel | None
|
|
176
183
|
|
|
177
184
|
def __init__(
|
|
178
185
|
self,
|
|
@@ -180,26 +187,47 @@ class FeatureField:
|
|
|
180
187
|
model_attribute=None,
|
|
181
188
|
model=None,
|
|
182
189
|
parent: ComplexFeatureField | None = None,
|
|
190
|
+
feature_type: FeatureType | None = None,
|
|
183
191
|
abstract=None,
|
|
184
192
|
xsd_class: type[XsdElement] | None = None,
|
|
185
193
|
):
|
|
194
|
+
"""
|
|
195
|
+
Initialize a single field of the feature.
|
|
196
|
+
|
|
197
|
+
:param name: Name of the model field.
|
|
198
|
+
:param model_attribute: Which model attribute to access. This can be a dotted field path.
|
|
199
|
+
:param model: Which model is accessed. Usually this is passed via :meth:`bind`.
|
|
200
|
+
:param parent: The parent field of this element. Usually this is passed via :meth:`bind`.
|
|
201
|
+
:param feature_type: The feature this field is a part of. Usually this is passed via :meth:`bind`.
|
|
202
|
+
:param abstract: The "help text" or short abstract/description for this field.
|
|
203
|
+
:param xsd_class: Override which class is used to construct the internal XsdElement.
|
|
204
|
+
This controls the schema rendering, value retrieval and rendering of the element.
|
|
205
|
+
"""
|
|
186
206
|
self.name = name
|
|
187
207
|
self.model_attribute = model_attribute
|
|
188
208
|
self.model = None
|
|
189
209
|
self.model_field = None
|
|
190
210
|
self.parent = parent
|
|
211
|
+
self.feature_type = feature_type
|
|
191
212
|
self.abstract = abstract
|
|
213
|
+
|
|
192
214
|
# Allow to override the class attribute on 'self',
|
|
193
215
|
# which avoids having to subclass this field class as well.
|
|
194
216
|
if xsd_class is not None:
|
|
195
217
|
self.xsd_element_class = xsd_class
|
|
196
218
|
|
|
197
219
|
self._nillable_relation = False
|
|
220
|
+
self._nillable_output_field = False
|
|
198
221
|
if model is not None:
|
|
199
|
-
self.bind(model)
|
|
222
|
+
self.bind(model, parent=parent, feature_type=feature_type)
|
|
200
223
|
|
|
201
224
|
def __repr__(self):
|
|
202
|
-
|
|
225
|
+
if self.model_field is None:
|
|
226
|
+
return f"<{self.__class__.__name__}: {self.name} (unbound)>"
|
|
227
|
+
else:
|
|
228
|
+
return (
|
|
229
|
+
f"<{self.__class__.__name__}: {self.name}, source={self.absolute_model_attribute}>"
|
|
230
|
+
)
|
|
203
231
|
|
|
204
232
|
def _get_xsd_type(self):
|
|
205
233
|
return get_basic_field_type(self.name, self.model_field)
|
|
@@ -208,6 +236,7 @@ class FeatureField:
|
|
|
208
236
|
self,
|
|
209
237
|
model: type[models.Model],
|
|
210
238
|
parent: ComplexFeatureField | None = None,
|
|
239
|
+
feature_type: FeatureType | None = None,
|
|
211
240
|
):
|
|
212
241
|
"""Late-binding for the model.
|
|
213
242
|
|
|
@@ -220,40 +249,61 @@ class FeatureField:
|
|
|
220
249
|
:param model: The model is field is linked to.
|
|
221
250
|
:param parent: When this element is part of a complex feature,
|
|
222
251
|
this links to the parent field.
|
|
252
|
+
:param feature_type: The original feature type that his element was mentioned in.
|
|
223
253
|
"""
|
|
224
254
|
if self.model is not None:
|
|
225
255
|
raise RuntimeError(f"Feature field '{self.name}' cannot be reused")
|
|
226
256
|
self.model = model
|
|
227
257
|
self.parent = parent
|
|
258
|
+
self.feature_type = feature_type
|
|
228
259
|
|
|
229
260
|
if self.model_attribute:
|
|
230
261
|
# Support dot based field traversal.
|
|
231
262
|
field_path = self.model_attribute.split(".")
|
|
232
263
|
try:
|
|
233
264
|
field: models.Field = self.model._meta.get_field(field_path[0])
|
|
265
|
+
|
|
266
|
+
for name in field_path[1:]:
|
|
267
|
+
if field.null:
|
|
268
|
+
self._nillable_relation = True
|
|
269
|
+
|
|
270
|
+
if not field.is_relation:
|
|
271
|
+
# Note this tests the previous loop variable, so it checks whether the
|
|
272
|
+
# field can be walked into in order to resolve the next dotted name below.
|
|
273
|
+
raise ImproperlyConfigured(
|
|
274
|
+
f"FeatureField '{self.name}' has an invalid model_attribute: "
|
|
275
|
+
f"field '{name}' is a '{field.__class__.__name__}', not a relation."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
field = field.related_model._meta.get_field(name)
|
|
279
|
+
self.model_field = field
|
|
234
280
|
except FieldDoesNotExist as e:
|
|
235
281
|
raise ImproperlyConfigured(
|
|
236
282
|
f"FeatureField '{self.name}' has an invalid"
|
|
237
283
|
f" model_attribute: '{self.model_attribute}' can't be"
|
|
238
284
|
f" resolved for model '{self.model.__name__}'."
|
|
239
285
|
) from e
|
|
286
|
+
else:
|
|
287
|
+
try:
|
|
288
|
+
self.model_field = self.model._meta.get_field(self.name)
|
|
289
|
+
if GeneratedField is not None and isinstance(self.model_field, GeneratedField):
|
|
290
|
+
self._nillable_output_field = self.model_field.output_field.null
|
|
291
|
+
except FieldDoesNotExist as e:
|
|
292
|
+
raise ImproperlyConfigured(
|
|
293
|
+
f"FeatureField '{self.name}' can't be resolved for model '{self.model.__name__}'."
|
|
294
|
+
" Either set 'model_attribute' or change the name."
|
|
295
|
+
) from e
|
|
240
296
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# Note this tests the previous loop variable, so it checks whether the
|
|
247
|
-
# field can be walked into in order to resolve the next dotted name below.
|
|
248
|
-
raise ImproperlyConfigured(
|
|
249
|
-
f"FeatureField '{field.name}' has an invalid model_attribute: "
|
|
250
|
-
f"field '{name}' is a '{field.__class__.__name__}', not a relation."
|
|
251
|
-
)
|
|
297
|
+
@cached_property
|
|
298
|
+
def absolute_model_attribute(self) -> str:
|
|
299
|
+
"""Determine the full attribute of the field."""
|
|
300
|
+
if self.model_field is None:
|
|
301
|
+
raise RuntimeError(f"bind() was not called for {self!r}")
|
|
252
302
|
|
|
253
|
-
|
|
254
|
-
self.
|
|
303
|
+
if self.parent is not None:
|
|
304
|
+
return f"{self.parent.absolute_model_attribute}.{self.model_attribute or self.name}"
|
|
255
305
|
else:
|
|
256
|
-
self.
|
|
306
|
+
return self.model_attribute or self.name
|
|
257
307
|
|
|
258
308
|
@cached_property
|
|
259
309
|
def xsd_element(self) -> XsdElement:
|
|
@@ -266,8 +316,10 @@ class FeatureField:
|
|
|
266
316
|
if self.model_field is None:
|
|
267
317
|
raise RuntimeError(f"bind() was not called for {self!r}")
|
|
268
318
|
|
|
319
|
+
xsd_type = self._get_xsd_type()
|
|
320
|
+
|
|
269
321
|
# Determine max number of occurrences.
|
|
270
|
-
if
|
|
322
|
+
if xsd_type.is_geometry:
|
|
271
323
|
max_occurs = 1 # be explicit here, like mapserver does.
|
|
272
324
|
elif self.model_field.many_to_many or self.model_field.one_to_many:
|
|
273
325
|
max_occurs = "unbounded" # M2M or reverse FK field
|
|
@@ -276,14 +328,24 @@ class FeatureField:
|
|
|
276
328
|
else:
|
|
277
329
|
max_occurs = None # default is 1, but attribute can be left out.
|
|
278
330
|
|
|
279
|
-
|
|
331
|
+
# Determine which subclass to use for the element.
|
|
332
|
+
xsd_element_class = self.xsd_element_class or (
|
|
333
|
+
GeometryXsdElement if xsd_type.is_geometry else XsdElement
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return xsd_element_class(
|
|
280
337
|
name=self.name,
|
|
281
|
-
type=
|
|
282
|
-
|
|
338
|
+
type=xsd_type,
|
|
339
|
+
namespace=self.feature_type.xml_namespace, # keep all types in the same namespace for now.
|
|
340
|
+
nillable=(
|
|
341
|
+
self.model_field.null or self._nillable_relation or self._nillable_output_field
|
|
342
|
+
),
|
|
283
343
|
min_occurs=0,
|
|
284
344
|
max_occurs=max_occurs,
|
|
285
345
|
model_attribute=self.model_attribute,
|
|
346
|
+
absolute_model_attribute=self.absolute_model_attribute,
|
|
286
347
|
source=self.model_field,
|
|
348
|
+
feature_type=self.feature_type,
|
|
287
349
|
)
|
|
288
350
|
|
|
289
351
|
|
|
@@ -307,13 +369,18 @@ class ComplexFeatureField(FeatureField):
|
|
|
307
369
|
model=None,
|
|
308
370
|
abstract=None,
|
|
309
371
|
xsd_class=None,
|
|
310
|
-
xsd_base_type=
|
|
372
|
+
xsd_base_type=None,
|
|
311
373
|
):
|
|
312
374
|
"""
|
|
313
375
|
:param name: Name of the model field.
|
|
314
|
-
:param fields:
|
|
315
|
-
be a list of :
|
|
376
|
+
:param fields: If the field exposes a foreign key, provide its child element names.
|
|
377
|
+
This can be a list of :func:`field` elements, or plain field names.
|
|
316
378
|
Using ``__all__`` also works but is not recommended outside testing.
|
|
379
|
+
:param model_attribute: Which model attribute to access. This can be a dotted field path.
|
|
380
|
+
:param abstract: The "help text" or short abstract/description for this field.
|
|
381
|
+
:param xsd_class: Override which class is used to construct the internal XsdElement.
|
|
382
|
+
This controls the schema rendering, value retrieval and rendering of the element.
|
|
383
|
+
:param xsd_base_type: Override which class is the base class for the element.
|
|
317
384
|
"""
|
|
318
385
|
super().__init__(
|
|
319
386
|
name,
|
|
@@ -328,7 +395,9 @@ class ComplexFeatureField(FeatureField):
|
|
|
328
395
|
@cached_property
|
|
329
396
|
def fields(self) -> list[FeatureField]:
|
|
330
397
|
"""Provide all fields that will be rendered as part of this complex field."""
|
|
331
|
-
return _get_model_fields(
|
|
398
|
+
return _get_model_fields(
|
|
399
|
+
self.target_model, self._fields, parent=self, feature_type=self.feature_type
|
|
400
|
+
)
|
|
332
401
|
|
|
333
402
|
def _get_xsd_type(self) -> XsdComplexType:
|
|
334
403
|
"""Generate the XSD description for the field with an object relation."""
|
|
@@ -336,6 +405,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
336
405
|
|
|
337
406
|
return XsdComplexType(
|
|
338
407
|
name=f"{self.target_model._meta.object_name}Type",
|
|
408
|
+
namespace=self.feature_type.xml_namespace, # keep all types in the same namespace for now.
|
|
339
409
|
elements=[field.xsd_element for field in self.fields],
|
|
340
410
|
attributes=[
|
|
341
411
|
# Add gml:id attribute definition, so it can be resolved in xpath
|
|
@@ -343,6 +413,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
343
413
|
type_name=self.name,
|
|
344
414
|
source=pk_field,
|
|
345
415
|
model_attribute=pk_field.name,
|
|
416
|
+
feature_type=self.feature_type,
|
|
346
417
|
)
|
|
347
418
|
],
|
|
348
419
|
base=self.xsd_base_type,
|
|
@@ -377,9 +448,13 @@ def field(
|
|
|
377
448
|
so little knowledge is needed about the internal working.
|
|
378
449
|
|
|
379
450
|
:param name: Name of the model field.
|
|
380
|
-
:param
|
|
451
|
+
:param model_attribute: Which model attribute to access. This can be a dotted field path.
|
|
452
|
+
:param abstract: The "help text" or short abstract/description for this field.
|
|
453
|
+
:param fields: If the field exposes a foreign key, provide its child element names.
|
|
381
454
|
This can be a list of :func:`field` elements, or plain field names.
|
|
382
455
|
Using ``__all__`` also works but is not recommended outside testing.
|
|
456
|
+
:param xsd_class: Override which class is used to construct the internal XsdElement.
|
|
457
|
+
This controls the schema rendering, value retrieval and rendering of the element.
|
|
383
458
|
"""
|
|
384
459
|
if fields is not None:
|
|
385
460
|
return ComplexFeatureField(
|
|
@@ -398,46 +473,10 @@ def field(
|
|
|
398
473
|
)
|
|
399
474
|
|
|
400
475
|
|
|
401
|
-
@dataclass
|
|
402
|
-
class FeatureRelation:
|
|
403
|
-
"""Tell which related fields are queried by the feature.
|
|
404
|
-
Each dict holds an ORM-path, with the relevant sub-elements.
|
|
405
|
-
"""
|
|
406
|
-
|
|
407
|
-
#: The ORM path that is queried for this particular relation
|
|
408
|
-
orm_path: str
|
|
409
|
-
#: The fields that will be retrieved for that path
|
|
410
|
-
orm_fields: set[str]
|
|
411
|
-
#: The model that is accessed for this relation (if set)
|
|
412
|
-
related_model: type[models.Model] | None
|
|
413
|
-
#: The source elements that access this relation.
|
|
414
|
-
xsd_elements: list[XsdElement]
|
|
415
|
-
|
|
416
|
-
@cached_property
|
|
417
|
-
def _local_model_field_names(self) -> list[str]:
|
|
418
|
-
"""Tell which local fields of the model will be accessed by this feature."""
|
|
419
|
-
|
|
420
|
-
meta = self.related_model._meta
|
|
421
|
-
result = []
|
|
422
|
-
for name in self.orm_fields:
|
|
423
|
-
model_field = meta.get_field(name)
|
|
424
|
-
if not model_field.many_to_many and not model_field.one_to_many:
|
|
425
|
-
result.append(name)
|
|
426
|
-
|
|
427
|
-
# When this relation is retrieved through a ManyToOneRel (reverse FK),
|
|
428
|
-
# the prefetch_related() also needs to have the original foreign key
|
|
429
|
-
# in order to link all prefetches to the proper parent instance.
|
|
430
|
-
for xsd_element in self.xsd_elements:
|
|
431
|
-
if xsd_element.source is not None and xsd_element.source.one_to_many:
|
|
432
|
-
result.append(xsd_element.source.field.name)
|
|
433
|
-
|
|
434
|
-
return result
|
|
435
|
-
|
|
436
|
-
|
|
437
476
|
class FeatureType:
|
|
438
477
|
"""Declare a feature that is exposed on the map.
|
|
439
478
|
|
|
440
|
-
All WFS operations use this class to read the feature
|
|
479
|
+
All WFS operations use this class to read the feature type.
|
|
441
480
|
You may subclass this class to provide extensions,
|
|
442
481
|
such as redefining :meth:`get_queryset`.
|
|
443
482
|
|
|
@@ -464,14 +503,14 @@ class FeatureType:
|
|
|
464
503
|
metadata_url: str | None = None,
|
|
465
504
|
# Settings
|
|
466
505
|
show_name_field: bool = True,
|
|
467
|
-
|
|
506
|
+
xml_namespace: str | None = None,
|
|
468
507
|
):
|
|
469
508
|
"""
|
|
470
509
|
:param queryset: The queryset to retrieve the data.
|
|
471
510
|
:param fields: Define which fields to show in the WFS data.
|
|
472
511
|
This can be a list of field names, or :class:`FeatureField` objects.
|
|
473
512
|
:param display_field_name: Name of the field that's used as general string representation.
|
|
474
|
-
:param
|
|
513
|
+
:param geometry_field_name: Name of the geometry field to expose (default = auto-detect).
|
|
475
514
|
:param name: Name, also used as XML tag name.
|
|
476
515
|
:param title: Used in WFS metadata.
|
|
477
516
|
:param abstract: Used in WFS metadata.
|
|
@@ -481,7 +520,7 @@ class FeatureType:
|
|
|
481
520
|
:param metadata_url: Used in WFS metadata.
|
|
482
521
|
:param show_name_field: Whether to show the ``gml:name`` or the GeoJSON ``geometry_name``
|
|
483
522
|
field. Default is to show a field when ``name_field`` is given.
|
|
484
|
-
:param
|
|
523
|
+
:param xml_namespace: The XML namespace to use, will be set by :meth:`bind_namespace` otherwise.
|
|
485
524
|
"""
|
|
486
525
|
if isinstance(queryset, models.QuerySet):
|
|
487
526
|
self.queryset = queryset
|
|
@@ -506,23 +545,34 @@ class FeatureType:
|
|
|
506
545
|
|
|
507
546
|
# Settings
|
|
508
547
|
self.show_name_field = show_name_field
|
|
509
|
-
self.
|
|
548
|
+
self.xml_namespace = xml_namespace
|
|
510
549
|
|
|
511
550
|
# Validate that the name doesn't require XML escaping.
|
|
512
551
|
if html.escape(self.name) != self.name or " " in self.name or ":" in self.name:
|
|
513
552
|
raise ValueError(f"Invalid feature name for XML: <{self.xml_name}>")
|
|
514
553
|
|
|
515
|
-
self._cached_resolver = lru_cache(
|
|
554
|
+
self._cached_resolver = lru_cache(200)(self._inner_resolve_element)
|
|
516
555
|
|
|
517
|
-
def
|
|
556
|
+
def bind_namespace(self, default_xml_namespace: str):
|
|
557
|
+
"""Make sure the feature type receives the settings from the parent view."""
|
|
558
|
+
if not self.xml_namespace:
|
|
559
|
+
self.xml_namespace = default_xml_namespace
|
|
560
|
+
|
|
561
|
+
def check_permissions(self, request: HttpRequest):
|
|
518
562
|
"""Hook that allows subclasses to reject access for datasets.
|
|
519
563
|
It may raise a Django PermissionDenied error.
|
|
564
|
+
|
|
565
|
+
This can check for example whether ``request.user`` may access this feature.
|
|
566
|
+
|
|
567
|
+
The parsed WFS request is available as ``request.ows_request``.
|
|
568
|
+
Currently, this can be a :class:`~gisserver.parsers.wfs20.GetFeature`
|
|
569
|
+
or :class:`~gisserver.parsers.wfs20.GetPropertyValue` instance.
|
|
520
570
|
"""
|
|
521
571
|
|
|
522
572
|
@cached_property
|
|
523
|
-
def xml_name(self):
|
|
524
|
-
"""Return the feature
|
|
525
|
-
return f"{self.
|
|
573
|
+
def xml_name(self) -> str:
|
|
574
|
+
"""Return the feature tag as XML Full Qualified name"""
|
|
575
|
+
return f"{{{self.xml_namespace}}}{self.name}" if self.xml_namespace else self.name
|
|
526
576
|
|
|
527
577
|
@cached_property
|
|
528
578
|
def supported_crs(self) -> list[CRS]:
|
|
@@ -530,61 +580,19 @@ class FeatureType:
|
|
|
530
580
|
return [self.crs] + self.other_crs
|
|
531
581
|
|
|
532
582
|
@cached_property
|
|
533
|
-
def
|
|
534
|
-
"""
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def orm_relations(self) -> list[FeatureRelation]:
|
|
545
|
-
"""Tell which fields will be retrieved from related fields.
|
|
546
|
-
|
|
547
|
-
This gives an object layout based on the XSD elements,
|
|
548
|
-
that can be used for prefetching data.
|
|
549
|
-
"""
|
|
550
|
-
models = {}
|
|
551
|
-
fields = defaultdict(set)
|
|
552
|
-
elements = defaultdict(list)
|
|
553
|
-
|
|
554
|
-
# Check all elements that render as "dotted" flattened relation
|
|
555
|
-
for xsd_element in self.xsd_type.flattened_elements:
|
|
556
|
-
if xsd_element.source is not None:
|
|
557
|
-
# Split "relation.field" notation into path, and take the field as child attribute.
|
|
558
|
-
obj_path, field = xsd_element.orm_relation
|
|
559
|
-
elements[obj_path].append(xsd_element)
|
|
560
|
-
fields[obj_path].add(field)
|
|
561
|
-
# field is already on relation:
|
|
562
|
-
models[obj_path] = xsd_element.source.model
|
|
563
|
-
|
|
564
|
-
# Check all elements that render as "nested" complex type:
|
|
565
|
-
for xsd_element in self.xsd_type.complex_elements:
|
|
566
|
-
# The complex element itself points to the root of the path,
|
|
567
|
-
# all sub elements become the child attributes.
|
|
568
|
-
obj_path = xsd_element.orm_path
|
|
569
|
-
elements[obj_path].append(xsd_element)
|
|
570
|
-
fields[obj_path] = {
|
|
571
|
-
f.orm_path
|
|
572
|
-
for f in xsd_element.type.elements
|
|
573
|
-
if not f.is_many or f.is_array # exclude M2M, but include ArrayField
|
|
574
|
-
}
|
|
575
|
-
if xsd_element.source:
|
|
576
|
-
# field references a related object:
|
|
577
|
-
models[obj_path] = xsd_element.source.related_model
|
|
578
|
-
|
|
579
|
-
return [
|
|
580
|
-
FeatureRelation(
|
|
581
|
-
orm_path=obj_path,
|
|
582
|
-
orm_fields=sub_fields,
|
|
583
|
-
related_model=models.get(obj_path),
|
|
584
|
-
xsd_elements=elements[obj_path],
|
|
583
|
+
def all_geometry_elements(self) -> list[GeometryXsdElement]:
|
|
584
|
+
"""Provide access to all geometry elements from *all* nested levels."""
|
|
585
|
+
return self.xsd_type.geometry_elements + list(
|
|
586
|
+
itertools.chain.from_iterable(
|
|
587
|
+
# Take the geometry elements at each object level.
|
|
588
|
+
xsd_element.type.geometry_elements
|
|
589
|
+
for xsd_element in self.xsd_type.all_complex_elements
|
|
590
|
+
# The 'None' level is the root node, which is already added before.
|
|
591
|
+
# For now, don't support geometry fields on an M2M relation.
|
|
592
|
+
# If that use-case is needed, it would require additional work to implement.
|
|
593
|
+
if xsd_element is not None and not xsd_element.is_many
|
|
585
594
|
)
|
|
586
|
-
|
|
587
|
-
]
|
|
595
|
+
)
|
|
588
596
|
|
|
589
597
|
@cached_property
|
|
590
598
|
def _local_model_field_names(self) -> list[str]:
|
|
@@ -600,6 +608,17 @@ class FeatureType:
|
|
|
600
608
|
@cached_property
|
|
601
609
|
def fields(self) -> list[FeatureField]:
|
|
602
610
|
"""Define which fields to render."""
|
|
611
|
+
if (
|
|
612
|
+
self._geometry_field_name
|
|
613
|
+
and "." in self._geometry_field_name
|
|
614
|
+
and (self._fields is None or self._fields == "__all__")
|
|
615
|
+
):
|
|
616
|
+
# If you want to define more complex relationships, please be explicit in which fields you want.
|
|
617
|
+
# Allowing autoconfiguration of this is complex and likely not what you're looking for either.
|
|
618
|
+
raise ImproperlyConfigured(
|
|
619
|
+
"Using a geometry_field_name path requires defining 'fields' explicitly."
|
|
620
|
+
)
|
|
621
|
+
|
|
603
622
|
# This lazy reading allows providing 'fields' as lazy value.
|
|
604
623
|
if self._fields is None:
|
|
605
624
|
# Autoconfig, no fields defined.
|
|
@@ -610,9 +629,10 @@ class FeatureType:
|
|
|
610
629
|
if isinstance(f, gis_models.GeometryField)
|
|
611
630
|
)
|
|
612
631
|
|
|
613
|
-
return [FeatureField(self._geometry_field_name, model=self.model)]
|
|
632
|
+
return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
|
|
614
633
|
else:
|
|
615
|
-
|
|
634
|
+
# Either __all__ or an explicit list.
|
|
635
|
+
return _get_model_fields(self.model, self._fields, feature_type=self)
|
|
616
636
|
|
|
617
637
|
@cached_property
|
|
618
638
|
def display_field(self) -> models.Field | None:
|
|
@@ -623,49 +643,36 @@ class FeatureType:
|
|
|
623
643
|
return self.model._meta.get_field(self.display_field_name)
|
|
624
644
|
|
|
625
645
|
@cached_property
|
|
626
|
-
def
|
|
627
|
-
"""Give access to the
|
|
628
|
-
if not self.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if field is None:
|
|
646
|
+
def main_geometry_element(self) -> GeometryXsdElement:
|
|
647
|
+
"""Give access to the main geometry element."""
|
|
648
|
+
if not self._geometry_field_name:
|
|
649
|
+
try:
|
|
650
|
+
# Take the first element that has a geometry.
|
|
651
|
+
return self.all_geometry_elements[0]
|
|
652
|
+
except IndexError:
|
|
653
|
+
raise ImproperlyConfigured(
|
|
654
|
+
f"FeatureType '{self.name}' does not expose any geometry field "
|
|
655
|
+
f"as part of its definition."
|
|
656
|
+
) from None
|
|
657
|
+
else:
|
|
658
|
+
try:
|
|
659
|
+
return next(
|
|
660
|
+
e
|
|
661
|
+
for e in self.all_geometry_elements
|
|
662
|
+
if e.absolute_model_attribute == self._geometry_field_name
|
|
663
|
+
)
|
|
664
|
+
except StopIteration:
|
|
646
665
|
raise ImproperlyConfigured(
|
|
647
666
|
f"FeatureType '{self.name}' does not expose the geometry field "
|
|
648
667
|
f"'{self._geometry_field_name}' as part of its definition."
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
return field
|
|
652
|
-
else:
|
|
653
|
-
# Default: take the first geometry
|
|
654
|
-
return self.geometry_fields[0]
|
|
655
|
-
|
|
656
|
-
@cached_property
|
|
657
|
-
def geometry_field_name(self) -> str:
|
|
658
|
-
"""Tell which field is the geometry field."""
|
|
659
|
-
# Due to internal reorganization this property is no longer needed,
|
|
660
|
-
# retained for backward compatibility and to reflect any used input parameters.
|
|
661
|
-
return self.geometry_field.name
|
|
668
|
+
) from None
|
|
662
669
|
|
|
663
670
|
@cached_property
|
|
664
671
|
def crs(self) -> CRS:
|
|
665
672
|
"""Tell which projection the data should be presented at."""
|
|
666
673
|
if self._crs is None:
|
|
667
674
|
# Default CRS
|
|
668
|
-
return CRS.from_srid(self.
|
|
675
|
+
return CRS.from_srid(self.main_geometry_element.source_srid) # checks lookup too
|
|
669
676
|
else:
|
|
670
677
|
return self._crs
|
|
671
678
|
|
|
@@ -673,6 +680,11 @@ class FeatureType:
|
|
|
673
680
|
"""Return the queryset that is used as basis for this feature."""
|
|
674
681
|
# Return a queryset that only retrieves the fields that are actually displayed.
|
|
675
682
|
# That that without .only(), use at least `self.queryset.all()` so a clone is returned.
|
|
683
|
+
logger.debug(
|
|
684
|
+
"QuerySet for %s default only retrieves: %r",
|
|
685
|
+
self.queryset.model._meta.label,
|
|
686
|
+
self._local_model_field_names,
|
|
687
|
+
)
|
|
676
688
|
return self.queryset.only(*self._local_model_field_names)
|
|
677
689
|
|
|
678
690
|
def get_related_queryset(self, feature_relation: FeatureRelation) -> models.QuerySet:
|
|
@@ -683,9 +695,15 @@ class FeatureType:
|
|
|
683
695
|
f"source model is not defined for: {feature_relation.xsd_elements!r}"
|
|
684
696
|
)
|
|
685
697
|
# Return a queryset that only retrieves the fields that are displayed.
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
logger.debug(
|
|
699
|
+
"QuerySet for %s by default only retrieves: %r",
|
|
700
|
+
feature_relation.related_model._meta.label,
|
|
701
|
+
feature_relation._local_model_field_names,
|
|
702
|
+
)
|
|
703
|
+
queryset = feature_relation.related_model.objects.only(
|
|
704
|
+
*feature_relation._local_model_field_names
|
|
688
705
|
)
|
|
706
|
+
return self.filter_related_queryset(queryset) # Allow overriding by FeatureType subclasses
|
|
689
707
|
|
|
690
708
|
def filter_related_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
|
|
691
709
|
"""When a related object returns a queryset, this hook allows extra filtering."""
|
|
@@ -698,37 +716,13 @@ class FeatureType:
|
|
|
698
716
|
when the database table is empty, or the custom queryset doesn't
|
|
699
717
|
return any results.
|
|
700
718
|
"""
|
|
701
|
-
if not self.
|
|
719
|
+
if not self.main_geometry_element:
|
|
702
720
|
return None
|
|
703
721
|
|
|
704
|
-
geo_expression =
|
|
705
|
-
self.geometry_field.name, self.geometry_field.srid, WGS84.srid
|
|
706
|
-
)
|
|
707
|
-
|
|
722
|
+
geo_expression = get_db_geometry_target(self.main_geometry_element, WGS84)
|
|
708
723
|
bbox = self.get_queryset().aggregate(a=Extent(geo_expression))["a"]
|
|
709
724
|
return BoundingBox(*bbox, crs=WGS84) if bbox else None
|
|
710
725
|
|
|
711
|
-
def get_envelope(self, instance, crs: CRS | None = None) -> BoundingBox | None:
|
|
712
|
-
"""Get the bounding box for a single instance.
|
|
713
|
-
|
|
714
|
-
This is only used for native Python rendering. When the database
|
|
715
|
-
rendering is enabled (GISSERVER_USE_DB_RENDERING=True), the calculation
|
|
716
|
-
is entirely performed within the query.
|
|
717
|
-
"""
|
|
718
|
-
geometries = [
|
|
719
|
-
geom
|
|
720
|
-
for geom in (getattr(instance, f.name) for f in self.geometry_fields)
|
|
721
|
-
if geom is not None
|
|
722
|
-
]
|
|
723
|
-
if not geometries:
|
|
724
|
-
return None
|
|
725
|
-
|
|
726
|
-
# Perform the combining of geometries inside libgeos
|
|
727
|
-
geometry = geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
|
|
728
|
-
if crs is not None and geometry.srid != crs.srid:
|
|
729
|
-
crs.apply_to(geometry) # avoid clone
|
|
730
|
-
return BoundingBox.from_geometry(geometry, crs=crs)
|
|
731
|
-
|
|
732
726
|
def get_display_value(self, instance: models.Model) -> str:
|
|
733
727
|
"""Generate the display name value"""
|
|
734
728
|
if self.display_field_name:
|
|
@@ -747,12 +741,8 @@ class FeatureType:
|
|
|
747
741
|
"""
|
|
748
742
|
pk_field = self.model._meta.pk
|
|
749
743
|
|
|
750
|
-
# Define <gml:boundedBy
|
|
751
|
-
base_elements = []
|
|
752
|
-
if self.geometry_fields:
|
|
753
|
-
# Without a geometry, boundaries are not possible.
|
|
754
|
-
base_elements.append(GmlBoundedByElement(feature_type=self))
|
|
755
|
-
|
|
744
|
+
# Define <gml:boundedBy>
|
|
745
|
+
base_elements = [GmlBoundedByElement(feature_type=self)]
|
|
756
746
|
if self.show_name_field:
|
|
757
747
|
# Add <gml:name>
|
|
758
748
|
gml_name = GmlNameElement(
|
|
@@ -762,16 +752,19 @@ class FeatureType:
|
|
|
762
752
|
)
|
|
763
753
|
base_elements.insert(0, gml_name)
|
|
764
754
|
|
|
755
|
+
# Write out the definition of XsdTypes.gmlAbstractFeatureType as actual class definition.
|
|
756
|
+
# By having these base elements, the XPath queries can also resolve these like any other feature field elements.
|
|
765
757
|
return XsdComplexType(
|
|
766
|
-
prefix="gml",
|
|
767
758
|
name="AbstractFeatureType",
|
|
759
|
+
namespace=xmlns.gml.value,
|
|
768
760
|
elements=base_elements,
|
|
769
761
|
attributes=[
|
|
770
|
-
# Add gml:id attribute definition so it can be resolved in xpath
|
|
762
|
+
# Add gml:id="..." attribute definition so it can be resolved in xpath
|
|
771
763
|
GmlIdAttribute(
|
|
772
764
|
type_name=self.name,
|
|
773
765
|
source=pk_field,
|
|
774
766
|
model_attribute=pk_field.name,
|
|
767
|
+
feature_type=self,
|
|
775
768
|
)
|
|
776
769
|
],
|
|
777
770
|
base=XsdTypes.gmlAbstractGMLType,
|
|
@@ -781,13 +774,14 @@ class FeatureType:
|
|
|
781
774
|
def xsd_type(self) -> XsdComplexType:
|
|
782
775
|
"""Return the definition of this feature as an XSD Complex Type."""
|
|
783
776
|
return self.xsd_type_class(
|
|
784
|
-
name=f"{self.name.
|
|
777
|
+
name=f"{self.name[0].upper()}{self.name[1:]}Type",
|
|
785
778
|
elements=[field.xsd_element for field in self.fields],
|
|
779
|
+
namespace=self.xml_namespace,
|
|
786
780
|
base=self.xsd_base_type,
|
|
787
781
|
source=self.model,
|
|
788
782
|
)
|
|
789
783
|
|
|
790
|
-
def resolve_element(self, xpath: str) -> XPathMatch:
|
|
784
|
+
def resolve_element(self, xpath: str, ns_aliases: dict[str, str]) -> XPathMatch:
|
|
791
785
|
"""Resolve the element, and the matching object.
|
|
792
786
|
|
|
793
787
|
This is used to convert XPath references in requests
|
|
@@ -795,13 +789,28 @@ class FeatureType:
|
|
|
795
789
|
|
|
796
790
|
Internally, this method caches results.
|
|
797
791
|
"""
|
|
798
|
-
|
|
792
|
+
# When the XML POST request used xmlns="http://www.opengis.net/wfs/2.0",
|
|
793
|
+
# this will become the default namespace elements are translated into.
|
|
794
|
+
# However, the XPath elements should be interpreted within our application namespace instead.
|
|
795
|
+
# When the default namespace is missing, it should also resolve to our feature.
|
|
796
|
+
ns_aliases = ns_aliases.copy()
|
|
797
|
+
ns_aliases[""] = self.xml_namespace
|
|
798
|
+
|
|
799
|
+
if len(ns_aliases) > 10:
|
|
800
|
+
# Avoid filling memory by caching large namespace blobs.
|
|
801
|
+
nodes = self._inner_resolve_element(xpath, ns_aliases)
|
|
802
|
+
else:
|
|
803
|
+
# Go through lru_cache() for faster lookup of the same elements.
|
|
804
|
+
# Note 1: the cache will be less effective when clients use different namespace aliases.
|
|
805
|
+
# Note 2: when WFSView overrides get_feature_types() the cache may only exist per request.
|
|
806
|
+
nodes = self._cached_resolver(xpath, HDict(ns_aliases))
|
|
807
|
+
|
|
799
808
|
if nodes is None:
|
|
800
809
|
raise ExternalValueError(f"Field '{xpath}' does not exist.")
|
|
801
810
|
|
|
802
811
|
return XPathMatch(self, nodes, query=xpath)
|
|
803
812
|
|
|
804
|
-
def _inner_resolve_element(self, xpath: str):
|
|
813
|
+
def _inner_resolve_element(self, xpath: str, ns_aliases: dict[str, str]):
|
|
805
814
|
"""Inner part of resolve_element() that is cached.
|
|
806
815
|
This performs any additional checks that happen at the root-level only.
|
|
807
816
|
"""
|
|
@@ -818,16 +827,34 @@ class FeatureType:
|
|
|
818
827
|
elif "(" in xpath:
|
|
819
828
|
raise NotImplementedError(f"XPath selectors with functions are not supported: {xpath}")
|
|
820
829
|
|
|
821
|
-
# Allow /app:ElementName/.. as "absolute" path.
|
|
822
|
-
#
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
830
|
+
# Allow /app:ElementName/.. as "absolute" path, simply strip it for now.
|
|
831
|
+
# This is only actually needed to support join queries, which we don't.
|
|
832
|
+
xpath = xpath.lstrip("/")
|
|
833
|
+
root_name, _, _ = xpath.partition("/")
|
|
834
|
+
root_xml_name = parse_qname(root_name, ns_aliases)
|
|
835
|
+
if root_xml_name == self.xml_name:
|
|
836
|
+
xpath = xpath[len(root_name) + 1 :]
|
|
837
|
+
|
|
838
|
+
return self.xsd_type.resolve_element_path(xpath, ns_aliases)
|
|
839
|
+
|
|
840
|
+
def resolve_crs(self, crs: CRS, locator="") -> CRS:
|
|
841
|
+
"""Check a parsed CRS against the list of supported types."""
|
|
842
|
+
pos = self.supported_crs.index(crs)
|
|
843
|
+
if pos != -1:
|
|
844
|
+
# Replace the parsed CRS with the declared one, which may have a 'backend' configured.
|
|
845
|
+
return self.supported_crs[pos]
|
|
846
|
+
|
|
847
|
+
if conf.GISSERVER_SUPPORTED_CRS_ONLY:
|
|
848
|
+
raise InvalidParameterValue(
|
|
849
|
+
f"Feature '{self.name}' does not support SRID {crs.srid}.",
|
|
850
|
+
locator=locator,
|
|
851
|
+
)
|
|
852
|
+
else:
|
|
853
|
+
return crs
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
class HDict(dict):
|
|
857
|
+
"""Dict that can be used in lru_cache()."""
|
|
832
858
|
|
|
833
|
-
|
|
859
|
+
def __hash__(self):
|
|
860
|
+
return hash(frozenset(self.items()))
|