django-gisserver 1.5.0__py3-none-any.whl → 2.0__py3-none-any.whl

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