django-gisserver 1.4.1__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.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.4.1.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 +63 -60
- gisserver/exceptions.py +47 -9
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +267 -240
- gisserver/geometries.py +34 -39
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +129 -305
- gisserver/operations/wfs20.py +428 -336
- gisserver/output/__init__.py +10 -48
- gisserver/output/base.py +198 -143
- gisserver/output/csv.py +81 -85
- gisserver/output/geojson.py +63 -72
- gisserver/output/gml32.py +310 -281
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +71 -30
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -154
- gisserver/output/xmlschema.py +86 -47
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +15 -11
- gisserver/parsers/fes20/expressions.py +89 -50
- 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 +336 -128
- gisserver/parsers/fes20/sorting.py +107 -34
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +6 -3
- 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 +11 -11
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +375 -258
- gisserver/views.py +206 -75
- django_gisserver-1.4.1.dist-info/RECORD +0 -53
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -275
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -34
- gisserver/queries/adhoc.py +0 -181
- gisserver/queries/base.py +0 -146
- gisserver/queries/stored.py +0 -205
- 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.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/types.py
CHANGED
|
@@ -26,41 +26,37 @@ 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
|
|
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
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_unbounded = Literal["unbounded"]
|
|
55
|
-
except ImportError:
|
|
56
|
-
_unbounded = str
|
|
57
|
-
|
|
58
|
-
if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
59
|
-
from django.contrib.postgres.fields import ArrayField
|
|
60
|
-
else:
|
|
61
|
-
ArrayField = None
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
_unbounded = Literal["unbounded"]
|
|
62
55
|
|
|
63
56
|
__all__ = [
|
|
57
|
+
"GeometryXsdElement",
|
|
58
|
+
"GmlIdAttribute",
|
|
59
|
+
"GmlNameElement",
|
|
64
60
|
"ORMPath",
|
|
65
61
|
"XPathMatch",
|
|
66
62
|
"XsdAnyType",
|
|
@@ -69,41 +65,33 @@ __all__ = [
|
|
|
69
65
|
"XsdElement",
|
|
70
66
|
"XsdNode",
|
|
71
67
|
"XsdTypes",
|
|
72
|
-
"split_xml_name",
|
|
73
|
-
"FES20",
|
|
74
|
-
"GML21",
|
|
75
|
-
"GML32",
|
|
76
|
-
"XSI",
|
|
77
68
|
]
|
|
78
69
|
|
|
79
|
-
GML21 = "http://www.opengis.net/gml"
|
|
80
|
-
GML32 = "http://www.opengis.net/gml/3.2"
|
|
81
|
-
XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
|
82
|
-
FES20 = "http://www.opengis.net/fes/2.0"
|
|
83
|
-
|
|
84
70
|
RE_XPATH_ATTR = re.compile(r"\[[^\]]+\]$") # match [@attr=..]
|
|
85
|
-
TYPES_TO_PYTHON = {}
|
|
86
71
|
|
|
87
72
|
|
|
88
73
|
class XsdAnyType:
|
|
89
|
-
"""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
|
+
"""
|
|
90
78
|
|
|
79
|
+
#: Local name of the XML element
|
|
91
80
|
name: str
|
|
92
|
-
|
|
81
|
+
|
|
82
|
+
#: Namespace of the XML element
|
|
83
|
+
namespace = None
|
|
84
|
+
|
|
85
|
+
#: Whether this is a complex type
|
|
93
86
|
is_complex_type = False
|
|
94
|
-
|
|
87
|
+
|
|
88
|
+
#: Whether this is a geometry
|
|
89
|
+
is_geometry = False # Overwritten for some gml types.
|
|
95
90
|
|
|
96
91
|
def __str__(self):
|
|
97
|
-
"""Return the type name"""
|
|
92
|
+
"""Return the type name (in full XML format)"""
|
|
98
93
|
raise NotImplementedError()
|
|
99
94
|
|
|
100
|
-
def with_prefix(self, prefix="xs"):
|
|
101
|
-
xml_name = str(self)
|
|
102
|
-
if ":" in xml_name:
|
|
103
|
-
return xml_name
|
|
104
|
-
else:
|
|
105
|
-
return f"{prefix}:{xml_name}"
|
|
106
|
-
|
|
107
95
|
def to_python(self, raw_value):
|
|
108
96
|
"""Convert a raw string value to this type representation"""
|
|
109
97
|
return raw_value
|
|
@@ -119,78 +107,77 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
119
107
|
Based on https://www.w3.org/TR/xmlschema-2/#built-in-datatypes
|
|
120
108
|
"""
|
|
121
109
|
|
|
122
|
-
anyType = "anyType" #
|
|
123
|
-
string = "string"
|
|
124
|
-
boolean = "boolean"
|
|
125
|
-
decimal = "decimal" # the base type for all numbers too.
|
|
126
|
-
integer = "integer" # integer value
|
|
127
|
-
float = "float"
|
|
128
|
-
double = "double"
|
|
129
|
-
time = "time"
|
|
130
|
-
date = "date"
|
|
131
|
-
dateTime = "dateTime"
|
|
132
|
-
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")
|
|
133
121
|
|
|
134
122
|
# Number variations
|
|
135
|
-
byte = "byte" # signed 8-bit integer
|
|
136
|
-
short = "short" # signed 16-bit integer
|
|
137
|
-
int = "int" # signed 32-bit integer
|
|
138
|
-
long = "long" # signed 64-bit integer
|
|
139
|
-
unsignedByte = "unsignedByte" # unsigned 8-bit integer
|
|
140
|
-
unsignedShort = "unsignedShort" # unsigned 16-bit integer
|
|
141
|
-
unsignedInt = "unsignedInt" # unsigned 32-bit integer
|
|
142
|
-
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
|
|
143
131
|
|
|
144
132
|
# Less common, but useful nonetheless:
|
|
145
|
-
duration = "duration"
|
|
146
|
-
nonNegativeInteger = "nonNegativeInteger"
|
|
147
|
-
gYear = "gYear"
|
|
148
|
-
hexBinary = "hexBinary"
|
|
149
|
-
base64Binary = "base64Binary"
|
|
150
|
-
token = "token" # noqa: S105
|
|
151
|
-
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")
|
|
152
140
|
|
|
153
141
|
# Types that contain a GML value as member:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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")
|
|
162
151
|
|
|
163
152
|
# Other typical GML values
|
|
164
|
-
gmlCodeType = "
|
|
165
|
-
gmlBoundingShapeType = "
|
|
153
|
+
gmlCodeType = xmlns.gml.qname("CodeType") # for <gml:name>
|
|
154
|
+
gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType") # for <gml:boundedBy>
|
|
166
155
|
|
|
167
156
|
#: A direct geometry value (used as function argument type)
|
|
168
|
-
gmlAbstractGeometryType = "
|
|
157
|
+
gmlAbstractGeometryType = xmlns.gml.qname("AbstractGeometryType")
|
|
169
158
|
|
|
170
|
-
#: A feature that has
|
|
171
|
-
gmlAbstractFeatureType = "
|
|
172
|
-
gmlAbstractGMLType = "
|
|
159
|
+
#: A feature that has a gml:name and gml:boundedBy as possible child element.
|
|
160
|
+
gmlAbstractFeatureType = xmlns.gml.qname("AbstractFeatureType")
|
|
161
|
+
gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType") # base of gml:AbstractFeatureType
|
|
173
162
|
|
|
174
163
|
def __str__(self):
|
|
175
164
|
return self.value
|
|
176
165
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
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
|
|
183
171
|
|
|
184
172
|
@cached_property
|
|
185
|
-
def
|
|
186
|
-
"""
|
|
187
|
-
|
|
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
|
|
188
178
|
|
|
189
179
|
@cached_property
|
|
190
180
|
def _to_python_func(self):
|
|
191
|
-
if not TYPES_TO_PYTHON:
|
|
192
|
-
_init_types_to_python()
|
|
193
|
-
|
|
194
181
|
try:
|
|
195
182
|
return TYPES_TO_PYTHON[self]
|
|
196
183
|
except KeyError:
|
|
@@ -208,40 +195,53 @@ class XsdTypes(XsdAnyType, Enum):
|
|
|
208
195
|
raise # subclass of ValueError so explicitly caught and reraised
|
|
209
196
|
except (TypeError, ValueError, ArithmeticError) as e:
|
|
210
197
|
# ArithmeticError is base of DecimalException
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
245
245
|
|
|
246
246
|
|
|
247
247
|
class XsdNode:
|
|
@@ -257,35 +257,68 @@ class XsdNode:
|
|
|
257
257
|
|
|
258
258
|
name: str
|
|
259
259
|
type: XsdAnyType # Both XsdComplexType and XsdType are allowed
|
|
260
|
-
|
|
260
|
+
|
|
261
|
+
#: XML Namespace of the element
|
|
262
|
+
namespace: xmlns | str | None
|
|
261
263
|
|
|
262
264
|
#: Which field to read from the model to get the value
|
|
263
265
|
#: This supports dot notation to access related attributes.
|
|
264
|
-
source: models.Field | ForeignObjectRel | None
|
|
266
|
+
source: models.Field | models.ForeignObjectRel | None
|
|
265
267
|
|
|
266
268
|
#: Which field to read from the model to get the value
|
|
267
269
|
#: This supports dot notation to access related attributes.
|
|
268
270
|
model_attribute: str | None
|
|
269
271
|
|
|
272
|
+
#: A link back to the parent that described the featuyre this node is a part of.
|
|
273
|
+
#: This helps to perform additional filtering in side meth:get_value: based on user policies.
|
|
274
|
+
feature_type: FeatureType | None
|
|
275
|
+
|
|
270
276
|
def __init__(
|
|
271
277
|
self,
|
|
272
278
|
name: str,
|
|
273
279
|
type: XsdAnyType,
|
|
280
|
+
namespace: xmlns | str | None,
|
|
274
281
|
*,
|
|
275
|
-
|
|
276
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
282
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
277
283
|
model_attribute: str | None = None,
|
|
284
|
+
absolute_model_attribute: str | None = None,
|
|
285
|
+
feature_type: FeatureType | None = None,
|
|
278
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
|
+
|
|
279
305
|
# Using plain assignment instead of dataclass turns out to be needed
|
|
280
306
|
# for flexibility and easier subclassing.
|
|
281
307
|
self.name = name
|
|
282
308
|
self.type = type
|
|
283
|
-
self.
|
|
309
|
+
self.namespace = str(namespace) if namespace is not None else None # cast enum members.
|
|
284
310
|
self.source = source
|
|
285
311
|
self.model_attribute = model_attribute or self.name
|
|
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.
|
|
314
|
+
self.feature_type = feature_type
|
|
286
315
|
|
|
287
|
-
if
|
|
288
|
-
|
|
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")
|
|
289
322
|
|
|
290
323
|
self._attrgetter = operator.attrgetter(self.model_attribute)
|
|
291
324
|
self._valuegetter = self._build_valuegetter(self.model_attribute, self.source)
|
|
@@ -299,7 +332,7 @@ class XsdNode:
|
|
|
299
332
|
@staticmethod
|
|
300
333
|
def _build_valuegetter(
|
|
301
334
|
model_attribute: str,
|
|
302
|
-
field: models.Field | ForeignObjectRel | None,
|
|
335
|
+
field: models.Field | models.ForeignObjectRel | None,
|
|
303
336
|
):
|
|
304
337
|
"""Select the most efficient read function to retrieves the value.
|
|
305
338
|
|
|
@@ -309,7 +342,7 @@ class XsdNode:
|
|
|
309
342
|
since this will be much faster than using ``getattr()``.
|
|
310
343
|
The custom ``value_from_object()`` is fully supported too.
|
|
311
344
|
"""
|
|
312
|
-
if field is None or isinstance(field, ForeignObjectRel):
|
|
345
|
+
if field is None or isinstance(field, models.ForeignObjectRel):
|
|
313
346
|
# No model field, can only use getattr(). The attrgetter() function is both faster,
|
|
314
347
|
# and has built-in support for traversing model attributes with dots.
|
|
315
348
|
return operator.attrgetter(model_attribute)
|
|
@@ -317,7 +350,7 @@ class XsdNode:
|
|
|
317
350
|
if field.value_from_object.__func__ is models.Field.value_from_object:
|
|
318
351
|
# No custom value_from_object(), this can be fully emulated with attrgetter() too.
|
|
319
352
|
# Still allow the final node to have a custom attname,
|
|
320
|
-
#
|
|
353
|
+
# which is what Field.value_from_object() does.
|
|
321
354
|
names = model_attribute.split(".")
|
|
322
355
|
names[-1] = field.attname
|
|
323
356
|
return operator.attrgetter(".".join(names))
|
|
@@ -336,11 +369,6 @@ class XsdNode:
|
|
|
336
369
|
|
|
337
370
|
return _related_get_value_from_object
|
|
338
371
|
|
|
339
|
-
@cached_property
|
|
340
|
-
def is_geometry(self) -> bool:
|
|
341
|
-
"""Tell whether the XML node/element should be handed as GML geometry."""
|
|
342
|
-
return self.type.is_geometry or isinstance(self.source, GeometryField)
|
|
343
|
-
|
|
344
372
|
@cached_property
|
|
345
373
|
def is_array(self) -> bool:
|
|
346
374
|
"""Tell whether this node is backed by an PostgreSQL Array Field."""
|
|
@@ -354,34 +382,48 @@ class XsdNode:
|
|
|
354
382
|
@cached_property
|
|
355
383
|
def xml_name(self):
|
|
356
384
|
"""The XML element/attribute name."""
|
|
357
|
-
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) :]
|
|
358
397
|
|
|
359
398
|
@cached_property
|
|
360
|
-
def
|
|
399
|
+
def local_orm_path(self) -> str:
|
|
361
400
|
"""The ORM field lookup to perform."""
|
|
362
401
|
if self.model_attribute is None:
|
|
363
402
|
raise ValueError(f"Node {self.xml_name} has no 'model_attribute' set.")
|
|
364
403
|
return self.model_attribute.replace(".", "__")
|
|
365
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
|
+
|
|
366
412
|
@cached_property
|
|
367
413
|
def orm_field(self) -> str:
|
|
368
|
-
"""The direct ORM field that provides this property.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
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]
|
|
372
418
|
|
|
373
419
|
@cached_property
|
|
374
420
|
def orm_relation(self) -> tuple[str | None, str]:
|
|
375
|
-
"""The ORM field and parent relation
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
except ValueError:
|
|
382
|
-
return None, self.model_attribute
|
|
383
|
-
else:
|
|
384
|
-
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
|
|
385
427
|
|
|
386
428
|
def build_lhs_part(self, compiler: CompiledQuery, match: ORMPath):
|
|
387
429
|
"""Give the ORM part when this element is used as left-hand-side of a comparison.
|
|
@@ -407,6 +449,8 @@ class XsdNode:
|
|
|
407
449
|
if self.is_many and isinstance(value, models.Manager):
|
|
408
450
|
# Make sure callers can read the individual items by iterating over the value.
|
|
409
451
|
value = value.all()
|
|
452
|
+
if self.feature_type is not None:
|
|
453
|
+
return self.feature_type.filter_related_queryset(value)
|
|
410
454
|
return value
|
|
411
455
|
else:
|
|
412
456
|
# the _valuegetter() supports value_from_object() on custom fields.
|
|
@@ -421,11 +465,6 @@ class XsdNode:
|
|
|
421
465
|
"""
|
|
422
466
|
return value
|
|
423
467
|
|
|
424
|
-
@cached_property
|
|
425
|
-
def _form_field(self):
|
|
426
|
-
"""Internal cached field for to_python()"""
|
|
427
|
-
return self.source.formfield()
|
|
428
|
-
|
|
429
468
|
def to_python(self, raw_value: str):
|
|
430
469
|
"""Convert a raw value to the Python data type for this element type."""
|
|
431
470
|
try:
|
|
@@ -448,6 +487,11 @@ class XsdNode:
|
|
|
448
487
|
|
|
449
488
|
The raw string value can be passed here. Auto-cased values could
|
|
450
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.
|
|
451
495
|
"""
|
|
452
496
|
if self.source is not None:
|
|
453
497
|
# Not calling self.source.validate() as that checks for allowed choices,
|
|
@@ -461,9 +505,16 @@ class XsdNode:
|
|
|
461
505
|
isinstance(self.source, RelatedField)
|
|
462
506
|
and self.source.target_field.get_lookup(lookup) is None
|
|
463
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
|
+
)
|
|
464
515
|
raise OperationProcessingFailed(
|
|
465
|
-
"filter",
|
|
466
516
|
f"Operator '{tag}' is not supported for the '{self.name}' property.",
|
|
517
|
+
locator="filter",
|
|
467
518
|
status_code=400, # not HTTP 500 here. Spec allows both.
|
|
468
519
|
)
|
|
469
520
|
|
|
@@ -489,41 +540,35 @@ class XsdElement(XsdNode):
|
|
|
489
540
|
|
|
490
541
|
nillable: bool | None
|
|
491
542
|
min_occurs: int | None
|
|
492
|
-
max_occurs: int | None
|
|
543
|
+
max_occurs: int | _unbounded | None
|
|
493
544
|
|
|
494
545
|
def __init__(
|
|
495
546
|
self,
|
|
496
547
|
name: str,
|
|
497
548
|
type: XsdAnyType,
|
|
549
|
+
namespace: xmlns | str | None,
|
|
498
550
|
*,
|
|
499
|
-
prefix: str | None = "app",
|
|
500
551
|
nillable: bool | None = None,
|
|
501
552
|
min_occurs: int | None = None,
|
|
502
553
|
max_occurs: int | _unbounded | None = None,
|
|
503
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
554
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
504
555
|
model_attribute: str | None = None,
|
|
556
|
+
absolute_model_attribute: str | None = None,
|
|
557
|
+
feature_type: FeatureType | None = None,
|
|
505
558
|
):
|
|
506
|
-
super().__init__(
|
|
559
|
+
super().__init__(
|
|
560
|
+
name,
|
|
561
|
+
type,
|
|
562
|
+
namespace=namespace,
|
|
563
|
+
source=source,
|
|
564
|
+
model_attribute=model_attribute,
|
|
565
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
566
|
+
feature_type=feature_type,
|
|
567
|
+
)
|
|
507
568
|
self.nillable = nillable
|
|
508
569
|
self.min_occurs = min_occurs
|
|
509
570
|
self.max_occurs = max_occurs
|
|
510
571
|
|
|
511
|
-
@cached_property
|
|
512
|
-
def as_xml(self):
|
|
513
|
-
attributes = [f'name="{self.name}" type="{self.type}"']
|
|
514
|
-
if self.min_occurs is not None:
|
|
515
|
-
attributes.append(f'minOccurs="{self.min_occurs}"')
|
|
516
|
-
if self.max_occurs is not None:
|
|
517
|
-
attributes.append(f'maxOccurs="{self.max_occurs}"')
|
|
518
|
-
if self.nillable:
|
|
519
|
-
str_bool = "true" if self.nillable else "false"
|
|
520
|
-
attributes.append(f'nillable="{str_bool}"')
|
|
521
|
-
|
|
522
|
-
return "<element {} />".format(" ".join(attributes))
|
|
523
|
-
|
|
524
|
-
def __str__(self):
|
|
525
|
-
return self.as_xml
|
|
526
|
-
|
|
527
572
|
@cached_property
|
|
528
573
|
def is_many(self) -> bool:
|
|
529
574
|
"""Tell whether the XML element can be rendered multiple times.
|
|
@@ -563,15 +608,58 @@ class XsdAttribute(XsdNode):
|
|
|
563
608
|
name: str,
|
|
564
609
|
type: XsdAnyType = XsdTypes.string, # added default
|
|
565
610
|
*,
|
|
566
|
-
|
|
611
|
+
namespace: xmlns | str | None = None,
|
|
567
612
|
use: str = "optional",
|
|
568
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
613
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
569
614
|
model_attribute: str | None = None,
|
|
615
|
+
absolute_model_attribute: str | None = None,
|
|
616
|
+
feature_type: FeatureType | None = None,
|
|
570
617
|
):
|
|
571
|
-
super().__init__(
|
|
618
|
+
super().__init__(
|
|
619
|
+
name,
|
|
620
|
+
type,
|
|
621
|
+
namespace=namespace,
|
|
622
|
+
source=source,
|
|
623
|
+
model_attribute=model_attribute,
|
|
624
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
625
|
+
feature_type=feature_type,
|
|
626
|
+
)
|
|
572
627
|
self.use = use
|
|
573
628
|
|
|
574
629
|
|
|
630
|
+
class GeometryXsdElement(XsdElement):
|
|
631
|
+
"""A subtype for the :class:`XsdElement` that provides access to geometry data.
|
|
632
|
+
|
|
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`.
|
|
645
|
+
"""
|
|
646
|
+
|
|
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
|
|
661
|
+
|
|
662
|
+
|
|
575
663
|
class GmlIdAttribute(XsdAttribute):
|
|
576
664
|
"""A virtual 'gml:id' attribute that can be queried.
|
|
577
665
|
This subclass has overwritten get_value() logic to format the value.
|
|
@@ -582,10 +670,19 @@ class GmlIdAttribute(XsdAttribute):
|
|
|
582
670
|
def __init__(
|
|
583
671
|
self,
|
|
584
672
|
type_name: str,
|
|
585
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
673
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
586
674
|
model_attribute="pk",
|
|
675
|
+
absolute_model_attribute=None,
|
|
676
|
+
feature_type: FeatureType | None = None,
|
|
587
677
|
):
|
|
588
|
-
super().__init__(
|
|
678
|
+
super().__init__(
|
|
679
|
+
name="id",
|
|
680
|
+
namespace=xmlns.gml,
|
|
681
|
+
source=source,
|
|
682
|
+
model_attribute=model_attribute,
|
|
683
|
+
absolute_model_attribute=absolute_model_attribute,
|
|
684
|
+
feature_type=feature_type,
|
|
685
|
+
)
|
|
589
686
|
object.__setattr__(self, "type_name", type_name)
|
|
590
687
|
|
|
591
688
|
def get_value(self, instance: models.Model):
|
|
@@ -609,19 +706,19 @@ class GmlNameElement(XsdElement):
|
|
|
609
706
|
def __init__(
|
|
610
707
|
self,
|
|
611
708
|
model_attribute: str,
|
|
612
|
-
source: models.Field | ForeignObjectRel | None = None,
|
|
709
|
+
source: models.Field | models.ForeignObjectRel | None = None,
|
|
613
710
|
feature_type=None,
|
|
614
711
|
):
|
|
615
712
|
# Prefill most known fields
|
|
616
713
|
super().__init__(
|
|
617
|
-
prefix="gml",
|
|
618
714
|
name="name",
|
|
619
715
|
type=XsdTypes.gmlCodeType,
|
|
716
|
+
namespace=xmlns.gml,
|
|
620
717
|
min_occurs=0,
|
|
621
718
|
source=source,
|
|
622
719
|
model_attribute=model_attribute,
|
|
720
|
+
feature_type=feature_type,
|
|
623
721
|
)
|
|
624
|
-
self.feature_type = feature_type
|
|
625
722
|
|
|
626
723
|
def get_value(self, instance: models.Model):
|
|
627
724
|
"""Override value retrieval to retrieve the value from the feature type."""
|
|
@@ -641,17 +738,15 @@ class GmlBoundedByElement(XsdElement):
|
|
|
641
738
|
Its value is the complete bounding box of the feature type data.
|
|
642
739
|
"""
|
|
643
740
|
|
|
644
|
-
is_geometry = True # Override type
|
|
645
|
-
|
|
646
741
|
def __init__(self, feature_type):
|
|
647
742
|
# Prefill most known fields
|
|
648
743
|
super().__init__(
|
|
649
|
-
prefix="gml",
|
|
650
744
|
name="boundedBy",
|
|
651
745
|
type=XsdTypes.gmlBoundingShapeType,
|
|
746
|
+
namespace=xmlns.gml,
|
|
652
747
|
min_occurs=0,
|
|
748
|
+
feature_type=feature_type,
|
|
653
749
|
)
|
|
654
|
-
self.feature_type = feature_type
|
|
655
750
|
self.model_attribute = None
|
|
656
751
|
|
|
657
752
|
def build_lhs_part(self, compiler: CompiledQuery, match: ORMPath):
|
|
@@ -663,10 +758,37 @@ class GmlBoundedByElement(XsdElement):
|
|
|
663
758
|
"""Give the ORM part when this element would be used as right-hand-side"""
|
|
664
759
|
raise NotImplementedError("queries against <gml:boundedBy> are not supported")
|
|
665
760
|
|
|
666
|
-
def get_value(self, instance: models.Model, crs: CRS | None = None):
|
|
667
|
-
"""Provide the value of the <gml:boundedBy> field
|
|
668
|
-
|
|
669
|
-
|
|
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)
|
|
670
792
|
|
|
671
793
|
|
|
672
794
|
@dataclass(frozen=True)
|
|
@@ -692,41 +814,51 @@ class XsdComplexType(XsdAnyType):
|
|
|
692
814
|
which allows child elements like <gml:name> and <gml:boundedBy>.
|
|
693
815
|
"""
|
|
694
816
|
|
|
695
|
-
#: Internal class name (without XML prefix)
|
|
817
|
+
#: Internal class name (without XML namespace/prefix)
|
|
696
818
|
name: str
|
|
697
819
|
|
|
698
|
-
#:
|
|
699
|
-
|
|
820
|
+
#: The XML namespace
|
|
821
|
+
namespace: str | None
|
|
822
|
+
|
|
823
|
+
#: All local elements in this class
|
|
824
|
+
elements: list[XsdElement] = field(default_factory=list)
|
|
700
825
|
|
|
701
826
|
#: All attributes in this class
|
|
702
827
|
attributes: list[XsdAttribute] = field(default_factory=list)
|
|
703
828
|
|
|
704
829
|
#: The base class of this type. Typically gml:AbstractFeatureType,
|
|
705
830
|
#: which provides the <gml:name> and <gml:boundedBy> elements.
|
|
706
|
-
base: XsdAnyType =
|
|
707
|
-
|
|
708
|
-
#: The prefix alias to use for the namespace.
|
|
709
|
-
prefix: str = "app"
|
|
831
|
+
base: XsdAnyType | None = None
|
|
710
832
|
|
|
711
833
|
#: The Django model class that this type was based on.
|
|
712
834
|
source: type[models.Model] | None = None
|
|
713
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
|
+
|
|
714
846
|
def __str__(self):
|
|
715
847
|
return self.xml_name
|
|
716
848
|
|
|
717
849
|
@cached_property
|
|
718
850
|
def xml_name(self):
|
|
719
|
-
"""Name in the XMLSchema (e.g.
|
|
720
|
-
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
|
|
721
853
|
|
|
722
854
|
@property
|
|
723
855
|
def is_complex_type(self):
|
|
724
856
|
return True # a property to avoid being used as field.
|
|
725
857
|
|
|
726
858
|
@cached_property
|
|
727
|
-
def
|
|
728
|
-
"""
|
|
729
|
-
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:
|
|
730
862
|
# Add all base class members, in their correct ordering
|
|
731
863
|
# By having these as XsdElement objects instead of hard-coded writes,
|
|
732
864
|
# the query/filter logic also works for these elements.
|
|
@@ -735,21 +867,35 @@ class XsdComplexType(XsdAnyType):
|
|
|
735
867
|
return self.elements
|
|
736
868
|
|
|
737
869
|
@cached_property
|
|
738
|
-
def geometry_elements(self) -> list[
|
|
870
|
+
def geometry_elements(self) -> list[GeometryXsdElement]:
|
|
739
871
|
"""Shortcut to get all geometry elements"""
|
|
740
|
-
return [e for e in self.elements if e.is_geometry]
|
|
872
|
+
return [e for e in self.elements if e.type.is_geometry]
|
|
741
873
|
|
|
742
874
|
@cached_property
|
|
743
875
|
def complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
744
|
-
"""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
|
+
"""
|
|
745
879
|
return [e for e in self.elements if e.type.is_complex_type]
|
|
746
880
|
|
|
747
881
|
@cached_property
|
|
748
882
|
def flattened_elements(self) -> list[XsdElement]:
|
|
749
|
-
"""Shortcut to get all elements with a flattened model
|
|
883
|
+
"""Shortcut to get all elements with a flattened model attribute"""
|
|
750
884
|
return [e for e in self.elements if e.is_flattened]
|
|
751
885
|
|
|
752
|
-
|
|
886
|
+
@cached_property
|
|
887
|
+
def all_complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
888
|
+
"""Shortcut to get all elements with children.
|
|
889
|
+
This mainly exists to provide a structure to mimic what's used
|
|
890
|
+
when PROPERTYNAME is part of the request.
|
|
891
|
+
"""
|
|
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)
|
|
896
|
+
return child_nodes
|
|
897
|
+
|
|
898
|
+
def resolve_element_path(self, xpath: str, ns_aliases: dict[str, str]) -> list[XsdNode] | None:
|
|
753
899
|
"""Resolve a xpath reference to the actual node.
|
|
754
900
|
This returns the whole path, including in-between relations, if a match was found.
|
|
755
901
|
|
|
@@ -757,7 +903,7 @@ class XsdComplexType(XsdAnyType):
|
|
|
757
903
|
to convert a request XPath element into the ORM attributes for database queries.
|
|
758
904
|
"""
|
|
759
905
|
try:
|
|
760
|
-
pos = xpath.
|
|
906
|
+
pos = xpath.index("/")
|
|
761
907
|
node_name = xpath[:pos]
|
|
762
908
|
except ValueError:
|
|
763
909
|
node_name = xpath
|
|
@@ -771,12 +917,11 @@ class XsdComplexType(XsdAnyType):
|
|
|
771
917
|
if pos:
|
|
772
918
|
return None # invalid attribute
|
|
773
919
|
|
|
774
|
-
# Remove app: prefixes, or any alias of it (see explanation below)
|
|
775
920
|
xml_name = node_name[1:]
|
|
776
|
-
attribute = self._find_attribute(xml_name=xml_name)
|
|
921
|
+
attribute = self._find_attribute(xml_name=parse_qname(xml_name, ns_aliases))
|
|
777
922
|
return [attribute] if attribute is not None else None
|
|
778
923
|
else:
|
|
779
|
-
element = self._find_element(node_name)
|
|
924
|
+
element = self._find_element(xml_name=parse_qname(node_name, ns_aliases))
|
|
780
925
|
if element is None:
|
|
781
926
|
return None
|
|
782
927
|
|
|
@@ -785,62 +930,34 @@ class XsdComplexType(XsdAnyType):
|
|
|
785
930
|
return None
|
|
786
931
|
else:
|
|
787
932
|
# Recurse into the child node to find the next part
|
|
788
|
-
child_path = element.type.resolve_element_path(xpath[pos + 1 :])
|
|
933
|
+
child_path = element.type.resolve_element_path(xpath[pos + 1 :], ns_aliases)
|
|
789
934
|
return [element] + child_path if child_path is not None else None
|
|
790
935
|
else:
|
|
791
936
|
return [element]
|
|
792
937
|
|
|
793
|
-
def _find_element(self, xml_name) -> XsdElement | None:
|
|
938
|
+
def _find_element(self, xml_name: str) -> XsdElement | None:
|
|
794
939
|
"""Locate an element by name"""
|
|
795
940
|
for element in self.elements:
|
|
796
941
|
if element.xml_name == xml_name:
|
|
797
942
|
return element
|
|
798
943
|
|
|
799
|
-
prefix, name = split_xml_name(xml_name)
|
|
800
|
-
if prefix != "gml" and prefix != self.prefix:
|
|
801
|
-
# Ignore current app namespace. Note this should actually compare the
|
|
802
|
-
# xmlns URI's, but this will suffice for now. The ElementTree parser
|
|
803
|
-
# doesn't provide access to 'xmlns' definitions on the element (or it's
|
|
804
|
-
# parents), so a tag like this is essentially not parsable for us:
|
|
805
|
-
# <ValueReference xmlns:tns="http://...">tns:fieldname</ValueReference>
|
|
806
|
-
for element in self.elements:
|
|
807
|
-
if element.name == name:
|
|
808
|
-
return element
|
|
809
|
-
|
|
810
944
|
# When there is a base class, resolve elements there too.
|
|
811
|
-
if self.base.is_complex_type:
|
|
945
|
+
if self.base is not None and self.base.is_complex_type:
|
|
812
946
|
return self.base._find_element(xml_name)
|
|
813
947
|
return None
|
|
814
948
|
|
|
815
|
-
def _find_attribute(self, xml_name) -> XsdAttribute | None:
|
|
949
|
+
def _find_attribute(self, xml_name: str) -> XsdAttribute | None:
|
|
816
950
|
"""Locate an attribute by name"""
|
|
817
951
|
for attribute in self.attributes:
|
|
818
952
|
if attribute.xml_name == xml_name:
|
|
819
953
|
return attribute
|
|
820
954
|
|
|
821
|
-
prefix, name = split_xml_name(xml_name)
|
|
822
|
-
if prefix != "gml" and prefix != self.prefix:
|
|
823
|
-
# Allow any namespace to match, since the stdlib ElementTree parser
|
|
824
|
-
# can't resolve namespaces at all.
|
|
825
|
-
for attribute in self.attributes:
|
|
826
|
-
if attribute.name == name:
|
|
827
|
-
return attribute
|
|
828
|
-
|
|
829
955
|
# When there is a base class, resolve attributes there too.
|
|
830
|
-
if self.base.is_complex_type:
|
|
956
|
+
if self.base is not None and self.base.is_complex_type:
|
|
831
957
|
return self.base._find_attribute(xml_name)
|
|
832
958
|
return None
|
|
833
959
|
|
|
834
960
|
|
|
835
|
-
def split_xml_name(xml_name: str) -> tuple[str | None, str]:
|
|
836
|
-
"""Remove the namespace prefix from an element."""
|
|
837
|
-
try:
|
|
838
|
-
prefix, name = xml_name.split(":", 1)
|
|
839
|
-
return prefix, name
|
|
840
|
-
except ValueError:
|
|
841
|
-
return None, xml_name
|
|
842
|
-
|
|
843
|
-
|
|
844
961
|
class ORMPath:
|
|
845
962
|
"""Base class to provide raw XPath results.
|
|
846
963
|
|
|
@@ -884,7 +1001,7 @@ class ORMPath:
|
|
|
884
1001
|
class XPathMatch(ORMPath):
|
|
885
1002
|
"""The ORM path result from am XPath query.
|
|
886
1003
|
|
|
887
|
-
This result object defines how to resolve an XPath to
|
|
1004
|
+
This result object defines how to resolve an XPath to an ORM object.
|
|
888
1005
|
"""
|
|
889
1006
|
|
|
890
1007
|
#: The matched element, with all it's parents.
|
|
@@ -907,10 +1024,10 @@ class XPathMatch(ORMPath):
|
|
|
907
1024
|
# the build_...() logic should return a Q() object.
|
|
908
1025
|
raise NotImplementedError(f"Complex XPath queries are not supported yet: {self.query}")
|
|
909
1026
|
|
|
910
|
-
@
|
|
1027
|
+
@property
|
|
911
1028
|
def orm_path(self) -> str:
|
|
912
1029
|
"""Give the Django ORM path (field__relation__relation2) to the result."""
|
|
913
|
-
return
|
|
1030
|
+
return self.nodes[-1].orm_path
|
|
914
1031
|
|
|
915
1032
|
def __iter__(self):
|
|
916
1033
|
return iter(self.nodes)
|
|
@@ -950,4 +1067,4 @@ class XPathMatch(ORMPath):
|
|
|
950
1067
|
|
|
951
1068
|
if TYPE_CHECKING:
|
|
952
1069
|
from .features import FeatureType
|
|
953
|
-
from .parsers.
|
|
1070
|
+
from .parsers.query import CompiledQuery
|