django-gisserver 1.5.0__py3-none-any.whl → 2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/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
|
|
32
33
|
from decimal import Decimal as D
|
|
33
34
|
from enum import Enum
|
|
34
|
-
from functools import cached_property
|
|
35
|
-
from typing import TYPE_CHECKING, Literal
|
|
35
|
+
from functools import cached_property, reduce
|
|
36
|
+
from typing import TYPE_CHECKING, Literal
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
import django
|
|
38
39
|
from django.contrib.gis.db.models import F, GeometryField
|
|
40
|
+
from django.contrib.gis.geos import GEOSGeometry
|
|
39
41
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
40
42
|
from django.db import models
|
|
41
43
|
from django.db.models import Q
|
|
42
|
-
from django.db.models.fields.related import
|
|
43
|
-
ForeignObjectRel,
|
|
44
|
-
RelatedField,
|
|
45
|
-
)
|
|
44
|
+
from django.db.models.fields.related import RelatedField
|
|
46
45
|
from django.utils import dateparse
|
|
47
46
|
|
|
47
|
+
from gisserver.compat import ArrayField, GeneratedField
|
|
48
48
|
from gisserver.exceptions import ExternalParsingError, OperationProcessingFailed
|
|
49
|
-
from gisserver.geometries import CRS,
|
|
49
|
+
from gisserver.geometries import CRS, BoundingBox
|
|
50
|
+
from gisserver.parsers import values
|
|
51
|
+
from gisserver.parsers.xml import parse_qname, split_ns, xmlns
|
|
50
52
|
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
51
54
|
_unbounded = Literal["unbounded"]
|
|
52
55
|
|
|
53
|
-
if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
54
|
-
from django.contrib.postgres.fields import ArrayField
|
|
55
|
-
else:
|
|
56
|
-
ArrayField = None
|
|
57
|
-
|
|
58
56
|
__all__ = [
|
|
59
|
-
"
|
|
57
|
+
"GeometryXsdElement",
|
|
60
58
|
"GmlIdAttribute",
|
|
61
59
|
"GmlNameElement",
|
|
62
60
|
"ORMPath",
|
|
@@ -67,41 +65,33 @@ __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
|
|
@@ -117,78 +107,77 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
117
107
|
Based on https://www.w3.org/TR/xmlschema-2/#built-in-datatypes
|
|
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
|
-
|
|
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")
|
|
160
151
|
|
|
161
152
|
# Other typical GML values
|
|
162
|
-
gmlCodeType = "
|
|
163
|
-
gmlBoundingShapeType = "
|
|
153
|
+
gmlCodeType = xmlns.gml.qname("CodeType") # for <gml:name>
|
|
154
|
+
gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType") # for <gml:boundedBy>
|
|
164
155
|
|
|
165
156
|
#: A direct geometry value (used as function argument type)
|
|
166
|
-
gmlAbstractGeometryType = "
|
|
157
|
+
gmlAbstractGeometryType = xmlns.gml.qname("AbstractGeometryType")
|
|
167
158
|
|
|
168
159
|
#: A feature that has a gml:name and gml:boundedBy as possible child element.
|
|
169
|
-
gmlAbstractFeatureType = "
|
|
170
|
-
gmlAbstractGMLType = "
|
|
160
|
+
gmlAbstractFeatureType = xmlns.gml.qname("AbstractFeatureType")
|
|
161
|
+
gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType") # base of gml:AbstractFeatureType
|
|
171
162
|
|
|
172
163
|
def __str__(self):
|
|
173
164
|
return self.value
|
|
174
165
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return xml_name[:colon] if colon else None
|
|
166
|
+
def __init__(self, value):
|
|
167
|
+
# Parse XML namespace data once, which to_qname() uses.
|
|
168
|
+
# Can't set enum.name, so will use a property for that.
|
|
169
|
+
self.namespace, self._localname = split_ns(value)
|
|
170
|
+
self.is_geometry = False # redefined below
|
|
181
171
|
|
|
182
172
|
@cached_property
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
185
|
-
|
|
173
|
+
def name(self) -> str:
|
|
174
|
+
"""Overwrites enum.name to return the XML local name.
|
|
175
|
+
This is used for to_qname().
|
|
176
|
+
"""
|
|
177
|
+
return self._localname
|
|
186
178
|
|
|
187
179
|
@cached_property
|
|
188
180
|
def _to_python_func(self):
|
|
189
|
-
if not TYPES_TO_PYTHON:
|
|
190
|
-
_init_types_to_python()
|
|
191
|
-
|
|
192
181
|
try:
|
|
193
182
|
return TYPES_TO_PYTHON[self]
|
|
194
183
|
except KeyError:
|
|
@@ -206,40 +195,53 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
206
195
|
raise # subclass of ValueError so explicitly caught and reraised
|
|
207
196
|
except (TypeError, ValueError, ArithmeticError) as e:
|
|
208
197
|
# 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
|
-
|
|
198
|
+
name = self.name if self.namespace == xmlns.xsd.value else self.value
|
|
199
|
+
raise ExternalParsingError(f"Can't cast '{raw_value}' to {name}.") from e
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
for _type in (
|
|
203
|
+
XsdTypes.gmlGeometryPropertyType,
|
|
204
|
+
XsdTypes.gmlPointPropertyType,
|
|
205
|
+
XsdTypes.gmlCurvePropertyType,
|
|
206
|
+
XsdTypes.gmlSurfacePropertyType,
|
|
207
|
+
XsdTypes.gmlMultiSurfacePropertyType,
|
|
208
|
+
XsdTypes.gmlMultiPointPropertyType,
|
|
209
|
+
XsdTypes.gmlMultiCurvePropertyType,
|
|
210
|
+
XsdTypes.gmlMultiGeometryPropertyType,
|
|
211
|
+
# gml:boundedBy is technically a geometry, which we don't support in queries currently.
|
|
212
|
+
XsdTypes.gmlBoundingShapeType,
|
|
213
|
+
):
|
|
214
|
+
# One of the reasons the code checks for "xsd_element.type.is_geometry"
|
|
215
|
+
# is because profiling showed that isinstance(xsd_element, ...) is really slow.
|
|
216
|
+
# When rendering 5000 objects with 10+ elements, isinstance() started showing up as hotspot.
|
|
217
|
+
_type.is_geometry = True
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _as_is(v):
|
|
221
|
+
return v
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
TYPES_TO_PYTHON = {
|
|
225
|
+
XsdTypes.date: dateparse.parse_date,
|
|
226
|
+
XsdTypes.dateTime: values.parse_iso_datetime,
|
|
227
|
+
XsdTypes.time: dateparse.parse_time,
|
|
228
|
+
XsdTypes.string: _as_is,
|
|
229
|
+
XsdTypes.boolean: values.parse_bool,
|
|
230
|
+
XsdTypes.integer: int,
|
|
231
|
+
XsdTypes.int: int,
|
|
232
|
+
XsdTypes.long: int,
|
|
233
|
+
XsdTypes.short: int,
|
|
234
|
+
XsdTypes.byte: int,
|
|
235
|
+
XsdTypes.unsignedInt: int,
|
|
236
|
+
XsdTypes.unsignedLong: int,
|
|
237
|
+
XsdTypes.unsignedShort: int,
|
|
238
|
+
XsdTypes.unsignedByte: int,
|
|
239
|
+
XsdTypes.float: D,
|
|
240
|
+
XsdTypes.double: D,
|
|
241
|
+
XsdTypes.decimal: D,
|
|
242
|
+
XsdTypes.gmlCodeType: _as_is,
|
|
243
|
+
XsdTypes.anyType: values.auto_cast,
|
|
244
|
+
}
|
|
243
245
|
|
|
244
246
|
|
|
245
247
|
class XsdNode:
|
|
@@ -255,11 +257,13 @@ class XsdNode:
|
|
|
255
257
|
|
|
256
258
|
name: str
|
|
257
259
|
type: XsdAnyType # Both XsdComplexType and XsdType are allowed
|
|
258
|
-
|
|
260
|
+
|
|
261
|
+
#: XML Namespace of the element
|
|
262
|
+
namespace: xmlns | str | None
|
|
259
263
|
|
|
260
264
|
#: Which field to read from the model to get the value
|
|
261
265
|
#: This supports dot notation to access related attributes.
|
|
262
|
-
source: models.Field | ForeignObjectRel | None
|
|
266
|
+
source: models.Field | models.ForeignObjectRel | None
|
|
263
267
|
|
|
264
268
|
#: Which field to read from the model to get the value
|
|
265
269
|
#: This supports dot notation to access related attributes.
|
|
@@ -273,24 +277,48 @@ class XsdNode:
|
|
|
273
277
|
self,
|
|
274
278
|
name: str,
|
|
275
279
|
type: XsdAnyType,
|
|
280
|
+
namespace: xmlns | str | None,
|
|
276
281
|
*,
|
|
277
|
-
|
|
278
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
282
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
279
283
|
model_attribute: str | None = None,
|
|
284
|
+
absolute_model_attribute: str | None = None,
|
|
280
285
|
feature_type: FeatureType | None = None,
|
|
281
286
|
):
|
|
287
|
+
"""
|
|
288
|
+
:param name: The local name of the element.
|
|
289
|
+
:param type: The XML Schema type of the element, can also be a XsdComplexType.
|
|
290
|
+
:param namespace: XML namespace URI.
|
|
291
|
+
:param source: Original Model field, which can provide more metadata/parsing.
|
|
292
|
+
:param model_attribute: The Django model path that this element accesses.
|
|
293
|
+
:param absolute_model_attribute: The full path, including parent elements.
|
|
294
|
+
:param feature_type: Typically assigned in :meth:`bind`, needed by some :meth:`get_value` functions.
|
|
295
|
+
"""
|
|
296
|
+
if ":" in name:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
"XsdNode should receive the localname, not the QName in ns:localname format."
|
|
299
|
+
)
|
|
300
|
+
elif "}" in name:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"XsdNode should receive the localname, not the full name in {uri}name format."
|
|
303
|
+
)
|
|
304
|
+
|
|
282
305
|
# Using plain assignment instead of dataclass turns out to be needed
|
|
283
306
|
# for flexibility and easier subclassing.
|
|
284
307
|
self.name = name
|
|
285
308
|
self.type = type
|
|
286
|
-
self.
|
|
309
|
+
self.namespace = str(namespace) if namespace is not None else None # cast enum members.
|
|
287
310
|
self.source = source
|
|
288
311
|
self.model_attribute = model_attribute or self.name
|
|
289
|
-
|
|
312
|
+
self.absolute_model_attribute = absolute_model_attribute or self.model_attribute
|
|
313
|
+
# link back to top-level parent, some get_value() functions need it.
|
|
290
314
|
self.feature_type = feature_type
|
|
291
315
|
|
|
292
|
-
if
|
|
293
|
-
|
|
316
|
+
if (
|
|
317
|
+
self.model_attribute
|
|
318
|
+
and self.absolute_model_attribute
|
|
319
|
+
and not self.absolute_model_attribute.endswith(self.model_attribute)
|
|
320
|
+
):
|
|
321
|
+
raise ValueError("Inconsistent 'absolute_model_attribute' and 'model_attribute' value")
|
|
294
322
|
|
|
295
323
|
self._attrgetter = operator.attrgetter(self.model_attribute)
|
|
296
324
|
self._valuegetter = self._build_valuegetter(self.model_attribute, self.source)
|
|
@@ -304,7 +332,7 @@ class XsdNode:
|
|
|
304
332
|
@staticmethod
|
|
305
333
|
def _build_valuegetter(
|
|
306
334
|
model_attribute: str,
|
|
307
|
-
field: models.Field | ForeignObjectRel | None,
|
|
335
|
+
field: models.Field | models.ForeignObjectRel | None,
|
|
308
336
|
):
|
|
309
337
|
"""Select the most efficient read function to retrieves the value.
|
|
310
338
|
|
|
@@ -314,7 +342,7 @@ class XsdNode:
|
|
|
314
342
|
since this will be much faster than using ``getattr()``.
|
|
315
343
|
The custom ``value_from_object()`` is fully supported too.
|
|
316
344
|
"""
|
|
317
|
-
if field is None or isinstance(field, ForeignObjectRel):
|
|
345
|
+
if field is None or isinstance(field, models.ForeignObjectRel):
|
|
318
346
|
# No model field, can only use getattr(). The attrgetter() function is both faster,
|
|
319
347
|
# and has built-in support for traversing model attributes with dots.
|
|
320
348
|
return operator.attrgetter(model_attribute)
|
|
@@ -341,11 +369,6 @@ class XsdNode:
|
|
|
341
369
|
|
|
342
370
|
return _related_get_value_from_object
|
|
343
371
|
|
|
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
372
|
@cached_property
|
|
350
373
|
def is_array(self) -> bool:
|
|
351
374
|
"""Tell whether this node is backed by an PostgreSQL Array Field."""
|
|
@@ -359,33 +382,48 @@ class XsdNode:
|
|
|
359
382
|
@cached_property
|
|
360
383
|
def xml_name(self):
|
|
361
384
|
"""The XML element/attribute name."""
|
|
362
|
-
return f"{self.
|
|
385
|
+
return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
|
|
386
|
+
|
|
387
|
+
def relative_orm_path(self, parent: XsdElement | None = None) -> str:
|
|
388
|
+
"""The ORM field lookup to perform, relative to the parent element."""
|
|
389
|
+
if parent is None:
|
|
390
|
+
return self.orm_path
|
|
391
|
+
|
|
392
|
+
prefix = f"{parent.orm_path}__"
|
|
393
|
+
if not self.orm_path.startswith(prefix):
|
|
394
|
+
raise ValueError(f"Node '{self}' is not a child of '{parent}.")
|
|
395
|
+
else:
|
|
396
|
+
return self.orm_path[len(prefix) :]
|
|
363
397
|
|
|
364
398
|
@cached_property
|
|
365
|
-
def
|
|
399
|
+
def local_orm_path(self) -> str:
|
|
366
400
|
"""The ORM field lookup to perform."""
|
|
367
401
|
if self.model_attribute is None:
|
|
368
402
|
raise ValueError(f"Node {self.xml_name} has no 'model_attribute' set.")
|
|
369
403
|
return self.model_attribute.replace(".", "__")
|
|
370
404
|
|
|
405
|
+
@cached_property
|
|
406
|
+
def orm_path(self) -> str:
|
|
407
|
+
"""The ORM field lookup to perform."""
|
|
408
|
+
if self.absolute_model_attribute is None:
|
|
409
|
+
raise ValueError(f"Node {self.xml_name} has no 'absolute_model_attribute' set.")
|
|
410
|
+
return self.absolute_model_attribute.replace(".", "__")
|
|
411
|
+
|
|
371
412
|
@cached_property
|
|
372
413
|
def orm_field(self) -> str:
|
|
373
|
-
"""The direct ORM field that provides this property.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return self.
|
|
414
|
+
"""The direct ORM field that provides this property; the first relative level.
|
|
415
|
+
Typically, this is the same as the field name.
|
|
416
|
+
"""
|
|
417
|
+
return self.orm_path.partition(".")[0]
|
|
377
418
|
|
|
378
419
|
@cached_property
|
|
379
420
|
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
|
|
421
|
+
"""The ORM field and parent relation.
|
|
422
|
+
Note this isn't something like "self.parent.orm_path",
|
|
423
|
+
as this mode may have a dotted-path to its source attribute.
|
|
424
|
+
"""
|
|
425
|
+
path, _, field = self.orm_path.rpartition("__")
|
|
426
|
+
return path or None, field
|
|
389
427
|
|
|
390
428
|
def build_lhs_part(self, compiler: CompiledQuery, match: ORMPath):
|
|
391
429
|
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
@@ -427,11 +465,6 @@ class XsdNode:
|
|
|
427
465
|
"""
|
|
428
466
|
return value
|
|
429
467
|
|
|
430
|
-
@cached_property
|
|
431
|
-
def _form_field(self):
|
|
432
|
-
"""Internal cached field for to_python()"""
|
|
433
|
-
return self.source.formfield()
|
|
434
|
-
|
|
435
468
|
def to_python(self, raw_value: str):
|
|
436
469
|
"""Convert a raw value to the Python data type for this element type."""
|
|
437
470
|
try:
|
|
@@ -454,6 +487,11 @@ class XsdNode:
|
|
|
454
487
|
|
|
455
488
|
The raw string value can be passed here. Auto-cased values could
|
|
456
489
|
raise an TypeError due to being unsupported by the validation.
|
|
490
|
+
|
|
491
|
+
:param raw_value: The string value taken from the XML node.
|
|
492
|
+
:param lookup: The ORM lookup (e.g. ``equals`` or ``fes_like``).
|
|
493
|
+
:param tag: The filter operator tag name, e.g. ``PropertyIsEqualTo``.
|
|
494
|
+
:returns: The parsed Python value.
|
|
457
495
|
"""
|
|
458
496
|
if self.source is not None:
|
|
459
497
|
# Not calling self.source.validate() as that checks for allowed choices,
|
|
@@ -467,6 +505,13 @@ class XsdNode:
|
|
|
467
505
|
isinstance(self.source, RelatedField)
|
|
468
506
|
and self.source.target_field.get_lookup(lookup) is None
|
|
469
507
|
):
|
|
508
|
+
logger.debug(
|
|
509
|
+
"Model field '%s.%s' does not support ORM lookup '%s' used by '%s'.",
|
|
510
|
+
self.feature_type.model._meta.model_name,
|
|
511
|
+
self.absolute_model_attribute,
|
|
512
|
+
lookup,
|
|
513
|
+
tag,
|
|
514
|
+
)
|
|
470
515
|
raise OperationProcessingFailed(
|
|
471
516
|
f"Operator '{tag}' is not supported for the '{self.name}' property.",
|
|
472
517
|
locator="filter",
|
|
@@ -501,43 +546,29 @@ class XsdElement(XsdNode):
|
|
|
501
546
|
self,
|
|
502
547
|
name: str,
|
|
503
548
|
type: XsdAnyType,
|
|
549
|
+
namespace: xmlns | str | None,
|
|
504
550
|
*,
|
|
505
|
-
prefix: str | None = "app",
|
|
506
551
|
nillable: bool | None = None,
|
|
507
552
|
min_occurs: int | None = None,
|
|
508
553
|
max_occurs: int | _unbounded | None = None,
|
|
509
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
554
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
510
555
|
model_attribute: str | None = None,
|
|
556
|
+
absolute_model_attribute: str | None = None,
|
|
511
557
|
feature_type: FeatureType | None = None,
|
|
512
558
|
):
|
|
513
559
|
super().__init__(
|
|
514
560
|
name,
|
|
515
561
|
type,
|
|
516
|
-
|
|
562
|
+
namespace=namespace,
|
|
517
563
|
source=source,
|
|
518
564
|
model_attribute=model_attribute,
|
|
565
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
519
566
|
feature_type=feature_type,
|
|
520
567
|
)
|
|
521
568
|
self.nillable = nillable
|
|
522
569
|
self.min_occurs = min_occurs
|
|
523
570
|
self.max_occurs = max_occurs
|
|
524
571
|
|
|
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
572
|
@cached_property
|
|
542
573
|
def is_many(self) -> bool:
|
|
543
574
|
"""Tell whether the XML element can be rendered multiple times.
|
|
@@ -577,30 +608,56 @@ class XsdAttribute(XsdNode):
|
|
|
577
608
|
name: str,
|
|
578
609
|
type: XsdAnyType = XsdTypes.string, # added default
|
|
579
610
|
*,
|
|
580
|
-
|
|
611
|
+
namespace: xmlns | str | None = None,
|
|
581
612
|
use: str = "optional",
|
|
582
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
613
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
583
614
|
model_attribute: str | None = None,
|
|
615
|
+
absolute_model_attribute: str | None = None,
|
|
584
616
|
feature_type: FeatureType | None = None,
|
|
585
617
|
):
|
|
586
618
|
super().__init__(
|
|
587
619
|
name,
|
|
588
620
|
type,
|
|
589
|
-
|
|
621
|
+
namespace=namespace,
|
|
590
622
|
source=source,
|
|
591
623
|
model_attribute=model_attribute,
|
|
624
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
592
625
|
feature_type=feature_type,
|
|
593
626
|
)
|
|
594
627
|
self.use = use
|
|
595
628
|
|
|
596
629
|
|
|
597
|
-
class
|
|
598
|
-
"""
|
|
630
|
+
class GeometryXsdElement(XsdElement):
|
|
631
|
+
"""A subtype for the :class:`XsdElement` that provides access to geometry data.
|
|
599
632
|
|
|
600
|
-
This
|
|
633
|
+
This declares an element such as::
|
|
634
|
+
|
|
635
|
+
<app:geometry>
|
|
636
|
+
<gml:Point>...</gml:Point>
|
|
637
|
+
</app:geometry>
|
|
638
|
+
|
|
639
|
+
Hence, the :attr:`namespace` of this element isn't the GML namespace,
|
|
640
|
+
only the type it points to is geometry data.
|
|
641
|
+
|
|
642
|
+
The :attr:`source` is guaranteed to point to a :class:`~django.contrib.gis.models.GeometryField`,
|
|
643
|
+
and can be a :class:`~django.db.models.GeneratedField` in Django 5
|
|
644
|
+
as long as its ``output_field`` points to a :class:`~django.contrib.gis.models.GeometryField`.
|
|
601
645
|
"""
|
|
602
646
|
|
|
603
|
-
|
|
647
|
+
if django.VERSION >= (5, 0):
|
|
648
|
+
source: GeometryField | models.GeneratedField
|
|
649
|
+
else:
|
|
650
|
+
source: GeometryField
|
|
651
|
+
|
|
652
|
+
@cached_property
|
|
653
|
+
def source_srid(self) -> int:
|
|
654
|
+
"""Tell which Spatial Reference Identifier the source information is stored under."""
|
|
655
|
+
if GeneratedField is not None and isinstance(self.source, GeneratedField):
|
|
656
|
+
# Allow GeometryField to be wrapped as:
|
|
657
|
+
# models.GeneratedField(SomeFunction("geofield"), output_field=models.GeometryField())
|
|
658
|
+
return self.source.output_field.srid
|
|
659
|
+
else:
|
|
660
|
+
return self.source.srid
|
|
604
661
|
|
|
605
662
|
|
|
606
663
|
class GmlIdAttribute(XsdAttribute):
|
|
@@ -613,15 +670,17 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
613
670
|
def __init__(
|
|
614
671
|
self,
|
|
615
672
|
type_name: str,
|
|
616
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
673
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
617
674
|
model_attribute="pk",
|
|
675
|
+
absolute_model_attribute=None,
|
|
618
676
|
feature_type: FeatureType | None = None,
|
|
619
677
|
):
|
|
620
678
|
super().__init__(
|
|
621
|
-
prefix="gml",
|
|
622
679
|
name="id",
|
|
680
|
+
namespace=xmlns.gml,
|
|
623
681
|
source=source,
|
|
624
682
|
model_attribute=model_attribute,
|
|
683
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
625
684
|
feature_type=feature_type,
|
|
626
685
|
)
|
|
627
686
|
object.__setattr__(self, "type_name", type_name)
|
|
@@ -644,19 +703,17 @@ class GmlNameElement(XsdElement):
|
|
|
644
703
|
(although that would make comparisons on ``element@gml:name`` more complex).
|
|
645
704
|
"""
|
|
646
705
|
|
|
647
|
-
is_geometry = False # Override type
|
|
648
|
-
|
|
649
706
|
def __init__(
|
|
650
707
|
self,
|
|
651
708
|
model_attribute: str,
|
|
652
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
709
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
653
710
|
feature_type=None,
|
|
654
711
|
):
|
|
655
712
|
# Prefill most known fields
|
|
656
713
|
super().__init__(
|
|
657
|
-
prefix="gml",
|
|
658
714
|
name="name",
|
|
659
715
|
type=XsdTypes.gmlCodeType,
|
|
716
|
+
namespace=xmlns.gml,
|
|
660
717
|
min_occurs=0,
|
|
661
718
|
source=source,
|
|
662
719
|
model_attribute=model_attribute,
|
|
@@ -681,14 +738,12 @@ class GmlBoundedByElement(XsdElement):
|
|
|
681
738
|
Its value is the complete bounding box of the feature type data.
|
|
682
739
|
"""
|
|
683
740
|
|
|
684
|
-
is_geometry = True # Override type
|
|
685
|
-
|
|
686
741
|
def __init__(self, feature_type):
|
|
687
742
|
# Prefill most known fields
|
|
688
743
|
super().__init__(
|
|
689
|
-
prefix="gml",
|
|
690
744
|
name="boundedBy",
|
|
691
745
|
type=XsdTypes.gmlBoundingShapeType,
|
|
746
|
+
namespace=xmlns.gml,
|
|
692
747
|
min_occurs=0,
|
|
693
748
|
feature_type=feature_type,
|
|
694
749
|
)
|
|
@@ -703,10 +758,37 @@ class GmlBoundedByElement(XsdElement):
|
|
|
703
758
|
"""Give the ORM part when this element would be used as right-hand-side"""
|
|
704
759
|
raise NotImplementedError("queries against <gml:boundedBy> are not supported")
|
|
705
760
|
|
|
706
|
-
def get_value(self, instance: models.Model, crs: CRS | None = None):
|
|
707
|
-
"""Provide the value of the <gml:boundedBy> field
|
|
708
|
-
|
|
709
|
-
|
|
761
|
+
def get_value(self, instance: models.Model, crs: CRS | None = None) -> BoundingBox | None:
|
|
762
|
+
"""Provide the value of the <gml:boundedBy> field,
|
|
763
|
+
which is the bounding box for a single instance.
|
|
764
|
+
|
|
765
|
+
This is only used for native Python rendering. When the database
|
|
766
|
+
rendering is enabled (GISSERVER_USE_DB_RENDERING=True), the calculation
|
|
767
|
+
is entirely performed within the query.
|
|
768
|
+
"""
|
|
769
|
+
geometries: list[GEOSGeometry] = list(
|
|
770
|
+
# remove 'None' values
|
|
771
|
+
filter(
|
|
772
|
+
None,
|
|
773
|
+
[
|
|
774
|
+
# support dotted paths here for geometries in a foreign key relation.
|
|
775
|
+
operator.attrgetter(geo_element.absolute_model_attribute)(instance)
|
|
776
|
+
for geo_element in self.feature_type.all_geometry_elements
|
|
777
|
+
],
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
if not geometries:
|
|
781
|
+
return None
|
|
782
|
+
|
|
783
|
+
# Perform the combining of geometries inside libgeos
|
|
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)
|
|
710
792
|
|
|
711
793
|
|
|
712
794
|
@dataclass(frozen=True)
|
|
@@ -732,41 +814,51 @@ class XsdComplexType(XsdAnyType):
|
|
|
732
814
|
which allows child elements like <gml:name> and <gml:boundedBy>.
|
|
733
815
|
"""
|
|
734
816
|
|
|
735
|
-
#: Internal class name (without XML prefix)
|
|
817
|
+
#: Internal class name (without XML namespace/prefix)
|
|
736
818
|
name: str
|
|
737
819
|
|
|
738
|
-
#:
|
|
739
|
-
|
|
820
|
+
#: The XML namespace
|
|
821
|
+
namespace: str | None
|
|
822
|
+
|
|
823
|
+
#: All local elements in this class
|
|
824
|
+
elements: list[XsdElement] = field(default_factory=list)
|
|
740
825
|
|
|
741
826
|
#: All attributes in this class
|
|
742
827
|
attributes: list[XsdAttribute] = field(default_factory=list)
|
|
743
828
|
|
|
744
829
|
#: The base class of this type. Typically gml:AbstractFeatureType,
|
|
745
830
|
#: 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"
|
|
831
|
+
base: XsdAnyType | None = None
|
|
750
832
|
|
|
751
833
|
#: The Django model class that this type was based on.
|
|
752
834
|
source: type[models.Model] | None = None
|
|
753
835
|
|
|
836
|
+
def __post_init__(self):
|
|
837
|
+
# Autodetect (or autocorrect) to have the proper base class when gml elements are present.
|
|
838
|
+
if self.base is None:
|
|
839
|
+
if any(e.type.is_geometry for e in self.elements):
|
|
840
|
+
# for <gml:name> and <gml:boundedBy> elements.
|
|
841
|
+
self.__dict__["base"] = XsdTypes.gmlAbstractFeatureType
|
|
842
|
+
elif any(e.type is XsdTypes.gmlCodeType for e in self.elements):
|
|
843
|
+
# for <gml:name> only
|
|
844
|
+
self.__dict__["base"] = XsdTypes.gmlAbstractGMLType
|
|
845
|
+
|
|
754
846
|
def __str__(self):
|
|
755
847
|
return self.xml_name
|
|
756
848
|
|
|
757
849
|
@cached_property
|
|
758
850
|
def xml_name(self):
|
|
759
|
-
"""Name in the XMLSchema (e.g.
|
|
760
|
-
return f"{self.
|
|
851
|
+
"""Name in the XMLSchema (e.g. {http://example.org/namespace}:SomeClass)."""
|
|
852
|
+
return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
|
|
761
853
|
|
|
762
854
|
@property
|
|
763
855
|
def is_complex_type(self):
|
|
764
856
|
return True # a property to avoid being used as field.
|
|
765
857
|
|
|
766
858
|
@cached_property
|
|
767
|
-
def
|
|
768
|
-
"""
|
|
769
|
-
if self.base.is_complex_type:
|
|
859
|
+
def elements_including_base(self) -> list[XsdElement]:
|
|
860
|
+
"""The local and inherited elements of this XSD type."""
|
|
861
|
+
if self.base is not None and self.base.is_complex_type:
|
|
770
862
|
# Add all base class members, in their correct ordering
|
|
771
863
|
# By having these as XsdElement objects instead of hard-coded writes,
|
|
772
864
|
# the query/filter logic also works for these elements.
|
|
@@ -775,35 +867,35 @@ class XsdComplexType(XsdAnyType):
|
|
|
775
867
|
return self.elements
|
|
776
868
|
|
|
777
869
|
@cached_property
|
|
778
|
-
def geometry_elements(self) -> list[
|
|
870
|
+
def geometry_elements(self) -> list[GeometryXsdElement]:
|
|
779
871
|
"""Shortcut to get all geometry elements"""
|
|
780
|
-
return [e for e in self.elements if e.is_geometry]
|
|
872
|
+
return [e for e in self.elements if e.type.is_geometry]
|
|
781
873
|
|
|
782
874
|
@cached_property
|
|
783
875
|
def complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
784
|
-
"""Shortcut to get all elements with a complex type
|
|
876
|
+
"""Shortcut to get all elements with a complex type.
|
|
877
|
+
To get all complex elements recursively, read :attr:`all_complex_elements`.
|
|
878
|
+
"""
|
|
785
879
|
return [e for e in self.elements if e.type.is_complex_type]
|
|
786
880
|
|
|
787
881
|
@cached_property
|
|
788
882
|
def flattened_elements(self) -> list[XsdElement]:
|
|
789
|
-
"""Shortcut to get all elements with a flattened model
|
|
883
|
+
"""Shortcut to get all elements with a flattened model attribute"""
|
|
790
884
|
return [e for e in self.elements if e.is_flattened]
|
|
791
885
|
|
|
792
886
|
@cached_property
|
|
793
|
-
def
|
|
887
|
+
def all_complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
794
888
|
"""Shortcut to get all elements with children.
|
|
795
889
|
This mainly exists to provide a structure to mimic what's used
|
|
796
890
|
when PROPERTYNAME is part of the request.
|
|
797
891
|
"""
|
|
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)
|
|
892
|
+
child_nodes = []
|
|
893
|
+
for xsd_element in self.complex_elements:
|
|
894
|
+
child_nodes.append(xsd_element)
|
|
895
|
+
child_nodes.extend(xsd_element.type.all_complex_elements)
|
|
804
896
|
return child_nodes
|
|
805
897
|
|
|
806
|
-
def resolve_element_path(self, xpath: str) -> list[XsdNode] | None:
|
|
898
|
+
def resolve_element_path(self, xpath: str, ns_aliases: dict[str, str]) -> list[XsdNode] | None:
|
|
807
899
|
"""Resolve a xpath reference to the actual node.
|
|
808
900
|
This returns the whole path, including in-between relations, if a match was found.
|
|
809
901
|
|
|
@@ -811,7 +903,7 @@ class XsdComplexType(XsdAnyType):
|
|
|
811
903
|
to convert a request XPath element into the ORM attributes for database queries.
|
|
812
904
|
"""
|
|
813
905
|
try:
|
|
814
|
-
pos = xpath.
|
|
906
|
+
pos = xpath.index("/")
|
|
815
907
|
node_name = xpath[:pos]
|
|
816
908
|
except ValueError:
|
|
817
909
|
node_name = xpath
|
|
@@ -825,12 +917,11 @@ class XsdComplexType(XsdAnyType):
|
|
|
825
917
|
if pos:
|
|
826
918
|
return None # invalid attribute
|
|
827
919
|
|
|
828
|
-
# Remove app: prefixes, or any alias of it (see explanation below)
|
|
829
920
|
xml_name = node_name[1:]
|
|
830
|
-
attribute = self._find_attribute(xml_name=xml_name)
|
|
921
|
+
attribute = self._find_attribute(xml_name=parse_qname(xml_name, ns_aliases))
|
|
831
922
|
return [attribute] if attribute is not None else None
|
|
832
923
|
else:
|
|
833
|
-
element = self._find_element(node_name)
|
|
924
|
+
element = self._find_element(xml_name=parse_qname(node_name, ns_aliases))
|
|
834
925
|
if element is None:
|
|
835
926
|
return None
|
|
836
927
|
|
|
@@ -839,62 +930,34 @@ class XsdComplexType(XsdAnyType):
|
|
|
839
930
|
return None
|
|
840
931
|
else:
|
|
841
932
|
# Recurse into the child node to find the next part
|
|
842
|
-
child_path = element.type.resolve_element_path(xpath[pos + 1 :])
|
|
933
|
+
child_path = element.type.resolve_element_path(xpath[pos + 1 :], ns_aliases)
|
|
843
934
|
return [element] + child_path if child_path is not None else None
|
|
844
935
|
else:
|
|
845
936
|
return [element]
|
|
846
937
|
|
|
847
|
-
def _find_element(self, xml_name) -> XsdElement | None:
|
|
938
|
+
def _find_element(self, xml_name: str) -> XsdElement | None:
|
|
848
939
|
"""Locate an element by name"""
|
|
849
940
|
for element in self.elements:
|
|
850
941
|
if element.xml_name == xml_name:
|
|
851
942
|
return element
|
|
852
943
|
|
|
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
944
|
# When there is a base class, resolve elements there too.
|
|
865
|
-
if self.base.is_complex_type:
|
|
945
|
+
if self.base is not None and self.base.is_complex_type:
|
|
866
946
|
return self.base._find_element(xml_name)
|
|
867
947
|
return None
|
|
868
948
|
|
|
869
|
-
def _find_attribute(self, xml_name) -> XsdAttribute | None:
|
|
949
|
+
def _find_attribute(self, xml_name: str) -> XsdAttribute | None:
|
|
870
950
|
"""Locate an attribute by name"""
|
|
871
951
|
for attribute in self.attributes:
|
|
872
952
|
if attribute.xml_name == xml_name:
|
|
873
953
|
return attribute
|
|
874
954
|
|
|
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
955
|
# When there is a base class, resolve attributes there too.
|
|
884
|
-
if self.base.is_complex_type:
|
|
956
|
+
if self.base is not None and self.base.is_complex_type:
|
|
885
957
|
return self.base._find_attribute(xml_name)
|
|
886
958
|
return None
|
|
887
959
|
|
|
888
960
|
|
|
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
961
|
class ORMPath:
|
|
899
962
|
"""Base class to provide raw XPath results.
|
|
900
963
|
|
|
@@ -961,10 +1024,10 @@ class XPathMatch(ORMPath):
|
|
|
961
1024
|
# the build_...() logic should return a Q() object.
|
|
962
1025
|
raise NotImplementedError(f"Complex XPath queries are not supported yet: {self.query}")
|
|
963
1026
|
|
|
964
|
-
@
|
|
1027
|
+
@property
|
|
965
1028
|
def orm_path(self) -> str:
|
|
966
1029
|
"""Give the Django ORM path (field__relation__relation2) to the result."""
|
|
967
|
-
return
|
|
1030
|
+
return self.nodes[-1].orm_path
|
|
968
1031
|
|
|
969
1032
|
def __iter__(self):
|
|
970
1033
|
return iter(self.nodes)
|
|
@@ -1004,4 +1067,4 @@ class XPathMatch(ORMPath):
|
|
|
1004
1067
|
|
|
1005
1068
|
if TYPE_CHECKING:
|
|
1006
1069
|
from .features import FeatureType
|
|
1007
|
-
from .parsers.
|
|
1070
|
+
from .parsers.query import CompiledQuery
|