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/types.py
CHANGED
|
@@ -26,37 +26,35 @@ Custom field types could also generate these field types.
|
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
|
+
import logging
|
|
29
30
|
import operator
|
|
30
31
|
import re
|
|
31
32
|
from dataclasses import dataclass, field
|
|
33
|
+
from datetime import date, datetime, time, timedelta
|
|
32
34
|
from decimal import Decimal as D
|
|
33
35
|
from enum import Enum
|
|
34
36
|
from functools import cached_property
|
|
35
|
-
from typing import TYPE_CHECKING, Literal
|
|
37
|
+
from typing import TYPE_CHECKING, Literal
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
import django
|
|
38
40
|
from django.contrib.gis.db.models import F, GeometryField
|
|
41
|
+
from django.contrib.gis.geos import GEOSGeometry
|
|
39
42
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
40
43
|
from django.db import models
|
|
41
44
|
from django.db.models import Q
|
|
42
|
-
from django.db.models.fields.related import
|
|
43
|
-
ForeignObjectRel,
|
|
44
|
-
RelatedField,
|
|
45
|
-
)
|
|
46
|
-
from django.utils import dateparse
|
|
45
|
+
from django.db.models.fields.related import RelatedField
|
|
47
46
|
|
|
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
|
|
51
|
+
from gisserver.parsers import values
|
|
52
|
+
from gisserver.parsers.xml import parse_qname, split_ns, xmlns
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
54
|
-
from django.contrib.postgres.fields import ArrayField
|
|
55
|
-
else:
|
|
56
|
-
ArrayField = None
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
57
55
|
|
|
58
56
|
__all__ = [
|
|
59
|
-
"
|
|
57
|
+
"GeometryXsdElement",
|
|
60
58
|
"GmlIdAttribute",
|
|
61
59
|
"GmlNameElement",
|
|
62
60
|
"ORMPath",
|
|
@@ -67,137 +65,140 @@ __all__ = [
|
|
|
67
65
|
"XsdElement",
|
|
68
66
|
"XsdNode",
|
|
69
67
|
"XsdTypes",
|
|
70
|
-
"split_xml_name",
|
|
71
|
-
"FES20",
|
|
72
|
-
"GML21",
|
|
73
|
-
"GML32",
|
|
74
|
-
"XSI",
|
|
75
68
|
]
|
|
76
69
|
|
|
77
|
-
GML21 = "http://www.opengis.net/gml"
|
|
78
|
-
GML32 = "http://www.opengis.net/gml/3.2"
|
|
79
|
-
XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
|
80
|
-
FES20 = "http://www.opengis.net/fes/2.0"
|
|
81
|
-
|
|
82
70
|
RE_XPATH_ATTR = re.compile(r"\[[^\]]+\]$") # match [@attr=..]
|
|
83
|
-
TYPES_TO_PYTHON = {}
|
|
84
71
|
|
|
85
72
|
|
|
86
73
|
class XsdAnyType:
|
|
87
|
-
"""Base class for all types used in the XML definition
|
|
74
|
+
"""Base class for all types used in the XML definition.
|
|
75
|
+
This includes the enum values (:class:`XsdTypes`) for well-known types,
|
|
76
|
+
adn the :class:`XsdComplexType` that represents a while class definition.
|
|
77
|
+
"""
|
|
88
78
|
|
|
79
|
+
#: Local name of the XML element
|
|
89
80
|
name: str
|
|
90
|
-
|
|
81
|
+
|
|
82
|
+
#: Namespace of the XML element
|
|
83
|
+
namespace = None
|
|
84
|
+
|
|
85
|
+
#: Whether this is a complex type
|
|
91
86
|
is_complex_type = False
|
|
92
|
-
|
|
87
|
+
|
|
88
|
+
#: Whether this is a geometry
|
|
89
|
+
is_geometry = False # Overwritten for some gml types.
|
|
93
90
|
|
|
94
91
|
def __str__(self):
|
|
95
|
-
"""Return the type name"""
|
|
92
|
+
"""Return the type name (in full XML format)"""
|
|
96
93
|
raise NotImplementedError()
|
|
97
94
|
|
|
98
|
-
def with_prefix(self, prefix="xs"):
|
|
99
|
-
xml_name = str(self)
|
|
100
|
-
if ":" in xml_name:
|
|
101
|
-
return xml_name
|
|
102
|
-
else:
|
|
103
|
-
return f"{prefix}:{xml_name}"
|
|
104
|
-
|
|
105
95
|
def to_python(self, raw_value):
|
|
106
96
|
"""Convert a raw string value to this type representation"""
|
|
107
97
|
return raw_value
|
|
108
98
|
|
|
109
99
|
|
|
110
100
|
class XsdTypes(XsdAnyType, Enum):
|
|
111
|
-
"""Brief enumeration of
|
|
101
|
+
"""Brief enumeration of common XMLSchema types.
|
|
112
102
|
|
|
113
103
|
The :class:`XsdElement` and :class:`XsdAttribute` can use these enum members
|
|
114
104
|
to indicate their value is a well-known XML Schema. Some GML types are included as well.
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
Each member value is a fully qualified XML name.
|
|
107
|
+
The output rendering will convert these to the chosen prefixes.
|
|
118
108
|
"""
|
|
119
109
|
|
|
120
|
-
anyType = "anyType" #
|
|
121
|
-
string = "string"
|
|
122
|
-
boolean = "boolean"
|
|
123
|
-
decimal = "decimal" # the base type for all numbers too.
|
|
124
|
-
integer = "integer" # integer value
|
|
125
|
-
float = "float"
|
|
126
|
-
double = "double"
|
|
127
|
-
time = "time"
|
|
128
|
-
date = "date"
|
|
129
|
-
dateTime = "dateTime"
|
|
130
|
-
anyURI = "anyURI"
|
|
110
|
+
anyType = xmlns.xs.qname("anyType") # not "xsd:any", that is an element.
|
|
111
|
+
string = xmlns.xs.qname("string")
|
|
112
|
+
boolean = xmlns.xs.qname("boolean")
|
|
113
|
+
decimal = xmlns.xs.qname("decimal") # the base type for all numbers too.
|
|
114
|
+
integer = xmlns.xs.qname("integer") # integer value
|
|
115
|
+
float = xmlns.xs.qname("float")
|
|
116
|
+
double = xmlns.xs.qname("double")
|
|
117
|
+
time = xmlns.xs.qname("time")
|
|
118
|
+
date = xmlns.xs.qname("date")
|
|
119
|
+
dateTime = xmlns.xs.qname("dateTime")
|
|
120
|
+
anyURI = xmlns.xs.qname("anyURI")
|
|
131
121
|
|
|
132
122
|
# Number variations
|
|
133
|
-
byte = "byte" # signed 8-bit integer
|
|
134
|
-
short = "short" # signed 16-bit integer
|
|
135
|
-
int = "int" # signed 32-bit integer
|
|
136
|
-
long = "long" # signed 64-bit integer
|
|
137
|
-
unsignedByte = "unsignedByte" # unsigned 8-bit integer
|
|
138
|
-
unsignedShort = "unsignedShort" # unsigned 16-bit integer
|
|
139
|
-
unsignedInt = "unsignedInt" # unsigned 32-bit integer
|
|
140
|
-
unsignedLong = "unsignedLong" # unsigned 64-bit integer
|
|
123
|
+
byte = xmlns.xs.qname("byte") # signed 8-bit integer
|
|
124
|
+
short = xmlns.xs.qname("short") # signed 16-bit integer
|
|
125
|
+
int = xmlns.xs.qname("int") # signed 32-bit integer
|
|
126
|
+
long = xmlns.xs.qname("long") # signed 64-bit integer
|
|
127
|
+
unsignedByte = xmlns.xs.qname("unsignedByte") # unsigned 8-bit integer
|
|
128
|
+
unsignedShort = xmlns.xs.qname("unsignedShort") # unsigned 16-bit integer
|
|
129
|
+
unsignedInt = xmlns.xs.qname("unsignedInt") # unsigned 32-bit integer
|
|
130
|
+
unsignedLong = xmlns.xs.qname("unsignedLong") # unsigned 64-bit integer
|
|
141
131
|
|
|
142
132
|
# Less common, but useful nonetheless:
|
|
143
|
-
duration = "duration"
|
|
144
|
-
nonNegativeInteger = "nonNegativeInteger"
|
|
145
|
-
gYear = "gYear"
|
|
146
|
-
hexBinary = "hexBinary"
|
|
147
|
-
base64Binary = "base64Binary"
|
|
148
|
-
token = "token" # noqa: S105
|
|
149
|
-
language = "language"
|
|
133
|
+
duration = xmlns.xs.qname("duration")
|
|
134
|
+
nonNegativeInteger = xmlns.xs.qname("nonNegativeInteger")
|
|
135
|
+
gYear = xmlns.xs.qname("gYear")
|
|
136
|
+
hexBinary = xmlns.xs.qname("hexBinary")
|
|
137
|
+
base64Binary = xmlns.xs.qname("base64Binary")
|
|
138
|
+
token = xmlns.xs.qname("token") # noqa: S105
|
|
139
|
+
language = xmlns.xs.qname("language")
|
|
150
140
|
|
|
151
141
|
# Types that contain a GML value as member:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
142
|
+
# Note these receive the "is_geometry = True" value below.
|
|
143
|
+
gmlGeometryPropertyType = xmlns.gml.qname("GeometryPropertyType")
|
|
144
|
+
gmlPointPropertyType = xmlns.gml.qname("PointPropertyType")
|
|
145
|
+
gmlCurvePropertyType = xmlns.gml.qname("CurvePropertyType") # curve is base for LineString
|
|
146
|
+
gmlSurfacePropertyType = xmlns.gml.qname("SurfacePropertyType") # GML2 had PolygonPropertyType
|
|
147
|
+
gmlMultiSurfacePropertyType = xmlns.gml.qname("MultiSurfacePropertyType")
|
|
148
|
+
gmlMultiPointPropertyType = xmlns.gml.qname("MultiPointPropertyType")
|
|
149
|
+
gmlMultiCurvePropertyType = xmlns.gml.qname("MultiCurvePropertyType")
|
|
150
|
+
gmlMultiGeometryPropertyType = xmlns.gml.qname("MultiGeometryPropertyType")
|
|
151
|
+
|
|
152
|
+
# Other typical GML values:
|
|
153
|
+
|
|
154
|
+
#: The type for ``<gml:name>`` elements.
|
|
155
|
+
gmlCodeType = xmlns.gml.qname("CodeType") # for <gml:name>
|
|
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.
|
|
164
|
+
gmlAbstractGeometryType = xmlns.gml.qname("AbstractGeometryType")
|
|
167
165
|
|
|
168
166
|
#: A feature that has a gml:name and gml:boundedBy as possible child element.
|
|
169
|
-
gmlAbstractFeatureType = "
|
|
170
|
-
|
|
167
|
+
gmlAbstractFeatureType = xmlns.gml.qname("AbstractFeatureType")
|
|
168
|
+
|
|
169
|
+
#: The base of gml:AbstractFeatureType
|
|
170
|
+
gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType")
|
|
171
171
|
|
|
172
172
|
def __str__(self):
|
|
173
173
|
return self.value
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return xml_name[:colon] if colon else None
|
|
175
|
+
def __init__(self, value):
|
|
176
|
+
# Parse XML namespace data once, which to_qname() uses.
|
|
177
|
+
# Can't set enum.name, so will use a property for that.
|
|
178
|
+
self.namespace, self._localname = split_ns(value)
|
|
179
|
+
self.is_geometry = False # redefined below
|
|
181
180
|
|
|
182
181
|
@cached_property
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
185
|
-
|
|
182
|
+
def name(self) -> str:
|
|
183
|
+
"""Overwrites enum.name to return the XML local name.
|
|
184
|
+
This is used for to_qname().
|
|
185
|
+
"""
|
|
186
|
+
return self._localname
|
|
186
187
|
|
|
187
188
|
@cached_property
|
|
188
189
|
def _to_python_func(self):
|
|
189
|
-
if not TYPES_TO_PYTHON:
|
|
190
|
-
_init_types_to_python()
|
|
191
|
-
|
|
192
190
|
try:
|
|
193
191
|
return TYPES_TO_PYTHON[self]
|
|
194
192
|
except KeyError:
|
|
195
193
|
raise NotImplementedError(f'Casting to "{self}" is not implemented.') from None
|
|
196
194
|
|
|
197
195
|
def to_python(self, raw_value):
|
|
198
|
-
"""Convert a raw string value to this type representation
|
|
199
|
-
|
|
200
|
-
|
|
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.
|
|
201
202
|
return raw_value
|
|
202
203
|
|
|
203
204
|
try:
|
|
@@ -206,40 +207,73 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
206
207
|
raise # subclass of ValueError so explicitly caught and reraised
|
|
207
208
|
except (TypeError, ValueError, ArithmeticError) as e:
|
|
208
209
|
# ArithmeticError is base of DecimalException
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
210
|
+
logger.debug("Parsing error for %r: %s", raw_value, e)
|
|
211
|
+
name = self.name if self.namespace == xmlns.xsd.value else self.value
|
|
212
|
+
raise ExternalParsingError(f"Can't cast '{raw_value}' to {name}.") from e
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
for _type in (
|
|
216
|
+
XsdTypes.gmlGeometryPropertyType,
|
|
217
|
+
XsdTypes.gmlPointPropertyType,
|
|
218
|
+
XsdTypes.gmlCurvePropertyType,
|
|
219
|
+
XsdTypes.gmlSurfacePropertyType,
|
|
220
|
+
XsdTypes.gmlMultiSurfacePropertyType,
|
|
221
|
+
XsdTypes.gmlMultiPointPropertyType,
|
|
222
|
+
XsdTypes.gmlMultiCurvePropertyType,
|
|
223
|
+
XsdTypes.gmlMultiGeometryPropertyType,
|
|
224
|
+
# gml:boundedBy is technically a geometry, which we don't support in queries currently.
|
|
225
|
+
XsdTypes.gmlBoundingShapeType,
|
|
226
|
+
):
|
|
227
|
+
# One of the reasons the code checks for "xsd_element.type.is_geometry"
|
|
228
|
+
# is because profiling showed that isinstance(xsd_element, ...) is really slow.
|
|
229
|
+
# When rendering 5000 objects with 10+ elements, isinstance() started showing up as hotspot.
|
|
230
|
+
_type.is_geometry = True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _as_is(v):
|
|
234
|
+
return v
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
TYPES_AS_PYTHON = {
|
|
238
|
+
XsdTypes.date: date,
|
|
239
|
+
XsdTypes.dateTime: datetime,
|
|
240
|
+
XsdTypes.time: time,
|
|
241
|
+
XsdTypes.string: str,
|
|
242
|
+
XsdTypes.boolean: bool,
|
|
243
|
+
XsdTypes.integer: int,
|
|
244
|
+
XsdTypes.int: int,
|
|
245
|
+
XsdTypes.long: int,
|
|
246
|
+
XsdTypes.short: int,
|
|
247
|
+
XsdTypes.byte: int,
|
|
248
|
+
XsdTypes.unsignedInt: int,
|
|
249
|
+
XsdTypes.unsignedLong: int,
|
|
250
|
+
XsdTypes.unsignedShort: int,
|
|
251
|
+
XsdTypes.unsignedByte: int,
|
|
252
|
+
XsdTypes.float: D, # auto_cast() always converts to decimal
|
|
253
|
+
XsdTypes.double: D,
|
|
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,
|
|
274
|
+
XsdTypes.gmlCodeType: _as_is,
|
|
275
|
+
XsdTypes.anyType: values.auto_cast,
|
|
276
|
+
}
|
|
243
277
|
|
|
244
278
|
|
|
245
279
|
class XsdNode:
|
|
@@ -250,22 +284,29 @@ class XsdNode:
|
|
|
250
284
|
parse query input and read model attributes to write as output.
|
|
251
285
|
"""
|
|
252
286
|
|
|
287
|
+
#: Whether this node is an :class:`XsdAttribute` (avoids slow ``isinstance()`` checks)
|
|
253
288
|
is_attribute = False
|
|
289
|
+
#: Whether this node can occur multiple times.
|
|
254
290
|
is_many = False
|
|
255
291
|
|
|
292
|
+
#: The local name of the XML element
|
|
256
293
|
name: str
|
|
257
|
-
|
|
258
|
-
|
|
294
|
+
|
|
295
|
+
#: The data type of the element/attribute, both :class:`XsdComplexType` and :class:`XsdTypes` are allowed.
|
|
296
|
+
type: XsdAnyType
|
|
297
|
+
|
|
298
|
+
#: XML Namespace of the element
|
|
299
|
+
namespace: xmlns | str | None
|
|
259
300
|
|
|
260
301
|
#: Which field to read from the model to get the value
|
|
261
302
|
#: This supports dot notation to access related attributes.
|
|
262
|
-
source: models.Field | ForeignObjectRel | None
|
|
303
|
+
source: models.Field | models.ForeignObjectRel | None
|
|
263
304
|
|
|
264
305
|
#: Which field to read from the model to get the value
|
|
265
306
|
#: This supports dot notation to access related attributes.
|
|
266
307
|
model_attribute: str | None
|
|
267
308
|
|
|
268
|
-
#: 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.
|
|
269
310
|
#: This helps to perform additional filtering in side meth:get_value: based on user policies.
|
|
270
311
|
feature_type: FeatureType | None
|
|
271
312
|
|
|
@@ -273,24 +314,49 @@ class XsdNode:
|
|
|
273
314
|
self,
|
|
274
315
|
name: str,
|
|
275
316
|
type: XsdAnyType,
|
|
317
|
+
namespace: xmlns | str | None,
|
|
276
318
|
*,
|
|
277
|
-
|
|
278
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
319
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
279
320
|
model_attribute: str | None = None,
|
|
321
|
+
absolute_model_attribute: str | None = None,
|
|
280
322
|
feature_type: FeatureType | None = None,
|
|
281
323
|
):
|
|
324
|
+
"""
|
|
325
|
+
:param name: The local name of the element.
|
|
326
|
+
:param type: The XML Schema type of the element, can also be a XsdComplexType.
|
|
327
|
+
:param namespace: XML namespace URI.
|
|
328
|
+
:param source: Original Model field, which can provide more metadata/parsing.
|
|
329
|
+
:param model_attribute: The Django model path that this element accesses.
|
|
330
|
+
:param absolute_model_attribute: The full path, including parent elements.
|
|
331
|
+
:param feature_type: Typically assigned in :meth:`~gisserver.features.FeatureField.bind`,
|
|
332
|
+
needed by some :meth:`get_value` functions.
|
|
333
|
+
"""
|
|
334
|
+
if ":" in name:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
"XsdNode should receive the localname, not the QName in ns:localname format."
|
|
337
|
+
)
|
|
338
|
+
elif "}" in name:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
"XsdNode should receive the localname, not the full name in {uri}name format."
|
|
341
|
+
)
|
|
342
|
+
|
|
282
343
|
# Using plain assignment instead of dataclass turns out to be needed
|
|
283
344
|
# for flexibility and easier subclassing.
|
|
284
345
|
self.name = name
|
|
285
346
|
self.type = type
|
|
286
|
-
self.
|
|
347
|
+
self.namespace = str(namespace) if namespace is not None else None # cast enum members.
|
|
287
348
|
self.source = source
|
|
288
349
|
self.model_attribute = model_attribute or self.name
|
|
289
|
-
|
|
350
|
+
self.absolute_model_attribute = absolute_model_attribute or self.model_attribute
|
|
351
|
+
# link back to top-level parent, some get_value() functions need it.
|
|
290
352
|
self.feature_type = feature_type
|
|
291
353
|
|
|
292
|
-
if
|
|
293
|
-
|
|
354
|
+
if (
|
|
355
|
+
self.model_attribute
|
|
356
|
+
and self.absolute_model_attribute
|
|
357
|
+
and not self.absolute_model_attribute.endswith(self.model_attribute)
|
|
358
|
+
):
|
|
359
|
+
raise ValueError("Inconsistent 'absolute_model_attribute' and 'model_attribute' value")
|
|
294
360
|
|
|
295
361
|
self._attrgetter = operator.attrgetter(self.model_attribute)
|
|
296
362
|
self._valuegetter = self._build_valuegetter(self.model_attribute, self.source)
|
|
@@ -304,7 +370,7 @@ class XsdNode:
|
|
|
304
370
|
@staticmethod
|
|
305
371
|
def _build_valuegetter(
|
|
306
372
|
model_attribute: str,
|
|
307
|
-
field: models.Field | ForeignObjectRel | None,
|
|
373
|
+
field: models.Field | models.ForeignObjectRel | None,
|
|
308
374
|
):
|
|
309
375
|
"""Select the most efficient read function to retrieves the value.
|
|
310
376
|
|
|
@@ -314,7 +380,7 @@ class XsdNode:
|
|
|
314
380
|
since this will be much faster than using ``getattr()``.
|
|
315
381
|
The custom ``value_from_object()`` is fully supported too.
|
|
316
382
|
"""
|
|
317
|
-
if field is None or isinstance(field, ForeignObjectRel):
|
|
383
|
+
if field is None or isinstance(field, models.ForeignObjectRel):
|
|
318
384
|
# No model field, can only use getattr(). The attrgetter() function is both faster,
|
|
319
385
|
# and has built-in support for traversing model attributes with dots.
|
|
320
386
|
return operator.attrgetter(model_attribute)
|
|
@@ -341,11 +407,6 @@ class XsdNode:
|
|
|
341
407
|
|
|
342
408
|
return _related_get_value_from_object
|
|
343
409
|
|
|
344
|
-
@cached_property
|
|
345
|
-
def is_geometry(self) -> bool:
|
|
346
|
-
"""Tell whether the XML node/element should be handed as GML geometry."""
|
|
347
|
-
return self.type.is_geometry or isinstance(self.source, GeometryField)
|
|
348
|
-
|
|
349
410
|
@cached_property
|
|
350
411
|
def is_array(self) -> bool:
|
|
351
412
|
"""Tell whether this node is backed by an PostgreSQL Array Field."""
|
|
@@ -359,33 +420,48 @@ class XsdNode:
|
|
|
359
420
|
@cached_property
|
|
360
421
|
def xml_name(self):
|
|
361
422
|
"""The XML element/attribute name."""
|
|
362
|
-
return f"{self.
|
|
423
|
+
return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
|
|
424
|
+
|
|
425
|
+
def relative_orm_path(self, parent: XsdElement | None = None) -> str:
|
|
426
|
+
"""The ORM field lookup to perform, relative to the parent element."""
|
|
427
|
+
if parent is None:
|
|
428
|
+
return self.orm_path
|
|
429
|
+
|
|
430
|
+
prefix = f"{parent.orm_path}__"
|
|
431
|
+
if not self.orm_path.startswith(prefix):
|
|
432
|
+
raise ValueError(f"Node '{self}' is not a child of '{parent}.")
|
|
433
|
+
else:
|
|
434
|
+
return self.orm_path[len(prefix) :]
|
|
363
435
|
|
|
364
436
|
@cached_property
|
|
365
|
-
def
|
|
437
|
+
def local_orm_path(self) -> str:
|
|
366
438
|
"""The ORM field lookup to perform."""
|
|
367
439
|
if self.model_attribute is None:
|
|
368
440
|
raise ValueError(f"Node {self.xml_name} has no 'model_attribute' set.")
|
|
369
441
|
return self.model_attribute.replace(".", "__")
|
|
370
442
|
|
|
443
|
+
@cached_property
|
|
444
|
+
def orm_path(self) -> str:
|
|
445
|
+
"""The ORM field lookup to perform."""
|
|
446
|
+
if self.absolute_model_attribute is None:
|
|
447
|
+
raise ValueError(f"Node {self.xml_name} has no 'absolute_model_attribute' set.")
|
|
448
|
+
return self.absolute_model_attribute.replace(".", "__")
|
|
449
|
+
|
|
371
450
|
@cached_property
|
|
372
451
|
def orm_field(self) -> str:
|
|
373
|
-
"""The direct ORM field that provides this property.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return self.
|
|
452
|
+
"""The direct ORM field that provides this property; the first relative level.
|
|
453
|
+
Typically, this is the same as the field name.
|
|
454
|
+
"""
|
|
455
|
+
return self.orm_path.partition(".")[0]
|
|
377
456
|
|
|
378
457
|
@cached_property
|
|
379
458
|
def orm_relation(self) -> tuple[str | None, str]:
|
|
380
|
-
"""The ORM field and parent relation
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
path, _, field = self.
|
|
385
|
-
|
|
386
|
-
return None, field
|
|
387
|
-
else:
|
|
388
|
-
return path.replace(".", "__"), field
|
|
459
|
+
"""The ORM field and parent relation.
|
|
460
|
+
Note this isn't something like "self.parent.orm_path",
|
|
461
|
+
as this mode may have a dotted-path to its source attribute.
|
|
462
|
+
"""
|
|
463
|
+
path, _, field = self.orm_path.rpartition("__")
|
|
464
|
+
return path or None, field
|
|
389
465
|
|
|
390
466
|
def build_lhs_part(self, compiler: CompiledQuery, match: ORMPath):
|
|
391
467
|
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
@@ -427,13 +503,10 @@ class XsdNode:
|
|
|
427
503
|
"""
|
|
428
504
|
return value
|
|
429
505
|
|
|
430
|
-
@cached_property
|
|
431
|
-
def _form_field(self):
|
|
432
|
-
"""Internal cached field for to_python()"""
|
|
433
|
-
return self.source.formfield()
|
|
434
|
-
|
|
435
506
|
def to_python(self, raw_value: str):
|
|
436
|
-
"""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
|
+
"""
|
|
437
510
|
try:
|
|
438
511
|
raw_value = self.type.to_python(raw_value)
|
|
439
512
|
if self.source is not None:
|
|
@@ -454,24 +527,39 @@ class XsdNode:
|
|
|
454
527
|
|
|
455
528
|
The raw string value can be passed here. Auto-cased values could
|
|
456
529
|
raise an TypeError due to being unsupported by the validation.
|
|
530
|
+
|
|
531
|
+
:param raw_value: The string value taken from the XML node.
|
|
532
|
+
:param lookup: The ORM lookup (e.g. ``equals`` or ``fes_like``).
|
|
533
|
+
:param tag: The filter operator tag name, e.g. ``PropertyIsEqualTo``.
|
|
534
|
+
:returns: The parsed Python value.
|
|
457
535
|
"""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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 (
|
|
467
547
|
isinstance(self.source, RelatedField)
|
|
468
548
|
and self.source.target_field.get_lookup(lookup) is None
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
)
|
|
475
563
|
|
|
476
564
|
return raw_value
|
|
477
565
|
|
|
@@ -484,60 +572,49 @@ class XsdElement(XsdNode):
|
|
|
484
572
|
This holds the definition for a single property in the WFS server.
|
|
485
573
|
It's used in ``DescribeFeatureType`` to output the field metadata,
|
|
486
574
|
and used in ``GetFeature`` to access the actual value from the object.
|
|
487
|
-
Overriding :meth:`get_value` allows to override this logic.
|
|
575
|
+
Overriding :meth:`XsdNode.get_value` allows to override this logic.
|
|
488
576
|
|
|
489
|
-
The :attr:`name` may differ from the underlying :attr:`model_attribute`,
|
|
577
|
+
The :attr:`name` may differ from the underlying :attr:`XsdNode.model_attribute`,
|
|
490
578
|
so the WFS server can use other field names then the underlying model.
|
|
491
579
|
|
|
492
|
-
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
|
|
493
581
|
a related field. For the WFS client, the data appears to be flattened.
|
|
494
582
|
"""
|
|
495
583
|
|
|
584
|
+
#: Whether the element can be null
|
|
496
585
|
nillable: bool | None
|
|
586
|
+
#: The minimal number of times the element occurs in the output.
|
|
497
587
|
min_occurs: int | None
|
|
498
|
-
|
|
588
|
+
#: The maximum number of times this element occurs in the output.
|
|
589
|
+
max_occurs: int | Literal["unbounded"] | None
|
|
499
590
|
|
|
500
591
|
def __init__(
|
|
501
592
|
self,
|
|
502
593
|
name: str,
|
|
503
594
|
type: XsdAnyType,
|
|
595
|
+
namespace: xmlns | str | None,
|
|
504
596
|
*,
|
|
505
|
-
prefix: str | None = "app",
|
|
506
597
|
nillable: bool | None = None,
|
|
507
598
|
min_occurs: int | None = None,
|
|
508
|
-
max_occurs: int |
|
|
509
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
599
|
+
max_occurs: int | Literal["unbounded"] | None = None,
|
|
600
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
510
601
|
model_attribute: str | None = None,
|
|
602
|
+
absolute_model_attribute: str | None = None,
|
|
511
603
|
feature_type: FeatureType | None = None,
|
|
512
604
|
):
|
|
513
605
|
super().__init__(
|
|
514
606
|
name,
|
|
515
607
|
type,
|
|
516
|
-
|
|
608
|
+
namespace=namespace,
|
|
517
609
|
source=source,
|
|
518
610
|
model_attribute=model_attribute,
|
|
611
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
519
612
|
feature_type=feature_type,
|
|
520
613
|
)
|
|
521
614
|
self.nillable = nillable
|
|
522
615
|
self.min_occurs = min_occurs
|
|
523
616
|
self.max_occurs = max_occurs
|
|
524
617
|
|
|
525
|
-
@cached_property
|
|
526
|
-
def as_xml(self):
|
|
527
|
-
attributes = [f'name="{self.name}" type="{self.type}"']
|
|
528
|
-
if self.min_occurs is not None:
|
|
529
|
-
attributes.append(f'minOccurs="{self.min_occurs}"')
|
|
530
|
-
if self.max_occurs is not None:
|
|
531
|
-
attributes.append(f'maxOccurs="{self.max_occurs}"')
|
|
532
|
-
if self.nillable:
|
|
533
|
-
str_bool = "true" if self.nillable else "false"
|
|
534
|
-
attributes.append(f'nillable="{str_bool}"')
|
|
535
|
-
|
|
536
|
-
return "<element {} />".format(" ".join(attributes))
|
|
537
|
-
|
|
538
|
-
def __str__(self):
|
|
539
|
-
return self.as_xml
|
|
540
|
-
|
|
541
618
|
@cached_property
|
|
542
619
|
def is_many(self) -> bool:
|
|
543
620
|
"""Tell whether the XML element can be rendered multiple times.
|
|
@@ -577,35 +654,61 @@ class XsdAttribute(XsdNode):
|
|
|
577
654
|
name: str,
|
|
578
655
|
type: XsdAnyType = XsdTypes.string, # added default
|
|
579
656
|
*,
|
|
580
|
-
|
|
657
|
+
namespace: xmlns | str | None = None,
|
|
581
658
|
use: str = "optional",
|
|
582
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
659
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
583
660
|
model_attribute: str | None = None,
|
|
661
|
+
absolute_model_attribute: str | None = None,
|
|
584
662
|
feature_type: FeatureType | None = None,
|
|
585
663
|
):
|
|
586
664
|
super().__init__(
|
|
587
665
|
name,
|
|
588
666
|
type,
|
|
589
|
-
|
|
667
|
+
namespace=namespace,
|
|
590
668
|
source=source,
|
|
591
669
|
model_attribute=model_attribute,
|
|
670
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
592
671
|
feature_type=feature_type,
|
|
593
672
|
)
|
|
594
673
|
self.use = use
|
|
595
674
|
|
|
596
675
|
|
|
597
|
-
class
|
|
598
|
-
"""
|
|
676
|
+
class GeometryXsdElement(XsdElement):
|
|
677
|
+
"""A subtype for the :class:`XsdElement` that provides access to geometry data.
|
|
678
|
+
|
|
679
|
+
This declares an element such as::
|
|
680
|
+
|
|
681
|
+
<app:geometry>
|
|
682
|
+
<gml:Point>...</gml:Point>
|
|
683
|
+
</app:geometry>
|
|
599
684
|
|
|
600
|
-
|
|
685
|
+
Hence, the :attr:`namespace` of this element isn't the GML namespace,
|
|
686
|
+
only the type it points to is geometry data.
|
|
687
|
+
|
|
688
|
+
The :attr:`source` is guaranteed to point to a :class:`~django.contrib.gis.models.GeometryField`,
|
|
689
|
+
and can be a :class:`~django.db.models.GeneratedField` in Django 5
|
|
690
|
+
as long as its ``output_field`` points to a :class:`~django.contrib.gis.models.GeometryField`.
|
|
601
691
|
"""
|
|
602
692
|
|
|
603
|
-
|
|
693
|
+
if django.VERSION >= (5, 0):
|
|
694
|
+
source: GeometryField | models.GeneratedField
|
|
695
|
+
else:
|
|
696
|
+
source: GeometryField
|
|
697
|
+
|
|
698
|
+
@cached_property
|
|
699
|
+
def source_srid(self) -> int:
|
|
700
|
+
"""Tell which Spatial Reference Identifier the source information is stored under."""
|
|
701
|
+
if GeneratedField is not None and isinstance(self.source, GeneratedField):
|
|
702
|
+
# Allow GeometryField to be wrapped as:
|
|
703
|
+
# models.GeneratedField(SomeFunction("geofield"), output_field=models.GeometryField())
|
|
704
|
+
return self.source.output_field.srid
|
|
705
|
+
else:
|
|
706
|
+
return self.source.srid
|
|
604
707
|
|
|
605
708
|
|
|
606
709
|
class GmlIdAttribute(XsdAttribute):
|
|
607
|
-
"""A virtual
|
|
608
|
-
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.
|
|
609
712
|
"""
|
|
610
713
|
|
|
611
714
|
type_name: str
|
|
@@ -613,20 +716,23 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
613
716
|
def __init__(
|
|
614
717
|
self,
|
|
615
718
|
type_name: str,
|
|
616
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
719
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
617
720
|
model_attribute="pk",
|
|
721
|
+
absolute_model_attribute=None,
|
|
618
722
|
feature_type: FeatureType | None = None,
|
|
619
723
|
):
|
|
620
724
|
super().__init__(
|
|
621
|
-
prefix="gml",
|
|
622
725
|
name="id",
|
|
726
|
+
namespace=xmlns.gml,
|
|
623
727
|
source=source,
|
|
624
728
|
model_attribute=model_attribute,
|
|
729
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
625
730
|
feature_type=feature_type,
|
|
626
731
|
)
|
|
627
732
|
object.__setattr__(self, "type_name", type_name)
|
|
628
733
|
|
|
629
734
|
def get_value(self, instance: models.Model):
|
|
735
|
+
"""Render the value."""
|
|
630
736
|
pk = super().get_value(instance) # handle dotted-name notations
|
|
631
737
|
return f"{self.type_name}.{pk}"
|
|
632
738
|
|
|
@@ -636,7 +742,7 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
636
742
|
|
|
637
743
|
|
|
638
744
|
class GmlNameElement(XsdElement):
|
|
639
|
-
"""A subclass to handle the
|
|
745
|
+
"""A subclass to handle the ``<gml:name>`` element.
|
|
640
746
|
This displays a human-readable title for the object.
|
|
641
747
|
|
|
642
748
|
Currently, this just reads a single attribute,
|
|
@@ -644,19 +750,17 @@ class GmlNameElement(XsdElement):
|
|
|
644
750
|
(although that would make comparisons on ``element@gml:name`` more complex).
|
|
645
751
|
"""
|
|
646
752
|
|
|
647
|
-
is_geometry = False # Override type
|
|
648
|
-
|
|
649
753
|
def __init__(
|
|
650
754
|
self,
|
|
651
755
|
model_attribute: str,
|
|
652
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
756
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
653
757
|
feature_type=None,
|
|
654
758
|
):
|
|
655
759
|
# Prefill most known fields
|
|
656
760
|
super().__init__(
|
|
657
|
-
prefix="gml",
|
|
658
761
|
name="name",
|
|
659
762
|
type=XsdTypes.gmlCodeType,
|
|
763
|
+
namespace=xmlns.gml,
|
|
660
764
|
min_occurs=0,
|
|
661
765
|
source=source,
|
|
662
766
|
model_attribute=model_attribute,
|
|
@@ -674,21 +778,19 @@ class GmlNameElement(XsdElement):
|
|
|
674
778
|
|
|
675
779
|
|
|
676
780
|
class GmlBoundedByElement(XsdElement):
|
|
677
|
-
"""A subclass to handle the
|
|
781
|
+
"""A subclass to handle the ``<gml:boundedBy>`` element.
|
|
678
782
|
|
|
679
783
|
This override makes sure this non-model element data
|
|
680
784
|
can be included in the XML tree like every other element.
|
|
681
785
|
Its value is the complete bounding box of the feature type data.
|
|
682
786
|
"""
|
|
683
787
|
|
|
684
|
-
is_geometry = True # Override type
|
|
685
|
-
|
|
686
788
|
def __init__(self, feature_type):
|
|
687
789
|
# Prefill most known fields
|
|
688
790
|
super().__init__(
|
|
689
|
-
prefix="gml",
|
|
690
791
|
name="boundedBy",
|
|
691
792
|
type=XsdTypes.gmlBoundingShapeType,
|
|
793
|
+
namespace=xmlns.gml,
|
|
692
794
|
min_occurs=0,
|
|
693
795
|
feature_type=feature_type,
|
|
694
796
|
)
|
|
@@ -703,70 +805,125 @@ class GmlBoundedByElement(XsdElement):
|
|
|
703
805
|
"""Give the ORM part when this element would be used as right-hand-side"""
|
|
704
806
|
raise NotImplementedError("queries against <gml:boundedBy> are not supported")
|
|
705
807
|
|
|
706
|
-
def get_value(self, instance: models.Model, crs: CRS | None = None):
|
|
707
|
-
"""Provide the value of the <gml:boundedBy> field
|
|
708
|
-
|
|
709
|
-
|
|
808
|
+
def get_value(self, instance: models.Model, crs: CRS | None = None) -> BoundingBox | None:
|
|
809
|
+
"""Provide the value of the <gml:boundedBy> field,
|
|
810
|
+
which is the bounding box for a single instance.
|
|
811
|
+
|
|
812
|
+
This is only used for native Python rendering. When the database
|
|
813
|
+
rendering is enabled (GISSERVER_USE_DB_RENDERING=True), the calculation
|
|
814
|
+
is entirely performed within the query.
|
|
815
|
+
"""
|
|
816
|
+
geometries: list[GEOSGeometry] = list(
|
|
817
|
+
# remove 'None' values
|
|
818
|
+
filter(
|
|
819
|
+
None,
|
|
820
|
+
[
|
|
821
|
+
# support dotted paths here for geometries in a foreign key relation.
|
|
822
|
+
operator.attrgetter(geo_element.absolute_model_attribute)(instance)
|
|
823
|
+
for geo_element in self.feature_type.all_geometry_elements
|
|
824
|
+
],
|
|
825
|
+
)
|
|
826
|
+
)
|
|
827
|
+
if not geometries:
|
|
828
|
+
return None
|
|
829
|
+
|
|
830
|
+
return BoundingBox.from_geometries(geometries, crs)
|
|
710
831
|
|
|
711
832
|
|
|
712
833
|
@dataclass(frozen=True)
|
|
713
834
|
class XsdComplexType(XsdAnyType):
|
|
714
|
-
"""Define an
|
|
835
|
+
"""Define an ``<xsd:complexType>`` that represents a whole class definition.
|
|
715
836
|
|
|
716
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
|
+
)
|
|
717
859
|
|
|
718
860
|
A complex type can hold multiple :class:`XsdElement` and :class:`XsdAttribute`
|
|
719
|
-
nodes as children, composing an object.
|
|
720
|
-
|
|
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.
|
|
721
865
|
That also allows embedding models with their relations into a single response.
|
|
722
866
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
Incoming XPath queries are parsed using this object to resolve the XPath to model attributes.
|
|
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:
|
|
727
870
|
|
|
728
|
-
|
|
729
|
-
|
|
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.
|
|
730
873
|
|
|
731
|
-
|
|
732
|
-
|
|
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.
|
|
876
|
+
|
|
877
|
+
By default, The :attr:`base` type is detected as ``<gml:AbstractFeatureType>``,
|
|
878
|
+
when there is a geometry element in the definition.
|
|
733
879
|
"""
|
|
734
880
|
|
|
735
|
-
#: Internal class name (without XML prefix)
|
|
881
|
+
#: Internal class name (without XML namespace/prefix)
|
|
736
882
|
name: str
|
|
737
883
|
|
|
738
|
-
#:
|
|
739
|
-
|
|
884
|
+
#: The XML namespace
|
|
885
|
+
namespace: str | None
|
|
886
|
+
|
|
887
|
+
#: All local elements in this class
|
|
888
|
+
elements: list[XsdElement] = field(default_factory=list)
|
|
740
889
|
|
|
741
890
|
#: All attributes in this class
|
|
742
891
|
attributes: list[XsdAttribute] = field(default_factory=list)
|
|
743
892
|
|
|
744
893
|
#: The base class of this type. Typically gml:AbstractFeatureType,
|
|
745
894
|
#: which provides the <gml:name> and <gml:boundedBy> elements.
|
|
746
|
-
base: XsdAnyType =
|
|
747
|
-
|
|
748
|
-
#: The prefix alias to use for the namespace.
|
|
749
|
-
prefix: str = "app"
|
|
895
|
+
base: XsdAnyType | None = None
|
|
750
896
|
|
|
751
897
|
#: The Django model class that this type was based on.
|
|
752
898
|
source: type[models.Model] | None = None
|
|
753
899
|
|
|
900
|
+
def __post_init__(self):
|
|
901
|
+
# Autodetect (or autocorrect) to have the proper base class when gml elements are present.
|
|
902
|
+
if self.base is None:
|
|
903
|
+
if any(e.type.is_geometry for e in self.elements):
|
|
904
|
+
# for <gml:name> and <gml:boundedBy> elements.
|
|
905
|
+
self.__dict__["base"] = XsdTypes.gmlAbstractFeatureType
|
|
906
|
+
elif any(e.type is XsdTypes.gmlCodeType for e in self.elements):
|
|
907
|
+
# for <gml:name> only
|
|
908
|
+
self.__dict__["base"] = XsdTypes.gmlAbstractGMLType
|
|
909
|
+
|
|
754
910
|
def __str__(self):
|
|
755
911
|
return self.xml_name
|
|
756
912
|
|
|
757
913
|
@cached_property
|
|
758
914
|
def xml_name(self):
|
|
759
|
-
"""Name in the XMLSchema (e.g.
|
|
760
|
-
return f"{self.
|
|
915
|
+
"""Name in the XMLSchema (e.g. {http://example.org/namespace}:SomeClass)."""
|
|
916
|
+
return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
|
|
761
917
|
|
|
762
918
|
@property
|
|
763
|
-
def is_complex_type(self):
|
|
919
|
+
def is_complex_type(self) -> bool:
|
|
920
|
+
"""Always indicates this is a complex type."""
|
|
764
921
|
return True # a property to avoid being used as field.
|
|
765
922
|
|
|
766
923
|
@cached_property
|
|
767
|
-
def
|
|
768
|
-
"""
|
|
769
|
-
if self.base.is_complex_type:
|
|
924
|
+
def elements_including_base(self) -> list[XsdElement]:
|
|
925
|
+
"""The local and inherited elements of this XSD type."""
|
|
926
|
+
if self.base is not None and self.base.is_complex_type:
|
|
770
927
|
# Add all base class members, in their correct ordering
|
|
771
928
|
# By having these as XsdElement objects instead of hard-coded writes,
|
|
772
929
|
# the query/filter logic also works for these elements.
|
|
@@ -775,35 +932,35 @@ class XsdComplexType(XsdAnyType):
|
|
|
775
932
|
return self.elements
|
|
776
933
|
|
|
777
934
|
@cached_property
|
|
778
|
-
def geometry_elements(self) -> list[
|
|
935
|
+
def geometry_elements(self) -> list[GeometryXsdElement]:
|
|
779
936
|
"""Shortcut to get all geometry elements"""
|
|
780
|
-
return [e for e in self.elements if e.is_geometry]
|
|
937
|
+
return [e for e in self.elements if e.type.is_geometry]
|
|
781
938
|
|
|
782
939
|
@cached_property
|
|
783
940
|
def complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
784
|
-
"""Shortcut to get all elements with a complex type
|
|
941
|
+
"""Shortcut to get all elements with a complex type.
|
|
942
|
+
To get all complex elements recursively, read :attr:`all_complex_elements`.
|
|
943
|
+
"""
|
|
785
944
|
return [e for e in self.elements if e.type.is_complex_type]
|
|
786
945
|
|
|
787
946
|
@cached_property
|
|
788
947
|
def flattened_elements(self) -> list[XsdElement]:
|
|
789
|
-
"""Shortcut to get all elements with a flattened model
|
|
948
|
+
"""Shortcut to get all elements with a flattened model attribute"""
|
|
790
949
|
return [e for e in self.elements if e.is_flattened]
|
|
791
950
|
|
|
792
951
|
@cached_property
|
|
793
|
-
def
|
|
952
|
+
def all_complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
794
953
|
"""Shortcut to get all elements with children.
|
|
795
954
|
This mainly exists to provide a structure to mimic what's used
|
|
796
955
|
when PROPERTYNAME is part of the request.
|
|
797
956
|
"""
|
|
798
|
-
child_nodes =
|
|
799
|
-
for xsd_element in self.
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
child_nodes[xsd_element] = sub_type.elements
|
|
803
|
-
child_nodes.update(sub_type.elements_with_children)
|
|
957
|
+
child_nodes = []
|
|
958
|
+
for xsd_element in self.complex_elements:
|
|
959
|
+
child_nodes.append(xsd_element)
|
|
960
|
+
child_nodes.extend(xsd_element.type.all_complex_elements)
|
|
804
961
|
return child_nodes
|
|
805
962
|
|
|
806
|
-
def resolve_element_path(self, xpath: str) -> list[XsdNode] | None:
|
|
963
|
+
def resolve_element_path(self, xpath: str, ns_aliases: dict[str, str]) -> list[XsdNode] | None:
|
|
807
964
|
"""Resolve a xpath reference to the actual node.
|
|
808
965
|
This returns the whole path, including in-between relations, if a match was found.
|
|
809
966
|
|
|
@@ -811,7 +968,7 @@ class XsdComplexType(XsdAnyType):
|
|
|
811
968
|
to convert a request XPath element into the ORM attributes for database queries.
|
|
812
969
|
"""
|
|
813
970
|
try:
|
|
814
|
-
pos = xpath.
|
|
971
|
+
pos = xpath.index("/")
|
|
815
972
|
node_name = xpath[:pos]
|
|
816
973
|
except ValueError:
|
|
817
974
|
node_name = xpath
|
|
@@ -825,12 +982,11 @@ class XsdComplexType(XsdAnyType):
|
|
|
825
982
|
if pos:
|
|
826
983
|
return None # invalid attribute
|
|
827
984
|
|
|
828
|
-
# Remove app: prefixes, or any alias of it (see explanation below)
|
|
829
985
|
xml_name = node_name[1:]
|
|
830
|
-
attribute = self._find_attribute(xml_name=xml_name)
|
|
986
|
+
attribute = self._find_attribute(xml_name=parse_qname(xml_name, ns_aliases))
|
|
831
987
|
return [attribute] if attribute is not None else None
|
|
832
988
|
else:
|
|
833
|
-
element = self._find_element(node_name)
|
|
989
|
+
element = self._find_element(xml_name=parse_qname(node_name, ns_aliases))
|
|
834
990
|
if element is None:
|
|
835
991
|
return None
|
|
836
992
|
|
|
@@ -839,62 +995,34 @@ class XsdComplexType(XsdAnyType):
|
|
|
839
995
|
return None
|
|
840
996
|
else:
|
|
841
997
|
# Recurse into the child node to find the next part
|
|
842
|
-
child_path = element.type.resolve_element_path(xpath[pos + 1 :])
|
|
998
|
+
child_path = element.type.resolve_element_path(xpath[pos + 1 :], ns_aliases)
|
|
843
999
|
return [element] + child_path if child_path is not None else None
|
|
844
1000
|
else:
|
|
845
1001
|
return [element]
|
|
846
1002
|
|
|
847
|
-
def _find_element(self, xml_name) -> XsdElement | None:
|
|
1003
|
+
def _find_element(self, xml_name: str) -> XsdElement | None:
|
|
848
1004
|
"""Locate an element by name"""
|
|
849
1005
|
for element in self.elements:
|
|
850
1006
|
if element.xml_name == xml_name:
|
|
851
1007
|
return element
|
|
852
1008
|
|
|
853
|
-
prefix, name = split_xml_name(xml_name)
|
|
854
|
-
if prefix != "gml" and prefix != self.prefix:
|
|
855
|
-
# Ignore current app namespace. Note this should actually compare the
|
|
856
|
-
# xmlns URI's, but this will suffice for now. The ElementTree parser
|
|
857
|
-
# doesn't provide access to 'xmlns' definitions on the element (or it's
|
|
858
|
-
# parents), so a tag like this is essentially not parsable for us:
|
|
859
|
-
# <ValueReference xmlns:tns="http://...">tns:fieldname</ValueReference>
|
|
860
|
-
for element in self.elements:
|
|
861
|
-
if element.name == name:
|
|
862
|
-
return element
|
|
863
|
-
|
|
864
1009
|
# When there is a base class, resolve elements there too.
|
|
865
|
-
if self.base.is_complex_type:
|
|
1010
|
+
if self.base is not None and self.base.is_complex_type:
|
|
866
1011
|
return self.base._find_element(xml_name)
|
|
867
1012
|
return None
|
|
868
1013
|
|
|
869
|
-
def _find_attribute(self, xml_name) -> XsdAttribute | None:
|
|
1014
|
+
def _find_attribute(self, xml_name: str) -> XsdAttribute | None:
|
|
870
1015
|
"""Locate an attribute by name"""
|
|
871
1016
|
for attribute in self.attributes:
|
|
872
1017
|
if attribute.xml_name == xml_name:
|
|
873
1018
|
return attribute
|
|
874
1019
|
|
|
875
|
-
prefix, name = split_xml_name(xml_name)
|
|
876
|
-
if prefix != "gml" and prefix != self.prefix:
|
|
877
|
-
# Allow any namespace to match, since the stdlib ElementTree parser
|
|
878
|
-
# can't resolve namespaces at all.
|
|
879
|
-
for attribute in self.attributes:
|
|
880
|
-
if attribute.name == name:
|
|
881
|
-
return attribute
|
|
882
|
-
|
|
883
1020
|
# When there is a base class, resolve attributes there too.
|
|
884
|
-
if self.base.is_complex_type:
|
|
1021
|
+
if self.base is not None and self.base.is_complex_type:
|
|
885
1022
|
return self.base._find_attribute(xml_name)
|
|
886
1023
|
return None
|
|
887
1024
|
|
|
888
1025
|
|
|
889
|
-
def split_xml_name(xml_name: str) -> tuple[str | None, str]:
|
|
890
|
-
"""Remove the namespace prefix from an element."""
|
|
891
|
-
try:
|
|
892
|
-
prefix, name = xml_name.split(":", 1)
|
|
893
|
-
return prefix, name
|
|
894
|
-
except ValueError:
|
|
895
|
-
return None, xml_name
|
|
896
|
-
|
|
897
|
-
|
|
898
1026
|
class ORMPath:
|
|
899
1027
|
"""Base class to provide raw XPath results.
|
|
900
1028
|
|
|
@@ -918,7 +1046,7 @@ class ORMPath:
|
|
|
918
1046
|
|
|
919
1047
|
def build_lhs(self, compiler: CompiledQuery):
|
|
920
1048
|
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
921
|
-
For example:
|
|
1049
|
+
For example: ``path == value``.
|
|
922
1050
|
"""
|
|
923
1051
|
if self.is_many:
|
|
924
1052
|
compiler.add_distinct()
|
|
@@ -928,7 +1056,7 @@ class ORMPath:
|
|
|
928
1056
|
|
|
929
1057
|
def build_rhs(self, compiler: CompiledQuery):
|
|
930
1058
|
"""Give the ORM part when this element would be used as right-hand-side.
|
|
931
|
-
For example:
|
|
1059
|
+
For example: ``path1 == path2`` or ``value == path``.
|
|
932
1060
|
"""
|
|
933
1061
|
if self.is_many:
|
|
934
1062
|
compiler.add_distinct()
|
|
@@ -961,10 +1089,10 @@ class XPathMatch(ORMPath):
|
|
|
961
1089
|
# the build_...() logic should return a Q() object.
|
|
962
1090
|
raise NotImplementedError(f"Complex XPath queries are not supported yet: {self.query}")
|
|
963
1091
|
|
|
964
|
-
@
|
|
1092
|
+
@property
|
|
965
1093
|
def orm_path(self) -> str:
|
|
966
1094
|
"""Give the Django ORM path (field__relation__relation2) to the result."""
|
|
967
|
-
return
|
|
1095
|
+
return self.nodes[-1].orm_path
|
|
968
1096
|
|
|
969
1097
|
def __iter__(self):
|
|
970
1098
|
return iter(self.nodes)
|
|
@@ -986,7 +1114,9 @@ class XPathMatch(ORMPath):
|
|
|
986
1114
|
return any(node.is_many for node in self.nodes)
|
|
987
1115
|
|
|
988
1116
|
def build_lhs(self, compiler: CompiledQuery):
|
|
989
|
-
"""
|
|
1117
|
+
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
1118
|
+
For example: ``path == value``.
|
|
1119
|
+
"""
|
|
990
1120
|
if self.is_many:
|
|
991
1121
|
compiler.add_distinct()
|
|
992
1122
|
if self.orm_filters:
|
|
@@ -994,7 +1124,9 @@ class XPathMatch(ORMPath):
|
|
|
994
1124
|
return self.child.build_lhs_part(compiler, self)
|
|
995
1125
|
|
|
996
1126
|
def build_rhs(self, compiler: CompiledQuery):
|
|
997
|
-
"""
|
|
1127
|
+
"""Give the ORM part when this element would be used as right-hand-side.
|
|
1128
|
+
For example: ``path1 == path2`` or ``value == path``.
|
|
1129
|
+
"""
|
|
998
1130
|
if self.is_many:
|
|
999
1131
|
compiler.add_distinct()
|
|
1000
1132
|
if self.orm_filters:
|
|
@@ -1004,4 +1136,4 @@ class XPathMatch(ORMPath):
|
|
|
1004
1136
|
|
|
1005
1137
|
if TYPE_CHECKING:
|
|
1006
1138
|
from .features import FeatureType
|
|
1007
|
-
from .parsers.
|
|
1139
|
+
from .parsers.query import CompiledQuery
|