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
gisserver/features.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
"""The configuration
|
|
1
|
+
"""The main configuration for exposing model data in the WFS server.
|
|
2
2
|
|
|
3
3
|
The "feature type" definitions define what models and attributes are exposed in the WFS server.
|
|
4
4
|
When a model attribute is mentioned in the feature type, it can be exposed and queried against.
|
|
5
5
|
Any field that is not mentioned in a definition, will therefore not be available, nor queryable.
|
|
6
6
|
This metadata is used in the ``GetCapabilities`` call to advertise all available feature types.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
The "feature type" definitions ares translated internally into
|
|
9
|
+
an internal XML Schema Definition (made from :mod:`gisserver.types`).
|
|
10
10
|
That schema maps all model attributes to a specific XML layout, and includes
|
|
11
11
|
all XSD Complex Types, elements and attributes linked to the Django model metadata.
|
|
12
|
+
|
|
12
13
|
The feature type classes (and field types) offer a flexible translation
|
|
13
14
|
from attribute listings into a schema definition.
|
|
14
15
|
For example, model relationships can be modelled to a different XML layout.
|
|
@@ -17,24 +18,28 @@ For example, model relationships can be modelled to a different XML layout.
|
|
|
17
18
|
from __future__ import annotations
|
|
18
19
|
|
|
19
20
|
import html
|
|
20
|
-
import
|
|
21
|
+
import itertools
|
|
22
|
+
import logging
|
|
21
23
|
from dataclasses import dataclass
|
|
22
|
-
from functools import cached_property, lru_cache
|
|
23
|
-
from typing import TYPE_CHECKING, Literal
|
|
24
|
+
from functools import cached_property, lru_cache
|
|
25
|
+
from typing import TYPE_CHECKING, Literal
|
|
24
26
|
|
|
25
|
-
from django.conf import settings
|
|
26
27
|
from django.contrib.gis.db import models as gis_models
|
|
27
|
-
from django.contrib.gis.db.models import
|
|
28
|
+
from django.contrib.gis.db.models import GeometryField
|
|
28
29
|
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
|
29
30
|
from django.db import models
|
|
30
|
-
from django.
|
|
31
|
-
|
|
32
|
-
from gisserver
|
|
33
|
-
from gisserver.
|
|
34
|
-
from gisserver.
|
|
31
|
+
from django.http import HttpRequest
|
|
32
|
+
|
|
33
|
+
from gisserver import conf
|
|
34
|
+
from gisserver.compat import ArrayField, GeneratedField
|
|
35
|
+
from gisserver.crs import CRS
|
|
36
|
+
from gisserver.db import get_wgs84_bounding_box
|
|
37
|
+
from gisserver.exceptions import ExternalValueError, InvalidParameterValue
|
|
38
|
+
from gisserver.geometries import WGS84BoundingBox
|
|
39
|
+
from gisserver.parsers.xml import parse_qname, xmlns
|
|
35
40
|
from gisserver.types import (
|
|
41
|
+
GeometryXsdElement,
|
|
36
42
|
GmlBoundedByElement,
|
|
37
|
-
GmlElement,
|
|
38
43
|
GmlIdAttribute,
|
|
39
44
|
GmlNameElement,
|
|
40
45
|
XPathMatch,
|
|
@@ -44,35 +49,33 @@ from gisserver.types import (
|
|
|
44
49
|
XsdTypes,
|
|
45
50
|
)
|
|
46
51
|
|
|
47
|
-
if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
48
|
-
from django.contrib.postgres.fields import ArrayField
|
|
49
|
-
else:
|
|
50
|
-
ArrayField = None
|
|
51
|
-
|
|
52
52
|
if TYPE_CHECKING:
|
|
53
|
-
from gisserver.
|
|
54
|
-
|
|
55
|
-
_all_ = Literal["__all__"]
|
|
53
|
+
from gisserver.projection import FeatureRelation
|
|
56
54
|
|
|
57
55
|
__all__ = [
|
|
58
56
|
"FeatureType",
|
|
59
57
|
"field",
|
|
60
58
|
"FeatureField",
|
|
61
59
|
"ComplexFeatureField",
|
|
62
|
-
"get_basic_field_type",
|
|
63
60
|
]
|
|
64
61
|
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
65
64
|
XSD_TYPES = {
|
|
66
65
|
models.CharField: XsdTypes.string,
|
|
67
66
|
models.TextField: XsdTypes.string,
|
|
68
67
|
models.BooleanField: XsdTypes.boolean,
|
|
69
68
|
models.IntegerField: XsdTypes.integer,
|
|
69
|
+
models.PositiveIntegerField: XsdTypes.nonNegativeInteger,
|
|
70
|
+
models.PositiveBigIntegerField: XsdTypes.nonNegativeInteger,
|
|
71
|
+
models.PositiveSmallIntegerField: XsdTypes.nonNegativeInteger,
|
|
70
72
|
models.AutoField: XsdTypes.integer, # Only as of Django 3.0 this extends from IntegerField
|
|
71
73
|
models.FloatField: XsdTypes.double,
|
|
72
74
|
models.DecimalField: XsdTypes.decimal,
|
|
73
75
|
models.TimeField: XsdTypes.time,
|
|
74
76
|
models.DateTimeField: XsdTypes.dateTime, # note: DateTimeField extends DateField!
|
|
75
77
|
models.DateField: XsdTypes.date,
|
|
78
|
+
models.DurationField: XsdTypes.duration,
|
|
76
79
|
models.URLField: XsdTypes.anyURI,
|
|
77
80
|
gis_models.PointField: XsdTypes.gmlPointPropertyType,
|
|
78
81
|
gis_models.PolygonField: XsdTypes.gmlSurfacePropertyType,
|
|
@@ -87,8 +90,8 @@ XSD_TYPES = {
|
|
|
87
90
|
DEFAULT_XSD_TYPE = XsdTypes.anyType
|
|
88
91
|
|
|
89
92
|
|
|
90
|
-
def
|
|
91
|
-
field_name: str, model_field: models.Field | ForeignObjectRel
|
|
93
|
+
def _get_basic_field_type(
|
|
94
|
+
field_name: str, model_field: models.Field | models.ForeignObjectRel
|
|
92
95
|
) -> XsdAnyType:
|
|
93
96
|
"""Determine the XSD field type for a Django field."""
|
|
94
97
|
if ArrayField is not None and isinstance(model_field, ArrayField):
|
|
@@ -96,6 +99,10 @@ def get_basic_field_type(
|
|
|
96
99
|
# The array notation is written as "is_many"
|
|
97
100
|
model_field = model_field.base_field
|
|
98
101
|
|
|
102
|
+
if GeneratedField is not None and isinstance(model_field, GeneratedField):
|
|
103
|
+
# Allow things like: models.GeneratedField(SomeFunction("geofield"), output_field=models.GeometryField())
|
|
104
|
+
model_field = model_field.output_field
|
|
105
|
+
|
|
99
106
|
try:
|
|
100
107
|
# Direct instance, quickly resolved!
|
|
101
108
|
return XSD_TYPES[model_field.__class__]
|
|
@@ -104,10 +111,10 @@ def get_basic_field_type(
|
|
|
104
111
|
|
|
105
112
|
if isinstance(model_field, models.ForeignKey):
|
|
106
113
|
# Don't let it query on the relation value yet
|
|
107
|
-
return
|
|
108
|
-
elif isinstance(model_field, ForeignObjectRel):
|
|
114
|
+
return _get_basic_field_type(field_name, model_field.target_field)
|
|
115
|
+
elif isinstance(model_field, models.ForeignObjectRel):
|
|
109
116
|
# e.g. ManyToOneRel descriptor of a foreignkey_id field.
|
|
110
|
-
return
|
|
117
|
+
return _get_basic_field_type(field_name, model_field.remote_field.target_field)
|
|
111
118
|
else:
|
|
112
119
|
# Subclass checks:
|
|
113
120
|
for field_cls, xsd_type in XSD_TYPES.items():
|
|
@@ -124,7 +131,7 @@ def get_basic_field_type(
|
|
|
124
131
|
|
|
125
132
|
def _get_model_fields(
|
|
126
133
|
model: type[models.Model],
|
|
127
|
-
fields:
|
|
134
|
+
fields: list[str] | Literal["__all__"],
|
|
128
135
|
parent: ComplexFeatureField | None = None,
|
|
129
136
|
feature_type: FeatureType | None = None,
|
|
130
137
|
):
|
|
@@ -141,12 +148,12 @@ def _get_model_fields(
|
|
|
141
148
|
feature_type=feature_type,
|
|
142
149
|
)
|
|
143
150
|
for f in model._meta.get_fields()
|
|
144
|
-
if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey
|
|
151
|
+
if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey, OneToOneField
|
|
145
152
|
]
|
|
146
153
|
else:
|
|
147
154
|
# Only defined fields
|
|
148
155
|
fields = [f if isinstance(f, FeatureField) else FeatureField(f) for f in fields]
|
|
149
|
-
for field in fields:
|
|
156
|
+
for field in fields: # type: FeatureField
|
|
150
157
|
field.bind(model, parent=parent, feature_type=feature_type)
|
|
151
158
|
return fields
|
|
152
159
|
|
|
@@ -172,10 +179,10 @@ class FeatureField:
|
|
|
172
179
|
"""
|
|
173
180
|
|
|
174
181
|
#: Allow to override the XSD element type that this field will generate.
|
|
175
|
-
xsd_element_class: type[XsdElement] =
|
|
182
|
+
xsd_element_class: type[XsdElement] = None
|
|
176
183
|
|
|
177
184
|
model: type[models.Model] | None
|
|
178
|
-
model_field: models.Field | ForeignObjectRel | None
|
|
185
|
+
model_field: models.Field | models.ForeignObjectRel | None
|
|
179
186
|
|
|
180
187
|
def __init__(
|
|
181
188
|
self,
|
|
@@ -213,14 +220,20 @@ class FeatureField:
|
|
|
213
220
|
self.xsd_element_class = xsd_class
|
|
214
221
|
|
|
215
222
|
self._nillable_relation = False
|
|
223
|
+
self._nillable_output_field = False
|
|
216
224
|
if model is not None:
|
|
217
225
|
self.bind(model, parent=parent, feature_type=feature_type)
|
|
218
226
|
|
|
219
227
|
def __repr__(self):
|
|
220
|
-
|
|
228
|
+
if self.model_field is None:
|
|
229
|
+
return f"<{self.__class__.__name__}: {self.name} (unbound)>"
|
|
230
|
+
else:
|
|
231
|
+
return (
|
|
232
|
+
f"<{self.__class__.__name__}: {self.name}, source={self.absolute_model_attribute}>"
|
|
233
|
+
)
|
|
221
234
|
|
|
222
235
|
def _get_xsd_type(self):
|
|
223
|
-
return
|
|
236
|
+
return _get_basic_field_type(self.name, self.model_field)
|
|
224
237
|
|
|
225
238
|
def bind(
|
|
226
239
|
self,
|
|
@@ -252,29 +265,48 @@ class FeatureField:
|
|
|
252
265
|
field_path = self.model_attribute.split(".")
|
|
253
266
|
try:
|
|
254
267
|
field: models.Field = self.model._meta.get_field(field_path[0])
|
|
268
|
+
|
|
269
|
+
for name in field_path[1:]:
|
|
270
|
+
if field.null:
|
|
271
|
+
self._nillable_relation = True
|
|
272
|
+
|
|
273
|
+
if not field.is_relation:
|
|
274
|
+
# Note this tests the previous loop variable, so it checks whether the
|
|
275
|
+
# field can be walked into in order to resolve the next dotted name below.
|
|
276
|
+
raise ImproperlyConfigured(
|
|
277
|
+
f"FeatureField '{self.name}' has an invalid model_attribute: "
|
|
278
|
+
f"field '{name}' is a '{field.__class__.__name__}', not a relation."
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
field = field.related_model._meta.get_field(name)
|
|
282
|
+
self.model_field = field
|
|
255
283
|
except FieldDoesNotExist as e:
|
|
256
284
|
raise ImproperlyConfigured(
|
|
257
285
|
f"FeatureField '{self.name}' has an invalid"
|
|
258
286
|
f" model_attribute: '{self.model_attribute}' can't be"
|
|
259
287
|
f" resolved for model '{self.model.__name__}'."
|
|
260
288
|
) from e
|
|
289
|
+
else:
|
|
290
|
+
try:
|
|
291
|
+
self.model_field = self.model._meta.get_field(self.name)
|
|
292
|
+
if GeneratedField is not None and isinstance(self.model_field, GeneratedField):
|
|
293
|
+
self._nillable_output_field = self.model_field.output_field.null
|
|
294
|
+
except FieldDoesNotExist as e:
|
|
295
|
+
raise ImproperlyConfigured(
|
|
296
|
+
f"FeatureField '{self.name}' can't be resolved for model '{self.model.__name__}'."
|
|
297
|
+
" Either set 'model_attribute' or change the name."
|
|
298
|
+
) from e
|
|
261
299
|
|
|
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
|
-
)
|
|
300
|
+
@cached_property
|
|
301
|
+
def absolute_model_attribute(self) -> str:
|
|
302
|
+
"""Determine the full attribute of the field."""
|
|
303
|
+
if self.model_field is None:
|
|
304
|
+
raise RuntimeError(f"bind() was not called for {self!r}")
|
|
273
305
|
|
|
274
|
-
|
|
275
|
-
self.
|
|
306
|
+
if self.parent is not None:
|
|
307
|
+
return f"{self.parent.absolute_model_attribute}.{self.model_attribute or self.name}"
|
|
276
308
|
else:
|
|
277
|
-
self.
|
|
309
|
+
return self.model_attribute or self.name
|
|
278
310
|
|
|
279
311
|
@cached_property
|
|
280
312
|
def xsd_element(self) -> XsdElement:
|
|
@@ -287,8 +319,10 @@ class FeatureField:
|
|
|
287
319
|
if self.model_field is None:
|
|
288
320
|
raise RuntimeError(f"bind() was not called for {self!r}")
|
|
289
321
|
|
|
322
|
+
xsd_type = self._get_xsd_type()
|
|
323
|
+
|
|
290
324
|
# Determine max number of occurrences.
|
|
291
|
-
if
|
|
325
|
+
if xsd_type.is_geometry:
|
|
292
326
|
max_occurs = 1 # be explicit here, like mapserver does.
|
|
293
327
|
elif self.model_field.many_to_many or self.model_field.one_to_many:
|
|
294
328
|
max_occurs = "unbounded" # M2M or reverse FK field
|
|
@@ -297,22 +331,27 @@ class FeatureField:
|
|
|
297
331
|
else:
|
|
298
332
|
max_occurs = None # default is 1, but attribute can be left out.
|
|
299
333
|
|
|
300
|
-
|
|
334
|
+
# Determine which subclass to use for the element.
|
|
335
|
+
xsd_element_class = self.xsd_element_class or (
|
|
336
|
+
GeometryXsdElement if xsd_type.is_geometry else XsdElement
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return xsd_element_class(
|
|
301
340
|
name=self.name,
|
|
302
|
-
type=
|
|
303
|
-
|
|
341
|
+
type=xsd_type,
|
|
342
|
+
namespace=self.feature_type.xml_namespace, # keep all types in the same namespace for now.
|
|
343
|
+
nillable=(
|
|
344
|
+
self.model_field.null or self._nillable_relation or self._nillable_output_field
|
|
345
|
+
),
|
|
304
346
|
min_occurs=0,
|
|
305
347
|
max_occurs=max_occurs,
|
|
306
348
|
model_attribute=self.model_attribute,
|
|
349
|
+
absolute_model_attribute=self.absolute_model_attribute,
|
|
307
350
|
source=self.model_field,
|
|
308
351
|
feature_type=self.feature_type,
|
|
309
352
|
)
|
|
310
353
|
|
|
311
354
|
|
|
312
|
-
_FieldDefinition = Union[str, FeatureField]
|
|
313
|
-
_FieldDefinitions = Union[_all_, list[_FieldDefinition]]
|
|
314
|
-
|
|
315
|
-
|
|
316
355
|
class ComplexFeatureField(FeatureField):
|
|
317
356
|
"""The configuration for an embedded relation field.
|
|
318
357
|
|
|
@@ -324,12 +363,12 @@ class ComplexFeatureField(FeatureField):
|
|
|
324
363
|
def __init__(
|
|
325
364
|
self,
|
|
326
365
|
name: str,
|
|
327
|
-
fields:
|
|
366
|
+
fields: list[str | FeatureField] | Literal["__all__"],
|
|
328
367
|
model_attribute=None,
|
|
329
368
|
model=None,
|
|
330
369
|
abstract=None,
|
|
331
370
|
xsd_class=None,
|
|
332
|
-
xsd_base_type=
|
|
371
|
+
xsd_base_type=None,
|
|
333
372
|
):
|
|
334
373
|
"""
|
|
335
374
|
:param name: Name of the model field.
|
|
@@ -355,7 +394,9 @@ class ComplexFeatureField(FeatureField):
|
|
|
355
394
|
@cached_property
|
|
356
395
|
def fields(self) -> list[FeatureField]:
|
|
357
396
|
"""Provide all fields that will be rendered as part of this complex field."""
|
|
358
|
-
return _get_model_fields(
|
|
397
|
+
return _get_model_fields(
|
|
398
|
+
self.target_model, self._fields, parent=self, feature_type=self.feature_type
|
|
399
|
+
)
|
|
359
400
|
|
|
360
401
|
def _get_xsd_type(self) -> XsdComplexType:
|
|
361
402
|
"""Generate the XSD description for the field with an object relation."""
|
|
@@ -363,6 +404,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
363
404
|
|
|
364
405
|
return XsdComplexType(
|
|
365
406
|
name=f"{self.target_model._meta.object_name}Type",
|
|
407
|
+
namespace=self.feature_type.xml_namespace, # keep all types in the same namespace for now.
|
|
366
408
|
elements=[field.xsd_element for field in self.fields],
|
|
367
409
|
attributes=[
|
|
368
410
|
# Add gml:id attribute definition, so it can be resolved in xpath
|
|
@@ -396,7 +438,7 @@ def field(
|
|
|
396
438
|
*,
|
|
397
439
|
model_attribute=None,
|
|
398
440
|
abstract: str | None = None,
|
|
399
|
-
fields:
|
|
441
|
+
fields: list[str | FeatureField] | Literal["__all__"] | None = None,
|
|
400
442
|
xsd_class: type[XsdElement] | None = None,
|
|
401
443
|
) -> FeatureField:
|
|
402
444
|
"""Shortcut to define a WFS field.
|
|
@@ -433,7 +475,7 @@ def field(
|
|
|
433
475
|
class FeatureType:
|
|
434
476
|
"""Declare a feature that is exposed on the map.
|
|
435
477
|
|
|
436
|
-
All WFS operations use this class to read the feature
|
|
478
|
+
All WFS operations use this class to read the feature type.
|
|
437
479
|
You may subclass this class to provide extensions,
|
|
438
480
|
such as redefining :meth:`get_queryset`.
|
|
439
481
|
|
|
@@ -447,7 +489,7 @@ class FeatureType:
|
|
|
447
489
|
self,
|
|
448
490
|
queryset: models.QuerySet,
|
|
449
491
|
*,
|
|
450
|
-
fields:
|
|
492
|
+
fields: list[str | FeatureField] | Literal["__all__"] | None = None,
|
|
451
493
|
display_field_name: str | None = None,
|
|
452
494
|
geometry_field_name: str | None = None,
|
|
453
495
|
name: str | None = None,
|
|
@@ -460,7 +502,7 @@ class FeatureType:
|
|
|
460
502
|
metadata_url: str | None = None,
|
|
461
503
|
# Settings
|
|
462
504
|
show_name_field: bool = True,
|
|
463
|
-
|
|
505
|
+
xml_namespace: str | None = None,
|
|
464
506
|
):
|
|
465
507
|
"""
|
|
466
508
|
:param queryset: The queryset to retrieve the data.
|
|
@@ -477,7 +519,7 @@ class FeatureType:
|
|
|
477
519
|
:param metadata_url: Used in WFS metadata.
|
|
478
520
|
:param show_name_field: Whether to show the ``gml:name`` or the GeoJSON ``geometry_name``
|
|
479
521
|
field. Default is to show a field when ``name_field`` is given.
|
|
480
|
-
:param
|
|
522
|
+
:param xml_namespace: The XML namespace to use, will be set by :meth:`bind_namespace` otherwise.
|
|
481
523
|
"""
|
|
482
524
|
if isinstance(queryset, models.QuerySet):
|
|
483
525
|
self.queryset = queryset
|
|
@@ -502,23 +544,41 @@ class FeatureType:
|
|
|
502
544
|
|
|
503
545
|
# Settings
|
|
504
546
|
self.show_name_field = show_name_field
|
|
505
|
-
self.
|
|
547
|
+
self.xml_namespace = xml_namespace
|
|
506
548
|
|
|
507
549
|
# Validate that the name doesn't require XML escaping.
|
|
508
550
|
if html.escape(self.name) != self.name or " " in self.name or ":" in self.name:
|
|
509
551
|
raise ValueError(f"Invalid feature name for XML: <{self.xml_name}>")
|
|
510
552
|
|
|
511
|
-
self._cached_resolver = lru_cache(
|
|
553
|
+
self._cached_resolver = lru_cache(200)(self._inner_resolve_element)
|
|
512
554
|
|
|
513
|
-
def
|
|
555
|
+
def __repr__(self):
|
|
556
|
+
return (
|
|
557
|
+
f"<{self.__class__.__qualname__}: {self.xml_name},"
|
|
558
|
+
f" fields={self.fields!r},"
|
|
559
|
+
f" geometry_field_name={self.main_geometry_element.absolute_model_attribute!r}>"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def bind_namespace(self, default_xml_namespace: str):
|
|
563
|
+
"""Make sure the feature type receives the settings from the parent view."""
|
|
564
|
+
if not self.xml_namespace:
|
|
565
|
+
self.xml_namespace = default_xml_namespace
|
|
566
|
+
|
|
567
|
+
def check_permissions(self, request: HttpRequest):
|
|
514
568
|
"""Hook that allows subclasses to reject access for datasets.
|
|
515
569
|
It may raise a Django PermissionDenied error.
|
|
570
|
+
|
|
571
|
+
This can check for example whether ``request.user`` may access this feature.
|
|
572
|
+
|
|
573
|
+
The parsed WFS request is available as ``request.ows_request``.
|
|
574
|
+
Currently, this can be a :class:`~gisserver.parsers.wfs20.GetFeature`
|
|
575
|
+
or :class:`~gisserver.parsers.wfs20.GetPropertyValue` instance.
|
|
516
576
|
"""
|
|
517
577
|
|
|
518
578
|
@cached_property
|
|
519
|
-
def xml_name(self):
|
|
520
|
-
"""Return the feature
|
|
521
|
-
return f"{self.
|
|
579
|
+
def xml_name(self) -> str:
|
|
580
|
+
"""Return the feature tag as XML Full Qualified name"""
|
|
581
|
+
return f"{{{self.xml_namespace}}}{self.name}" if self.xml_namespace else self.name
|
|
522
582
|
|
|
523
583
|
@cached_property
|
|
524
584
|
def supported_crs(self) -> list[CRS]:
|
|
@@ -526,20 +586,19 @@ class FeatureType:
|
|
|
526
586
|
return [self.crs] + self.other_crs
|
|
527
587
|
|
|
528
588
|
@cached_property
|
|
529
|
-
def
|
|
530
|
-
"""
|
|
531
|
-
return
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
]
|
|
589
|
+
def all_geometry_elements(self) -> list[GeometryXsdElement]:
|
|
590
|
+
"""Provide access to all geometry elements from *all* nested levels."""
|
|
591
|
+
return self.xsd_type.geometry_elements + list(
|
|
592
|
+
itertools.chain.from_iterable(
|
|
593
|
+
# Take the geometry elements at each object level.
|
|
594
|
+
xsd_element.type.geometry_elements
|
|
595
|
+
for xsd_element in self.xsd_type.all_complex_elements
|
|
596
|
+
# The 'None' level is the root node, which is already added before.
|
|
597
|
+
# For now, don't support geometry fields on an M2M relation.
|
|
598
|
+
# If that use-case is needed, it would require additional work to implement.
|
|
599
|
+
if xsd_element is not None and not xsd_element.is_many
|
|
600
|
+
)
|
|
601
|
+
)
|
|
543
602
|
|
|
544
603
|
@cached_property
|
|
545
604
|
def _local_model_field_names(self) -> list[str]:
|
|
@@ -555,6 +614,17 @@ class FeatureType:
|
|
|
555
614
|
@cached_property
|
|
556
615
|
def fields(self) -> list[FeatureField]:
|
|
557
616
|
"""Define which fields to render."""
|
|
617
|
+
if (
|
|
618
|
+
self._geometry_field_name
|
|
619
|
+
and "." in self._geometry_field_name
|
|
620
|
+
and (self._fields is None or self._fields == "__all__")
|
|
621
|
+
):
|
|
622
|
+
# If you want to define more complex relationships, please be explicit in which fields you want.
|
|
623
|
+
# Allowing autoconfiguration of this is complex and likely not what you're looking for either.
|
|
624
|
+
raise ImproperlyConfigured(
|
|
625
|
+
"Using a geometry_field_name path requires defining 'fields' explicitly."
|
|
626
|
+
)
|
|
627
|
+
|
|
558
628
|
# This lazy reading allows providing 'fields' as lazy value.
|
|
559
629
|
if self._fields is None:
|
|
560
630
|
# Autoconfig, no fields defined.
|
|
@@ -567,6 +637,7 @@ class FeatureType:
|
|
|
567
637
|
|
|
568
638
|
return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
|
|
569
639
|
else:
|
|
640
|
+
# Either __all__ or an explicit list.
|
|
570
641
|
return _get_model_fields(self.model, self._fields, feature_type=self)
|
|
571
642
|
|
|
572
643
|
@cached_property
|
|
@@ -578,56 +649,36 @@ class FeatureType:
|
|
|
578
649
|
return self.model._meta.get_field(self.display_field_name)
|
|
579
650
|
|
|
580
651
|
@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:
|
|
652
|
+
def main_geometry_element(self) -> GeometryXsdElement:
|
|
653
|
+
"""Give access to the main geometry element."""
|
|
654
|
+
if not self._geometry_field_name:
|
|
655
|
+
try:
|
|
656
|
+
# Take the first element that has a geometry.
|
|
657
|
+
return self.all_geometry_elements[0]
|
|
658
|
+
except IndexError:
|
|
659
|
+
raise ImproperlyConfigured(
|
|
660
|
+
f"FeatureType '{self.name}' does not expose any geometry field "
|
|
661
|
+
f"as part of its definition."
|
|
662
|
+
) from None
|
|
663
|
+
else:
|
|
664
|
+
try:
|
|
665
|
+
return next(
|
|
666
|
+
e
|
|
667
|
+
for e in self.all_geometry_elements
|
|
668
|
+
if e.absolute_model_attribute == self._geometry_field_name
|
|
669
|
+
)
|
|
670
|
+
except StopIteration:
|
|
601
671
|
raise ImproperlyConfigured(
|
|
602
672
|
f"FeatureType '{self.name}' does not expose the geometry field "
|
|
603
673
|
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
|
|
674
|
+
) from None
|
|
624
675
|
|
|
625
676
|
@cached_property
|
|
626
677
|
def crs(self) -> CRS:
|
|
627
678
|
"""Tell which projection the data should be presented at."""
|
|
628
679
|
if self._crs is None:
|
|
629
680
|
# Default CRS
|
|
630
|
-
return CRS.from_srid(self.
|
|
681
|
+
return CRS.from_srid(self.main_geometry_element.source_srid) # checks lookup too
|
|
631
682
|
else:
|
|
632
683
|
return self._crs
|
|
633
684
|
|
|
@@ -635,6 +686,11 @@ class FeatureType:
|
|
|
635
686
|
"""Return the queryset that is used as basis for this feature."""
|
|
636
687
|
# Return a queryset that only retrieves the fields that are actually displayed.
|
|
637
688
|
# That that without .only(), use at least `self.queryset.all()` so a clone is returned.
|
|
689
|
+
logger.debug(
|
|
690
|
+
"QuerySet for %s default only retrieves: %r",
|
|
691
|
+
self.queryset.model._meta.label,
|
|
692
|
+
self._local_model_field_names,
|
|
693
|
+
)
|
|
638
694
|
return self.queryset.only(*self._local_model_field_names)
|
|
639
695
|
|
|
640
696
|
def get_related_queryset(self, feature_relation: FeatureRelation) -> models.QuerySet:
|
|
@@ -645,51 +701,34 @@ class FeatureType:
|
|
|
645
701
|
f"source model is not defined for: {feature_relation.xsd_elements!r}"
|
|
646
702
|
)
|
|
647
703
|
# Return a queryset that only retrieves the fields that are displayed.
|
|
648
|
-
|
|
649
|
-
|
|
704
|
+
logger.debug(
|
|
705
|
+
"QuerySet for %s by default only retrieves: %r",
|
|
706
|
+
feature_relation.related_model._meta.label,
|
|
707
|
+
feature_relation._local_model_field_names,
|
|
650
708
|
)
|
|
709
|
+
queryset = feature_relation.related_model.objects.only(
|
|
710
|
+
*feature_relation._local_model_field_names
|
|
711
|
+
)
|
|
712
|
+
return self.filter_related_queryset(queryset) # Allow overriding by FeatureType subclasses
|
|
651
713
|
|
|
652
714
|
def filter_related_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
|
|
653
715
|
"""When a related object returns a queryset, this hook allows extra filtering."""
|
|
654
716
|
return queryset
|
|
655
717
|
|
|
656
|
-
def get_bounding_box(self) ->
|
|
718
|
+
def get_bounding_box(self) -> WGS84BoundingBox | None:
|
|
657
719
|
"""Returns a WGS84 BoundingBox for the complete feature.
|
|
658
720
|
|
|
659
721
|
This is used by the GetCapabilities request. It may return ``None``
|
|
660
722
|
when the database table is empty, or the custom queryset doesn't
|
|
661
723
|
return any results.
|
|
662
|
-
"""
|
|
663
|
-
if not self.geometry_fields:
|
|
664
|
-
return None
|
|
665
|
-
|
|
666
|
-
geo_expression = conditional_transform(
|
|
667
|
-
self.geometry_field.name, self.geometry_field.srid, WGS84.srid
|
|
668
|
-
)
|
|
669
|
-
|
|
670
|
-
bbox = self.get_queryset().aggregate(a=Extent(geo_expression))["a"]
|
|
671
|
-
return BoundingBox(*bbox, crs=WGS84) if bbox else None
|
|
672
|
-
|
|
673
|
-
def get_envelope(self, instance, crs: CRS | None = None) -> BoundingBox | None:
|
|
674
|
-
"""Get the bounding box for a single instance.
|
|
675
724
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
is entirely performed within the query.
|
|
725
|
+
Note that the ``<ows:WGS84BoundingBox>`` element always uses longitude/latitude,
|
|
726
|
+
as it doesn't describe a CRS.
|
|
679
727
|
"""
|
|
680
|
-
|
|
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:
|
|
728
|
+
if not self.main_geometry_element:
|
|
686
729
|
return None
|
|
687
730
|
|
|
688
|
-
|
|
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)
|
|
731
|
+
return get_wgs84_bounding_box(self.get_queryset(), self.main_geometry_element)
|
|
693
732
|
|
|
694
733
|
def get_display_value(self, instance: models.Model) -> str:
|
|
695
734
|
"""Generate the display name value"""
|
|
@@ -709,12 +748,8 @@ class FeatureType:
|
|
|
709
748
|
"""
|
|
710
749
|
pk_field = self.model._meta.pk
|
|
711
750
|
|
|
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
|
-
|
|
751
|
+
# Define <gml:boundedBy>
|
|
752
|
+
base_elements = [GmlBoundedByElement(feature_type=self)]
|
|
718
753
|
if self.show_name_field:
|
|
719
754
|
# Add <gml:name>
|
|
720
755
|
gml_name = GmlNameElement(
|
|
@@ -724,12 +759,14 @@ class FeatureType:
|
|
|
724
759
|
)
|
|
725
760
|
base_elements.insert(0, gml_name)
|
|
726
761
|
|
|
762
|
+
# Write out the definition of XsdTypes.gmlAbstractFeatureType as actual class definition.
|
|
763
|
+
# By having these base elements, the XPath queries can also resolve these like any other feature field elements.
|
|
727
764
|
return XsdComplexType(
|
|
728
|
-
prefix="gml",
|
|
729
765
|
name="AbstractFeatureType",
|
|
766
|
+
namespace=xmlns.gml.value,
|
|
730
767
|
elements=base_elements,
|
|
731
768
|
attributes=[
|
|
732
|
-
# Add gml:id attribute definition so it can be resolved in xpath
|
|
769
|
+
# Add gml:id="..." attribute definition so it can be resolved in xpath
|
|
733
770
|
GmlIdAttribute(
|
|
734
771
|
type_name=self.name,
|
|
735
772
|
source=pk_field,
|
|
@@ -744,13 +781,14 @@ class FeatureType:
|
|
|
744
781
|
def xsd_type(self) -> XsdComplexType:
|
|
745
782
|
"""Return the definition of this feature as an XSD Complex Type."""
|
|
746
783
|
return self.xsd_type_class(
|
|
747
|
-
name=f"{self.name.
|
|
784
|
+
name=f"{self.name[0].upper()}{self.name[1:]}Type",
|
|
748
785
|
elements=[field.xsd_element for field in self.fields],
|
|
786
|
+
namespace=self.xml_namespace,
|
|
749
787
|
base=self.xsd_base_type,
|
|
750
788
|
source=self.model,
|
|
751
789
|
)
|
|
752
790
|
|
|
753
|
-
def resolve_element(self, xpath: str) -> XPathMatch:
|
|
791
|
+
def resolve_element(self, xpath: str, ns_aliases: dict[str, str]) -> XPathMatch:
|
|
754
792
|
"""Resolve the element, and the matching object.
|
|
755
793
|
|
|
756
794
|
This is used to convert XPath references in requests
|
|
@@ -758,13 +796,28 @@ class FeatureType:
|
|
|
758
796
|
|
|
759
797
|
Internally, this method caches results.
|
|
760
798
|
"""
|
|
761
|
-
|
|
799
|
+
# When the XML POST request used xmlns="http://www.opengis.net/wfs/2.0",
|
|
800
|
+
# this will become the default namespace elements are translated into.
|
|
801
|
+
# However, the XPath elements should be interpreted within our application namespace instead.
|
|
802
|
+
# When the default namespace is missing, it should also resolve to our feature.
|
|
803
|
+
ns_aliases = ns_aliases.copy()
|
|
804
|
+
ns_aliases[""] = self.xml_namespace
|
|
805
|
+
|
|
806
|
+
if len(ns_aliases) > 10:
|
|
807
|
+
# Avoid filling memory by caching large namespace blobs.
|
|
808
|
+
nodes = self._inner_resolve_element(xpath, ns_aliases)
|
|
809
|
+
else:
|
|
810
|
+
# Go through lru_cache() for faster lookup of the same elements.
|
|
811
|
+
# Note 1: the cache will be less effective when clients use different namespace aliases.
|
|
812
|
+
# Note 2: when WFSView overrides get_feature_types() the cache may only exist per request.
|
|
813
|
+
nodes = self._cached_resolver(xpath, HDict(ns_aliases))
|
|
814
|
+
|
|
762
815
|
if nodes is None:
|
|
763
816
|
raise ExternalValueError(f"Field '{xpath}' does not exist.")
|
|
764
817
|
|
|
765
818
|
return XPathMatch(self, nodes, query=xpath)
|
|
766
819
|
|
|
767
|
-
def _inner_resolve_element(self, xpath: str):
|
|
820
|
+
def _inner_resolve_element(self, xpath: str, ns_aliases: dict[str, str]):
|
|
768
821
|
"""Inner part of resolve_element() that is cached.
|
|
769
822
|
This performs any additional checks that happen at the root-level only.
|
|
770
823
|
"""
|
|
@@ -781,16 +834,35 @@ class FeatureType:
|
|
|
781
834
|
elif "(" in xpath:
|
|
782
835
|
raise NotImplementedError(f"XPath selectors with functions are not supported: {xpath}")
|
|
783
836
|
|
|
784
|
-
# Allow /app:ElementName/.. as "absolute" path.
|
|
785
|
-
#
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
837
|
+
# Allow /app:ElementName/.. as "absolute" path, simply strip it for now.
|
|
838
|
+
# This is only actually needed to support join queries, which we don't.
|
|
839
|
+
xpath = xpath.lstrip("/")
|
|
840
|
+
root_name, _, _ = xpath.partition("/")
|
|
841
|
+
root_xml_name = parse_qname(root_name, ns_aliases)
|
|
842
|
+
if root_xml_name == self.xml_name:
|
|
843
|
+
xpath = xpath[len(root_name) + 1 :]
|
|
844
|
+
|
|
845
|
+
return self.xsd_type.resolve_element_path(xpath, ns_aliases)
|
|
846
|
+
|
|
847
|
+
def resolve_crs(self, crs: CRS, locator="") -> CRS:
|
|
848
|
+
"""Check a parsed CRS against the list of supported types."""
|
|
849
|
+
try:
|
|
850
|
+
pos = self.supported_crs.index(crs)
|
|
851
|
+
|
|
852
|
+
# Replace the parsed CRS with the declared one, which may have a 'backend' configured.
|
|
853
|
+
return self.supported_crs[pos]
|
|
854
|
+
except ValueError:
|
|
855
|
+
if conf.GISSERVER_SUPPORTED_CRS_ONLY:
|
|
856
|
+
raise InvalidParameterValue(
|
|
857
|
+
f"Feature '{self.name}' does not support SRID {crs.srid}.",
|
|
858
|
+
locator=locator,
|
|
859
|
+
) from None
|
|
860
|
+
else:
|
|
861
|
+
return crs
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
class HDict(dict):
|
|
865
|
+
"""Dict that can be used in lru_cache()."""
|
|
866
|
+
|
|
867
|
+
def __hash__(self):
|
|
868
|
+
return hash(frozenset(self.items()))
|