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

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