django-gisserver 2.0__py3-none-any.whl → 2.1.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-2.0.dist-info → django_gisserver-2.1.1.dist-info}/METADATA +27 -10
- django_gisserver-2.1.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/conf.py +23 -1
- gisserver/crs.py +452 -0
- gisserver/db.py +78 -6
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +46 -33
- gisserver/geometries.py +64 -306
- gisserver/management/commands/loadgeojson.py +41 -21
- gisserver/operations/base.py +11 -7
- gisserver/operations/wfs20.py +31 -93
- gisserver/output/__init__.py +6 -2
- gisserver/output/base.py +28 -13
- gisserver/output/csv.py +18 -6
- gisserver/output/geojson.py +7 -6
- gisserver/output/gml32.py +86 -27
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +177 -68
- gisserver/parsers/fes20/__init__.py +76 -4
- gisserver/parsers/fes20/expressions.py +97 -27
- gisserver/parsers/fes20/filters.py +9 -6
- gisserver/parsers/fes20/identifiers.py +27 -7
- gisserver/parsers/fes20/lookups.py +8 -6
- gisserver/parsers/fes20/operators.py +101 -49
- gisserver/parsers/fes20/sorting.py +14 -6
- gisserver/parsers/gml/__init__.py +10 -19
- gisserver/parsers/gml/base.py +32 -14
- gisserver/parsers/gml/geometries.py +54 -21
- gisserver/parsers/ows/kvp.py +10 -2
- gisserver/parsers/ows/requests.py +6 -4
- gisserver/parsers/query.py +6 -2
- gisserver/parsers/values.py +61 -4
- gisserver/parsers/wfs20/__init__.py +2 -0
- gisserver/parsers/wfs20/adhoc.py +28 -18
- gisserver/parsers/wfs20/base.py +12 -7
- gisserver/parsers/wfs20/projection.py +3 -3
- gisserver/parsers/wfs20/requests.py +1 -0
- gisserver/parsers/wfs20/stored.py +3 -2
- gisserver/parsers/xml.py +12 -0
- gisserver/projection.py +17 -7
- gisserver/static/gisserver/index.css +27 -6
- gisserver/templates/gisserver/base.html +15 -0
- gisserver/templates/gisserver/index.html +10 -16
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +44 -13
- gisserver/types.py +152 -82
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/top_level.txt +0 -0
gisserver/types.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
These types are the internal schema definition, and the foundation for all output generation.
|
|
4
4
|
|
|
5
5
|
The end-users of this library typically create a WFS feature type definition by using
|
|
6
|
-
the :class
|
|
6
|
+
the :class:`~gisserver.features.FeatureType` / :class:`~gisserver.features.FeatureField` classes.
|
|
7
7
|
|
|
8
8
|
The feature type classes use the model metadata to construct the internal XMLSchema structure.
|
|
9
9
|
Nearly all WFS requests are handled by walking this structure (like ``DescribeFeatureType``
|
|
@@ -30,9 +30,10 @@ import logging
|
|
|
30
30
|
import operator
|
|
31
31
|
import re
|
|
32
32
|
from dataclasses import dataclass, field
|
|
33
|
+
from datetime import date, datetime, time, timedelta
|
|
33
34
|
from decimal import Decimal as D
|
|
34
35
|
from enum import Enum
|
|
35
|
-
from functools import cached_property
|
|
36
|
+
from functools import cached_property
|
|
36
37
|
from typing import TYPE_CHECKING, Literal
|
|
37
38
|
|
|
38
39
|
import django
|
|
@@ -42,21 +43,21 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
|
42
43
|
from django.db import models
|
|
43
44
|
from django.db.models import Q
|
|
44
45
|
from django.db.models.fields.related import RelatedField
|
|
45
|
-
from django.utils import dateparse
|
|
46
46
|
|
|
47
47
|
from gisserver.compat import ArrayField, GeneratedField
|
|
48
|
+
from gisserver.crs import CRS
|
|
48
49
|
from gisserver.exceptions import ExternalParsingError, OperationProcessingFailed
|
|
49
|
-
from gisserver.geometries import
|
|
50
|
+
from gisserver.geometries import BoundingBox
|
|
50
51
|
from gisserver.parsers import values
|
|
51
52
|
from gisserver.parsers.xml import parse_qname, split_ns, xmlns
|
|
52
53
|
|
|
53
54
|
logger = logging.getLogger(__name__)
|
|
54
|
-
_unbounded = Literal["unbounded"]
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"GeometryXsdElement",
|
|
58
58
|
"GmlIdAttribute",
|
|
59
59
|
"GmlNameElement",
|
|
60
|
+
"GmlBoundedByElement",
|
|
60
61
|
"ORMPath",
|
|
61
62
|
"XPathMatch",
|
|
62
63
|
"XsdAnyType",
|
|
@@ -98,13 +99,13 @@ class XsdAnyType:
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
class XsdTypes(XsdAnyType, Enum):
|
|
101
|
-
"""Brief enumeration of
|
|
102
|
+
"""Brief enumeration of common XMLSchema types.
|
|
102
103
|
|
|
103
104
|
The :class:`XsdElement` and :class:`XsdAttribute` can use these enum members
|
|
104
105
|
to indicate their value is a well-known XML Schema. Some GML types are included as well.
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
Each member value is a fully qualified XML name.
|
|
108
|
+
The output rendering will convert these to the chosen prefixes.
|
|
108
109
|
"""
|
|
109
110
|
|
|
110
111
|
anyType = xmlns.xs.qname("anyType") # not "xsd:any", that is an element.
|
|
@@ -149,16 +150,25 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
149
150
|
gmlMultiCurvePropertyType = xmlns.gml.qname("MultiCurvePropertyType")
|
|
150
151
|
gmlMultiGeometryPropertyType = xmlns.gml.qname("MultiGeometryPropertyType")
|
|
151
152
|
|
|
152
|
-
# Other typical GML values
|
|
153
|
+
# Other typical GML values:
|
|
154
|
+
|
|
155
|
+
#: The type for ``<gml:name>`` elements.
|
|
153
156
|
gmlCodeType = xmlns.gml.qname("CodeType") # for <gml:name>
|
|
154
|
-
gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType") # for <gml:boundedBy>
|
|
155
157
|
|
|
156
|
-
#:
|
|
158
|
+
#: The type for ``<gml:boundedBy>`` elements.
|
|
159
|
+
gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType")
|
|
160
|
+
|
|
161
|
+
#: The type for ``<gml:Envelope>`` elements, sometimes used as function argument type.
|
|
162
|
+
gmlEnvelopeType = xmlns.gml.qname("EnvelopeType")
|
|
163
|
+
|
|
164
|
+
#: A direct geometry value, sometimes used as function argument type.
|
|
157
165
|
gmlAbstractGeometryType = xmlns.gml.qname("AbstractGeometryType")
|
|
158
166
|
|
|
159
167
|
#: A feature that has a gml:name and gml:boundedBy as possible child element.
|
|
160
168
|
gmlAbstractFeatureType = xmlns.gml.qname("AbstractFeatureType")
|
|
161
|
-
|
|
169
|
+
|
|
170
|
+
#: The base of gml:AbstractFeatureType
|
|
171
|
+
gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType")
|
|
162
172
|
|
|
163
173
|
def __str__(self):
|
|
164
174
|
return self.value
|
|
@@ -184,9 +194,12 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
184
194
|
raise NotImplementedError(f'Casting to "{self}" is not implemented.') from None
|
|
185
195
|
|
|
186
196
|
def to_python(self, raw_value):
|
|
187
|
-
"""Convert a raw string value to this type representation
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
"""Convert a raw string value to this type representation.
|
|
198
|
+
|
|
199
|
+
:raises ExternalParsingError: When the value can't be converted to the proper type.
|
|
200
|
+
"""
|
|
201
|
+
if self.is_geometry or isinstance(raw_value, TYPES_AS_PYTHON[self]):
|
|
202
|
+
# Detect when the value was already parsed, no need to reparse a date for example.
|
|
190
203
|
return raw_value
|
|
191
204
|
|
|
192
205
|
try:
|
|
@@ -195,6 +208,7 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
195
208
|
raise # subclass of ValueError so explicitly caught and reraised
|
|
196
209
|
except (TypeError, ValueError, ArithmeticError) as e:
|
|
197
210
|
# ArithmeticError is base of DecimalException
|
|
211
|
+
logger.debug("Parsing error for %r: %s", raw_value, e)
|
|
198
212
|
name = self.name if self.namespace == xmlns.xsd.value else self.value
|
|
199
213
|
raise ExternalParsingError(f"Can't cast '{raw_value}' to {name}.") from e
|
|
200
214
|
|
|
@@ -221,12 +235,12 @@ def _as_is(v):
|
|
|
221
235
|
return v
|
|
222
236
|
|
|
223
237
|
|
|
224
|
-
|
|
225
|
-
XsdTypes.date:
|
|
226
|
-
XsdTypes.dateTime:
|
|
227
|
-
XsdTypes.time:
|
|
228
|
-
XsdTypes.string:
|
|
229
|
-
XsdTypes.boolean:
|
|
238
|
+
TYPES_AS_PYTHON = {
|
|
239
|
+
XsdTypes.date: date,
|
|
240
|
+
XsdTypes.dateTime: datetime,
|
|
241
|
+
XsdTypes.time: time,
|
|
242
|
+
XsdTypes.string: str,
|
|
243
|
+
XsdTypes.boolean: bool,
|
|
230
244
|
XsdTypes.integer: int,
|
|
231
245
|
XsdTypes.int: int,
|
|
232
246
|
XsdTypes.long: int,
|
|
@@ -236,9 +250,28 @@ TYPES_TO_PYTHON = {
|
|
|
236
250
|
XsdTypes.unsignedLong: int,
|
|
237
251
|
XsdTypes.unsignedShort: int,
|
|
238
252
|
XsdTypes.unsignedByte: int,
|
|
239
|
-
XsdTypes.float: D,
|
|
253
|
+
XsdTypes.float: D, # auto_cast() always converts to decimal
|
|
240
254
|
XsdTypes.double: D,
|
|
241
255
|
XsdTypes.decimal: D,
|
|
256
|
+
XsdTypes.duration: timedelta,
|
|
257
|
+
XsdTypes.nonNegativeInteger: int,
|
|
258
|
+
XsdTypes.gYear: int,
|
|
259
|
+
XsdTypes.hexBinary: bytes,
|
|
260
|
+
XsdTypes.base64Binary: bytes,
|
|
261
|
+
XsdTypes.token: str,
|
|
262
|
+
XsdTypes.language: str,
|
|
263
|
+
XsdTypes.gmlCodeType: str,
|
|
264
|
+
XsdTypes.anyType: type(Ellipsis),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
TYPES_TO_PYTHON = {
|
|
268
|
+
**TYPES_AS_PYTHON,
|
|
269
|
+
XsdTypes.date: values.parse_iso_date,
|
|
270
|
+
XsdTypes.dateTime: values.parse_iso_datetime,
|
|
271
|
+
XsdTypes.time: values.parse_iso_time,
|
|
272
|
+
XsdTypes.string: _as_is,
|
|
273
|
+
XsdTypes.boolean: values.parse_bool,
|
|
274
|
+
XsdTypes.duration: values.parse_iso_duration,
|
|
242
275
|
XsdTypes.gmlCodeType: _as_is,
|
|
243
276
|
XsdTypes.anyType: values.auto_cast,
|
|
244
277
|
}
|
|
@@ -252,11 +285,16 @@ class XsdNode:
|
|
|
252
285
|
parse query input and read model attributes to write as output.
|
|
253
286
|
"""
|
|
254
287
|
|
|
288
|
+
#: Whether this node is an :class:`XsdAttribute` (avoids slow ``isinstance()`` checks)
|
|
255
289
|
is_attribute = False
|
|
290
|
+
#: Whether this node can occur multiple times.
|
|
256
291
|
is_many = False
|
|
257
292
|
|
|
293
|
+
#: The local name of the XML element
|
|
258
294
|
name: str
|
|
259
|
-
|
|
295
|
+
|
|
296
|
+
#: The data type of the element/attribute, both :class:`XsdComplexType` and :class:`XsdTypes` are allowed.
|
|
297
|
+
type: XsdAnyType
|
|
260
298
|
|
|
261
299
|
#: XML Namespace of the element
|
|
262
300
|
namespace: xmlns | str | None
|
|
@@ -269,7 +307,7 @@ class XsdNode:
|
|
|
269
307
|
#: This supports dot notation to access related attributes.
|
|
270
308
|
model_attribute: str | None
|
|
271
309
|
|
|
272
|
-
#: A link back to the parent that described the
|
|
310
|
+
#: A link back to the parent that described the feature this node is a part of.
|
|
273
311
|
#: This helps to perform additional filtering in side meth:get_value: based on user policies.
|
|
274
312
|
feature_type: FeatureType | None
|
|
275
313
|
|
|
@@ -291,7 +329,8 @@ class XsdNode:
|
|
|
291
329
|
:param source: Original Model field, which can provide more metadata/parsing.
|
|
292
330
|
:param model_attribute: The Django model path that this element accesses.
|
|
293
331
|
:param absolute_model_attribute: The full path, including parent elements.
|
|
294
|
-
:param feature_type: Typically assigned in :meth
|
|
332
|
+
:param feature_type: Typically assigned in :meth:`~gisserver.features.FeatureField.bind`,
|
|
333
|
+
needed by some :meth:`get_value` functions.
|
|
295
334
|
"""
|
|
296
335
|
if ":" in name:
|
|
297
336
|
raise ValueError(
|
|
@@ -466,7 +505,9 @@ class XsdNode:
|
|
|
466
505
|
return value
|
|
467
506
|
|
|
468
507
|
def to_python(self, raw_value: str):
|
|
469
|
-
"""Convert a raw value to the Python data type for this element type.
|
|
508
|
+
"""Convert a raw value to the Python data type for this element type.
|
|
509
|
+
:raises ValidationError: When the value isn't allowed for the field type.
|
|
510
|
+
"""
|
|
470
511
|
try:
|
|
471
512
|
raw_value = self.type.to_python(raw_value)
|
|
472
513
|
if self.source is not None:
|
|
@@ -493,30 +534,33 @@ class XsdNode:
|
|
|
493
534
|
:param tag: The filter operator tag name, e.g. ``PropertyIsEqualTo``.
|
|
494
535
|
:returns: The parsed Python value.
|
|
495
536
|
"""
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
537
|
+
# Not calling self.source.validate() as that checks for allowed choices,
|
|
538
|
+
# which shouldn't be checked against for a filter query.
|
|
539
|
+
raw_value = self.to_python(raw_value)
|
|
540
|
+
|
|
541
|
+
# Check whether the Django model field supports the lookup
|
|
542
|
+
# This prevents calling LIKE on a datetime or float field.
|
|
543
|
+
# For foreign keys, this depends on the target field type.
|
|
544
|
+
if (
|
|
545
|
+
self.source is not None
|
|
546
|
+
and self.source.get_lookup(lookup) is None
|
|
547
|
+
or (
|
|
505
548
|
isinstance(self.source, RelatedField)
|
|
506
549
|
and self.source.target_field.get_lookup(lookup) is None
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
550
|
+
)
|
|
551
|
+
):
|
|
552
|
+
logger.debug(
|
|
553
|
+
"Model field '%s.%s' does not support ORM lookup '%s' used by '%s'.",
|
|
554
|
+
self.feature_type.model._meta.model_name,
|
|
555
|
+
self.absolute_model_attribute,
|
|
556
|
+
lookup,
|
|
557
|
+
tag,
|
|
558
|
+
)
|
|
559
|
+
raise OperationProcessingFailed(
|
|
560
|
+
f"Operator '{tag}' is not supported for the '{self.name}' property.",
|
|
561
|
+
locator="filter",
|
|
562
|
+
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
563
|
+
)
|
|
520
564
|
|
|
521
565
|
return raw_value
|
|
522
566
|
|
|
@@ -529,18 +573,21 @@ class XsdElement(XsdNode):
|
|
|
529
573
|
This holds the definition for a single property in the WFS server.
|
|
530
574
|
It's used in ``DescribeFeatureType`` to output the field metadata,
|
|
531
575
|
and used in ``GetFeature`` to access the actual value from the object.
|
|
532
|
-
Overriding :meth:`get_value` allows to override this logic.
|
|
576
|
+
Overriding :meth:`XsdNode.get_value` allows to override this logic.
|
|
533
577
|
|
|
534
|
-
The :attr:`name` may differ from the underlying :attr:`model_attribute`,
|
|
578
|
+
The :attr:`name` may differ from the underlying :attr:`XsdNode.model_attribute`,
|
|
535
579
|
so the WFS server can use other field names then the underlying model.
|
|
536
580
|
|
|
537
|
-
A dotted-path notation can be used for :attr:`model_attribute` to access
|
|
581
|
+
A dotted-path notation can be used for :attr:`XsdNode.model_attribute` to access
|
|
538
582
|
a related field. For the WFS client, the data appears to be flattened.
|
|
539
583
|
"""
|
|
540
584
|
|
|
585
|
+
#: Whether the element can be null
|
|
541
586
|
nillable: bool | None
|
|
587
|
+
#: The minimal number of times the element occurs in the output.
|
|
542
588
|
min_occurs: int | None
|
|
543
|
-
|
|
589
|
+
#: The maximum number of times this element occurs in the output.
|
|
590
|
+
max_occurs: int | Literal["unbounded"] | None
|
|
544
591
|
|
|
545
592
|
def __init__(
|
|
546
593
|
self,
|
|
@@ -550,7 +597,7 @@ class XsdElement(XsdNode):
|
|
|
550
597
|
*,
|
|
551
598
|
nillable: bool | None = None,
|
|
552
599
|
min_occurs: int | None = None,
|
|
553
|
-
max_occurs: int |
|
|
600
|
+
max_occurs: int | Literal["unbounded"] | None = None,
|
|
554
601
|
source: models.Field | models.ForeignObjectRel | None = None,
|
|
555
602
|
model_attribute: str | None = None,
|
|
556
603
|
absolute_model_attribute: str | None = None,
|
|
@@ -661,8 +708,8 @@ class GeometryXsdElement(XsdElement):
|
|
|
661
708
|
|
|
662
709
|
|
|
663
710
|
class GmlIdAttribute(XsdAttribute):
|
|
664
|
-
"""A virtual
|
|
665
|
-
This subclass has overwritten get_value
|
|
711
|
+
"""A virtual ``gml:id="..."`` attribute that can be queried.
|
|
712
|
+
This subclass has overwritten :meth:`get_value` logic to format the value.
|
|
666
713
|
"""
|
|
667
714
|
|
|
668
715
|
type_name: str
|
|
@@ -686,6 +733,7 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
686
733
|
object.__setattr__(self, "type_name", type_name)
|
|
687
734
|
|
|
688
735
|
def get_value(self, instance: models.Model):
|
|
736
|
+
"""Render the value."""
|
|
689
737
|
pk = super().get_value(instance) # handle dotted-name notations
|
|
690
738
|
return f"{self.type_name}.{pk}"
|
|
691
739
|
|
|
@@ -695,7 +743,7 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
695
743
|
|
|
696
744
|
|
|
697
745
|
class GmlNameElement(XsdElement):
|
|
698
|
-
"""A subclass to handle the
|
|
746
|
+
"""A subclass to handle the ``<gml:name>`` element.
|
|
699
747
|
This displays a human-readable title for the object.
|
|
700
748
|
|
|
701
749
|
Currently, this just reads a single attribute,
|
|
@@ -731,7 +779,7 @@ class GmlNameElement(XsdElement):
|
|
|
731
779
|
|
|
732
780
|
|
|
733
781
|
class GmlBoundedByElement(XsdElement):
|
|
734
|
-
"""A subclass to handle the
|
|
782
|
+
"""A subclass to handle the ``<gml:boundedBy>`` element.
|
|
735
783
|
|
|
736
784
|
This override makes sure this non-model element data
|
|
737
785
|
can be included in the XML tree like every other element.
|
|
@@ -780,38 +828,55 @@ class GmlBoundedByElement(XsdElement):
|
|
|
780
828
|
if not geometries:
|
|
781
829
|
return None
|
|
782
830
|
|
|
783
|
-
|
|
784
|
-
if len(geometries) == 1:
|
|
785
|
-
geometry = geometries[0]
|
|
786
|
-
else:
|
|
787
|
-
geometry = reduce(operator.or_, geometries)
|
|
788
|
-
if crs is not None and geometry.srid != crs.srid:
|
|
789
|
-
crs.apply_to(geometry) # avoid clone
|
|
790
|
-
|
|
791
|
-
return BoundingBox.from_geometry(geometry, crs=crs)
|
|
831
|
+
return BoundingBox.from_geometries(geometries, crs)
|
|
792
832
|
|
|
793
833
|
|
|
794
834
|
@dataclass(frozen=True)
|
|
795
835
|
class XsdComplexType(XsdAnyType):
|
|
796
|
-
"""Define an
|
|
836
|
+
"""Define an ``<xsd:complexType>`` that represents a whole class definition.
|
|
797
837
|
|
|
798
838
|
Typically, this maps into a Django model, with each element pointing to a model field.
|
|
839
|
+
For example:
|
|
840
|
+
|
|
841
|
+
.. code-block:: python
|
|
842
|
+
|
|
843
|
+
XsdComplexType(
|
|
844
|
+
"PersonType",
|
|
845
|
+
elements=[
|
|
846
|
+
XsdElement("name", type=XsdTypes.string),
|
|
847
|
+
XsdElement("age", type=XsdTypes.integer),
|
|
848
|
+
XsdElement("address", type=XsdComplexType(
|
|
849
|
+
"AddressType",
|
|
850
|
+
elements=[
|
|
851
|
+
XsdElement("street", type=XsdTypes.string),
|
|
852
|
+
...
|
|
853
|
+
]
|
|
854
|
+
)),
|
|
855
|
+
],
|
|
856
|
+
attributes=[
|
|
857
|
+
XsdAttribute("id", type=XsdTypes.integer),
|
|
858
|
+
],
|
|
859
|
+
)
|
|
799
860
|
|
|
800
861
|
A complex type can hold multiple :class:`XsdElement` and :class:`XsdAttribute`
|
|
801
|
-
nodes as children, composing an object.
|
|
802
|
-
|
|
862
|
+
nodes as children, composing an object. Its :attr:`base` may point to a :class:`XsdComplexType`
|
|
863
|
+
as base class, allowing to define those inherited elements too.
|
|
864
|
+
|
|
865
|
+
Each element can be a complex type themselves, to create a nested class structure.
|
|
803
866
|
That also allows embedding models with their relations into a single response.
|
|
804
867
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
868
|
+
.. note:: Good to know
|
|
869
|
+
This object definition is the internal "source of truth" regarding
|
|
870
|
+
which field names and field elements are used in the WFS server:
|
|
871
|
+
|
|
872
|
+
* The ``DescribeFeatureType`` request uses this definition to render the matching XMLSchema.
|
|
873
|
+
* Incoming XPath queries are parsed using this object to resolve the XPath to model attributes.
|
|
809
874
|
|
|
810
|
-
Objects of this type are typically generated by the
|
|
811
|
-
|
|
875
|
+
Objects of this type are typically generated by the :class:`~gisserver.features.FeatureType` and
|
|
876
|
+
:class:`~gisserver.features.ComplexFeatureField` classes, using the Django model data.
|
|
812
877
|
|
|
813
|
-
By default, The type is
|
|
814
|
-
|
|
878
|
+
By default, The :attr:`base` type is detected as ``<gml:AbstractFeatureType>``,
|
|
879
|
+
when there is a geometry element in the definition.
|
|
815
880
|
"""
|
|
816
881
|
|
|
817
882
|
#: Internal class name (without XML namespace/prefix)
|
|
@@ -852,7 +917,8 @@ class XsdComplexType(XsdAnyType):
|
|
|
852
917
|
return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
|
|
853
918
|
|
|
854
919
|
@property
|
|
855
|
-
def is_complex_type(self):
|
|
920
|
+
def is_complex_type(self) -> bool:
|
|
921
|
+
"""Always indicates this is a complex type."""
|
|
856
922
|
return True # a property to avoid being used as field.
|
|
857
923
|
|
|
858
924
|
@cached_property
|
|
@@ -981,7 +1047,7 @@ class ORMPath:
|
|
|
981
1047
|
|
|
982
1048
|
def build_lhs(self, compiler: CompiledQuery):
|
|
983
1049
|
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
984
|
-
For example:
|
|
1050
|
+
For example: ``path == value``.
|
|
985
1051
|
"""
|
|
986
1052
|
if self.is_many:
|
|
987
1053
|
compiler.add_distinct()
|
|
@@ -991,7 +1057,7 @@ class ORMPath:
|
|
|
991
1057
|
|
|
992
1058
|
def build_rhs(self, compiler: CompiledQuery):
|
|
993
1059
|
"""Give the ORM part when this element would be used as right-hand-side.
|
|
994
|
-
For example:
|
|
1060
|
+
For example: ``path1 == path2`` or ``value == path``.
|
|
995
1061
|
"""
|
|
996
1062
|
if self.is_many:
|
|
997
1063
|
compiler.add_distinct()
|
|
@@ -1049,7 +1115,9 @@ class XPathMatch(ORMPath):
|
|
|
1049
1115
|
return any(node.is_many for node in self.nodes)
|
|
1050
1116
|
|
|
1051
1117
|
def build_lhs(self, compiler: CompiledQuery):
|
|
1052
|
-
"""
|
|
1118
|
+
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
1119
|
+
For example: ``path == value``.
|
|
1120
|
+
"""
|
|
1053
1121
|
if self.is_many:
|
|
1054
1122
|
compiler.add_distinct()
|
|
1055
1123
|
if self.orm_filters:
|
|
@@ -1057,7 +1125,9 @@ class XPathMatch(ORMPath):
|
|
|
1057
1125
|
return self.child.build_lhs_part(compiler, self)
|
|
1058
1126
|
|
|
1059
1127
|
def build_rhs(self, compiler: CompiledQuery):
|
|
1060
|
-
"""
|
|
1128
|
+
"""Give the ORM part when this element would be used as right-hand-side.
|
|
1129
|
+
For example: ``path1 == path2`` or ``value == path``.
|
|
1130
|
+
"""
|
|
1061
1131
|
if self.is_many:
|
|
1062
1132
|
compiler.add_distinct()
|
|
1063
1133
|
if self.orm_filters:
|
gisserver/views.py
CHANGED
|
@@ -21,6 +21,7 @@ from gisserver.exceptions import (
|
|
|
21
21
|
OperationProcessingFailed,
|
|
22
22
|
OWSException,
|
|
23
23
|
PermissionDenied,
|
|
24
|
+
XmlElementNotSupported,
|
|
24
25
|
)
|
|
25
26
|
from gisserver.features import FeatureType, ServiceDescription
|
|
26
27
|
from gisserver.operations import base, wfs20
|
|
@@ -46,7 +47,7 @@ class OWSView(View):
|
|
|
46
47
|
#: Define the namespace to use in the XML
|
|
47
48
|
xml_namespace = "http://example.org/gisserver"
|
|
48
49
|
|
|
49
|
-
#: Define namespace aliases to use, default is {"app": self.xml_namespace}
|
|
50
|
+
#: Define namespace aliases to use, default is ``{"app": self.xml_namespace}``.
|
|
50
51
|
xml_namespace_aliases = None
|
|
51
52
|
|
|
52
53
|
#: Default version to use
|
|
@@ -102,6 +103,7 @@ class OWSView(View):
|
|
|
102
103
|
def get_xml_namespace_aliases(cls) -> dict[str, str]:
|
|
103
104
|
"""Provide all namespaces aliases with a namespace.
|
|
104
105
|
This is most useful for parsing input.
|
|
106
|
+
The default is: ``{"app": cls.xml_namespace}``.
|
|
105
107
|
"""
|
|
106
108
|
return cls.xml_namespace_aliases or {"app": cls.xml_namespace}
|
|
107
109
|
|
|
@@ -109,6 +111,7 @@ class OWSView(View):
|
|
|
109
111
|
def get_xml_namespaces_to_prefixes(cls) -> dict[str, str]:
|
|
110
112
|
"""Provide a mapping from namespace to prefix.
|
|
111
113
|
This is most useful for rendering output.
|
|
114
|
+
The default is: ``{cls.xml_namespace: "app"}``.
|
|
112
115
|
"""
|
|
113
116
|
return {
|
|
114
117
|
xml_namespace: prefix
|
|
@@ -119,7 +122,7 @@ class OWSView(View):
|
|
|
119
122
|
"""Entry point to handle HTTP GET requests.
|
|
120
123
|
|
|
121
124
|
This parses the 'SERVICE' and 'REQUEST' parameters,
|
|
122
|
-
to call the proper
|
|
125
|
+
to call the proper :class:`~gisserver.operations.base.WFSOperation`.
|
|
123
126
|
|
|
124
127
|
All query parameters are handled as case-insensitive.
|
|
125
128
|
"""
|
|
@@ -127,7 +130,9 @@ class OWSView(View):
|
|
|
127
130
|
if logger.isEnabledFor(logging.DEBUG):
|
|
128
131
|
logger.debug(
|
|
129
132
|
"Parsing GET parameters:\n%s",
|
|
130
|
-
unquote_plus(
|
|
133
|
+
unquote_plus(
|
|
134
|
+
request.META["QUERY_STRING"].replace("\n&", "\n").replace("&", "\n")
|
|
135
|
+
).rstrip(),
|
|
131
136
|
)
|
|
132
137
|
self.kvp = kvp = KVPRequest(request.GET, ns_aliases=self.get_xml_namespace_aliases())
|
|
133
138
|
|
|
@@ -159,7 +164,7 @@ class OWSView(View):
|
|
|
159
164
|
"""Entry point to handle HTTP POST requests.
|
|
160
165
|
|
|
161
166
|
This parses the XML to get the correct service and operation,
|
|
162
|
-
to call the proper
|
|
167
|
+
to call the proper :class:`~gisserver.operations.base.WFSOperation`.
|
|
163
168
|
"""
|
|
164
169
|
# Parse the XML body
|
|
165
170
|
if logger.isEnabledFor(logging.DEBUG):
|
|
@@ -177,17 +182,24 @@ class OWSView(View):
|
|
|
177
182
|
if self.default_service
|
|
178
183
|
else root.get_str_attribute("service")
|
|
179
184
|
)
|
|
180
|
-
|
|
185
|
+
|
|
186
|
+
# Perform early version check. version can be omitted for GetCapabilities
|
|
187
|
+
self.set_version(service, root.attrib.get("version"))
|
|
188
|
+
|
|
189
|
+
# Find the registered operation that handles the request
|
|
190
|
+
namespace, operation = split_ns(root.tag)
|
|
181
191
|
wfs_operation_cls = self.get_operation_class(service, operation)
|
|
182
192
|
|
|
183
|
-
# Parse the request syntax
|
|
184
|
-
request_cls = wfs_operation_cls.parser_class or resolve_xml_parser_class(root)
|
|
185
193
|
try:
|
|
194
|
+
# Parse the request syntax
|
|
195
|
+
request_cls = wfs_operation_cls.parser_class or resolve_xml_parser_class(root)
|
|
186
196
|
self.ows_request = request_cls.from_xml(root)
|
|
187
|
-
self.set_version(service, self.ows_request.version)
|
|
188
197
|
|
|
189
198
|
# Process the request!
|
|
190
199
|
return self.call_operation(wfs_operation_cls)
|
|
200
|
+
except XmlElementNotSupported as e:
|
|
201
|
+
# Unknown XML element, e.g. wrong namespace.
|
|
202
|
+
raise OperationNotSupported(str(e), locator=root.attrib.get("handle")) from e
|
|
191
203
|
except (OperationParsingFailed, OperationProcessingFailed) as e:
|
|
192
204
|
# The WFS spec dictates that these exceptions
|
|
193
205
|
# (and ResponseCacheExpired, CannotLockAllFeatures, FeaturesNotLocked which we don't raise)
|
|
@@ -275,7 +287,7 @@ class OWSView(View):
|
|
|
275
287
|
except KeyError:
|
|
276
288
|
allowed = ", ".join(sorted(self.accept_operations.keys()))
|
|
277
289
|
raise InvalidParameterValue(
|
|
278
|
-
f"'{service}' is
|
|
290
|
+
f"'{service}' is not supported, available are: {allowed}.",
|
|
279
291
|
locator="service",
|
|
280
292
|
) from None
|
|
281
293
|
|
|
@@ -305,6 +317,13 @@ class OWSView(View):
|
|
|
305
317
|
|
|
306
318
|
def set_version(self, service: str, version: str | None):
|
|
307
319
|
"""Enforce a particular version based on the request."""
|
|
320
|
+
if service.upper() not in self.accept_operations:
|
|
321
|
+
allowed = ", ".join(sorted(self.accept_operations.keys()))
|
|
322
|
+
raise InvalidParameterValue(
|
|
323
|
+
f"'{service}' is not supported, available are: {allowed}.",
|
|
324
|
+
locator="service",
|
|
325
|
+
) from None
|
|
326
|
+
|
|
308
327
|
if not version:
|
|
309
328
|
return
|
|
310
329
|
|
|
@@ -314,7 +333,7 @@ class OWSView(View):
|
|
|
314
333
|
|
|
315
334
|
if version not in self.accept_versions:
|
|
316
335
|
raise InvalidParameterValue(
|
|
317
|
-
f"
|
|
336
|
+
f"This server does not support {service} version {version}.", locator="version"
|
|
318
337
|
)
|
|
319
338
|
|
|
320
339
|
# Enforce the requested version
|
|
@@ -417,30 +436,34 @@ class WFSView(OWSView):
|
|
|
417
436
|
feature_type.bind_namespace(default_xml_namespace=self.xml_namespace)
|
|
418
437
|
return feature_types
|
|
419
438
|
|
|
420
|
-
def get_index_context_data(self, **kwargs):
|
|
421
|
-
"""
|
|
422
|
-
get_feature_operation = self.accept_operations["WFS"]["GetFeature"]
|
|
423
|
-
operation = get_feature_operation(self, ows_request=None)
|
|
424
|
-
|
|
425
|
-
# Remove aliases
|
|
426
|
-
wfs_output_formats = []
|
|
427
|
-
seen = set()
|
|
428
|
-
for output_format in operation.get_output_formats():
|
|
429
|
-
if output_format.identifier not in seen:
|
|
430
|
-
wfs_output_formats.append(output_format)
|
|
431
|
-
seen.add(output_format.identifier)
|
|
432
|
-
|
|
439
|
+
def get_index_context_data(self, **kwargs) -> dict:
|
|
440
|
+
"""Get the context data for the index template"""
|
|
433
441
|
context = super().get_index_context_data(**kwargs)
|
|
434
442
|
context.update(
|
|
435
443
|
{
|
|
444
|
+
"GISSERVER_SUPPORTED_CRS_ONLY": conf.GISSERVER_SUPPORTED_CRS_ONLY,
|
|
436
445
|
"wfs_features": self.get_bound_feature_types(),
|
|
437
|
-
"wfs_output_formats":
|
|
446
|
+
"wfs_output_formats": self._get_wfs_output_formats(),
|
|
438
447
|
"wfs_filter_capabilities": self.wfs_filter_capabilities,
|
|
439
448
|
"wfs_service_constraints": self.wfs_service_constraints,
|
|
440
449
|
}
|
|
441
450
|
)
|
|
442
451
|
return context
|
|
443
452
|
|
|
453
|
+
def _get_wfs_output_formats(self) -> list[base.OutputFormat]:
|
|
454
|
+
"""Find the output formats of the ``GetFeature`` operation for the HTML index."""
|
|
455
|
+
get_feature_operation = self.accept_operations["WFS"]["GetFeature"]
|
|
456
|
+
operation = get_feature_operation(self, ows_request=None)
|
|
457
|
+
|
|
458
|
+
# Get output formats, remove duplicates (e.g. GeoJSON/geojson alias)
|
|
459
|
+
wfs_output_formats = []
|
|
460
|
+
seen = set()
|
|
461
|
+
for output_format in operation.get_output_formats():
|
|
462
|
+
if output_format.identifier not in seen:
|
|
463
|
+
wfs_output_formats.append(output_format)
|
|
464
|
+
seen.add(output_format.identifier)
|
|
465
|
+
return wfs_output_formats
|
|
466
|
+
|
|
444
467
|
def get_xml_schema_url(self, feature_types: list[FeatureType]) -> str:
|
|
445
468
|
"""Return the XML schema URL for the given feature types.
|
|
446
469
|
This is used in the GML output rendering.
|