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/output/gml32.py CHANGED
@@ -11,6 +11,7 @@ much extra method calls per field. Some bits are non-DRY inlined for this reason
11
11
  from __future__ import annotations
12
12
 
13
13
  import itertools
14
+ from collections import defaultdict
14
15
  from datetime import date, datetime, time, timezone
15
16
  from decimal import Decimal as D
16
17
  from io import StringIO
@@ -18,29 +19,33 @@ from operator import itemgetter
18
19
  from typing import cast
19
20
 
20
21
  from django.contrib.gis import geos
22
+ from django.core.exceptions import ImproperlyConfigured
21
23
  from django.db import models
22
24
  from django.http import HttpResponse
23
25
 
24
26
  from gisserver.db import (
25
27
  AsGML,
26
- build_db_annotations,
27
- conditional_transform,
28
- get_db_annotation,
29
- get_db_geometry_selects,
30
28
  get_db_geometry_target,
29
+ get_db_rendered_geometry,
31
30
  get_geometries_union,
31
+ replace_queryset_geometries,
32
32
  )
33
- from gisserver.exceptions import NotFound
34
- from gisserver.features import FeatureRelation, FeatureType
35
- from gisserver.geometries import CRS
36
- from gisserver.parsers.fes20 import ValueReference
37
- from gisserver.types import XsdComplexType, XsdElement, XsdNode
33
+ from gisserver.exceptions import NotFound, WFSException
34
+ from gisserver.geometries import BoundingBox
35
+ from gisserver.parsers.xml import xmlns
36
+ from gisserver.projection import FeatureProjection, FeatureRelation
37
+ from gisserver.types import GeometryXsdElement, GmlBoundedByElement, XsdElement, XsdNode, XsdTypes
38
38
 
39
- from .base import OutputRenderer
39
+ from .base import CollectionOutputRenderer, XmlOutputRenderer
40
40
  from .results import SimpleFeatureCollection
41
+ from .utils import (
42
+ attr_escape,
43
+ tag_escape,
44
+ value_to_text,
45
+ value_to_xml_string,
46
+ )
41
47
 
42
48
  GML_RENDER_FUNCTIONS = {}
43
- AUTO_STR = (int, float, D, date, time)
44
49
 
45
50
 
46
51
  def register_geos_type(geos_type):
@@ -51,55 +56,62 @@ def register_geos_type(geos_type):
51
56
  return _inc
52
57
 
53
58
 
54
- def _tag_escape(s: str):
55
- return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
56
-
57
-
58
- def _attr_escape(s: str):
59
- # Slightly faster then html.escape() as it doesn't replace single quotes.
60
- # Having tried all possible variants, this code still outperforms other forms of escaping.
61
- return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
62
-
63
-
64
- def _value_to_xml_string(value):
65
- # Simple scalar value
66
- if isinstance(value, str): # most cases
67
- return _tag_escape(value)
68
- elif isinstance(value, datetime):
69
- return value.astimezone(timezone.utc).isoformat()
70
- elif isinstance(value, bool):
71
- return "true" if value else "false"
72
- elif isinstance(value, AUTO_STR):
73
- return value # no need for _tag_escape(), and f"{value}" works faster.
74
- else:
75
- return _tag_escape(str(value))
76
-
77
-
78
- def _value_to_text(value):
79
- # Simple scalar value, no XML escapes
80
- if isinstance(value, str): # most cases
81
- return value
82
- elif isinstance(value, datetime):
83
- return value.astimezone(timezone.utc).isoformat()
84
- elif isinstance(value, bool):
85
- return "true" if value else "false"
86
- else:
87
- return value # f"{value} works faster and produces the right format.
88
-
89
-
90
- class GML32Renderer(OutputRenderer):
59
+ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
91
60
  """Render the GetFeature XML output in GML 3.2 format"""
92
61
 
93
62
  content_type = "text/xml; charset=utf-8"
63
+ content_disposition = 'inline; filename="{typenames} {page} {date}.xml"'
94
64
  xml_collection_tag = "FeatureCollection"
65
+ xml_sub_collection_tag = "FeatureCollection" # Mapserver does not use SimpleFeatureCollection
95
66
  chunk_size = 40_000
96
67
  gml_seq = 0
97
68
 
69
+ # Aliases to use for XML namespaces
70
+ xml_namespaces = {
71
+ "http://www.opengis.net/wfs/2.0": "wfs",
72
+ "http://www.opengis.net/gml/3.2": "gml",
73
+ "http://www.w3.org/2001/XMLSchema-instance": "xsi", # for xsi:nil="true" and xsi:schemaLocation.
74
+ }
75
+
76
+ def __init__(self, *args, **kwargs):
77
+ super().__init__(*args, **kwargs)
78
+ self.feature_types = [
79
+ feature_type
80
+ for sub_collection in self.collection.results
81
+ for feature_type in sub_collection.feature_types
82
+ ]
83
+
84
+ self.build_namespace_map()
85
+
86
+ def build_namespace_map(
87
+ self,
88
+ ):
89
+ """Collect all namespaces which namespaces are used by features."""
90
+ try:
91
+ # Make aliases for the feature type elements.
92
+ self.feature_qnames = {
93
+ feature_type: self.feature_to_qname(feature_type)
94
+ for feature_type in self.feature_types
95
+ }
96
+ self.xml_qnames = self._build_xml_qnames()
97
+ except KeyError as e:
98
+ raise ImproperlyConfigured(f"No XML namespace alias defined in WFSView for {e}") from e
99
+
100
+ def _build_xml_qnames(self) -> dict[XsdNode, str]:
101
+ """Collect aliases for all rendered elements.
102
+ This uses a defaultdict so optional attributes (e.g. by GetPropertyValue) can also be resolved.
103
+ The dict lookup also speeds up, no need to call ``node_to_qname()`` for each tag.
104
+ """
105
+ known_tags = {
106
+ xsd_element: self.to_qname(xsd_element)
107
+ for sub_collection in self.collection.results
108
+ for xsd_element in sub_collection.projection.all_elements
109
+ }
110
+ return defaultdict(self.to_qname, known_tags)
111
+
98
112
  def get_response(self):
99
113
  """Render the output as streaming response."""
100
- from gisserver.queries import GetFeatureById
101
-
102
- if isinstance(self.source_query, GetFeatureById):
114
+ if self.collection.results and self.collection.results[0].projection.output_standalone:
103
115
  # WFS spec requires that GetFeatureById output only returns the contents.
104
116
  # The streaming response is avoided here, to allow returning a 404.
105
117
  return self.get_by_id_response()
@@ -110,15 +122,18 @@ class GML32Renderer(OutputRenderer):
110
122
  def get_by_id_response(self):
111
123
  """Render a standalone item, for GetFeatureById"""
112
124
  sub_collection = self.collection.results[0]
125
+ sub_collection.source_query.finalize_results(sub_collection) # Allow 404
113
126
  self.start_collection(sub_collection)
127
+
114
128
  instance = sub_collection.first()
115
129
  if instance is None:
116
130
  raise NotFound("Feature not found.")
117
131
 
132
+ self.app_namespaces.pop(xmlns.wfs20.value) # not rendering wfs tags.
118
133
  self.output = StringIO()
119
134
  self._write = self.output.write
120
135
  self.write_by_id_response(
121
- sub_collection, instance, extra_xmlns=self.render_xmlns_standalone()
136
+ sub_collection, instance, extra_xmlns=f" {self.render_xmlns_attributes()}"
122
137
  )
123
138
  content = self.output.getvalue()
124
139
  return HttpResponse(content, content_type=self.content_type)
@@ -127,40 +142,59 @@ class GML32Renderer(OutputRenderer):
127
142
  """Default behavior for standalone response is writing a feature (can be changed by GetPropertyValue)"""
128
143
  self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
129
144
  self.write_feature(
130
- feature_type=sub_collection.feature_type,
145
+ projection=sub_collection.projection,
131
146
  instance=instance,
132
147
  extra_xmlns=extra_xmlns,
133
148
  )
134
149
 
135
- def render_xmlns(self):
136
- """Generate the xmlns block that the document needs"""
137
- xsd_typenames = ",".join(
138
- sub_collection.feature_type.name for sub_collection in self.collection.results
139
- )
140
- schema_location = [
141
- f"{self.app_xml_namespace} {self.server_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES={xsd_typenames}", # noqa: E501
150
+ def render_xsi_schema_location(self):
151
+ """Render the value for the xsi:schemaLocation="..." block."""
152
+ # Find which namespaces are exposed
153
+ types_by_namespace = defaultdict(list)
154
+ for feature_type in self.feature_types:
155
+ types_by_namespace[feature_type.xml_namespace].append(feature_type)
156
+
157
+ # Schema location are pairs of "{namespace-uri} {schema-url} {namespace-uri2} {schema-url2}"
158
+ # WFS server types refer to the endpoint that generates the XML Schema for the given feature.
159
+ schema_locations = []
160
+ for xml_namespace, feature_types in types_by_namespace.items():
161
+ schema_locations.append(xml_namespace)
162
+ schema_locations.append(self.operation.view.get_xml_schema_url(feature_types))
163
+
164
+ schema_locations += [
142
165
  "http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd",
143
166
  "http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd",
144
167
  ]
168
+ return " ".join(schema_locations)
145
169
 
146
- return (
147
- 'xmlns:wfs="http://www.opengis.net/wfs/2.0" '
148
- 'xmlns:gml="http://www.opengis.net/gml/3.2" '
149
- 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
150
- 'xmlns:app="{app_xml_namespace}" '
151
- 'xsi:schemaLocation="{schema_location}"'
152
- ).format(
153
- app_xml_namespace=_attr_escape(self.app_xml_namespace),
154
- schema_location=_attr_escape(" ".join(schema_location)),
155
- )
170
+ def render_exception(self, exception: Exception):
171
+ """Render the exception in a format that fits with the output.
156
172
 
157
- def render_xmlns_standalone(self):
158
- """Generate the xmlns block that the document needs"""
159
- # xsi is needed for "xsi:nil="true"' attributes.
173
+ The WSF XSD spec has a hidden gem: an <wfs:truncatedResponse> element
174
+ can be rendered at the end of a feature collection
175
+ to inform the client an error happened during rendering.
176
+ """
177
+ message = super().render_exception(exception)
178
+ buffer = self.output.getvalue()
179
+
180
+ # Wrap into <ows:ExceptionReport> tag.
181
+ if not isinstance(exception, WFSException):
182
+ exception = WFSException(message, code=exception.__class__.__name__, status_code=500)
183
+ exception.debug_hint = False
184
+
185
+ # Only at the top-level, an exception report can be rendered
186
+ closing_child = (
187
+ f"</wfs:{self.xml_sub_collection_tag}></wfs:member>\n"
188
+ if len(self.collection.results) > 1
189
+ else ""
190
+ )
160
191
  return (
161
- f' xmlns:app="{_attr_escape(self.app_xml_namespace)}"'
162
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
163
- ' xmlns:gml="http://www.opengis.net/gml/3.2"'
192
+ f"{buffer}"
193
+ f"{closing_child}"
194
+ " <wfs:truncatedResponse>"
195
+ f"{exception.as_xml()}"
196
+ " </wfs:truncatedResponse>\n"
197
+ f"</wfs:{self.xml_collection_tag}>\n"
164
198
  )
165
199
 
166
200
  def render_stream(self):
@@ -170,19 +204,19 @@ class GML32Renderer(OutputRenderer):
170
204
  collection = self.collection
171
205
  self.output = output = StringIO()
172
206
  self._write = self.output.write
173
- xmlns = self.render_xmlns().strip()
174
207
  number_matched = collection.number_matched
175
208
  number_matched = int(number_matched) if number_matched is not None else "unknown"
176
209
  number_returned = collection.number_returned
177
210
  next = previous = ""
178
211
  if collection.next:
179
- next = f' next="{_attr_escape(collection.next)}"'
212
+ next = f' next="{attr_escape(collection.next)}"'
180
213
  if collection.previous:
181
- previous = f' previous="{_attr_escape(collection.previous)}"'
214
+ previous = f' previous="{attr_escape(collection.previous)}"'
182
215
 
183
216
  self._write(
184
217
  f"""<?xml version='1.0' encoding="UTF-8" ?>\n"""
185
- f"<wfs:{self.xml_collection_tag} {xmlns}"
218
+ f"<wfs:{self.xml_collection_tag} {self.render_xmlns_attributes()}"
219
+ f' xsi:schemaLocation="{attr_escape(self.render_xsi_schema_location())}"'
186
220
  f' timeStamp="{collection.timestamp}"'
187
221
  f' numberMatched="{number_matched}"'
188
222
  f' numberReturned="{int(number_returned)}"'
@@ -193,11 +227,12 @@ class GML32Renderer(OutputRenderer):
193
227
  has_multiple_collections = len(collection.results) > 1
194
228
 
195
229
  for sub_collection in collection.results:
230
+ projection = sub_collection.projection
196
231
  self.start_collection(sub_collection)
197
232
  if has_multiple_collections:
198
233
  self._write(
199
234
  f"<wfs:member>\n"
200
- f"<wfs:{self.xml_collection_tag}"
235
+ f"<wfs:{self.xml_sub_collection_tag}"
201
236
  f' timeStamp="{collection.timestamp}"'
202
237
  f' numberMatched="{int(sub_collection.number_matched)}"'
203
238
  f' numberReturned="{int(sub_collection.number_returned)}">\n'
@@ -206,7 +241,7 @@ class GML32Renderer(OutputRenderer):
206
241
  for instance in sub_collection:
207
242
  self.gml_seq = 0 # need to increment this between write_xml_field calls
208
243
  self._write("<wfs:member>\n")
209
- self.write_feature(sub_collection.feature_type, instance)
244
+ self.write_feature(projection, instance)
210
245
  self._write("</wfs:member>\n")
211
246
 
212
247
  # Only perform a 'yield' every once in a while,
@@ -218,7 +253,7 @@ class GML32Renderer(OutputRenderer):
218
253
  yield xml_chunk
219
254
 
220
255
  if has_multiple_collections:
221
- self._write(f"</wfs:{self.xml_collection_tag}>\n</wfs:member>\n")
256
+ self._write(f"</wfs:{self.xml_sub_collection_tag}>\n</wfs:member>\n")
222
257
 
223
258
  self._write(f"</wfs:{self.xml_collection_tag}>\n")
224
259
  yield output.getvalue()
@@ -227,86 +262,66 @@ class GML32Renderer(OutputRenderer):
227
262
  """Hook to allow initialization per feature type"""
228
263
 
229
264
  def write_feature(
230
- self, feature_type: FeatureType, instance: models.Model, extra_xmlns=""
265
+ self, projection: FeatureProjection, instance: models.Model, extra_xmlns=""
231
266
  ) -> None:
232
267
  """Write the contents of the object value.
233
268
 
234
269
  This output is typically wrapped in <wfs:member> tags
235
270
  unless it's used for a GetPropertyById response.
236
271
  """
237
- # Write <app:FeatureTypeName> start node
238
- pk = _tag_escape(str(instance.pk))
239
- self._write(f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
272
+ feature_type = projection.feature_type
273
+ feature_xml_qname = self.feature_qnames[feature_type]
240
274
 
275
+ # Write <app:FeatureTypeName> start node
276
+ pk = tag_escape(str(instance.pk))
277
+ self._write(f'<{feature_xml_qname} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
241
278
  # Write all fields, both base class and local elements.
242
- for xsd_element in feature_type.xsd_type.all_elements:
279
+ for xsd_element in projection.xsd_root_elements:
243
280
  # Note that writing 5000 features with 30 tags means this code make 150.000 method calls.
244
281
  # Hence, branching to different rendering styles is branched here instead of making such
245
282
  # call into a generic "write_field()" function and stepping out from there.
246
- if xsd_element.is_geometry:
247
- if xsd_element.xml_name == "gml:boundedBy":
248
- # Special case for <gml:boundedBy>, so it will render with
249
- # the output CRS and can be overwritten with DB-rendered GML.
250
- self.write_bounds(feature_type, instance)
251
- else:
252
- # Separate call which can be optimized (no need to overload write_xml_field() for all calls).
253
- self.write_gml_field(feature_type, xsd_element, instance)
283
+
284
+ if xsd_element.type.is_geometry:
285
+ # Separate call which can be optimized (no need to overload write_xml_field() for all calls).
286
+ self.write_gml_field(projection, xsd_element, instance)
254
287
  else:
255
288
  value = xsd_element.get_value(instance)
256
289
  if xsd_element.is_many:
257
- self.write_many(feature_type, xsd_element, value)
290
+ self.write_many(projection, xsd_element, value)
258
291
  else:
259
292
  # e.g. <gml:name>, or all other <app:...> nodes.
260
- self.write_xml_field(feature_type, xsd_element, value)
261
-
262
- self._write(f"</{feature_type.xml_name}>\n")
263
-
264
- def write_bounds(self, feature_type, instance) -> None:
265
- """Render the GML bounds for the complete instance"""
266
- envelope = feature_type.get_envelope(instance, self.output_crs)
267
- if envelope is not None:
268
- lower = " ".join(map(str, envelope.lower_corner))
269
- upper = " ".join(map(str, envelope.upper_corner))
270
- self._write(
271
- f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{self.xml_srs_name}">
272
- <gml:lowerCorner>{lower}</gml:lowerCorner>
273
- <gml:upperCorner>{upper}</gml:upperCorner>
274
- </gml:Envelope></gml:boundedBy>\n"""
275
- )
293
+ self.write_xml_field(projection, xsd_element, value)
294
+
295
+ self._write(f"</{feature_xml_qname}>\n")
276
296
 
277
- def write_many(self, feature_type: FeatureType, xsd_element: XsdElement, value) -> None:
297
+ def write_many(self, projection: FeatureProjection, xsd_element: XsdElement, value) -> None:
278
298
  """Write a node that has multiple values (e.g. array or queryset)."""
279
299
  # some <app:...> node that has multiple values
280
300
  if value is None:
281
301
  # No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
282
302
  if xsd_element.min_occurs:
283
- self._write(f'<{xsd_element.xml_name} xsi:nil="true"/>\n')
303
+ xml_qname = self.xml_qnames[xsd_element]
304
+ self._write(f'<{xml_qname} xsi:nil="true"/>\n')
284
305
  else:
285
- # Render the tag multiple times
286
- if xsd_element.type.is_complex_type:
287
- # If the retrieved QuerySet was not filtered yet, do so now. This can't
288
- # be done in get_value() because the FeatureType is not known there.
289
- value = feature_type.filter_related_queryset(value)
290
-
291
306
  for item in value:
292
- self.write_xml_field(feature_type, xsd_element, value=item)
307
+ self.write_xml_field(projection, xsd_element, value=item)
293
308
 
294
309
  def write_xml_field(
295
- self, feature_type: FeatureType, xsd_element: XsdElement, value, extra_xmlns=""
310
+ self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
296
311
  ):
297
312
  """Write the value of a single field."""
298
- xml_name = xsd_element.xml_name
313
+ xml_qname = self.xml_qnames[xsd_element]
299
314
  if value is None:
300
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
315
+ self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
301
316
  elif xsd_element.type.is_complex_type:
302
317
  # Expanded foreign relation / dictionary
303
- self.write_xml_complex_type(feature_type, xsd_element, value, extra_xmlns=extra_xmlns)
318
+ self.write_xml_complex_type(projection, xsd_element, value, extra_xmlns=extra_xmlns)
304
319
  else:
305
320
  # As this is likely called 150.000 times during a request, this is optimized.
306
- # Avoided a separate call to _value_to_xml_string() and avoided isinstance() here.
321
+ # Avoided a separate call to value_to_xml_string() and avoided isinstance() here.
307
322
  value_cls = value.__class__
308
323
  if value_cls is str: # most cases
309
- value = _tag_escape(value)
324
+ value = tag_escape(value)
310
325
  elif value_cls is datetime:
311
326
  value = value.astimezone(timezone.utc).isoformat()
312
327
  elif value_cls is bool:
@@ -320,49 +335,88 @@ class GML32Renderer(OutputRenderer):
320
335
  ):
321
336
  # Non-string or custom field that extended a scalar.
322
337
  # Any of the other types have a faster f"{value}" translation that produces the correct text.
323
- value = _value_to_xml_string(value)
338
+ value = value_to_xml_string(value)
324
339
 
325
- self._write(f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n")
340
+ self._write(f"<{xml_qname}{extra_xmlns}>{value}</{xml_qname}>\n")
326
341
 
327
- def write_xml_complex_type(self, feature_type, xsd_element, value, extra_xmlns="") -> None:
342
+ def write_xml_complex_type(
343
+ self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
344
+ ) -> None:
328
345
  """Write a single field, that consists of sub elements"""
329
- xsd_type = cast(XsdComplexType, xsd_element.type)
330
- self._write(f"<{xsd_element.xml_name}{extra_xmlns}>\n")
331
- for sub_element in xsd_type.elements:
332
- sub_value = sub_element.get_value(value)
333
- if sub_element.is_many:
334
- self.write_many(feature_type, sub_element, sub_value)
346
+ xml_qname = self.xml_qnames[xsd_element]
347
+ self._write(f"<{xml_qname}{extra_xmlns}>\n")
348
+ for sub_element in projection.xsd_child_nodes[xsd_element]:
349
+ if sub_element.type.is_geometry:
350
+ # Separate call which can be optimized (no need to overload write_xml_field() for all calls).
351
+ self.write_gml_field(projection, sub_element, value)
335
352
  else:
336
- self.write_xml_field(feature_type, sub_element, sub_value)
337
- self._write(f"</{xsd_element.xml_name}>\n")
353
+ sub_value = sub_element.get_value(value)
354
+ if sub_element.is_many:
355
+ self.write_many(projection, sub_element, sub_value)
356
+ else:
357
+ self.write_xml_field(projection, sub_element, sub_value)
358
+ self._write(f"</{xml_qname}>\n")
338
359
 
339
360
  def write_gml_field(
340
- self, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
361
+ self,
362
+ projection: FeatureProjection,
363
+ geo_element: GeometryXsdElement,
364
+ instance: models.Model,
365
+ extra_xmlns="",
341
366
  ) -> None:
342
367
  """Separate method to allow overriding this for db-performance optimizations."""
343
- # Need to have instance.pk data here (and instance['pk'] for value rendering)
344
- value = xsd_element.get_value(instance)
345
- xml_name = xsd_element.xml_name
346
- if value is None:
347
- # Avoid incrementing gml_seq
348
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
349
- return
368
+ if geo_element.type is XsdTypes.gmlBoundingShapeType:
369
+ # Special case for <gml:boundedBy>, which doesn't need xsd:nil values, nor a gml:id.
370
+ # The value is not a GEOSGeometry either, as that is not exposed by django.contrib.gis.
371
+ value = cast(GmlBoundedByElement, geo_element).get_value(
372
+ instance, crs=projection.output_crs
373
+ )
374
+ if value is not None:
375
+ self._write(self.render_gml_bounds(value))
376
+ else:
377
+ # Regular geometry elements.
378
+ xml_qname = self.xml_qnames[geo_element]
379
+ value = geo_element.get_value(instance)
380
+ if value is None:
381
+ # Avoid incrementing gml_seq
382
+ self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
383
+ else:
384
+ gml_id = self.get_gml_id(instance._meta.object_name, instance.pk)
350
385
 
351
- gml_id = self.get_gml_id(feature_type, instance.pk)
386
+ # the following is somewhat faster, but will render GML 2, not GML 3.2:
387
+ # gml = value.ogr.gml
388
+ # pos = gml.find(">") # Will inject the gml:id="..." tag.
389
+ # gml = f"{gml[:pos]} gml:id="{attr_escape(gml_id)}"{gml[pos:]}"
352
390
 
353
- # the following is somewhat faster, but will render GML 2, not GML 3.2:
354
- # gml = value.ogr.gml
355
- # pos = gml.find(">") # Will inject the gml:id="..." tag.
356
- # gml = f"{gml[:pos]} gml:id="{_attr_escape(gml_id)}"{gml[pos:]}"
391
+ gml = self.render_gml_value(projection, gml_id, value)
392
+ self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
357
393
 
358
- gml = self.render_gml_value(gml_id, value)
359
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
394
+ def get_gml_id(self, prefix: str, object_id) -> str:
395
+ """Generate the gml:id value, which is required for GML 3.2 objects."""
396
+ self.gml_seq += 1
397
+ return f"{prefix}.{object_id}.{self.gml_seq}"
360
398
 
361
- def render_gml_value(self, gml_id, value: geos.GEOSGeometry | None, extra_xmlns="") -> str:
399
+ def render_gml_bounds(self, envelope: BoundingBox) -> str:
400
+ """Render the gml:boundedBy element that contains an Envelope.
401
+ Note this uses an internal object type,
402
+ as :mod:`django.contrib.gis.geos` only provides a Point/Polygon or 4-tuple envelope.
403
+ """
404
+ return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(envelope.crs.urn)}">
405
+ <gml:lowerCorner>{envelope.south} {envelope.west}</gml:lowerCorner>
406
+ <gml:upperCorner>{envelope.north} {envelope.east}</gml:upperCorner>
407
+ </gml:Envelope></gml:boundedBy>\n"""
408
+
409
+ def render_gml_value(
410
+ self,
411
+ projection: FeatureProjection,
412
+ gml_id,
413
+ value: geos.GEOSGeometry | None,
414
+ extra_xmlns="",
415
+ ) -> str:
362
416
  """Normal case: 'value' is raw geometry data.."""
363
417
  # In case this is a standalone response, this will be the top-level element, hence includes the xmlns.
364
- base_attrs = f' gml:id="{_attr_escape(gml_id)}" srsName="{self.xml_srs_name}"{extra_xmlns}'
365
- self.output_crs.apply_to(value)
418
+ base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(projection.output_crs.urn)}"{extra_xmlns}'
419
+ projection.output_crs.apply_to(value)
366
420
  return self._render_gml_type(value, base_attrs=base_attrs)
367
421
 
368
422
  def _render_gml_type(self, value: geos.GEOSGeometry, base_attrs=""):
@@ -397,11 +451,16 @@ class GML32Renderer(OutputRenderer):
397
451
  buf.write("</gml:Polygon>")
398
452
  return buf.getvalue()
399
453
 
400
- @register_geos_type(geos.MultiPolygon)
454
+ @register_geos_type(geos.GeometryCollection)
401
455
  def render_gml_multi_geometry(self, value: geos.GeometryCollection, base_attrs):
402
456
  children = "".join(self._render_gml_type(child) for child in value)
403
457
  return f"<gml:MultiGeometry{base_attrs}>{children}</gml:MultiGeometry>"
404
458
 
459
+ @register_geos_type(geos.MultiPolygon)
460
+ def render_gml_multi_polygon(self, value: geos.GeometryCollection, base_attrs):
461
+ children = "".join(self._render_gml_type(child) for child in value)
462
+ return f"<gml:MultiPolygon{base_attrs}>{children}</gml:MultiPolygon>"
463
+
405
464
  @register_geos_type(geos.MultiLineString)
406
465
  def render_gml_multi_line_string(self, value: geos.MultiPoint, base_attrs):
407
466
  children = "</gml:lineStringMember><gml:lineStringMember>".join(
@@ -447,15 +506,10 @@ class GML32Renderer(OutputRenderer):
447
506
  "</gml:LineString>"
448
507
  )
449
508
 
450
- def get_gml_id(self, feature_type: FeatureType, object_id) -> str:
451
- """Generate the gml:id value, which is required for GML 3.2 objects."""
452
- self.gml_seq += 1
453
- return f"{feature_type.name}.{object_id}.{self.gml_seq}"
454
-
455
509
 
456
510
  class DBGMLRenderingMixin:
457
511
 
458
- def render_gml_value(self, gml_id, value: str, extra_xmlns=""):
512
+ def render_gml_value(self, projection, gml_id, value: str, extra_xmlns=""):
459
513
  """DB optimized: 'value' is pre-rendered GML XML string."""
460
514
  # Write the gml:id inside the first tag
461
515
  end_pos = value.find(">")
@@ -463,13 +517,13 @@ class DBGMLRenderingMixin:
463
517
  id_pos = gml_tag.find("gml:id=")
464
518
  if id_pos == -1:
465
519
  # Inject
466
- return f'{gml_tag} gml:id="{_attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
520
+ return f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
467
521
  else:
468
522
  # Replace
469
523
  end_pos1 = gml_tag.find('"', id_pos + 8)
470
524
  return (
471
525
  f"{gml_tag[:id_pos]}"
472
- f'gml:id="{_attr_escape(gml_id)}'
526
+ f'gml:id="{attr_escape(gml_id)}'
473
527
  f"{value[end_pos1:end_pos]}" # from " right until >
474
528
  f"{extra_xmlns}" # extra namespaces?
475
529
  f"{value[end_pos:]}" # from > and beyond
@@ -479,111 +533,89 @@ class DBGMLRenderingMixin:
479
533
  class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
480
534
  """Faster GetFeature renderer that uses the database to render GML 3.2"""
481
535
 
482
- @classmethod
483
- def decorate_queryset(
484
- cls,
485
- feature_type: FeatureType,
486
- queryset: models.QuerySet,
487
- output_crs: CRS,
488
- **params,
489
- ):
536
+ def decorate_queryset(self, projection: FeatureProjection, queryset: models.QuerySet):
490
537
  """Update the queryset to let the database render the GML output.
491
- This is far more efficient then GeoDjango's logic, which performs a
538
+ This is far more efficient than GeoDjango's logic, which performs a
492
539
  C-API call for every single coordinate of a geometry.
493
540
  """
494
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
541
+ queryset = super().decorate_queryset(projection, queryset)
495
542
 
496
- # Retrieve geometries as pre-rendered instead.
497
- gml_elements = feature_type.xsd_type.geometry_elements
498
- geo_selects = get_db_geometry_selects(gml_elements, output_crs)
499
- if geo_selects:
500
- queryset = queryset.defer(*geo_selects.keys()).annotate(
501
- _as_envelope_gml=cls.get_db_envelope_as_gml(feature_type, queryset, output_crs),
502
- **build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
543
+ # Retrieve gml:boundedBy in pre-rendered format.
544
+ if projection.has_bounded_by:
545
+ queryset = queryset.annotate(
546
+ _as_envelope_gml=self.get_db_envelope_as_gml(projection, queryset),
503
547
  )
504
548
 
505
- return queryset
549
+ # Retrieve geometries as pre-rendered instead.
550
+ # Only take the geometries of the current level.
551
+ # The annotations for relations will be handled by prefetches and get_prefetch_queryset()
552
+ return replace_queryset_geometries(
553
+ queryset, projection.geometry_elements, projection.output_crs, AsGML
554
+ )
506
555
 
507
- @classmethod
508
556
  def get_prefetch_queryset(
509
- cls,
510
- feature_type: FeatureType,
557
+ self,
558
+ projection: FeatureProjection,
511
559
  feature_relation: FeatureRelation,
512
- output_crs: CRS,
513
560
  ) -> models.QuerySet | None:
514
561
  """Perform DB annotations for prefetched relations too."""
515
- base = super().get_prefetch_queryset(feature_type, feature_relation, output_crs)
516
- if base is None:
562
+ queryset = super().get_prefetch_queryset(projection, feature_relation)
563
+ if queryset is None:
517
564
  return None
518
565
 
519
566
  # Find which fields are GML elements
520
- gml_elements = []
521
- for e in feature_relation.xsd_elements:
522
- if e.is_geometry:
523
- # Prefetching a flattened relation
524
- gml_elements.append(e)
525
- elif e.type.is_complex_type:
526
- # Prefetching a complex type
527
- xsd_type: XsdComplexType = cast(XsdComplexType, e.type)
528
- gml_elements.extend(xsd_type.geometry_elements)
529
-
530
- geometries = get_db_geometry_selects(gml_elements, output_crs)
531
- if geometries:
532
- # Exclude geometries from the fields, fetch them as pre-rendered annotations instead.
533
- return base.defer(geometries.keys()).annotate(
534
- **build_db_annotations(geometries, "_as_gml_{name}", AsGML),
535
- )
536
- else:
537
- return base
567
+ return replace_queryset_geometries(
568
+ queryset, feature_relation.geometry_elements, projection.output_crs, AsGML
569
+ )
538
570
 
539
- @classmethod
540
- def get_db_envelope_as_gml(cls, feature_type, queryset, output_crs) -> AsGML:
571
+ def get_db_envelope_as_gml(self, projection: FeatureProjection, queryset) -> AsGML:
541
572
  """Offload the GML rendering of the envelope to the database.
542
573
 
543
574
  This also avoids offloads the geometry union calculation to the DB.
544
575
  """
545
- geo_fields_union = cls._get_geometries_union(feature_type, queryset, output_crs)
576
+ geo_fields_union = self._get_geometries_union(projection, queryset)
546
577
  return AsGML(geo_fields_union, envelope=True)
547
578
 
548
- @classmethod
549
- def _get_geometries_union(cls, feature_type: FeatureType, queryset, output_crs):
579
+ def _get_geometries_union(self, projection: FeatureProjection, queryset):
550
580
  """Combine all geometries of the model in a single SQL function."""
551
581
  # Apply transforms where needed, in case some geometries use a different SRID.
552
582
  return get_geometries_union(
553
583
  [
554
- conditional_transform(
555
- model_field.name,
556
- model_field.srid,
557
- output_srid=output_crs.srid,
558
- )
559
- for model_field in feature_type.geometry_fields
584
+ get_db_geometry_target(geo_element, output_crs=projection.output_crs)
585
+ for geo_element in projection.all_geometry_elements
586
+ if geo_element.source is not None # excludes GmlBoundedByElement
560
587
  ],
561
588
  using=queryset.db,
562
589
  )
563
590
 
564
591
  def write_gml_field(
565
- self, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
592
+ self,
593
+ projection: FeatureProjection,
594
+ geo_element: GeometryXsdElement,
595
+ instance: models.Model,
596
+ extra_xmlns="",
566
597
  ) -> None:
567
598
  """Write the value of an GML tag.
568
599
 
569
600
  This optimized version takes a pre-rendered XML from the database query.
570
601
  """
571
- value = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
572
- xml_name = xsd_element.xml_name
573
- if value is None:
574
- # Avoid incrementing gml_seq
575
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
576
- return
602
+ xml_qname = self.xml_qnames[geo_element]
603
+ if geo_element.type is XsdTypes.gmlBoundingShapeType:
604
+ gml = instance._as_envelope_gml
605
+ if gml is None:
606
+ return
607
+ else:
608
+ value = get_db_rendered_geometry(instance, geo_element, AsGML)
609
+ if value is None:
610
+ # Avoid incrementing gml_seq, make nil tag.
611
+ self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
612
+ return
577
613
 
578
- gml_id = self.get_gml_id(feature_type, instance.pk)
579
- gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
580
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
614
+ # Get gml tag to write as value.
615
+ gml_id = self.get_gml_id(instance._meta.object_name, instance.pk)
616
+ gml = self.render_gml_value(projection, gml_id, value, extra_xmlns=extra_xmlns)
581
617
 
582
- def write_bounds(self, feature_type, instance) -> None:
583
- """Generate the <gml:boundedBy> from DB prerendering."""
584
- gml = instance._as_envelope_gml
585
- if gml is not None:
586
- self._write(f"<gml:boundedBy>{gml}</gml:boundedBy>\n")
618
+ self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
587
619
 
588
620
 
589
621
  class GML32ValueRenderer(GML32Renderer):
@@ -596,67 +628,55 @@ class GML32ValueRenderer(GML32Renderer):
596
628
 
597
629
  content_type = "text/xml; charset=utf-8"
598
630
  content_type_plain = "text/plain; charset=utf-8"
631
+ content_disposition = 'inline; filename="{typenames} {page} {date}-value.xml"'
632
+ content_disposition_plain = 'inline; filename="{typenames} {page} {date}-value.txt"'
599
633
  xml_collection_tag = "ValueCollection"
600
- _escape_value = staticmethod(_value_to_xml_string)
634
+ xml_sub_collection_tag = "ValueCollection"
635
+ _escape_value = staticmethod(value_to_xml_string)
601
636
  gml_value_getter = itemgetter("member")
602
637
 
603
- def __init__(self, *args, value_reference: ValueReference, **kwargs):
604
- self.value_reference = value_reference
605
- super().__init__(*args, **kwargs)
606
- self.xsd_node: XsdNode | None = None
607
-
608
- @classmethod
609
- def decorate_queryset(
610
- cls,
611
- feature_type: FeatureType,
612
- queryset: models.QuerySet,
613
- output_crs: CRS,
614
- **params,
615
- ):
638
+ def decorate_queryset(self, projection: FeatureProjection, queryset: models.QuerySet):
616
639
  # Don't optimize queryset, it only retrieves one value
617
640
  # The data is already limited to a ``queryset.values()`` in ``QueryExpression.get_queryset()``.
618
641
  return queryset
619
642
 
620
- def start_collection(self, sub_collection: SimpleFeatureCollection):
621
- # Resolve which XsdNode is being rendered
622
- match = sub_collection.feature_type.resolve_element(self.value_reference.xpath)
623
- self.xsd_node = match.child
624
-
625
643
  def write_by_id_response(
626
644
  self, sub_collection: SimpleFeatureCollection, instance: dict, extra_xmlns
627
645
  ):
628
646
  """The value rendering only renders the value. not a complete feature"""
629
- if self.xsd_node.is_attribute:
647
+ if sub_collection.projection.property_value_node.is_attribute:
630
648
  # Output as plain text
631
649
  self.content_type = self.content_type_plain # change for this instance!
632
- self._escape_value = _value_to_text # avoid XML escaping
650
+ self.content_disposition = self.content_disposition_plain
651
+ self._escape_value = value_to_text # avoid XML escaping
633
652
  else:
634
653
  self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
635
654
 
636
655
  # Write the single tag, no <wfs:member> around it.
637
- self.write_feature(sub_collection.feature_type, instance, extra_xmlns=extra_xmlns)
656
+ self.write_feature(sub_collection.projection, instance, extra_xmlns=extra_xmlns)
638
657
 
639
- def write_feature(self, feature_type: FeatureType, instance: dict, extra_xmlns="") -> None:
658
+ def write_feature(self, projection: FeatureProjection, instance: dict, extra_xmlns="") -> None:
640
659
  """Write the XML for a single object.
641
660
  In this case, it's only a single XML tag.
642
661
  """
643
- if self.xsd_node.is_geometry:
662
+ xsd_node = projection.property_value_node
663
+ if xsd_node.type.is_geometry:
644
664
  self.write_gml_field(
645
- feature_type,
646
- cast(XsdElement, self.xsd_node),
665
+ projection,
666
+ cast(GeometryXsdElement, xsd_node),
647
667
  instance,
648
668
  extra_xmlns=extra_xmlns,
649
669
  )
650
- elif self.xsd_node.is_attribute:
670
+ elif xsd_node.is_attribute:
651
671
  value = instance["member"]
652
672
  if value is not None:
653
- value = self.xsd_node.format_raw_value(instance["member"]) # for gml:id
673
+ value = xsd_node.format_raw_value(instance["member"]) # for gml:id
654
674
  value = self._escape_value(value)
655
675
  self._write(value)
656
- elif self.xsd_node.is_array:
676
+ elif xsd_node.is_array:
657
677
  if (value := instance["member"]) is not None:
658
678
  # <wfs:member> doesn't allow multiple items as children, for new render as separate members.
659
- xml_name = self.xsd_node.xml_name
679
+ xml_qname = self.xml_qnames[xsd_node]
660
680
  first = True
661
681
  for item in value:
662
682
  if item is not None:
@@ -664,33 +684,42 @@ class GML32ValueRenderer(GML32Renderer):
664
684
  self._write("</wfs:member>\n<wfs:member>")
665
685
 
666
686
  item = self._escape_value(item)
667
- self._write(f"<{xml_name}>{item}</{xml_name}>\n")
687
+ self._write(f"<{xml_qname}>{item}</{xml_qname}>\n")
668
688
  first = False
669
- elif self.xsd_node.type.is_complex_type:
689
+ elif xsd_node.type.is_complex_type:
670
690
  raise NotImplementedError("GetPropertyValue with complex types is not implemented")
671
- # self.write_xml_complex_type(feature_type, self.xsd_node, instance['member'])
691
+ # self.write_xml_complex_type(projection, self.xsd_node, instance['member'])
672
692
  else:
673
693
  self.write_xml_field(
674
- feature_type,
675
- cast(XsdElement, self.xsd_node),
694
+ projection,
695
+ cast(XsdElement, xsd_node),
676
696
  value=instance["member"],
677
697
  extra_xmlns=extra_xmlns,
678
698
  )
679
699
 
680
700
  def write_gml_field(
681
- self, feature_type, xsd_element: XsdElement, instance: dict, extra_xmlns=""
701
+ self,
702
+ projection: FeatureProjection,
703
+ geo_element: GeometryXsdElement,
704
+ instance: dict,
705
+ extra_xmlns="",
682
706
  ) -> None:
683
707
  """Overwritten to allow dict access instead of model access."""
708
+ if geo_element.type is XsdTypes.gmlBoundingShapeType:
709
+ raise NotImplementedError(
710
+ "rendering <gml:boundedBy> in GetPropertyValue is not implemented."
711
+ )
712
+
684
713
  value = self.gml_value_getter(instance) # "member" or "gml_member"
685
- xml_name = xsd_element.xml_name
714
+ xml_qname = self.xml_qnames[geo_element]
686
715
  if value is None:
687
716
  # Avoid incrementing gml_seq
688
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
717
+ self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
689
718
  return
690
719
 
691
- gml_id = self.get_gml_id(feature_type, instance["pk"])
692
- gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
693
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
720
+ gml_id = self.get_gml_id(geo_element.source.model._meta.object_name, instance["pk"])
721
+ gml = self.render_gml_value(projection, gml_id, value, extra_xmlns=extra_xmlns)
722
+ self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
694
723
 
695
724
 
696
725
  class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
@@ -698,15 +727,15 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
698
727
 
699
728
  gml_value_getter = itemgetter("gml_member")
700
729
 
701
- @classmethod
702
- def decorate_queryset(cls, feature_type: FeatureType, queryset, output_crs, **params):
730
+ def decorate_queryset(self, projection: FeatureProjection, queryset):
703
731
  """Update the queryset to let the database render the GML output."""
704
- value_reference = params["valueReference"]
705
- match = feature_type.resolve_element(value_reference.xpath)
706
- if match.child.is_geometry:
732
+ # As this is a classmethod, self.value_reference is not available yet.
733
+ element = projection.property_value_node
734
+ if element.type.is_geometry:
707
735
  # Add 'gml_member' to point to the pre-rendered GML version.
736
+ geo_element = cast(GeometryXsdElement, element)
708
737
  return queryset.values(
709
- "pk", gml_member=AsGML(get_db_geometry_target(match, output_crs))
738
+ "pk", gml_member=AsGML(get_db_geometry_target(geo_element, projection.output_crs))
710
739
  )
711
740
  else:
712
741
  return queryset