django-gisserver 1.5.0__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.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.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 +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- 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 +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- 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 +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- 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.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/features.py
CHANGED
|
@@ -17,24 +17,27 @@ 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
|
|
20
|
+
import itertools
|
|
21
|
+
import logging
|
|
21
22
|
from dataclasses import dataclass
|
|
22
|
-
from functools import cached_property, lru_cache
|
|
23
|
+
from functools import cached_property, lru_cache
|
|
23
24
|
from typing import TYPE_CHECKING, Literal, Union
|
|
24
25
|
|
|
25
|
-
from django.conf import settings
|
|
26
26
|
from django.contrib.gis.db import models as gis_models
|
|
27
27
|
from django.contrib.gis.db.models import Extent, GeometryField
|
|
28
28
|
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
|
29
29
|
from django.db import models
|
|
30
|
-
from django.
|
|
30
|
+
from django.http import HttpRequest
|
|
31
31
|
|
|
32
|
-
from gisserver
|
|
33
|
-
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
|
|
34
36
|
from gisserver.geometries import CRS, WGS84, BoundingBox
|
|
37
|
+
from gisserver.parsers.xml import parse_qname, xmlns
|
|
35
38
|
from gisserver.types import (
|
|
39
|
+
GeometryXsdElement,
|
|
36
40
|
GmlBoundedByElement,
|
|
37
|
-
GmlElement,
|
|
38
41
|
GmlIdAttribute,
|
|
39
42
|
GmlNameElement,
|
|
40
43
|
XPathMatch,
|
|
@@ -44,13 +47,8 @@ from gisserver.types import (
|
|
|
44
47
|
XsdTypes,
|
|
45
48
|
)
|
|
46
49
|
|
|
47
|
-
if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
48
|
-
from django.contrib.postgres.fields import ArrayField
|
|
49
|
-
else:
|
|
50
|
-
ArrayField = None
|
|
51
|
-
|
|
52
50
|
if TYPE_CHECKING:
|
|
53
|
-
from gisserver.
|
|
51
|
+
from gisserver.projection import FeatureRelation
|
|
54
52
|
|
|
55
53
|
_all_ = Literal["__all__"]
|
|
56
54
|
|
|
@@ -62,6 +60,8 @@ __all__ = [
|
|
|
62
60
|
"get_basic_field_type",
|
|
63
61
|
]
|
|
64
62
|
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
65
|
XSD_TYPES = {
|
|
66
66
|
models.CharField: XsdTypes.string,
|
|
67
67
|
models.TextField: XsdTypes.string,
|
|
@@ -88,7 +88,7 @@ DEFAULT_XSD_TYPE = XsdTypes.anyType
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def get_basic_field_type(
|
|
91
|
-
field_name: str, model_field: models.Field | ForeignObjectRel
|
|
91
|
+
field_name: str, model_field: models.Field | models.ForeignObjectRel
|
|
92
92
|
) -> XsdAnyType:
|
|
93
93
|
"""Determine the XSD field type for a Django field."""
|
|
94
94
|
if ArrayField is not None and isinstance(model_field, ArrayField):
|
|
@@ -96,6 +96,10 @@ def get_basic_field_type(
|
|
|
96
96
|
# The array notation is written as "is_many"
|
|
97
97
|
model_field = model_field.base_field
|
|
98
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
|
+
|
|
99
103
|
try:
|
|
100
104
|
# Direct instance, quickly resolved!
|
|
101
105
|
return XSD_TYPES[model_field.__class__]
|
|
@@ -105,7 +109,7 @@ def get_basic_field_type(
|
|
|
105
109
|
if isinstance(model_field, models.ForeignKey):
|
|
106
110
|
# Don't let it query on the relation value yet
|
|
107
111
|
return get_basic_field_type(field_name, model_field.target_field)
|
|
108
|
-
elif isinstance(model_field, ForeignObjectRel):
|
|
112
|
+
elif isinstance(model_field, models.ForeignObjectRel):
|
|
109
113
|
# e.g. ManyToOneRel descriptor of a foreignkey_id field.
|
|
110
114
|
return get_basic_field_type(field_name, model_field.remote_field.target_field)
|
|
111
115
|
else:
|
|
@@ -141,12 +145,12 @@ def _get_model_fields(
|
|
|
141
145
|
feature_type=feature_type,
|
|
142
146
|
)
|
|
143
147
|
for f in model._meta.get_fields()
|
|
144
|
-
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
|
|
145
149
|
]
|
|
146
150
|
else:
|
|
147
151
|
# Only defined fields
|
|
148
152
|
fields = [f if isinstance(f, FeatureField) else FeatureField(f) for f in fields]
|
|
149
|
-
for field in fields:
|
|
153
|
+
for field in fields: # type: FeatureField
|
|
150
154
|
field.bind(model, parent=parent, feature_type=feature_type)
|
|
151
155
|
return fields
|
|
152
156
|
|
|
@@ -172,10 +176,10 @@ class FeatureField:
|
|
|
172
176
|
"""
|
|
173
177
|
|
|
174
178
|
#: Allow to override the XSD element type that this field will generate.
|
|
175
|
-
xsd_element_class: type[XsdElement] =
|
|
179
|
+
xsd_element_class: type[XsdElement] = None
|
|
176
180
|
|
|
177
181
|
model: type[models.Model] | None
|
|
178
|
-
model_field: models.Field | ForeignObjectRel | None
|
|
182
|
+
model_field: models.Field | models.ForeignObjectRel | None
|
|
179
183
|
|
|
180
184
|
def __init__(
|
|
181
185
|
self,
|
|
@@ -213,11 +217,17 @@ class FeatureField:
|
|
|
213
217
|
self.xsd_element_class = xsd_class
|
|
214
218
|
|
|
215
219
|
self._nillable_relation = False
|
|
220
|
+
self._nillable_output_field = False
|
|
216
221
|
if model is not None:
|
|
217
222
|
self.bind(model, parent=parent, feature_type=feature_type)
|
|
218
223
|
|
|
219
224
|
def __repr__(self):
|
|
220
|
-
|
|
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
|
+
)
|
|
221
231
|
|
|
222
232
|
def _get_xsd_type(self):
|
|
223
233
|
return get_basic_field_type(self.name, self.model_field)
|
|
@@ -252,29 +262,48 @@ class FeatureField:
|
|
|
252
262
|
field_path = self.model_attribute.split(".")
|
|
253
263
|
try:
|
|
254
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
|
|
255
280
|
except FieldDoesNotExist as e:
|
|
256
281
|
raise ImproperlyConfigured(
|
|
257
282
|
f"FeatureField '{self.name}' has an invalid"
|
|
258
283
|
f" model_attribute: '{self.model_attribute}' can't be"
|
|
259
284
|
f" resolved for model '{self.model.__name__}'."
|
|
260
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
|
|
261
296
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# Note this tests the previous loop variable, so it checks whether the
|
|
268
|
-
# field can be walked into in order to resolve the next dotted name below.
|
|
269
|
-
raise ImproperlyConfigured(
|
|
270
|
-
f"FeatureField '{field.name}' has an invalid model_attribute: "
|
|
271
|
-
f"field '{name}' is a '{field.__class__.__name__}', not a relation."
|
|
272
|
-
)
|
|
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}")
|
|
273
302
|
|
|
274
|
-
|
|
275
|
-
self.
|
|
303
|
+
if self.parent is not None:
|
|
304
|
+
return f"{self.parent.absolute_model_attribute}.{self.model_attribute or self.name}"
|
|
276
305
|
else:
|
|
277
|
-
self.
|
|
306
|
+
return self.model_attribute or self.name
|
|
278
307
|
|
|
279
308
|
@cached_property
|
|
280
309
|
def xsd_element(self) -> XsdElement:
|
|
@@ -287,8 +316,10 @@ class FeatureField:
|
|
|
287
316
|
if self.model_field is None:
|
|
288
317
|
raise RuntimeError(f"bind() was not called for {self!r}")
|
|
289
318
|
|
|
319
|
+
xsd_type = self._get_xsd_type()
|
|
320
|
+
|
|
290
321
|
# Determine max number of occurrences.
|
|
291
|
-
if
|
|
322
|
+
if xsd_type.is_geometry:
|
|
292
323
|
max_occurs = 1 # be explicit here, like mapserver does.
|
|
293
324
|
elif self.model_field.many_to_many or self.model_field.one_to_many:
|
|
294
325
|
max_occurs = "unbounded" # M2M or reverse FK field
|
|
@@ -297,13 +328,22 @@ class FeatureField:
|
|
|
297
328
|
else:
|
|
298
329
|
max_occurs = None # default is 1, but attribute can be left out.
|
|
299
330
|
|
|
300
|
-
|
|
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(
|
|
301
337
|
name=self.name,
|
|
302
|
-
type=
|
|
303
|
-
|
|
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
|
+
),
|
|
304
343
|
min_occurs=0,
|
|
305
344
|
max_occurs=max_occurs,
|
|
306
345
|
model_attribute=self.model_attribute,
|
|
346
|
+
absolute_model_attribute=self.absolute_model_attribute,
|
|
307
347
|
source=self.model_field,
|
|
308
348
|
feature_type=self.feature_type,
|
|
309
349
|
)
|
|
@@ -329,7 +369,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
329
369
|
model=None,
|
|
330
370
|
abstract=None,
|
|
331
371
|
xsd_class=None,
|
|
332
|
-
xsd_base_type=
|
|
372
|
+
xsd_base_type=None,
|
|
333
373
|
):
|
|
334
374
|
"""
|
|
335
375
|
:param name: Name of the model field.
|
|
@@ -355,7 +395,9 @@ class ComplexFeatureField(FeatureField):
|
|
|
355
395
|
@cached_property
|
|
356
396
|
def fields(self) -> list[FeatureField]:
|
|
357
397
|
"""Provide all fields that will be rendered as part of this complex field."""
|
|
358
|
-
return _get_model_fields(
|
|
398
|
+
return _get_model_fields(
|
|
399
|
+
self.target_model, self._fields, parent=self, feature_type=self.feature_type
|
|
400
|
+
)
|
|
359
401
|
|
|
360
402
|
def _get_xsd_type(self) -> XsdComplexType:
|
|
361
403
|
"""Generate the XSD description for the field with an object relation."""
|
|
@@ -363,6 +405,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
363
405
|
|
|
364
406
|
return XsdComplexType(
|
|
365
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.
|
|
366
409
|
elements=[field.xsd_element for field in self.fields],
|
|
367
410
|
attributes=[
|
|
368
411
|
# Add gml:id attribute definition, so it can be resolved in xpath
|
|
@@ -433,7 +476,7 @@ def field(
|
|
|
433
476
|
class FeatureType:
|
|
434
477
|
"""Declare a feature that is exposed on the map.
|
|
435
478
|
|
|
436
|
-
All WFS operations use this class to read the feature
|
|
479
|
+
All WFS operations use this class to read the feature type.
|
|
437
480
|
You may subclass this class to provide extensions,
|
|
438
481
|
such as redefining :meth:`get_queryset`.
|
|
439
482
|
|
|
@@ -460,7 +503,7 @@ class FeatureType:
|
|
|
460
503
|
metadata_url: str | None = None,
|
|
461
504
|
# Settings
|
|
462
505
|
show_name_field: bool = True,
|
|
463
|
-
|
|
506
|
+
xml_namespace: str | None = None,
|
|
464
507
|
):
|
|
465
508
|
"""
|
|
466
509
|
:param queryset: The queryset to retrieve the data.
|
|
@@ -477,7 +520,7 @@ class FeatureType:
|
|
|
477
520
|
:param metadata_url: Used in WFS metadata.
|
|
478
521
|
:param show_name_field: Whether to show the ``gml:name`` or the GeoJSON ``geometry_name``
|
|
479
522
|
field. Default is to show a field when ``name_field`` is given.
|
|
480
|
-
:param
|
|
523
|
+
:param xml_namespace: The XML namespace to use, will be set by :meth:`bind_namespace` otherwise.
|
|
481
524
|
"""
|
|
482
525
|
if isinstance(queryset, models.QuerySet):
|
|
483
526
|
self.queryset = queryset
|
|
@@ -502,23 +545,34 @@ class FeatureType:
|
|
|
502
545
|
|
|
503
546
|
# Settings
|
|
504
547
|
self.show_name_field = show_name_field
|
|
505
|
-
self.
|
|
548
|
+
self.xml_namespace = xml_namespace
|
|
506
549
|
|
|
507
550
|
# Validate that the name doesn't require XML escaping.
|
|
508
551
|
if html.escape(self.name) != self.name or " " in self.name or ":" in self.name:
|
|
509
552
|
raise ValueError(f"Invalid feature name for XML: <{self.xml_name}>")
|
|
510
553
|
|
|
511
|
-
self._cached_resolver = lru_cache(
|
|
554
|
+
self._cached_resolver = lru_cache(200)(self._inner_resolve_element)
|
|
555
|
+
|
|
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
|
|
512
560
|
|
|
513
|
-
def check_permissions(self, request):
|
|
561
|
+
def check_permissions(self, request: HttpRequest):
|
|
514
562
|
"""Hook that allows subclasses to reject access for datasets.
|
|
515
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.
|
|
516
570
|
"""
|
|
517
571
|
|
|
518
572
|
@cached_property
|
|
519
|
-
def xml_name(self):
|
|
520
|
-
"""Return the feature
|
|
521
|
-
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
|
|
522
576
|
|
|
523
577
|
@cached_property
|
|
524
578
|
def supported_crs(self) -> list[CRS]:
|
|
@@ -526,20 +580,19 @@ class FeatureType:
|
|
|
526
580
|
return [self.crs] + self.other_crs
|
|
527
581
|
|
|
528
582
|
@cached_property
|
|
529
|
-
def
|
|
530
|
-
"""
|
|
531
|
-
return
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
]
|
|
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
|
|
594
|
+
)
|
|
595
|
+
)
|
|
543
596
|
|
|
544
597
|
@cached_property
|
|
545
598
|
def _local_model_field_names(self) -> list[str]:
|
|
@@ -555,6 +608,17 @@ class FeatureType:
|
|
|
555
608
|
@cached_property
|
|
556
609
|
def fields(self) -> list[FeatureField]:
|
|
557
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
|
+
|
|
558
622
|
# This lazy reading allows providing 'fields' as lazy value.
|
|
559
623
|
if self._fields is None:
|
|
560
624
|
# Autoconfig, no fields defined.
|
|
@@ -567,6 +631,7 @@ class FeatureType:
|
|
|
567
631
|
|
|
568
632
|
return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
|
|
569
633
|
else:
|
|
634
|
+
# Either __all__ or an explicit list.
|
|
570
635
|
return _get_model_fields(self.model, self._fields, feature_type=self)
|
|
571
636
|
|
|
572
637
|
@cached_property
|
|
@@ -578,56 +643,36 @@ class FeatureType:
|
|
|
578
643
|
return self.model._meta.get_field(self.display_field_name)
|
|
579
644
|
|
|
580
645
|
@cached_property
|
|
581
|
-
def
|
|
582
|
-
"""Give access to the
|
|
583
|
-
if not self.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
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:
|
|
601
665
|
raise ImproperlyConfigured(
|
|
602
666
|
f"FeatureType '{self.name}' does not expose the geometry field "
|
|
603
667
|
f"'{self._geometry_field_name}' as part of its definition."
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
return field
|
|
607
|
-
else:
|
|
608
|
-
# Default: take the first geometry
|
|
609
|
-
return self.geometry_fields[0]
|
|
610
|
-
|
|
611
|
-
@cached_property
|
|
612
|
-
def main_geometry_element(self) -> GmlElement:
|
|
613
|
-
"""Give access to the main geometry element."""
|
|
614
|
-
# NOTE: this property should make it easier to move away from having a main geometry field
|
|
615
|
-
# at the model level, and allow a geometry field in any depth of the data model.
|
|
616
|
-
return next((e for e in self.geometry_elements if e.source is self.geometry_field), None)
|
|
617
|
-
|
|
618
|
-
@cached_property
|
|
619
|
-
def geometry_field_name(self) -> str:
|
|
620
|
-
"""Tell which field is the geometry field."""
|
|
621
|
-
# Due to internal reorganization this property is no longer needed,
|
|
622
|
-
# retained for backward compatibility and to reflect any used input parameters.
|
|
623
|
-
return self.geometry_field.name
|
|
668
|
+
) from None
|
|
624
669
|
|
|
625
670
|
@cached_property
|
|
626
671
|
def crs(self) -> CRS:
|
|
627
672
|
"""Tell which projection the data should be presented at."""
|
|
628
673
|
if self._crs is None:
|
|
629
674
|
# Default CRS
|
|
630
|
-
return CRS.from_srid(self.
|
|
675
|
+
return CRS.from_srid(self.main_geometry_element.source_srid) # checks lookup too
|
|
631
676
|
else:
|
|
632
677
|
return self._crs
|
|
633
678
|
|
|
@@ -635,6 +680,11 @@ class FeatureType:
|
|
|
635
680
|
"""Return the queryset that is used as basis for this feature."""
|
|
636
681
|
# Return a queryset that only retrieves the fields that are actually displayed.
|
|
637
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
|
+
)
|
|
638
688
|
return self.queryset.only(*self._local_model_field_names)
|
|
639
689
|
|
|
640
690
|
def get_related_queryset(self, feature_relation: FeatureRelation) -> models.QuerySet:
|
|
@@ -645,9 +695,15 @@ class FeatureType:
|
|
|
645
695
|
f"source model is not defined for: {feature_relation.xsd_elements!r}"
|
|
646
696
|
)
|
|
647
697
|
# Return a queryset that only retrieves the fields that are displayed.
|
|
648
|
-
|
|
649
|
-
|
|
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,
|
|
650
702
|
)
|
|
703
|
+
queryset = feature_relation.related_model.objects.only(
|
|
704
|
+
*feature_relation._local_model_field_names
|
|
705
|
+
)
|
|
706
|
+
return self.filter_related_queryset(queryset) # Allow overriding by FeatureType subclasses
|
|
651
707
|
|
|
652
708
|
def filter_related_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
|
|
653
709
|
"""When a related object returns a queryset, this hook allows extra filtering."""
|
|
@@ -660,37 +716,13 @@ class FeatureType:
|
|
|
660
716
|
when the database table is empty, or the custom queryset doesn't
|
|
661
717
|
return any results.
|
|
662
718
|
"""
|
|
663
|
-
if not self.
|
|
719
|
+
if not self.main_geometry_element:
|
|
664
720
|
return None
|
|
665
721
|
|
|
666
|
-
geo_expression =
|
|
667
|
-
self.geometry_field.name, self.geometry_field.srid, WGS84.srid
|
|
668
|
-
)
|
|
669
|
-
|
|
722
|
+
geo_expression = get_db_geometry_target(self.main_geometry_element, WGS84)
|
|
670
723
|
bbox = self.get_queryset().aggregate(a=Extent(geo_expression))["a"]
|
|
671
724
|
return BoundingBox(*bbox, crs=WGS84) if bbox else None
|
|
672
725
|
|
|
673
|
-
def get_envelope(self, instance, crs: CRS | None = None) -> BoundingBox | None:
|
|
674
|
-
"""Get the bounding box for a single instance.
|
|
675
|
-
|
|
676
|
-
This is only used for native Python rendering. When the database
|
|
677
|
-
rendering is enabled (GISSERVER_USE_DB_RENDERING=True), the calculation
|
|
678
|
-
is entirely performed within the query.
|
|
679
|
-
"""
|
|
680
|
-
geometries = [
|
|
681
|
-
geom
|
|
682
|
-
for geom in (getattr(instance, f.name) for f in self.geometry_fields)
|
|
683
|
-
if geom is not None
|
|
684
|
-
]
|
|
685
|
-
if not geometries:
|
|
686
|
-
return None
|
|
687
|
-
|
|
688
|
-
# Perform the combining of geometries inside libgeos
|
|
689
|
-
geometry = geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
|
|
690
|
-
if crs is not None and geometry.srid != crs.srid:
|
|
691
|
-
crs.apply_to(geometry) # avoid clone
|
|
692
|
-
return BoundingBox.from_geometry(geometry, crs=crs)
|
|
693
|
-
|
|
694
726
|
def get_display_value(self, instance: models.Model) -> str:
|
|
695
727
|
"""Generate the display name value"""
|
|
696
728
|
if self.display_field_name:
|
|
@@ -709,12 +741,8 @@ class FeatureType:
|
|
|
709
741
|
"""
|
|
710
742
|
pk_field = self.model._meta.pk
|
|
711
743
|
|
|
712
|
-
# Define <gml:boundedBy
|
|
713
|
-
base_elements = []
|
|
714
|
-
if self.geometry_fields:
|
|
715
|
-
# Without a geometry, boundaries are not possible.
|
|
716
|
-
base_elements.append(GmlBoundedByElement(feature_type=self))
|
|
717
|
-
|
|
744
|
+
# Define <gml:boundedBy>
|
|
745
|
+
base_elements = [GmlBoundedByElement(feature_type=self)]
|
|
718
746
|
if self.show_name_field:
|
|
719
747
|
# Add <gml:name>
|
|
720
748
|
gml_name = GmlNameElement(
|
|
@@ -724,12 +752,14 @@ class FeatureType:
|
|
|
724
752
|
)
|
|
725
753
|
base_elements.insert(0, gml_name)
|
|
726
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.
|
|
727
757
|
return XsdComplexType(
|
|
728
|
-
prefix="gml",
|
|
729
758
|
name="AbstractFeatureType",
|
|
759
|
+
namespace=xmlns.gml.value,
|
|
730
760
|
elements=base_elements,
|
|
731
761
|
attributes=[
|
|
732
|
-
# 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
|
|
733
763
|
GmlIdAttribute(
|
|
734
764
|
type_name=self.name,
|
|
735
765
|
source=pk_field,
|
|
@@ -744,13 +774,14 @@ class FeatureType:
|
|
|
744
774
|
def xsd_type(self) -> XsdComplexType:
|
|
745
775
|
"""Return the definition of this feature as an XSD Complex Type."""
|
|
746
776
|
return self.xsd_type_class(
|
|
747
|
-
name=f"{self.name.
|
|
777
|
+
name=f"{self.name[0].upper()}{self.name[1:]}Type",
|
|
748
778
|
elements=[field.xsd_element for field in self.fields],
|
|
779
|
+
namespace=self.xml_namespace,
|
|
749
780
|
base=self.xsd_base_type,
|
|
750
781
|
source=self.model,
|
|
751
782
|
)
|
|
752
783
|
|
|
753
|
-
def resolve_element(self, xpath: str) -> XPathMatch:
|
|
784
|
+
def resolve_element(self, xpath: str, ns_aliases: dict[str, str]) -> XPathMatch:
|
|
754
785
|
"""Resolve the element, and the matching object.
|
|
755
786
|
|
|
756
787
|
This is used to convert XPath references in requests
|
|
@@ -758,13 +789,28 @@ class FeatureType:
|
|
|
758
789
|
|
|
759
790
|
Internally, this method caches results.
|
|
760
791
|
"""
|
|
761
|
-
|
|
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
|
+
|
|
762
808
|
if nodes is None:
|
|
763
809
|
raise ExternalValueError(f"Field '{xpath}' does not exist.")
|
|
764
810
|
|
|
765
811
|
return XPathMatch(self, nodes, query=xpath)
|
|
766
812
|
|
|
767
|
-
def _inner_resolve_element(self, xpath: str):
|
|
813
|
+
def _inner_resolve_element(self, xpath: str, ns_aliases: dict[str, str]):
|
|
768
814
|
"""Inner part of resolve_element() that is cached.
|
|
769
815
|
This performs any additional checks that happen at the root-level only.
|
|
770
816
|
"""
|
|
@@ -781,16 +827,34 @@ class FeatureType:
|
|
|
781
827
|
elif "(" in xpath:
|
|
782
828
|
raise NotImplementedError(f"XPath selectors with functions are not supported: {xpath}")
|
|
783
829
|
|
|
784
|
-
# Allow /app:ElementName/.. as "absolute" path.
|
|
785
|
-
#
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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()."""
|
|
795
858
|
|
|
796
|
-
|
|
859
|
+
def __hash__(self):
|
|
860
|
+
return hash(frozenset(self.items()))
|