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