django-gisserver 1.2.7__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/METADATA +12 -12
- django_gisserver-1.4.0.dist-info/RECORD +54 -0
- {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/conf.py +9 -12
- gisserver/db.py +6 -10
- gisserver/exceptions.py +1 -0
- gisserver/features.py +18 -29
- gisserver/geometries.py +11 -25
- gisserver/operations/base.py +19 -40
- gisserver/operations/wfs20.py +8 -20
- gisserver/output/__init__.py +7 -2
- gisserver/output/base.py +4 -13
- gisserver/output/csv.py +15 -19
- gisserver/output/geojson.py +16 -20
- gisserver/output/gml32.py +310 -283
- gisserver/output/gml32_lxml.py +612 -0
- gisserver/output/results.py +107 -22
- gisserver/output/utils.py +15 -5
- gisserver/output/xmlschema.py +7 -8
- gisserver/parsers/base.py +2 -4
- gisserver/parsers/fes20/expressions.py +6 -13
- gisserver/parsers/fes20/filters.py +6 -5
- gisserver/parsers/fes20/functions.py +4 -4
- gisserver/parsers/fes20/identifiers.py +1 -0
- gisserver/parsers/fes20/operators.py +16 -43
- gisserver/parsers/fes20/query.py +1 -3
- gisserver/parsers/fes20/sorting.py +1 -3
- gisserver/parsers/gml/__init__.py +1 -0
- gisserver/parsers/gml/base.py +1 -0
- gisserver/parsers/values.py +1 -3
- gisserver/queries/__init__.py +1 -0
- gisserver/queries/adhoc.py +3 -6
- gisserver/queries/base.py +2 -6
- gisserver/queries/stored.py +3 -6
- gisserver/types.py +59 -46
- gisserver/views.py +7 -10
- django_gisserver-1.2.7.dist-info/RECORD +0 -54
- gisserver/output/buffer.py +0 -64
- {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/top_level.txt +0 -0
gisserver/output/gml32.py
CHANGED
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Note that the Django format_html() / mark_safe() logic is not used here,
|
|
4
4
|
as it's quite a performance improvement to just use html.escape().
|
|
5
|
+
|
|
6
|
+
We've tried replacing this code with lxml and that turned out to be much slower.
|
|
7
|
+
As some functions will be called 5000x, this code is also designed to avoid making
|
|
8
|
+
much extra method calls per field. Some bits are non-DRY inlined for this reason.
|
|
5
9
|
"""
|
|
10
|
+
|
|
6
11
|
from __future__ import annotations
|
|
7
12
|
|
|
8
13
|
import itertools
|
|
9
|
-
import
|
|
10
|
-
from
|
|
11
|
-
from
|
|
14
|
+
from datetime import date, datetime, time, timezone
|
|
15
|
+
from decimal import Decimal as D
|
|
16
|
+
from io import StringIO
|
|
17
|
+
from operator import itemgetter
|
|
12
18
|
from typing import cast
|
|
13
19
|
|
|
14
20
|
from django.contrib.gis import geos
|
|
15
21
|
from django.db import models
|
|
16
22
|
from django.http import HttpResponse
|
|
17
|
-
from django.utils.timezone import utc
|
|
18
23
|
|
|
19
24
|
from gisserver.db import (
|
|
20
25
|
AsGML,
|
|
@@ -29,21 +34,13 @@ from gisserver.exceptions import NotFound
|
|
|
29
34
|
from gisserver.features import FeatureRelation, FeatureType
|
|
30
35
|
from gisserver.geometries import CRS
|
|
31
36
|
from gisserver.parsers.fes20 import ValueReference
|
|
32
|
-
from gisserver.types import XsdComplexType, XsdElement
|
|
37
|
+
from gisserver.types import XsdComplexType, XsdElement, XsdNode
|
|
33
38
|
|
|
34
39
|
from .base import OutputRenderer
|
|
35
|
-
from .buffer import StringBuffer
|
|
36
40
|
from .results import SimpleFeatureCollection
|
|
37
41
|
|
|
38
42
|
GML_RENDER_FUNCTIONS = {}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def default_if_none(value, default):
|
|
43
|
-
if value is None:
|
|
44
|
-
return default
|
|
45
|
-
else:
|
|
46
|
-
return value
|
|
43
|
+
AUTO_STR = (int, float, D, date, time)
|
|
47
44
|
|
|
48
45
|
|
|
49
46
|
def register_geos_type(geos_type):
|
|
@@ -54,11 +51,49 @@ def register_geos_type(geos_type):
|
|
|
54
51
|
return _inc
|
|
55
52
|
|
|
56
53
|
|
|
54
|
+
def _tag_escape(s: str):
|
|
55
|
+
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
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("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
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
|
+
|
|
57
90
|
class GML32Renderer(OutputRenderer):
|
|
58
91
|
"""Render the GetFeature XML output in GML 3.2 format"""
|
|
59
92
|
|
|
60
93
|
content_type = "text/xml; charset=utf-8"
|
|
61
94
|
xml_collection_tag = "FeatureCollection"
|
|
95
|
+
chunk_size = 40_000
|
|
96
|
+
gml_seq = 0
|
|
62
97
|
|
|
63
98
|
def get_response(self):
|
|
64
99
|
"""Render the output as streaming response."""
|
|
@@ -67,12 +102,12 @@ class GML32Renderer(OutputRenderer):
|
|
|
67
102
|
if isinstance(self.source_query, GetFeatureById):
|
|
68
103
|
# WFS spec requires that GetFeatureById output only returns the contents.
|
|
69
104
|
# The streaming response is avoided here, to allow returning a 404.
|
|
70
|
-
return self.
|
|
105
|
+
return self.get_by_id_response()
|
|
71
106
|
else:
|
|
72
107
|
# Use default streaming response, with render_stream()
|
|
73
108
|
return super().get_response()
|
|
74
109
|
|
|
75
|
-
def
|
|
110
|
+
def get_by_id_response(self):
|
|
76
111
|
"""Render a standalone item, for GetFeatureById"""
|
|
77
112
|
sub_collection = self.collection.results[0]
|
|
78
113
|
self.start_collection(sub_collection)
|
|
@@ -80,27 +115,27 @@ class GML32Renderer(OutputRenderer):
|
|
|
80
115
|
if instance is None:
|
|
81
116
|
raise NotFound("Feature not found.")
|
|
82
117
|
|
|
83
|
-
|
|
118
|
+
self.output = StringIO()
|
|
119
|
+
self._write = self.output.write
|
|
120
|
+
self.write_by_id_response(
|
|
121
|
+
sub_collection, instance, extra_xmlns=self.render_xmlns_standalone()
|
|
122
|
+
)
|
|
123
|
+
content = self.output.getvalue()
|
|
124
|
+
return HttpResponse(content, content_type=self.content_type)
|
|
125
|
+
|
|
126
|
+
def write_by_id_response(self, sub_collection: SimpleFeatureCollection, instance, extra_xmlns):
|
|
127
|
+
"""Default behavior for standalone response is writing a feature (can be changed by GetPropertyValue)"""
|
|
128
|
+
self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
129
|
+
self.write_feature(
|
|
84
130
|
feature_type=sub_collection.feature_type,
|
|
85
131
|
instance=instance,
|
|
86
|
-
extra_xmlns=
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
if body.startswith("<"):
|
|
90
|
-
return HttpResponse(
|
|
91
|
-
content=f'<?xml version="1.0" encoding="UTF-8"?>\n{body}',
|
|
92
|
-
content_type=self.content_type,
|
|
93
|
-
)
|
|
94
|
-
else:
|
|
95
|
-
# Best guess for GetFeatureById combined with
|
|
96
|
-
# GetPropertyValue&VALUEREFERENCE=@gml:id
|
|
97
|
-
return HttpResponse(body, content_type="text/plain")
|
|
132
|
+
extra_xmlns=extra_xmlns,
|
|
133
|
+
)
|
|
98
134
|
|
|
99
135
|
def render_xmlns(self):
|
|
100
136
|
"""Generate the xmlns block that the document needs"""
|
|
101
137
|
xsd_typenames = ",".join(
|
|
102
|
-
sub_collection.feature_type.name
|
|
103
|
-
for sub_collection in self.collection.results
|
|
138
|
+
sub_collection.feature_type.name for sub_collection in self.collection.results
|
|
104
139
|
)
|
|
105
140
|
schema_location = [
|
|
106
141
|
f"{self.app_xml_namespace} {self.server_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES={xsd_typenames}", # noqa: E501
|
|
@@ -115,15 +150,15 @@ class GML32Renderer(OutputRenderer):
|
|
|
115
150
|
'xmlns:app="{app_xml_namespace}" '
|
|
116
151
|
'xsi:schemaLocation="{schema_location}"'
|
|
117
152
|
).format(
|
|
118
|
-
app_xml_namespace=
|
|
119
|
-
schema_location=
|
|
153
|
+
app_xml_namespace=_attr_escape(self.app_xml_namespace),
|
|
154
|
+
schema_location=_attr_escape(" ".join(schema_location)),
|
|
120
155
|
)
|
|
121
156
|
|
|
122
157
|
def render_xmlns_standalone(self):
|
|
123
158
|
"""Generate the xmlns block that the document needs"""
|
|
124
159
|
# xsi is needed for "xsi:nil="true"' attributes.
|
|
125
160
|
return (
|
|
126
|
-
f' xmlns:app="{
|
|
161
|
+
f' xmlns:app="{_attr_escape(self.app_xml_namespace)}"'
|
|
127
162
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
128
163
|
' xmlns:gml="http://www.opengis.net/gml/3.2"'
|
|
129
164
|
)
|
|
@@ -133,20 +168,19 @@ class GML32Renderer(OutputRenderer):
|
|
|
133
168
|
This renders the standard <wfs:FeatureCollection> / <wfs:ValueCollection>
|
|
134
169
|
"""
|
|
135
170
|
collection = self.collection
|
|
136
|
-
output =
|
|
171
|
+
self.output = output = StringIO()
|
|
172
|
+
self._write = self.output.write
|
|
137
173
|
xmlns = self.render_xmlns().strip()
|
|
138
174
|
number_matched = collection.number_matched
|
|
139
|
-
number_matched = (
|
|
140
|
-
int(number_matched) if number_matched is not None else "unknown"
|
|
141
|
-
)
|
|
175
|
+
number_matched = int(number_matched) if number_matched is not None else "unknown"
|
|
142
176
|
number_returned = collection.number_returned
|
|
143
177
|
next = previous = ""
|
|
144
178
|
if collection.next:
|
|
145
|
-
next = f' next="{
|
|
179
|
+
next = f' next="{_attr_escape(collection.next)}"'
|
|
146
180
|
if collection.previous:
|
|
147
|
-
previous = f' previous="{
|
|
181
|
+
previous = f' previous="{_attr_escape(collection.previous)}"'
|
|
148
182
|
|
|
149
|
-
|
|
183
|
+
self._write(
|
|
150
184
|
f"""<?xml version='1.0' encoding="UTF-8" ?>\n"""
|
|
151
185
|
f"<wfs:{self.xml_collection_tag} {xmlns}"
|
|
152
186
|
f' timeStamp="{collection.timestamp}"'
|
|
@@ -161,7 +195,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
161
195
|
for sub_collection in collection.results:
|
|
162
196
|
self.start_collection(sub_collection)
|
|
163
197
|
if has_multiple_collections:
|
|
164
|
-
|
|
198
|
+
self._write(
|
|
165
199
|
f"<wfs:member>\n"
|
|
166
200
|
f"<wfs:{self.xml_collection_tag}"
|
|
167
201
|
f' timeStamp="{collection.timestamp}"'
|
|
@@ -170,193 +204,175 @@ class GML32Renderer(OutputRenderer):
|
|
|
170
204
|
)
|
|
171
205
|
|
|
172
206
|
for instance in sub_collection:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
207
|
+
self.gml_seq = 0 # need to increment this between write_xml_field calls
|
|
208
|
+
self._write("<wfs:member>\n")
|
|
209
|
+
self.write_feature(sub_collection.feature_type, instance)
|
|
210
|
+
self._write("</wfs:member>\n")
|
|
176
211
|
|
|
177
212
|
# Only perform a 'yield' every once in a while,
|
|
178
213
|
# as it goes back-and-forth for writing it to the client.
|
|
179
|
-
if output.
|
|
180
|
-
|
|
214
|
+
if output.tell() > self.chunk_size:
|
|
215
|
+
xml_chunk = output.getvalue()
|
|
216
|
+
output.seek(0)
|
|
217
|
+
output.truncate(0)
|
|
218
|
+
yield xml_chunk
|
|
181
219
|
|
|
182
220
|
if has_multiple_collections:
|
|
183
|
-
|
|
221
|
+
self._write(f"</wfs:{self.xml_collection_tag}>\n</wfs:member>\n")
|
|
184
222
|
|
|
185
|
-
|
|
186
|
-
yield output.
|
|
223
|
+
self._write(f"</wfs:{self.xml_collection_tag}>\n")
|
|
224
|
+
yield output.getvalue()
|
|
187
225
|
|
|
188
226
|
def start_collection(self, sub_collection: SimpleFeatureCollection):
|
|
189
227
|
"""Hook to allow initialization per feature type"""
|
|
190
|
-
pass
|
|
191
228
|
|
|
192
|
-
def
|
|
229
|
+
def write_feature(
|
|
193
230
|
self, feature_type: FeatureType, instance: models.Model, extra_xmlns=""
|
|
194
|
-
):
|
|
195
|
-
"""Write the full <wfs:member> block."""
|
|
196
|
-
body = self.render_wfs_member_contents(
|
|
197
|
-
feature_type, instance, extra_xmlns=extra_xmlns
|
|
198
|
-
)
|
|
199
|
-
return f"<wfs:member>\n{body}</wfs:member>\n"
|
|
200
|
-
|
|
201
|
-
def render_wfs_member_contents(
|
|
202
|
-
self, feature_type: FeatureType, instance: models.Model, extra_xmlns=""
|
|
203
|
-
) -> str:
|
|
231
|
+
) -> None:
|
|
204
232
|
"""Write the contents of the object value.
|
|
205
233
|
|
|
206
234
|
This output is typically wrapped in <wfs:member> tags
|
|
207
235
|
unless it's used for a GetPropertyById response.
|
|
208
236
|
"""
|
|
209
|
-
self.gml_seq = 0 # need to increment this between render_xml_field calls
|
|
210
|
-
|
|
211
237
|
# Write <app:FeatureTypeName> start node
|
|
212
|
-
pk =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if feature_type.xsd_type.base.is_complex_type:
|
|
222
|
-
for xsd_element in feature_type.xsd_type.base.elements:
|
|
238
|
+
pk = _tag_escape(str(instance.pk))
|
|
239
|
+
self._write(f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
|
|
240
|
+
|
|
241
|
+
# Write all fields, both base class and local elements.
|
|
242
|
+
for xsd_element in feature_type.xsd_type.all_elements:
|
|
243
|
+
# Note that writing 5000 features with 30 tags means this code make 150.000 method calls.
|
|
244
|
+
# Hence, branching to different rendering styles is branched here instead of making such
|
|
245
|
+
# call into a generic "write_field()" function and stepping out from there.
|
|
246
|
+
if xsd_element.is_geometry:
|
|
223
247
|
if xsd_element.xml_name == "gml:boundedBy":
|
|
224
248
|
# Special case for <gml:boundedBy>, so it will render with
|
|
225
249
|
# the output CRS and can be overwritten with DB-rendered GML.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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)
|
|
254
|
+
else:
|
|
255
|
+
value = xsd_element.get_value(instance)
|
|
256
|
+
if xsd_element.is_many:
|
|
257
|
+
self.write_many(feature_type, xsd_element, value)
|
|
229
258
|
else:
|
|
230
259
|
# e.g. <gml:name>, or all other <app:...> nodes.
|
|
231
|
-
|
|
232
|
-
self.render_element(feature_type, xsd_element, instance)
|
|
233
|
-
)
|
|
260
|
+
self.write_xml_field(feature_type, xsd_element, value)
|
|
234
261
|
|
|
235
|
-
|
|
236
|
-
for xsd_element in feature_type.xsd_type.elements:
|
|
237
|
-
output.write(self.render_element(feature_type, xsd_element, instance))
|
|
262
|
+
self._write(f"</{feature_type.xml_name}>\n")
|
|
238
263
|
|
|
239
|
-
|
|
240
|
-
return output.getvalue()
|
|
241
|
-
|
|
242
|
-
def render_bounds(self, feature_type, instance) -> str | None:
|
|
264
|
+
def write_bounds(self, feature_type, instance) -> None:
|
|
243
265
|
"""Render the GML bounds for the complete instance"""
|
|
244
266
|
envelope = feature_type.get_envelope(instance, self.output_crs)
|
|
245
267
|
if envelope is not None:
|
|
246
268
|
lower = " ".join(map(str, envelope.lower_corner))
|
|
247
269
|
upper = " ".join(map(str, envelope.upper_corner))
|
|
248
|
-
|
|
270
|
+
self._write(
|
|
271
|
+
f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{self.xml_srs_name}">
|
|
249
272
|
<gml:lowerCorner>{lower}</gml:lowerCorner>
|
|
250
273
|
<gml:upperCorner>{upper}</gml:upperCorner>
|
|
251
274
|
</gml:Envelope></gml:boundedBy>\n"""
|
|
252
|
-
|
|
253
|
-
def render_element(
|
|
254
|
-
self, feature_type, xsd_element: XsdElement, instance: models.Model
|
|
255
|
-
):
|
|
256
|
-
"""Rendering of a single field."""
|
|
257
|
-
value = xsd_element.get_value(instance)
|
|
258
|
-
if xsd_element.is_geometry and value is not None:
|
|
259
|
-
# None check happens here to avoid incrementing for none values
|
|
260
|
-
self.gml_seq += 1
|
|
261
|
-
return self.render_gml_field(
|
|
262
|
-
feature_type,
|
|
263
|
-
xsd_element,
|
|
264
|
-
value,
|
|
265
|
-
gml_id=self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq),
|
|
266
275
|
)
|
|
267
|
-
else:
|
|
268
|
-
return self.render_xml_field(feature_type, xsd_element, value)
|
|
269
276
|
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if
|
|
276
|
-
|
|
277
|
-
if xsd_element.min_occurs == 0:
|
|
278
|
-
return ""
|
|
279
|
-
else:
|
|
280
|
-
return f'<{xsd_element.xml_name} xsi:nil="true"{extra_xmlns}/>\n'
|
|
281
|
-
else:
|
|
282
|
-
# Render the tag multiple times
|
|
283
|
-
if xsd_element.type.is_complex_type:
|
|
284
|
-
# If the retrieved QuerySet was not filtered yet, do so now. This can't
|
|
285
|
-
# be done in get_value() because the FeatureType is not known there.
|
|
286
|
-
value = feature_type.filter_related_queryset(value)
|
|
287
|
-
|
|
288
|
-
return "".join(
|
|
289
|
-
self._render_xml_field(
|
|
290
|
-
feature_type, xsd_element, value=item, extra_xmlns=extra_xmlns
|
|
291
|
-
)
|
|
292
|
-
for item in value
|
|
293
|
-
)
|
|
277
|
+
def write_many(self, feature_type: FeatureType, xsd_element: XsdElement, value) -> None:
|
|
278
|
+
"""Write a node that has multiple values (e.g. array or queryset)."""
|
|
279
|
+
# some <app:...> node that has multiple values
|
|
280
|
+
if value is None:
|
|
281
|
+
# No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
|
|
282
|
+
if xsd_element.min_occurs:
|
|
283
|
+
self._write(f'<{xsd_element.xml_name} xsi:nil="true"/>\n')
|
|
294
284
|
else:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
for item in value:
|
|
292
|
+
self.write_xml_field(feature_type, xsd_element, value=item)
|
|
298
293
|
|
|
299
|
-
def
|
|
294
|
+
def write_xml_field(
|
|
300
295
|
self, feature_type: FeatureType, xsd_element: XsdElement, value, extra_xmlns=""
|
|
301
|
-
)
|
|
296
|
+
):
|
|
297
|
+
"""Write the value of a single field."""
|
|
302
298
|
xml_name = xsd_element.xml_name
|
|
303
299
|
if value is None:
|
|
304
|
-
|
|
300
|
+
self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
|
|
305
301
|
elif xsd_element.type.is_complex_type:
|
|
306
302
|
# Expanded foreign relation / dictionary
|
|
307
|
-
|
|
308
|
-
elif isinstance(value, datetime):
|
|
309
|
-
value = value.astimezone(utc).isoformat()
|
|
310
|
-
elif isinstance(value, (date, time)):
|
|
311
|
-
value = value.isoformat()
|
|
312
|
-
elif isinstance(value, bool):
|
|
313
|
-
value = "true" if value else "false"
|
|
303
|
+
self.write_xml_complex_type(feature_type, xsd_element, value, extra_xmlns=extra_xmlns)
|
|
314
304
|
else:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
305
|
+
# 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.
|
|
307
|
+
value_cls = value.__class__
|
|
308
|
+
if value_cls is str: # most cases
|
|
309
|
+
value = _tag_escape(value)
|
|
310
|
+
elif value_cls is datetime:
|
|
311
|
+
value = value.astimezone(timezone.utc).isoformat()
|
|
312
|
+
elif value_cls is bool:
|
|
313
|
+
value = "true" if value else "false"
|
|
314
|
+
elif (
|
|
315
|
+
value_cls is not int
|
|
316
|
+
and value_cls is not float
|
|
317
|
+
and value_cls is not D
|
|
318
|
+
and value_cls is not date
|
|
319
|
+
and value_cls is not time
|
|
320
|
+
):
|
|
321
|
+
# Non-string or custom field that extended a scalar.
|
|
322
|
+
# Any of the other types have a faster f"{value}" translation that produces the correct text.
|
|
323
|
+
value = _value_to_xml_string(value)
|
|
324
|
+
|
|
325
|
+
self._write(f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n")
|
|
326
|
+
|
|
327
|
+
def write_xml_complex_type(self, feature_type, xsd_element, value, extra_xmlns="") -> None:
|
|
320
328
|
"""Write a single field, that consists of sub elements"""
|
|
321
329
|
xsd_type = cast(XsdComplexType, xsd_element.type)
|
|
322
|
-
|
|
323
|
-
output.write(f"<{xsd_element.xml_name}>\n")
|
|
330
|
+
self._write(f"<{xsd_element.xml_name}{extra_xmlns}>\n")
|
|
324
331
|
for sub_element in xsd_type.elements:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
self
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
332
|
+
sub_value = sub_element.get_value(value)
|
|
333
|
+
if sub_element.is_many:
|
|
334
|
+
self.write_many(feature_type, sub_element, sub_value)
|
|
335
|
+
else:
|
|
336
|
+
self.write_xml_field(feature_type, sub_element, sub_value)
|
|
337
|
+
self._write(f"</{xsd_element.xml_name}>\n")
|
|
338
|
+
|
|
339
|
+
def write_gml_field(
|
|
340
|
+
self, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
|
|
341
|
+
) -> None:
|
|
342
|
+
"""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)
|
|
338
345
|
xml_name = xsd_element.xml_name
|
|
339
|
-
|
|
340
|
-
|
|
346
|
+
if value is None:
|
|
347
|
+
# Avoid incrementing gml_seq
|
|
348
|
+
self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
gml_id = self.get_gml_id(feature_type, instance.pk)
|
|
341
352
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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:]}"
|
|
357
|
+
|
|
358
|
+
gml = self.render_gml_value(gml_id, value)
|
|
359
|
+
self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
|
|
360
|
+
|
|
361
|
+
def render_gml_value(self, gml_id, value: geos.GEOSGeometry | None, extra_xmlns="") -> str:
|
|
362
|
+
"""Normal case: 'value' is raw geometry data.."""
|
|
363
|
+
# 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}'
|
|
346
365
|
self.output_crs.apply_to(value)
|
|
347
|
-
base_attrs
|
|
348
|
-
f' gml:id="{escape(gml_id)}" srsName="{self.xml_srs_name}"{extra_xmlns}'
|
|
349
|
-
)
|
|
350
|
-
return self._render_gml_type(value, base_attrs)
|
|
366
|
+
return self._render_gml_type(value, base_attrs=base_attrs)
|
|
351
367
|
|
|
352
368
|
def _render_gml_type(self, value: geos.GEOSGeometry, base_attrs=""):
|
|
369
|
+
"""Render an GML value (this is also called from MultiPolygon)."""
|
|
353
370
|
try:
|
|
354
371
|
# Avoid isinstance checks, do a direct lookup
|
|
355
372
|
method = GML_RENDER_FUNCTIONS[value.__class__]
|
|
356
373
|
except KeyError:
|
|
357
374
|
return f"<!-- No rendering implemented for {value.geom_type} -->"
|
|
358
|
-
|
|
359
|
-
return method(self, value, base_attrs=base_attrs)
|
|
375
|
+
return method(self, value, base_attrs=base_attrs)
|
|
360
376
|
|
|
361
377
|
@register_geos_type(geos.Point)
|
|
362
378
|
def render_gml_point(self, value: geos.Point, base_attrs=""):
|
|
@@ -372,7 +388,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
372
388
|
def render_gml_polygon(self, value: geos.Polygon, base_attrs=""):
|
|
373
389
|
# lol: http://erouault.blogspot.com/2014/04/gml-madness.html
|
|
374
390
|
ext_ring = self.render_gml_linear_ring(value.exterior_ring)
|
|
375
|
-
buf =
|
|
391
|
+
buf = StringIO()
|
|
376
392
|
buf.write(f"<gml:Polygon{base_attrs}><gml:exterior>{ext_ring}</gml:exterior>")
|
|
377
393
|
for i in range(value.num_interior_rings):
|
|
378
394
|
buf.write("<gml:interior>")
|
|
@@ -410,6 +426,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
410
426
|
|
|
411
427
|
@register_geos_type(geos.LinearRing)
|
|
412
428
|
def render_gml_linear_ring(self, value: geos.LinearRing, base_attrs=""):
|
|
429
|
+
# NOTE: this is super slow. value.tuple performs a C-API call for every point!
|
|
413
430
|
coords = " ".join(map(str, itertools.chain.from_iterable(value.tuple)))
|
|
414
431
|
dim = "3" if value.hasz else "2"
|
|
415
432
|
# <gml:coordinates> is still valid in GML3, but deprecated (part of GML2).
|
|
@@ -421,6 +438,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
421
438
|
|
|
422
439
|
@register_geos_type(geos.LineString)
|
|
423
440
|
def render_gml_line_string(self, value: geos.LineString, base_attrs=""):
|
|
441
|
+
# NOTE: this is super slow. value.tuple performs a C-API call for every point!
|
|
424
442
|
coords = " ".join(map(str, itertools.chain.from_iterable(value.tuple)))
|
|
425
443
|
dim = "3" if value.hasz else "2"
|
|
426
444
|
return (
|
|
@@ -429,12 +447,36 @@ class GML32Renderer(OutputRenderer):
|
|
|
429
447
|
"</gml:LineString>"
|
|
430
448
|
)
|
|
431
449
|
|
|
432
|
-
def get_gml_id(self, feature_type: FeatureType, object_id
|
|
450
|
+
def get_gml_id(self, feature_type: FeatureType, object_id) -> str:
|
|
433
451
|
"""Generate the gml:id value, which is required for GML 3.2 objects."""
|
|
434
|
-
|
|
452
|
+
self.gml_seq += 1
|
|
453
|
+
return f"{feature_type.name}.{object_id}.{self.gml_seq}"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class DBGMLRenderingMixin:
|
|
457
|
+
|
|
458
|
+
def render_gml_value(self, gml_id, value: str, extra_xmlns=""):
|
|
459
|
+
"""DB optimized: 'value' is pre-rendered GML XML string."""
|
|
460
|
+
# Write the gml:id inside the first tag
|
|
461
|
+
end_pos = value.find(">")
|
|
462
|
+
gml_tag = value[:end_pos]
|
|
463
|
+
id_pos = gml_tag.find("gml:id=")
|
|
464
|
+
if id_pos == -1:
|
|
465
|
+
# Inject
|
|
466
|
+
return f'{gml_tag} gml:id="{_attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
|
|
467
|
+
else:
|
|
468
|
+
# Replace
|
|
469
|
+
end_pos1 = gml_tag.find('"', id_pos + 8)
|
|
470
|
+
return (
|
|
471
|
+
f"{gml_tag[:id_pos]}"
|
|
472
|
+
f'gml:id="{_attr_escape(gml_id)}'
|
|
473
|
+
f"{value[end_pos1:end_pos]}" # from " right until >
|
|
474
|
+
f"{extra_xmlns}" # extra namespaces?
|
|
475
|
+
f"{value[end_pos:]}" # from > and beyond
|
|
476
|
+
)
|
|
435
477
|
|
|
436
478
|
|
|
437
|
-
class DBGML32Renderer(GML32Renderer):
|
|
479
|
+
class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
438
480
|
"""Faster GetFeature renderer that uses the database to render GML 3.2"""
|
|
439
481
|
|
|
440
482
|
@classmethod
|
|
@@ -449,18 +491,14 @@ class DBGML32Renderer(GML32Renderer):
|
|
|
449
491
|
This is far more efficient then GeoDjango's logic, which performs a
|
|
450
492
|
C-API call for every single coordinate of a geometry.
|
|
451
493
|
"""
|
|
452
|
-
queryset = super().decorate_queryset(
|
|
453
|
-
feature_type, queryset, output_crs, **params
|
|
454
|
-
)
|
|
494
|
+
queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
|
|
455
495
|
|
|
456
496
|
# Retrieve geometries as pre-rendered instead.
|
|
457
497
|
gml_elements = feature_type.xsd_type.geometry_elements
|
|
458
498
|
geo_selects = get_db_geometry_selects(gml_elements, output_crs)
|
|
459
499
|
if geo_selects:
|
|
460
500
|
queryset = queryset.defer(*geo_selects.keys()).annotate(
|
|
461
|
-
_as_envelope_gml=cls.get_db_envelope_as_gml(
|
|
462
|
-
feature_type, queryset, output_crs
|
|
463
|
-
),
|
|
501
|
+
_as_envelope_gml=cls.get_db_envelope_as_gml(feature_type, queryset, output_crs),
|
|
464
502
|
**build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
|
|
465
503
|
)
|
|
466
504
|
|
|
@@ -523,67 +561,49 @@ class DBGML32Renderer(GML32Renderer):
|
|
|
523
561
|
using=queryset.db,
|
|
524
562
|
)
|
|
525
563
|
|
|
526
|
-
def
|
|
527
|
-
self, feature_type, xsd_element: XsdElement, instance: models.Model
|
|
528
|
-
):
|
|
529
|
-
|
|
530
|
-
# Optimized path, pre-rendered GML
|
|
531
|
-
value = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
|
|
532
|
-
if value is None:
|
|
533
|
-
# Avoid incrementing gml_seq
|
|
534
|
-
return f'<{xsd_element.xml_name} xsi:nil="true"/>\n'
|
|
535
|
-
|
|
536
|
-
self.gml_seq += 1
|
|
537
|
-
return self.render_db_gml_field(
|
|
538
|
-
feature_type,
|
|
539
|
-
xsd_element,
|
|
540
|
-
value,
|
|
541
|
-
gml_id=self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq),
|
|
542
|
-
)
|
|
543
|
-
else:
|
|
544
|
-
return super().render_element(feature_type, xsd_element, instance)
|
|
564
|
+
def write_gml_field(
|
|
565
|
+
self, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
|
|
566
|
+
) -> None:
|
|
567
|
+
"""Write the value of an GML tag.
|
|
545
568
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
xsd_element: XsdElement,
|
|
550
|
-
value,
|
|
551
|
-
gml_id,
|
|
552
|
-
extra_xmlns="",
|
|
553
|
-
) -> str:
|
|
554
|
-
"""Write the value of an GML tag"""
|
|
569
|
+
This optimized version takes a pre-rendered XML from the database query.
|
|
570
|
+
"""
|
|
571
|
+
value = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
|
|
555
572
|
xml_name = xsd_element.xml_name
|
|
556
573
|
if value is None:
|
|
557
|
-
|
|
574
|
+
# Avoid incrementing gml_seq
|
|
575
|
+
self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
|
|
576
|
+
return
|
|
558
577
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if "gml:id" in first_tag:
|
|
563
|
-
first_tag = RE_GML_ID.sub(f'gml:id="{escape(gml_id)}"', first_tag, 1)
|
|
564
|
-
else:
|
|
565
|
-
first_tag += f' gml:id="{escape(gml_id)}"'
|
|
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")
|
|
566
581
|
|
|
567
|
-
|
|
568
|
-
return f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n"
|
|
569
|
-
|
|
570
|
-
def render_bounds(self, feature_type, instance):
|
|
582
|
+
def write_bounds(self, feature_type, instance) -> None:
|
|
571
583
|
"""Generate the <gml:boundedBy> from DB prerendering."""
|
|
572
584
|
gml = instance._as_envelope_gml
|
|
573
585
|
if gml is not None:
|
|
574
|
-
|
|
586
|
+
self._write(f"<gml:boundedBy>{gml}</gml:boundedBy>\n")
|
|
575
587
|
|
|
576
588
|
|
|
577
589
|
class GML32ValueRenderer(GML32Renderer):
|
|
578
|
-
"""Render the GetPropertyValue XML output in GML 3.2 format
|
|
590
|
+
"""Render the GetPropertyValue XML output in GML 3.2 format.
|
|
591
|
+
|
|
592
|
+
Geoserver seems to generate the element tag inside each <wfs:member> element. We've applied this one.
|
|
593
|
+
The GML standard demonstrates to render only their content inside a <wfs:member> element
|
|
594
|
+
(either plain text or an <gml:...> tag). Not sure what is right here.
|
|
595
|
+
"""
|
|
579
596
|
|
|
580
597
|
content_type = "text/xml; charset=utf-8"
|
|
598
|
+
content_type_plain = "text/plain; charset=utf-8"
|
|
581
599
|
xml_collection_tag = "ValueCollection"
|
|
600
|
+
_escape_value = staticmethod(_value_to_xml_string)
|
|
601
|
+
gml_value_getter = itemgetter("member")
|
|
582
602
|
|
|
583
603
|
def __init__(self, *args, value_reference: ValueReference, **kwargs):
|
|
584
604
|
self.value_reference = value_reference
|
|
585
605
|
super().__init__(*args, **kwargs)
|
|
586
|
-
self.xsd_node = None
|
|
606
|
+
self.xsd_node: XsdNode | None = None
|
|
587
607
|
|
|
588
608
|
@classmethod
|
|
589
609
|
def decorate_queryset(
|
|
@@ -594,6 +614,7 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
594
614
|
**params,
|
|
595
615
|
):
|
|
596
616
|
# Don't optimize queryset, it only retrieves one value
|
|
617
|
+
# The data is already limited to a ``queryset.values()`` in ``QueryExpression.get_queryset()``.
|
|
597
618
|
return queryset
|
|
598
619
|
|
|
599
620
|
def start_collection(self, sub_collection: SimpleFeatureCollection):
|
|
@@ -601,60 +622,84 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
601
622
|
match = sub_collection.feature_type.resolve_element(self.value_reference.xpath)
|
|
602
623
|
self.xsd_node = match.child
|
|
603
624
|
|
|
604
|
-
def
|
|
605
|
-
self,
|
|
625
|
+
def write_by_id_response(
|
|
626
|
+
self, sub_collection: SimpleFeatureCollection, instance: dict, extra_xmlns
|
|
606
627
|
):
|
|
607
|
-
"""
|
|
628
|
+
"""The value rendering only renders the value. not a complete feature"""
|
|
608
629
|
if self.xsd_node.is_attribute:
|
|
609
|
-
#
|
|
610
|
-
#
|
|
611
|
-
|
|
612
|
-
body = self.xsd_node.format_value(instance["member"])
|
|
613
|
-
return f"<wfs:member>{body}</wfs:member>\n"
|
|
630
|
+
# Output as plain text
|
|
631
|
+
self.content_type = self.content_type_plain # change for this instance!
|
|
632
|
+
self._escape_value = _value_to_text # avoid XML escaping
|
|
614
633
|
else:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
def
|
|
621
|
-
self, feature_type: FeatureType, instance: dict, extra_xmlns=""
|
|
622
|
-
) -> str:
|
|
634
|
+
self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
635
|
+
|
|
636
|
+
# Write the single tag, no <wfs:member> around it.
|
|
637
|
+
self.write_feature(sub_collection.feature_type, instance, extra_xmlns=extra_xmlns)
|
|
638
|
+
|
|
639
|
+
def write_feature(self, feature_type: FeatureType, instance: dict, extra_xmlns="") -> None:
|
|
623
640
|
"""Write the XML for a single object.
|
|
624
641
|
In this case, it's only a single XML tag.
|
|
625
642
|
"""
|
|
626
|
-
value = instance["member"]
|
|
627
643
|
if self.xsd_node.is_geometry:
|
|
628
|
-
|
|
629
|
-
return self.render_gml_field(
|
|
644
|
+
self.write_gml_field(
|
|
630
645
|
feature_type,
|
|
631
|
-
self.xsd_node,
|
|
632
|
-
|
|
633
|
-
gml_id=gml_id,
|
|
646
|
+
cast(XsdElement, self.xsd_node),
|
|
647
|
+
instance,
|
|
634
648
|
extra_xmlns=extra_xmlns,
|
|
635
649
|
)
|
|
650
|
+
elif self.xsd_node.is_attribute:
|
|
651
|
+
value = instance["member"]
|
|
652
|
+
if value is not None:
|
|
653
|
+
value = self.xsd_node.format_raw_value(instance["member"]) # for gml:id
|
|
654
|
+
value = self._escape_value(value)
|
|
655
|
+
self._write(value)
|
|
656
|
+
elif self.xsd_node.is_array:
|
|
657
|
+
if (value := instance["member"]) is not None:
|
|
658
|
+
# <wfs:member> doesn't allow multiple items as children, for new render as separate members.
|
|
659
|
+
xml_name = self.xsd_node.xml_name
|
|
660
|
+
first = True
|
|
661
|
+
for item in value:
|
|
662
|
+
if item is not None:
|
|
663
|
+
if not first:
|
|
664
|
+
self._write("</wfs:member>\n<wfs:member>")
|
|
665
|
+
|
|
666
|
+
item = self._escape_value(item)
|
|
667
|
+
self._write(f"<{xml_name}>{item}</{xml_name}>\n")
|
|
668
|
+
first = False
|
|
669
|
+
elif self.xsd_node.type.is_complex_type:
|
|
670
|
+
raise NotImplementedError("GetPropertyValue with complex types is not implemented")
|
|
671
|
+
# self.write_xml_complex_type(feature_type, self.xsd_node, instance['member'])
|
|
636
672
|
else:
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
return self.render_xml_field(
|
|
644
|
-
feature_type,
|
|
645
|
-
cast(XsdElement, self.xsd_node),
|
|
646
|
-
value,
|
|
647
|
-
extra_xmlns=extra_xmlns,
|
|
648
|
-
)
|
|
673
|
+
self.write_xml_field(
|
|
674
|
+
feature_type,
|
|
675
|
+
cast(XsdElement, self.xsd_node),
|
|
676
|
+
value=instance["member"],
|
|
677
|
+
extra_xmlns=extra_xmlns,
|
|
678
|
+
)
|
|
649
679
|
|
|
680
|
+
def write_gml_field(
|
|
681
|
+
self, feature_type, xsd_element: XsdElement, instance: dict, extra_xmlns=""
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Overwritten to allow dict access instead of model access."""
|
|
684
|
+
value = self.gml_value_getter(instance) # "member" or "gml_member"
|
|
685
|
+
xml_name = xsd_element.xml_name
|
|
686
|
+
if value is None:
|
|
687
|
+
# Avoid incrementing gml_seq
|
|
688
|
+
self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
|
|
689
|
+
return
|
|
690
|
+
|
|
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")
|
|
650
694
|
|
|
651
|
-
|
|
695
|
+
|
|
696
|
+
class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
652
697
|
"""Faster GetPropertyValue renderer that uses the database to render GML 3.2"""
|
|
653
698
|
|
|
699
|
+
gml_value_getter = itemgetter("gml_member")
|
|
700
|
+
|
|
654
701
|
@classmethod
|
|
655
|
-
def decorate_queryset(
|
|
656
|
-
cls, feature_type: FeatureType, queryset, output_crs, **params
|
|
657
|
-
):
|
|
702
|
+
def decorate_queryset(cls, feature_type: FeatureType, queryset, output_crs, **params):
|
|
658
703
|
"""Update the queryset to let the database render the GML output."""
|
|
659
704
|
value_reference = params["valueReference"]
|
|
660
705
|
match = feature_type.resolve_element(value_reference.xpath)
|
|
@@ -665,21 +710,3 @@ class DBGML32ValueRenderer(DBGML32Renderer, GML32ValueRenderer):
|
|
|
665
710
|
)
|
|
666
711
|
else:
|
|
667
712
|
return queryset
|
|
668
|
-
|
|
669
|
-
def render_wfs_member(
|
|
670
|
-
self, feature_type: FeatureType, instance: dict, extra_xmlns=""
|
|
671
|
-
) -> str:
|
|
672
|
-
"""Write the XML for a single object."""
|
|
673
|
-
if "gml_member" in instance:
|
|
674
|
-
gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
|
|
675
|
-
body = self.render_db_gml_field(
|
|
676
|
-
feature_type,
|
|
677
|
-
self.xsd_node,
|
|
678
|
-
instance["gml_member"],
|
|
679
|
-
gml_id=gml_id,
|
|
680
|
-
)
|
|
681
|
-
return f"<wfs:member>\n{body}</wfs:member>\n"
|
|
682
|
-
else:
|
|
683
|
-
return super().render_wfs_member(
|
|
684
|
-
feature_type, instance, extra_xmlns=extra_xmlns
|
|
685
|
-
)
|