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.
Files changed (77) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/crs.py +401 -0
  8. gisserver/db.py +126 -51
  9. gisserver/exceptions.py +132 -4
  10. gisserver/extensions/__init__.py +4 -0
  11. gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
  12. gisserver/extensions/queries.py +266 -0
  13. gisserver/features.py +253 -181
  14. gisserver/geometries.py +64 -311
  15. gisserver/management/__init__.py +0 -0
  16. gisserver/management/commands/__init__.py +0 -0
  17. gisserver/management/commands/loadgeojson.py +311 -0
  18. gisserver/operations/base.py +130 -312
  19. gisserver/operations/wfs20.py +399 -375
  20. gisserver/output/__init__.py +14 -49
  21. gisserver/output/base.py +198 -144
  22. gisserver/output/csv.py +78 -75
  23. gisserver/output/geojson.py +37 -37
  24. gisserver/output/gml32.py +287 -259
  25. gisserver/output/iters.py +207 -0
  26. gisserver/output/results.py +73 -61
  27. gisserver/output/stored.py +143 -0
  28. gisserver/output/utils.py +81 -169
  29. gisserver/output/xmlschema.py +85 -46
  30. gisserver/parsers/__init__.py +10 -10
  31. gisserver/parsers/ast.py +426 -0
  32. gisserver/parsers/fes20/__init__.py +89 -31
  33. gisserver/parsers/fes20/expressions.py +172 -58
  34. gisserver/parsers/fes20/filters.py +116 -45
  35. gisserver/parsers/fes20/identifiers.py +66 -28
  36. gisserver/parsers/fes20/lookups.py +146 -0
  37. gisserver/parsers/fes20/operators.py +417 -161
  38. gisserver/parsers/fes20/sorting.py +113 -34
  39. gisserver/parsers/gml/__init__.py +17 -25
  40. gisserver/parsers/gml/base.py +36 -15
  41. gisserver/parsers/gml/geometries.py +105 -44
  42. gisserver/parsers/ows/__init__.py +25 -0
  43. gisserver/parsers/ows/kvp.py +198 -0
  44. gisserver/parsers/ows/requests.py +160 -0
  45. gisserver/parsers/query.py +179 -0
  46. gisserver/parsers/values.py +87 -4
  47. gisserver/parsers/wfs20/__init__.py +39 -0
  48. gisserver/parsers/wfs20/adhoc.py +253 -0
  49. gisserver/parsers/wfs20/base.py +148 -0
  50. gisserver/parsers/wfs20/projection.py +103 -0
  51. gisserver/parsers/wfs20/requests.py +483 -0
  52. gisserver/parsers/wfs20/stored.py +193 -0
  53. gisserver/parsers/xml.py +261 -0
  54. gisserver/projection.py +367 -0
  55. gisserver/static/gisserver/index.css +20 -4
  56. gisserver/templates/gisserver/base.html +12 -0
  57. gisserver/templates/gisserver/index.html +9 -15
  58. gisserver/templates/gisserver/service_description.html +12 -6
  59. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  60. gisserver/templates/gisserver/wfs/feature_field.html +3 -3
  61. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  62. gisserver/templatetags/gisserver_tags.py +20 -0
  63. gisserver/types.py +445 -313
  64. gisserver/views.py +227 -62
  65. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  66. gisserver/parsers/base.py +0 -149
  67. gisserver/parsers/fes20/query.py +0 -285
  68. gisserver/parsers/tags.py +0 -102
  69. gisserver/queries/__init__.py +0 -37
  70. gisserver/queries/adhoc.py +0 -185
  71. gisserver/queries/base.py +0 -186
  72. gisserver/queries/projection.py +0 -240
  73. gisserver/queries/stored.py +0 -206
  74. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  75. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  76. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  77. {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, cast
37
+ from typing import TYPE_CHECKING, Literal
36
38
 
37
- from django.conf import settings
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 ( # Django 2 imports
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 CRS, WGS84 # noqa: F401 / for backwards compatibility
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
- _unbounded = Literal["unbounded"]
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
- "GmlElement",
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
- prefix = None
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
- is_geometry = False
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 basic XMLSchema types.
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
- The default namespace is the "xs:" (XMLSchema).
117
- Based on https://www.w3.org/TR/xmlschema-2/#built-in-datatypes
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" # Needs to be anyType, as "xsd:any" is an element, not a type.
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
- gmlGeometryPropertyType = "gml:GeometryPropertyType"
153
- gmlPointPropertyType = "gml:PointPropertyType"
154
- gmlCurvePropertyType = "gml:CurvePropertyType" # curve is base for LineString
155
- gmlSurfacePropertyType = "gml:SurfacePropertyType" # GML2 had PolygonPropertyType
156
- gmlMultiSurfacePropertyType = "gml:MultiSurfacePropertyType"
157
- gmlMultiPointPropertyType = "gml:MultiPointPropertyType"
158
- gmlMultiCurvePropertyType = "gml:MultiCurvePropertyType"
159
- gmlMultiGeometryPropertyType = "gml:MultiGeometryPropertyType"
160
-
161
- # Other typical GML values
162
- gmlCodeType = "gml:CodeType" # for <gml:name>
163
- gmlBoundingShapeType = "gml:BoundingShapeType" # for <gml:boundedBy>
164
-
165
- #: A direct geometry value (used as function argument type)
166
- gmlAbstractGeometryType = "gml:AbstractGeometryType"
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 = "gml:AbstractFeatureType"
170
- gmlAbstractGMLType = "gml:AbstractGMLType" # base class of gml:AbstractFeatureType
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
- @property
176
- def prefix(self) -> str | None:
177
- """Extrapolate the prefix from the type name"""
178
- xml_name = str(self)
179
- colon = xml_name.find(":")
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 is_geometry(self):
184
- """Whether the value represents an element which contains a GML element."""
185
- return self.prefix == "gml" and self.value.endswith("PropertyType")
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
- if self.is_geometry:
200
- # Leave complex values as-is.
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
- raise ExternalParsingError(f"Can't cast '{raw_value}' to {self}.") from e
210
-
211
-
212
- def _init_types_to_python():
213
- """Define how well-known scalar types are parsed into python
214
- (mimicking Django's to_python()):
215
- """
216
- global TYPES_TO_PYTHON
217
- from gisserver.parsers import values # avoid cyclic import
218
-
219
- def as_is(v):
220
- return v
221
-
222
- TYPES_TO_PYTHON = {
223
- XsdTypes.date: dateparse.parse_date,
224
- XsdTypes.dateTime: values.parse_iso_datetime,
225
- XsdTypes.time: dateparse.parse_time,
226
- XsdTypes.string: as_is,
227
- XsdTypes.boolean: values.parse_bool,
228
- XsdTypes.integer: int,
229
- XsdTypes.int: int,
230
- XsdTypes.long: int,
231
- XsdTypes.short: int,
232
- XsdTypes.byte: int,
233
- XsdTypes.unsignedInt: int,
234
- XsdTypes.unsignedLong: int,
235
- XsdTypes.unsignedShort: int,
236
- XsdTypes.unsignedByte: int,
237
- XsdTypes.float: D,
238
- XsdTypes.double: D,
239
- XsdTypes.decimal: D,
240
- XsdTypes.gmlCodeType: as_is,
241
- XsdTypes.anyType: values.auto_cast,
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
- type: XsdAnyType # Both XsdComplexType and XsdType are allowed
258
- prefix: str | None
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 featuyre this node is a part of.
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
- prefix: str | None = "app",
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.prefix = prefix
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
- # link back to parent, some get_value() functions need it.
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 ":" in self.name:
293
- raise ValueError("Use 'prefix' argument for namespaces")
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.prefix}:{self.name}" if self.prefix else self.name
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 orm_path(self) -> str:
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
- if self.model_attribute is None:
375
- raise ValueError(f"Node {self.xml_name} has no 'model_attribute' set.")
376
- return self.model_attribute.partition(".")[0]
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
- if self.model_attribute is None:
382
- raise ValueError(f"Node {self.xml_name} has no 'model_attribute' set.")
383
-
384
- path, _, field = self.model_attribute.rpartition(".")
385
- if not path:
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
- if self.source is not None:
459
- # Not calling self.source.validate() as that checks for allowed choices,
460
- # which shouldn't be checked against for a filter query.
461
- raw_value = self.to_python(raw_value)
462
-
463
- # Check whether the Django model field supports the lookup
464
- # This prevents calling LIKE on a datetime or float field.
465
- # For foreign keys, this depends on the target field type.
466
- if self.source.get_lookup(lookup) is None or (
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
- raise OperationProcessingFailed(
471
- f"Operator '{tag}' is not supported for the '{self.name}' property.",
472
- locator="filter",
473
- status_code=400, # not HTTP 500 here. Spec allows both.
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
- max_occurs: int | _unbounded | None
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 | _unbounded | None = None,
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
- prefix=prefix,
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
- prefix: str | None = "app",
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
- prefix=prefix,
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 GmlElement(XsdElement):
598
- """Essentially the same as an XsdElement, but guarantee it returns a GeoDjango model field.
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
- This only exists as "protocol" for the type annotations.
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
- source: GeometryField
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 'gml:id' attribute that can be queried.
608
- This subclass has overwritten get_value() logic to format the 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 <gml:name> element.
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 <gml:boundedBy> element.
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
- (if this is not given by the database already)."""
709
- return self.feature_type.get_envelope(instance, crs=crs)
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 <xsd:complexType> that represents a whole class definition.
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. The elements themselves can point
720
- to a complex type themselves, to create a nested class structure.
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
- This object definition is the internal "source of truth" regarding
724
- which field names and field elements are used in the WFS server.
725
- The ``DescribeFeatureType`` request uses this definition to render the matching XMLSchema.
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
- Objects of this type are typically generated by the ``FeatureType`` and
729
- ``ComplexFeatureField`` classes, using the Django model data.
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
- By default, The type is declared as subclass of <gml:AbstractFeatureType>,
732
- which allows child elements like <gml:name> and <gml:boundedBy>.
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
- #: All elements in this class
739
- elements: list[XsdElement]
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 = XsdTypes.gmlAbstractFeatureType
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. app:SomeClass)."""
760
- return f"{self.prefix}:{self.name}"
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 all_elements(self) -> list[XsdElement]:
768
- """All elements, including inherited elements."""
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[GmlElement]:
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 attribite"""
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 elements_with_children(self) -> dict[_XsdElement_WithComplexType, list[XsdElement]]:
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.elements:
800
- if xsd_element.type.is_complex_type:
801
- sub_type = cast(XsdComplexType, xsd_element.type)
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.rindex("/")
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: "path == value".
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: "path == path" or "value == path".
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
- @cached_property
1092
+ @property
965
1093
  def orm_path(self) -> str:
966
1094
  """Give the Django ORM path (field__relation__relation2) to the result."""
967
- return "__".join(xsd_node.orm_path for xsd_node in self.nodes)
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
- """Delegate the LHS construction to the final XsdNode."""
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
- """Delegate the RHS construction to the final XsdNode."""
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.fes20 import CompiledQuery
1139
+ from .parsers.query import CompiledQuery