django-gisserver 2.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-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/crs.py +401 -0
- gisserver/db.py +71 -5
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +44 -36
- 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 +43 -23
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +171 -65
- 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 +48 -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 +25 -17
- 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 +8 -3
- 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/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/types.py +150 -81
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,19 +1,41 @@
|
|
|
1
|
-
{% load i18n %}
|
|
2
|
-
<h3>{{ feature_type.title }}</h3>
|
|
3
|
-
{% if feature_type.abstract %}{{ feature_type.abstract|linebreaks }}{% endif %}
|
|
1
|
+
{% load i18n gisserver_tags %}
|
|
4
2
|
|
|
5
|
-
{% block
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{%
|
|
3
|
+
{% block title %}<h3><a id="feature-{{ feature_type.name }}">{{ feature_type.title }}</a></h3>{% endblock %}
|
|
4
|
+
{% block description %}
|
|
5
|
+
{% if feature_type.abstract %}{{ feature_type.abstract|linebreaks }}{% endif %}
|
|
6
|
+
{% endblock %}
|
|
7
|
+
|
|
8
|
+
{% block metadata %}
|
|
9
|
+
<dl>
|
|
10
|
+
{% block metadata-items %}
|
|
11
|
+
<dt>{% translate "XML Namespace" %}:</dt><dd><code>{{ feature_type.xml_namespace }}</code></dd>
|
|
12
|
+
<dt>{% translate "Typename" %}:</dt><dd><abbr title="{{ feature_type.xml_name }}">{% feature_qname feature_type %}</abbr></dd>
|
|
13
|
+
<dt>{% translate "Supported CRS" %}:</dt>
|
|
14
|
+
<dd>
|
|
15
|
+
{% if GISSERVER_SUPPORTED_CRS_ONLY %}
|
|
16
|
+
{% blocktranslate with default_crs=feature_type.crs %}Any CRS value is supported, source data uses {{ default_crs }}.{% endblocktranslate %}
|
|
17
|
+
{% else %}
|
|
18
|
+
{{ feature_type.supported_crs|join:", " }}
|
|
19
|
+
{% endif %}
|
|
20
|
+
</dd>
|
|
21
|
+
{% if wfs_output_formats %}
|
|
22
|
+
<dt>{% translate "Formats" %}:</dt>
|
|
23
|
+
<dd>{% block metadata-formats %}
|
|
24
|
+
{% for output_format in wfs_output_formats %}
|
|
25
|
+
<a href="?{{ base_query }}SERVICE=WFS&VERSION={{ version }}&REQUEST=GetFeature&TYPENAMES={% feature_qname feature_type %}&OUTPUTFORMAT={{ output_format.identifier }}">{{ output_format.title|default:output_format }}</a>{% block format-sep %},{% endblock %}
|
|
26
|
+
{% endfor %}
|
|
27
|
+
<a href="?{{ base_query }}SERVICE=WFS&VERSION={{ version }}&REQUEST=DescribeFeatureType&TYPENAMES={% feature_qname feature_type %}">XML Schema</a>
|
|
28
|
+
{% endblock %}
|
|
29
|
+
</dd>
|
|
30
|
+
{% endif %}
|
|
31
|
+
{% endblock %}
|
|
32
|
+
</dl>
|
|
33
|
+
{% endblock %}
|
|
13
34
|
|
|
14
35
|
{% block fields %}
|
|
15
|
-
<p>{%
|
|
16
|
-
<table>
|
|
36
|
+
<p>{% translate "The following fields are available:" %}</p>
|
|
37
|
+
<table class="table table-striped">
|
|
38
|
+
<thead><tr><th>{% translate "Field Name" %}</th><th>{% translate "Type" %}</th><th>{% translate "Description" %}</th></tr></thead>
|
|
17
39
|
<tbody>
|
|
18
40
|
{% for field in feature_type.fields %}
|
|
19
41
|
{% include "gisserver/wfs/feature_field.html" with level=0 %}
|
gisserver/types.py
CHANGED
|
@@ -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,16 +43,15 @@ 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",
|
|
@@ -98,13 +98,13 @@ class XsdAnyType:
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
class XsdTypes(XsdAnyType, Enum):
|
|
101
|
-
"""Brief enumeration of
|
|
101
|
+
"""Brief enumeration of common XMLSchema types.
|
|
102
102
|
|
|
103
103
|
The :class:`XsdElement` and :class:`XsdAttribute` can use these enum members
|
|
104
104
|
to indicate their value is a well-known XML Schema. Some GML types are included as well.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
Each member value is a fully qualified XML name.
|
|
107
|
+
The output rendering will convert these to the chosen prefixes.
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
110
|
anyType = xmlns.xs.qname("anyType") # not "xsd:any", that is an element.
|
|
@@ -149,16 +149,25 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
149
149
|
gmlMultiCurvePropertyType = xmlns.gml.qname("MultiCurvePropertyType")
|
|
150
150
|
gmlMultiGeometryPropertyType = xmlns.gml.qname("MultiGeometryPropertyType")
|
|
151
151
|
|
|
152
|
-
# Other typical GML values
|
|
152
|
+
# Other typical GML values:
|
|
153
|
+
|
|
154
|
+
#: The type for ``<gml:name>`` elements.
|
|
153
155
|
gmlCodeType = xmlns.gml.qname("CodeType") # for <gml:name>
|
|
154
|
-
gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType") # for <gml:boundedBy>
|
|
155
156
|
|
|
156
|
-
#:
|
|
157
|
+
#: The type for ``<gml:boundedBy>`` elements.
|
|
158
|
+
gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType")
|
|
159
|
+
|
|
160
|
+
#: The type for ``<gml:Envelope>`` elements, sometimes used as function argument type.
|
|
161
|
+
gmlEnvelopeType = xmlns.gml.qname("EnvelopeType")
|
|
162
|
+
|
|
163
|
+
#: A direct geometry value, sometimes used as function argument type.
|
|
157
164
|
gmlAbstractGeometryType = xmlns.gml.qname("AbstractGeometryType")
|
|
158
165
|
|
|
159
166
|
#: A feature that has a gml:name and gml:boundedBy as possible child element.
|
|
160
167
|
gmlAbstractFeatureType = xmlns.gml.qname("AbstractFeatureType")
|
|
161
|
-
|
|
168
|
+
|
|
169
|
+
#: The base of gml:AbstractFeatureType
|
|
170
|
+
gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType")
|
|
162
171
|
|
|
163
172
|
def __str__(self):
|
|
164
173
|
return self.value
|
|
@@ -184,9 +193,12 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
184
193
|
raise NotImplementedError(f'Casting to "{self}" is not implemented.') from None
|
|
185
194
|
|
|
186
195
|
def to_python(self, raw_value):
|
|
187
|
-
"""Convert a raw string value to this type representation
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
"""Convert a raw string value to this type representation.
|
|
197
|
+
|
|
198
|
+
:raises ExternalParsingError: When the value can't be converted to the proper type.
|
|
199
|
+
"""
|
|
200
|
+
if self.is_geometry or isinstance(raw_value, TYPES_AS_PYTHON[self]):
|
|
201
|
+
# Detect when the value was already parsed, no need to reparse a date for example.
|
|
190
202
|
return raw_value
|
|
191
203
|
|
|
192
204
|
try:
|
|
@@ -195,6 +207,7 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
195
207
|
raise # subclass of ValueError so explicitly caught and reraised
|
|
196
208
|
except (TypeError, ValueError, ArithmeticError) as e:
|
|
197
209
|
# ArithmeticError is base of DecimalException
|
|
210
|
+
logger.debug("Parsing error for %r: %s", raw_value, e)
|
|
198
211
|
name = self.name if self.namespace == xmlns.xsd.value else self.value
|
|
199
212
|
raise ExternalParsingError(f"Can't cast '{raw_value}' to {name}.") from e
|
|
200
213
|
|
|
@@ -221,12 +234,12 @@ def _as_is(v):
|
|
|
221
234
|
return v
|
|
222
235
|
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
XsdTypes.date:
|
|
226
|
-
XsdTypes.dateTime:
|
|
227
|
-
XsdTypes.time:
|
|
228
|
-
XsdTypes.string:
|
|
229
|
-
XsdTypes.boolean:
|
|
237
|
+
TYPES_AS_PYTHON = {
|
|
238
|
+
XsdTypes.date: date,
|
|
239
|
+
XsdTypes.dateTime: datetime,
|
|
240
|
+
XsdTypes.time: time,
|
|
241
|
+
XsdTypes.string: str,
|
|
242
|
+
XsdTypes.boolean: bool,
|
|
230
243
|
XsdTypes.integer: int,
|
|
231
244
|
XsdTypes.int: int,
|
|
232
245
|
XsdTypes.long: int,
|
|
@@ -236,9 +249,28 @@ TYPES_TO_PYTHON = {
|
|
|
236
249
|
XsdTypes.unsignedLong: int,
|
|
237
250
|
XsdTypes.unsignedShort: int,
|
|
238
251
|
XsdTypes.unsignedByte: int,
|
|
239
|
-
XsdTypes.float: D,
|
|
252
|
+
XsdTypes.float: D, # auto_cast() always converts to decimal
|
|
240
253
|
XsdTypes.double: D,
|
|
241
254
|
XsdTypes.decimal: D,
|
|
255
|
+
XsdTypes.duration: timedelta,
|
|
256
|
+
XsdTypes.nonNegativeInteger: int,
|
|
257
|
+
XsdTypes.gYear: int,
|
|
258
|
+
XsdTypes.hexBinary: bytes,
|
|
259
|
+
XsdTypes.base64Binary: bytes,
|
|
260
|
+
XsdTypes.token: str,
|
|
261
|
+
XsdTypes.language: str,
|
|
262
|
+
XsdTypes.gmlCodeType: str,
|
|
263
|
+
XsdTypes.anyType: type(Ellipsis),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
TYPES_TO_PYTHON = {
|
|
267
|
+
**TYPES_AS_PYTHON,
|
|
268
|
+
XsdTypes.date: values.parse_iso_date,
|
|
269
|
+
XsdTypes.dateTime: values.parse_iso_datetime,
|
|
270
|
+
XsdTypes.time: values.parse_iso_time,
|
|
271
|
+
XsdTypes.string: _as_is,
|
|
272
|
+
XsdTypes.boolean: values.parse_bool,
|
|
273
|
+
XsdTypes.duration: values.parse_iso_duration,
|
|
242
274
|
XsdTypes.gmlCodeType: _as_is,
|
|
243
275
|
XsdTypes.anyType: values.auto_cast,
|
|
244
276
|
}
|
|
@@ -252,11 +284,16 @@ class XsdNode:
|
|
|
252
284
|
parse query input and read model attributes to write as output.
|
|
253
285
|
"""
|
|
254
286
|
|
|
287
|
+
#: Whether this node is an :class:`XsdAttribute` (avoids slow ``isinstance()`` checks)
|
|
255
288
|
is_attribute = False
|
|
289
|
+
#: Whether this node can occur multiple times.
|
|
256
290
|
is_many = False
|
|
257
291
|
|
|
292
|
+
#: The local name of the XML element
|
|
258
293
|
name: str
|
|
259
|
-
|
|
294
|
+
|
|
295
|
+
#: The data type of the element/attribute, both :class:`XsdComplexType` and :class:`XsdTypes` are allowed.
|
|
296
|
+
type: XsdAnyType
|
|
260
297
|
|
|
261
298
|
#: XML Namespace of the element
|
|
262
299
|
namespace: xmlns | str | None
|
|
@@ -269,7 +306,7 @@ class XsdNode:
|
|
|
269
306
|
#: This supports dot notation to access related attributes.
|
|
270
307
|
model_attribute: str | None
|
|
271
308
|
|
|
272
|
-
#: A link back to the parent that described the
|
|
309
|
+
#: A link back to the parent that described the feature this node is a part of.
|
|
273
310
|
#: This helps to perform additional filtering in side meth:get_value: based on user policies.
|
|
274
311
|
feature_type: FeatureType | None
|
|
275
312
|
|
|
@@ -291,7 +328,8 @@ class XsdNode:
|
|
|
291
328
|
:param source: Original Model field, which can provide more metadata/parsing.
|
|
292
329
|
:param model_attribute: The Django model path that this element accesses.
|
|
293
330
|
:param absolute_model_attribute: The full path, including parent elements.
|
|
294
|
-
:param feature_type: Typically assigned in :meth
|
|
331
|
+
:param feature_type: Typically assigned in :meth:`~gisserver.features.FeatureField.bind`,
|
|
332
|
+
needed by some :meth:`get_value` functions.
|
|
295
333
|
"""
|
|
296
334
|
if ":" in name:
|
|
297
335
|
raise ValueError(
|
|
@@ -466,7 +504,9 @@ class XsdNode:
|
|
|
466
504
|
return value
|
|
467
505
|
|
|
468
506
|
def to_python(self, raw_value: str):
|
|
469
|
-
"""Convert a raw value to the Python data type for this element type.
|
|
507
|
+
"""Convert a raw value to the Python data type for this element type.
|
|
508
|
+
:raises ValidationError: When the value isn't allowed for the field type.
|
|
509
|
+
"""
|
|
470
510
|
try:
|
|
471
511
|
raw_value = self.type.to_python(raw_value)
|
|
472
512
|
if self.source is not None:
|
|
@@ -493,30 +533,33 @@ class XsdNode:
|
|
|
493
533
|
:param tag: The filter operator tag name, e.g. ``PropertyIsEqualTo``.
|
|
494
534
|
:returns: The parsed Python value.
|
|
495
535
|
"""
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
536
|
+
# Not calling self.source.validate() as that checks for allowed choices,
|
|
537
|
+
# which shouldn't be checked against for a filter query.
|
|
538
|
+
raw_value = self.to_python(raw_value)
|
|
539
|
+
|
|
540
|
+
# Check whether the Django model field supports the lookup
|
|
541
|
+
# This prevents calling LIKE on a datetime or float field.
|
|
542
|
+
# For foreign keys, this depends on the target field type.
|
|
543
|
+
if (
|
|
544
|
+
self.source is not None
|
|
545
|
+
and self.source.get_lookup(lookup) is None
|
|
546
|
+
or (
|
|
505
547
|
isinstance(self.source, RelatedField)
|
|
506
548
|
and self.source.target_field.get_lookup(lookup) is None
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
549
|
+
)
|
|
550
|
+
):
|
|
551
|
+
logger.debug(
|
|
552
|
+
"Model field '%s.%s' does not support ORM lookup '%s' used by '%s'.",
|
|
553
|
+
self.feature_type.model._meta.model_name,
|
|
554
|
+
self.absolute_model_attribute,
|
|
555
|
+
lookup,
|
|
556
|
+
tag,
|
|
557
|
+
)
|
|
558
|
+
raise OperationProcessingFailed(
|
|
559
|
+
f"Operator '{tag}' is not supported for the '{self.name}' property.",
|
|
560
|
+
locator="filter",
|
|
561
|
+
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
562
|
+
)
|
|
520
563
|
|
|
521
564
|
return raw_value
|
|
522
565
|
|
|
@@ -529,18 +572,21 @@ class XsdElement(XsdNode):
|
|
|
529
572
|
This holds the definition for a single property in the WFS server.
|
|
530
573
|
It's used in ``DescribeFeatureType`` to output the field metadata,
|
|
531
574
|
and used in ``GetFeature`` to access the actual value from the object.
|
|
532
|
-
Overriding :meth:`get_value` allows to override this logic.
|
|
575
|
+
Overriding :meth:`XsdNode.get_value` allows to override this logic.
|
|
533
576
|
|
|
534
|
-
The :attr:`name` may differ from the underlying :attr:`model_attribute`,
|
|
577
|
+
The :attr:`name` may differ from the underlying :attr:`XsdNode.model_attribute`,
|
|
535
578
|
so the WFS server can use other field names then the underlying model.
|
|
536
579
|
|
|
537
|
-
A dotted-path notation can be used for :attr:`model_attribute` to access
|
|
580
|
+
A dotted-path notation can be used for :attr:`XsdNode.model_attribute` to access
|
|
538
581
|
a related field. For the WFS client, the data appears to be flattened.
|
|
539
582
|
"""
|
|
540
583
|
|
|
584
|
+
#: Whether the element can be null
|
|
541
585
|
nillable: bool | None
|
|
586
|
+
#: The minimal number of times the element occurs in the output.
|
|
542
587
|
min_occurs: int | None
|
|
543
|
-
|
|
588
|
+
#: The maximum number of times this element occurs in the output.
|
|
589
|
+
max_occurs: int | Literal["unbounded"] | None
|
|
544
590
|
|
|
545
591
|
def __init__(
|
|
546
592
|
self,
|
|
@@ -550,7 +596,7 @@ class XsdElement(XsdNode):
|
|
|
550
596
|
*,
|
|
551
597
|
nillable: bool | None = None,
|
|
552
598
|
min_occurs: int | None = None,
|
|
553
|
-
max_occurs: int |
|
|
599
|
+
max_occurs: int | Literal["unbounded"] | None = None,
|
|
554
600
|
source: models.Field | models.ForeignObjectRel | None = None,
|
|
555
601
|
model_attribute: str | None = None,
|
|
556
602
|
absolute_model_attribute: str | None = None,
|
|
@@ -661,8 +707,8 @@ class GeometryXsdElement(XsdElement):
|
|
|
661
707
|
|
|
662
708
|
|
|
663
709
|
class GmlIdAttribute(XsdAttribute):
|
|
664
|
-
"""A virtual
|
|
665
|
-
This subclass has overwritten get_value
|
|
710
|
+
"""A virtual ``gml:id="..."`` attribute that can be queried.
|
|
711
|
+
This subclass has overwritten :meth:`get_value` logic to format the value.
|
|
666
712
|
"""
|
|
667
713
|
|
|
668
714
|
type_name: str
|
|
@@ -686,6 +732,7 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
686
732
|
object.__setattr__(self, "type_name", type_name)
|
|
687
733
|
|
|
688
734
|
def get_value(self, instance: models.Model):
|
|
735
|
+
"""Render the value."""
|
|
689
736
|
pk = super().get_value(instance) # handle dotted-name notations
|
|
690
737
|
return f"{self.type_name}.{pk}"
|
|
691
738
|
|
|
@@ -695,7 +742,7 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
695
742
|
|
|
696
743
|
|
|
697
744
|
class GmlNameElement(XsdElement):
|
|
698
|
-
"""A subclass to handle the
|
|
745
|
+
"""A subclass to handle the ``<gml:name>`` element.
|
|
699
746
|
This displays a human-readable title for the object.
|
|
700
747
|
|
|
701
748
|
Currently, this just reads a single attribute,
|
|
@@ -731,7 +778,7 @@ class GmlNameElement(XsdElement):
|
|
|
731
778
|
|
|
732
779
|
|
|
733
780
|
class GmlBoundedByElement(XsdElement):
|
|
734
|
-
"""A subclass to handle the
|
|
781
|
+
"""A subclass to handle the ``<gml:boundedBy>`` element.
|
|
735
782
|
|
|
736
783
|
This override makes sure this non-model element data
|
|
737
784
|
can be included in the XML tree like every other element.
|
|
@@ -780,38 +827,55 @@ class GmlBoundedByElement(XsdElement):
|
|
|
780
827
|
if not geometries:
|
|
781
828
|
return None
|
|
782
829
|
|
|
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)
|
|
830
|
+
return BoundingBox.from_geometries(geometries, crs)
|
|
792
831
|
|
|
793
832
|
|
|
794
833
|
@dataclass(frozen=True)
|
|
795
834
|
class XsdComplexType(XsdAnyType):
|
|
796
|
-
"""Define an
|
|
835
|
+
"""Define an ``<xsd:complexType>`` that represents a whole class definition.
|
|
797
836
|
|
|
798
837
|
Typically, this maps into a Django model, with each element pointing to a model field.
|
|
838
|
+
For example:
|
|
839
|
+
|
|
840
|
+
.. code-block:: python
|
|
841
|
+
|
|
842
|
+
XsdComplexType(
|
|
843
|
+
"PersonType",
|
|
844
|
+
elements=[
|
|
845
|
+
XsdElement("name", type=XsdTypes.string),
|
|
846
|
+
XsdElement("age", type=XsdTypes.integer),
|
|
847
|
+
XsdElement("address", type=XsdComplexType(
|
|
848
|
+
"AddressType",
|
|
849
|
+
elements=[
|
|
850
|
+
XsdElement("street", type=XsdTypes.string),
|
|
851
|
+
...
|
|
852
|
+
]
|
|
853
|
+
)),
|
|
854
|
+
],
|
|
855
|
+
attributes=[
|
|
856
|
+
XsdAttribute("id", type=XsdTypes.integer),
|
|
857
|
+
],
|
|
858
|
+
)
|
|
799
859
|
|
|
800
860
|
A complex type can hold multiple :class:`XsdElement` and :class:`XsdAttribute`
|
|
801
|
-
nodes as children, composing an object.
|
|
802
|
-
|
|
861
|
+
nodes as children, composing an object. Its :attr:`base` may point to a :class:`XsdComplexType`
|
|
862
|
+
as base class, allowing to define those inherited elements too.
|
|
863
|
+
|
|
864
|
+
Each element can be a complex type themselves, to create a nested class structure.
|
|
803
865
|
That also allows embedding models with their relations into a single response.
|
|
804
866
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
867
|
+
.. note:: Good to know
|
|
868
|
+
This object definition is the internal "source of truth" regarding
|
|
869
|
+
which field names and field elements are used in the WFS server:
|
|
870
|
+
|
|
871
|
+
* The ``DescribeFeatureType`` request uses this definition to render the matching XMLSchema.
|
|
872
|
+
* Incoming XPath queries are parsed using this object to resolve the XPath to model attributes.
|
|
809
873
|
|
|
810
|
-
Objects of this type are typically generated by the
|
|
811
|
-
|
|
874
|
+
Objects of this type are typically generated by the :class:`~gisserver.features.FeatureType` and
|
|
875
|
+
:class:`~gisserver.features.ComplexFeatureField` classes, using the Django model data.
|
|
812
876
|
|
|
813
|
-
By default, The type is
|
|
814
|
-
|
|
877
|
+
By default, The :attr:`base` type is detected as ``<gml:AbstractFeatureType>``,
|
|
878
|
+
when there is a geometry element in the definition.
|
|
815
879
|
"""
|
|
816
880
|
|
|
817
881
|
#: Internal class name (without XML namespace/prefix)
|
|
@@ -852,7 +916,8 @@ class XsdComplexType(XsdAnyType):
|
|
|
852
916
|
return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
|
|
853
917
|
|
|
854
918
|
@property
|
|
855
|
-
def is_complex_type(self):
|
|
919
|
+
def is_complex_type(self) -> bool:
|
|
920
|
+
"""Always indicates this is a complex type."""
|
|
856
921
|
return True # a property to avoid being used as field.
|
|
857
922
|
|
|
858
923
|
@cached_property
|
|
@@ -981,7 +1046,7 @@ class ORMPath:
|
|
|
981
1046
|
|
|
982
1047
|
def build_lhs(self, compiler: CompiledQuery):
|
|
983
1048
|
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
984
|
-
For example:
|
|
1049
|
+
For example: ``path == value``.
|
|
985
1050
|
"""
|
|
986
1051
|
if self.is_many:
|
|
987
1052
|
compiler.add_distinct()
|
|
@@ -991,7 +1056,7 @@ class ORMPath:
|
|
|
991
1056
|
|
|
992
1057
|
def build_rhs(self, compiler: CompiledQuery):
|
|
993
1058
|
"""Give the ORM part when this element would be used as right-hand-side.
|
|
994
|
-
For example:
|
|
1059
|
+
For example: ``path1 == path2`` or ``value == path``.
|
|
995
1060
|
"""
|
|
996
1061
|
if self.is_many:
|
|
997
1062
|
compiler.add_distinct()
|
|
@@ -1049,7 +1114,9 @@ class XPathMatch(ORMPath):
|
|
|
1049
1114
|
return any(node.is_many for node in self.nodes)
|
|
1050
1115
|
|
|
1051
1116
|
def build_lhs(self, compiler: CompiledQuery):
|
|
1052
|
-
"""
|
|
1117
|
+
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
1118
|
+
For example: ``path == value``.
|
|
1119
|
+
"""
|
|
1053
1120
|
if self.is_many:
|
|
1054
1121
|
compiler.add_distinct()
|
|
1055
1122
|
if self.orm_filters:
|
|
@@ -1057,7 +1124,9 @@ class XPathMatch(ORMPath):
|
|
|
1057
1124
|
return self.child.build_lhs_part(compiler, self)
|
|
1058
1125
|
|
|
1059
1126
|
def build_rhs(self, compiler: CompiledQuery):
|
|
1060
|
-
"""
|
|
1127
|
+
"""Give the ORM part when this element would be used as right-hand-side.
|
|
1128
|
+
For example: ``path1 == path2`` or ``value == path``.
|
|
1129
|
+
"""
|
|
1061
1130
|
if self.is_many:
|
|
1062
1131
|
compiler.add_distinct()
|
|
1063
1132
|
if self.orm_filters:
|