django-gisserver 1.4.1__py3-none-any.whl → 1.5.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.4.1.dist-info → django_gisserver-1.5.0.dist-info}/METADATA +11 -11
- django_gisserver-1.5.0.dist-info/RECORD +54 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/db.py +14 -20
- gisserver/exceptions.py +23 -9
- gisserver/features.py +62 -99
- gisserver/geometries.py +2 -2
- gisserver/operations/base.py +31 -21
- gisserver/operations/wfs20.py +44 -38
- gisserver/output/__init__.py +2 -1
- gisserver/output/base.py +43 -27
- gisserver/output/csv.py +38 -33
- gisserver/output/geojson.py +43 -51
- gisserver/output/gml32.py +88 -67
- gisserver/output/results.py +23 -8
- gisserver/output/utils.py +18 -2
- gisserver/output/xmlschema.py +1 -1
- gisserver/parsers/base.py +2 -2
- gisserver/parsers/fes20/__init__.py +18 -0
- gisserver/parsers/fes20/expressions.py +7 -12
- gisserver/parsers/fes20/functions.py +1 -1
- gisserver/parsers/fes20/operators.py +7 -3
- gisserver/parsers/fes20/query.py +11 -1
- gisserver/parsers/fes20/sorting.py +3 -1
- gisserver/parsers/gml/base.py +1 -1
- gisserver/queries/__init__.py +3 -0
- gisserver/queries/adhoc.py +16 -12
- gisserver/queries/base.py +76 -36
- gisserver/queries/projection.py +240 -0
- gisserver/queries/stored.py +7 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
- gisserver/types.py +78 -24
- gisserver/views.py +9 -20
- django_gisserver-1.4.1.dist-info/RECORD +0 -53
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/top_level.txt +0 -0
gisserver/operations/base.py
CHANGED
|
@@ -32,12 +32,12 @@ RE_SAFE_FILENAME = re.compile(r"\A[A-Za-z0-9]+[A-Za-z0-9.]*") # no dot at the s
|
|
|
32
32
|
|
|
33
33
|
@dataclass(frozen=True)
|
|
34
34
|
class Parameter:
|
|
35
|
-
"""The definition of
|
|
35
|
+
"""The definition of a parameter for an WFS method.
|
|
36
36
|
|
|
37
37
|
These parameters should be defined in the
|
|
38
38
|
WFSMethod parameters attribute / get_parameters().
|
|
39
39
|
|
|
40
|
-
These fields
|
|
40
|
+
These fields are used to parse the incoming request.
|
|
41
41
|
The GetCapabilities method also reads the metadata of this value.
|
|
42
42
|
"""
|
|
43
43
|
|
|
@@ -81,9 +81,9 @@ class Parameter:
|
|
|
81
81
|
if not self.required:
|
|
82
82
|
return self.default
|
|
83
83
|
elif value is None:
|
|
84
|
-
raise MissingParameterValue(
|
|
84
|
+
raise MissingParameterValue(f"Missing {kvp_name} parameter", locator=self.name)
|
|
85
85
|
else:
|
|
86
|
-
raise InvalidParameterValue(
|
|
86
|
+
raise InvalidParameterValue(f"Empty {kvp_name} parameter", locator=self.name)
|
|
87
87
|
|
|
88
88
|
# Allow conversion into a python object
|
|
89
89
|
if self.parser is not None:
|
|
@@ -91,13 +91,15 @@ class Parameter:
|
|
|
91
91
|
value = self.parser(value)
|
|
92
92
|
except ExternalParsingError as e:
|
|
93
93
|
raise OperationParsingFailed(
|
|
94
|
-
|
|
94
|
+
f"Unable to parse {kvp_name} argument: {e}",
|
|
95
|
+
locator=self.name,
|
|
95
96
|
) from None
|
|
96
97
|
except (TypeError, ValueError, NotImplementedError) as e:
|
|
97
98
|
# TypeError/ValueError are raised by most handlers for unexpected data
|
|
98
99
|
# The NotImplementedError can be raised by fes parsing.
|
|
99
100
|
raise InvalidParameterValue(
|
|
100
|
-
|
|
101
|
+
f"Invalid {kvp_name} argument: {e}",
|
|
102
|
+
locator=self.name,
|
|
101
103
|
) from None
|
|
102
104
|
|
|
103
105
|
# Validate against value choices
|
|
@@ -111,14 +113,16 @@ class Parameter:
|
|
|
111
113
|
"""
|
|
112
114
|
if self.allowed_values is not None and value not in self.allowed_values:
|
|
113
115
|
msg = self.error_messages.get("invalid", "Invalid value for {name}: {value}")
|
|
114
|
-
raise InvalidParameterValue(
|
|
116
|
+
raise InvalidParameterValue(msg.format(name=self.name, value=value), locator=self.name)
|
|
115
117
|
|
|
116
118
|
|
|
117
119
|
class UnsupportedParameter(Parameter):
|
|
118
120
|
def value_from_query(self, KVP: dict):
|
|
119
121
|
kvp_name = self.name.upper()
|
|
120
122
|
if kvp_name in KVP:
|
|
121
|
-
raise InvalidParameterValue(
|
|
123
|
+
raise InvalidParameterValue(
|
|
124
|
+
f"Support for {self.name} is not implemented!", locator=self.name
|
|
125
|
+
)
|
|
122
126
|
return None
|
|
123
127
|
|
|
124
128
|
|
|
@@ -134,6 +138,7 @@ class OutputFormat:
|
|
|
134
138
|
renderer_class=None,
|
|
135
139
|
max_page_size=None,
|
|
136
140
|
title=None,
|
|
141
|
+
in_capabilities=True,
|
|
137
142
|
**extra,
|
|
138
143
|
):
|
|
139
144
|
"""
|
|
@@ -141,7 +146,8 @@ class OutputFormat:
|
|
|
141
146
|
:param renderer_class: The class that performs the output rendering.
|
|
142
147
|
If it's not given, the operation renders it's output using an XML template.
|
|
143
148
|
:param max_page_size: Used to override the ``max_page_size`` of the renderer_class.
|
|
144
|
-
:param title: A human-friendly name for
|
|
149
|
+
:param title: A human-friendly name for an HTML overview page.
|
|
150
|
+
:param in_capabilities: Whether this format needs to be advertised in GetCapabilities.
|
|
145
151
|
:param extra: Any additional key-value pairs for the definition.
|
|
146
152
|
Could include ``subtype`` as a shorter alias for the MIME-type.
|
|
147
153
|
"""
|
|
@@ -150,6 +156,7 @@ class OutputFormat:
|
|
|
150
156
|
self.subtype = self.extra.get("subtype")
|
|
151
157
|
self.renderer_class = renderer_class
|
|
152
158
|
self.title = title
|
|
159
|
+
self.in_capabilities = in_capabilities
|
|
153
160
|
self._max_page_size = max_page_size
|
|
154
161
|
|
|
155
162
|
def matches(self, value):
|
|
@@ -197,7 +204,7 @@ class WFSMethod:
|
|
|
197
204
|
|
|
198
205
|
do_not_call_in_templates = True # avoid __call__ execution in Django templates
|
|
199
206
|
|
|
200
|
-
#: List the
|
|
207
|
+
#: List the supported parameters for this method, extended by get_parameters()
|
|
201
208
|
parameters: list[Parameter] = []
|
|
202
209
|
|
|
203
210
|
#: List the supported output formats for this method.
|
|
@@ -258,20 +265,20 @@ class WFSMethod:
|
|
|
258
265
|
|
|
259
266
|
def _parse_output_format(self, value) -> OutputFormat:
|
|
260
267
|
"""Select the proper OutputFormat object based on the input value"""
|
|
261
|
-
# When using ?OUTPUTFORMAT=application/gml+xml", it is actually
|
|
262
|
-
# character. Hence spaces are replaced back to a '+' character to allow such notation
|
|
263
|
-
# instead of forcing it to
|
|
268
|
+
# When using ?OUTPUTFORMAT=application/gml+xml", it is actually a URL-encoded space
|
|
269
|
+
# character. Hence, spaces are replaced back to a '+' character to allow such notation
|
|
270
|
+
# instead of forcing it to be ?OUTPUTFORMAT=application/gml%2bxml".
|
|
264
271
|
for v in {value, value.replace(" ", "+")}:
|
|
265
272
|
for o in self.output_formats:
|
|
266
273
|
if o.matches(v):
|
|
267
274
|
return o
|
|
268
275
|
raise InvalidParameterValue(
|
|
269
|
-
"outputformat",
|
|
270
276
|
f"'{value}' is not a permitted output format for this operation.",
|
|
277
|
+
locator="outputformat",
|
|
271
278
|
) from None
|
|
272
279
|
|
|
273
280
|
def _parse_namespaces(self, value) -> dict[str, str]:
|
|
274
|
-
"""Parse the namespaces definition.
|
|
281
|
+
"""Parse the 'namespaces' definition.
|
|
275
282
|
|
|
276
283
|
The NAMESPACES parameter defines which namespaces are used in the KVP request.
|
|
277
284
|
When this parameter is not given, the default namespaces are assumed.
|
|
@@ -287,7 +294,9 @@ class WFSMethod:
|
|
|
287
294
|
tokens = iter(tokens)
|
|
288
295
|
for prefix in tokens:
|
|
289
296
|
if not prefix.startswith("xmlns("):
|
|
290
|
-
raise InvalidParameterValue(
|
|
297
|
+
raise InvalidParameterValue(
|
|
298
|
+
f"Expected xmlns(...) format: {value}", locator="namespaces"
|
|
299
|
+
)
|
|
291
300
|
if prefix.endswith(")"):
|
|
292
301
|
# xmlns(http://...)
|
|
293
302
|
prefix = ""
|
|
@@ -296,7 +305,8 @@ class WFSMethod:
|
|
|
296
305
|
uri = next(tokens, "")
|
|
297
306
|
if not uri.endswith(")"):
|
|
298
307
|
raise InvalidParameterValue(
|
|
299
|
-
|
|
308
|
+
f"Expected xmlns(prefix,uri) format: {value}",
|
|
309
|
+
locator="namespaces",
|
|
300
310
|
)
|
|
301
311
|
prefix = prefix[6:]
|
|
302
312
|
uri = uri[:-1]
|
|
@@ -361,12 +371,12 @@ class WFSMethod:
|
|
|
361
371
|
)
|
|
362
372
|
|
|
363
373
|
def _get_xml_template_name(self):
|
|
364
|
-
"""Generate the XML template name for this operation, and check
|
|
374
|
+
"""Generate the XML template name for this operation, and check its file pattern"""
|
|
365
375
|
service = self.view.KVP["SERVICE"].lower()
|
|
366
376
|
template_name = f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
|
|
367
377
|
|
|
368
378
|
# Since 'service' and 'version' are based on external input,
|
|
369
|
-
# these values are double
|
|
379
|
+
# these values are double-checked again to avoid remove file inclusion.
|
|
370
380
|
if not RE_SAFE_FILENAME.match(service) or not RE_SAFE_FILENAME.match(self.view.version):
|
|
371
381
|
raise SuspiciousOperation(f"Refusing to render template name {template_name}")
|
|
372
382
|
|
|
@@ -403,8 +413,8 @@ class WFSTypeNamesMethod(WFSMethod):
|
|
|
403
413
|
# This allows to perform multiple queries in a single request:
|
|
404
414
|
# TYPENAMES=(A)(B)&FILTER=(filter for A)(filter for B)
|
|
405
415
|
raise OperationParsingFailed(
|
|
406
|
-
"typenames",
|
|
407
416
|
"Parameter lists to perform multiple queries are not supported yet.",
|
|
417
|
+
locator="typenames",
|
|
408
418
|
)
|
|
409
419
|
|
|
410
420
|
return [self._parse_type_name(name, locator="typenames") for name in type_names.split(",")]
|
|
@@ -419,9 +429,9 @@ class WFSTypeNamesMethod(WFSMethod):
|
|
|
419
429
|
return self.all_feature_types_by_name[local_name]
|
|
420
430
|
except KeyError:
|
|
421
431
|
raise InvalidParameterValue(
|
|
422
|
-
locator,
|
|
423
432
|
f"Typename '{name}' doesn't exist in this server. "
|
|
424
433
|
f"Please check the capabilities and reformulate your request.",
|
|
434
|
+
locator=locator,
|
|
425
435
|
) from None
|
|
426
436
|
|
|
427
437
|
|
gisserver/operations/wfs20.py
CHANGED
|
@@ -44,11 +44,17 @@ RE_SAFE_FILENAME = re.compile(r"\A[A-Za-z0-9]+[A-Za-z0-9.]*") # no dot at the s
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class GetCapabilities(WFSMethod):
|
|
47
|
-
"""
|
|
47
|
+
"""This operation returns map features, and available operations this WFS server supports."""
|
|
48
48
|
|
|
49
49
|
output_formats = [
|
|
50
|
-
OutputFormat("application/gml+xml", version="3.2", title="GML"), # for FME
|
|
51
50
|
OutputFormat("text/xml"),
|
|
51
|
+
OutputFormat(
|
|
52
|
+
# for FME (Feature Manipulation Engine)
|
|
53
|
+
"application/gml+xml",
|
|
54
|
+
version="3.2",
|
|
55
|
+
title="GML",
|
|
56
|
+
in_capabilities=False,
|
|
57
|
+
),
|
|
52
58
|
]
|
|
53
59
|
xml_template_name = "get_capabilities.xml"
|
|
54
60
|
|
|
@@ -87,16 +93,17 @@ class GetCapabilities(WFSMethod):
|
|
|
87
93
|
if "VERSION" in self.view.KVP:
|
|
88
94
|
# Even GetCapabilities can still receive a VERSION argument to fixate it.
|
|
89
95
|
raise InvalidParameterValue(
|
|
90
|
-
"
|
|
96
|
+
"Can't provide both ACCEPTVERSIONS and VERSION",
|
|
97
|
+
locator="AcceptVersions",
|
|
91
98
|
)
|
|
92
99
|
|
|
93
100
|
matched_versions = set(accept_versions.split(",")).intersection(self.view.accept_versions)
|
|
94
101
|
if not matched_versions:
|
|
95
102
|
allowed = ", ".join(self.view.accept_versions)
|
|
96
103
|
raise VersionNegotiationFailed(
|
|
97
|
-
"acceptversions",
|
|
98
104
|
f"'{accept_versions}' does not contain supported versions, "
|
|
99
105
|
f"supported are: {allowed}.",
|
|
106
|
+
locator="acceptversions",
|
|
100
107
|
)
|
|
101
108
|
|
|
102
109
|
# Take the highest version (mapserver returns first matching)
|
|
@@ -115,7 +122,7 @@ class GetCapabilities(WFSMethod):
|
|
|
115
122
|
def get_context_data(self, **params):
|
|
116
123
|
view = self.view
|
|
117
124
|
|
|
118
|
-
# The 'service' is not
|
|
125
|
+
# The 'service' is not read from 'params' to avoid dependency on get_parameters()
|
|
119
126
|
service = view.KVP["SERVICE"] # is WFS
|
|
120
127
|
service_operations = view.accept_operations[service]
|
|
121
128
|
feature_output_formats = service_operations["GetFeature"].output_formats
|
|
@@ -138,18 +145,19 @@ class GetCapabilities(WFSMethod):
|
|
|
138
145
|
|
|
139
146
|
class DescribeFeatureType(WFSTypeNamesMethod):
|
|
140
147
|
"""This returns an XML Schema for the provided objects.
|
|
141
|
-
Each feature is exposed as an XSD definition with
|
|
148
|
+
Each feature is exposed as an XSD definition with its fields.
|
|
142
149
|
"""
|
|
143
150
|
|
|
144
151
|
output_formats = [
|
|
145
152
|
OutputFormat("XMLSCHEMA", renderer_class=output.XMLSchemaRenderer),
|
|
146
|
-
# At least one version of FME
|
|
147
|
-
# request with this output format.
|
|
148
|
-
# send it XML Schema.
|
|
153
|
+
# At least one version of FME (Feature Manipulation Engine) seems to
|
|
154
|
+
# send a DescribeFeatureType request with this GML as output format.
|
|
155
|
+
# Do what mapserver does and just send it XML Schema.
|
|
149
156
|
OutputFormat(
|
|
150
157
|
"application/gml+xml",
|
|
151
158
|
version="3.2",
|
|
152
159
|
renderer_class=output.XMLSchemaRenderer,
|
|
160
|
+
in_capabilities=False,
|
|
153
161
|
),
|
|
154
162
|
# OutputFormat("text/xml", subtype="gml/3.1.1"),
|
|
155
163
|
]
|
|
@@ -157,7 +165,7 @@ class DescribeFeatureType(WFSTypeNamesMethod):
|
|
|
157
165
|
def get_context_data(self, typeNames, **params):
|
|
158
166
|
if self.view.KVP.get("TYPENAMES") == "" or self.view.KVP.get("TYPENAME") == "":
|
|
159
167
|
# Using TYPENAMES= does result in an error.
|
|
160
|
-
raise MissingParameterValue("
|
|
168
|
+
raise MissingParameterValue("Empty TYPENAMES parameter", locator="typeNames")
|
|
161
169
|
elif typeNames is None:
|
|
162
170
|
# Not given, all types are returned
|
|
163
171
|
typeNames = self.all_feature_types
|
|
@@ -218,7 +226,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
218
226
|
# StandardInputParameters
|
|
219
227
|
Parameter("srsName", parser=CRS.from_string),
|
|
220
228
|
# Projection clause parameters
|
|
221
|
-
|
|
229
|
+
Parameter("propertyName", parser=fes20.parse_property_name),
|
|
222
230
|
# AdHoc Query parameters
|
|
223
231
|
Parameter("bbox", parser=BoundingBox.from_string),
|
|
224
232
|
Parameter(
|
|
@@ -239,7 +247,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
239
247
|
|
|
240
248
|
try:
|
|
241
249
|
if resultType == "HITS":
|
|
242
|
-
collection =
|
|
250
|
+
collection = query.get_hits()
|
|
243
251
|
elif resultType == "RESULTS":
|
|
244
252
|
# Validate StandardPresentationParameters
|
|
245
253
|
collection = self.get_paginated_results(query, **params)
|
|
@@ -248,16 +256,17 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
248
256
|
except ExternalParsingError as e:
|
|
249
257
|
# Bad input data
|
|
250
258
|
self._log_filter_error(query, logging.ERROR, e)
|
|
251
|
-
raise OperationParsingFailed(self._get_locator(**params)
|
|
259
|
+
raise OperationParsingFailed(str(e), locator=self._get_locator(**params)) from e
|
|
252
260
|
except ExternalValueError as e:
|
|
253
261
|
# Bad input data
|
|
254
262
|
self._log_filter_error(query, logging.ERROR, e)
|
|
255
|
-
raise InvalidParameterValue(self._get_locator(**params)
|
|
263
|
+
raise InvalidParameterValue(str(e), locator=self._get_locator(**params)) from e
|
|
256
264
|
except ValidationError as e:
|
|
257
265
|
# Bad input data
|
|
258
266
|
self._log_filter_error(query, logging.ERROR, e)
|
|
259
267
|
raise OperationParsingFailed(
|
|
260
|
-
|
|
268
|
+
"\n".join(map(str, e.messages)),
|
|
269
|
+
locator=self._get_locator(**params),
|
|
261
270
|
) from e
|
|
262
271
|
except FieldError as e:
|
|
263
272
|
# e.g. doing a LIKE on a foreign key, or requesting an unknown field.
|
|
@@ -265,7 +274,8 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
265
274
|
raise
|
|
266
275
|
self._log_filter_error(query, logging.ERROR, e)
|
|
267
276
|
raise InvalidParameterValue(
|
|
268
|
-
|
|
277
|
+
"Internal error when processing filter",
|
|
278
|
+
locator=self._get_locator(**params),
|
|
269
279
|
) from e
|
|
270
280
|
except (InternalError, ProgrammingError) as e:
|
|
271
281
|
# e.g. comparing datetime against integer
|
|
@@ -274,14 +284,15 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
274
284
|
logger.exception("WFS request failed: %s\nParams: %r", str(e), params)
|
|
275
285
|
msg = str(e)
|
|
276
286
|
locator = "srsName" if "Cannot find SRID" in msg else self._get_locator(**params)
|
|
277
|
-
raise InvalidParameterValue(
|
|
287
|
+
raise InvalidParameterValue(f"Invalid request: {msg}", locator=locator) from e
|
|
278
288
|
except (TypeError, ValueError) as e:
|
|
279
289
|
# TypeError/ValueError could reference a datatype mismatch in an
|
|
280
290
|
# ORM query, but it could also be an internal bug. In most cases,
|
|
281
291
|
# this is already caught by XsdElement.validate_comparison().
|
|
282
292
|
if self._is_orm_error(e):
|
|
283
293
|
raise InvalidParameterValue(
|
|
284
|
-
|
|
294
|
+
f"Invalid filter query: {e}",
|
|
295
|
+
locator=self._get_locator(**params),
|
|
285
296
|
) from e
|
|
286
297
|
raise
|
|
287
298
|
|
|
@@ -289,40 +300,35 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
289
300
|
if not output_crs and collection.results:
|
|
290
301
|
output_crs = collection.results[0].feature_type.crs
|
|
291
302
|
|
|
292
|
-
# These become init kwargs for the selected OutputRenderer class:
|
|
293
303
|
return {
|
|
304
|
+
# These become init kwargs for the selected OutputRenderer class:
|
|
294
305
|
"source_query": query,
|
|
295
306
|
"collection": collection,
|
|
296
307
|
"output_crs": output_crs,
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
def get_query(self, **params) -> queries.QueryExpression:
|
|
300
|
-
"""Create the query object that will process this request
|
|
311
|
+
"""Create or retrieve the query object that will process this request.
|
|
312
|
+
|
|
313
|
+
This can either retrieve the stored procedure (e.g. from a database),
|
|
314
|
+
or construct an ad-hoc query from request parameters.
|
|
315
|
+
"""
|
|
301
316
|
if params["STOREDQUERY_ID"]:
|
|
302
317
|
# The 'StoredQueryParameter' already parses the input into a complete object.
|
|
303
318
|
# When it's not provided, the regular Adhoc-query will be created from the KVP request.
|
|
304
319
|
query = params["STOREDQUERY_ID"]
|
|
320
|
+
query.bind(
|
|
321
|
+
# TODO: pass this in a cleaner way?
|
|
322
|
+
all_feature_types=self.all_feature_types_by_name, # for GetFeatureById
|
|
323
|
+
value_reference=params.get("valueReference"), # for GetPropertyValue
|
|
324
|
+
property_names=params.get("propertyName"), # for GetFeature with PropertyName
|
|
325
|
+
)
|
|
305
326
|
else:
|
|
306
|
-
query = queries.AdhocQuery.from_kvp_request(**params)
|
|
307
|
-
|
|
308
|
-
# TODO: pass this in a cleaner way?
|
|
309
|
-
query.bind(
|
|
310
|
-
all_feature_types=self.all_feature_types_by_name, # for GetFeatureById
|
|
311
|
-
value_reference=params.get("valueReference"), # for GetPropertyValue
|
|
312
|
-
)
|
|
327
|
+
query = queries.AdhocQuery.from_kvp_request(index=0, **params)
|
|
328
|
+
query.bind(all_feature_types=self.all_feature_types_by_name)
|
|
313
329
|
|
|
314
330
|
return query
|
|
315
331
|
|
|
316
|
-
def get_hits(self, query: queries.QueryExpression) -> output.FeatureCollection:
|
|
317
|
-
"""Return the number of hits"""
|
|
318
|
-
return query.get_hits()
|
|
319
|
-
|
|
320
|
-
def get_results(
|
|
321
|
-
self, query: queries.QueryExpression, start, count
|
|
322
|
-
) -> output.FeatureCollection:
|
|
323
|
-
"""Return the actual results"""
|
|
324
|
-
return query.get_results(start, count=count)
|
|
325
|
-
|
|
326
332
|
def get_paginated_results(
|
|
327
333
|
self, query: queries.QueryExpression, outputFormat, **params
|
|
328
334
|
) -> output.FeatureCollection:
|
|
@@ -336,7 +342,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
336
342
|
stop = start + page_size
|
|
337
343
|
|
|
338
344
|
# Perform query
|
|
339
|
-
collection =
|
|
345
|
+
collection = query.get_results(start, count=page_size)
|
|
340
346
|
|
|
341
347
|
# Allow presentation-layer to add extra logic.
|
|
342
348
|
if outputFormat.renderer_class is not None:
|
gisserver/output/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""All supported output formats"""
|
|
2
2
|
|
|
3
|
+
from .results import FeatureCollection, SimpleFeatureCollection # isort: skip (fixes import loops)
|
|
4
|
+
|
|
3
5
|
from django.utils.functional import classproperty
|
|
4
6
|
|
|
5
7
|
from gisserver import conf
|
|
@@ -13,7 +15,6 @@ from .gml32 import (
|
|
|
13
15
|
GML32Renderer,
|
|
14
16
|
GML32ValueRenderer,
|
|
15
17
|
)
|
|
16
|
-
from .results import FeatureCollection, SimpleFeatureCollection
|
|
17
18
|
from .xmlschema import XMLSchemaRenderer
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
gisserver/output/base.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
|
+
import typing
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
6
7
|
from django.db import models
|
|
@@ -9,11 +10,14 @@ from django.utils.html import escape
|
|
|
9
10
|
|
|
10
11
|
from gisserver import conf
|
|
11
12
|
from gisserver.exceptions import InvalidParameterValue
|
|
12
|
-
from gisserver.features import FeatureRelation, FeatureType
|
|
13
|
-
from gisserver.geometries import CRS
|
|
14
|
-
from gisserver.operations.base import WFSMethod
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
if typing.TYPE_CHECKING:
|
|
15
|
+
from gisserver.features import FeatureType
|
|
16
|
+
from gisserver.geometries import CRS
|
|
17
|
+
from gisserver.operations.base import WFSMethod
|
|
18
|
+
from gisserver.queries import FeatureProjection, FeatureRelation, QueryExpression
|
|
19
|
+
|
|
20
|
+
from .results import FeatureCollection, SimpleFeatureCollection
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class OutputRenderer:
|
|
@@ -35,17 +39,18 @@ class OutputRenderer:
|
|
|
35
39
|
def __init__(
|
|
36
40
|
self,
|
|
37
41
|
method: WFSMethod,
|
|
38
|
-
source_query,
|
|
42
|
+
source_query: QueryExpression,
|
|
39
43
|
collection: FeatureCollection,
|
|
40
44
|
output_crs: CRS,
|
|
41
45
|
):
|
|
42
46
|
"""
|
|
43
47
|
Receive the collected data to render.
|
|
48
|
+
These parameters are received from the ``get_context_data()`` method of the view.
|
|
44
49
|
|
|
45
50
|
:param method: The calling WFS Method (e.g. GetFeature class)
|
|
46
51
|
:param source_query: The query that generated this output.
|
|
47
|
-
:
|
|
48
|
-
:param
|
|
52
|
+
:param collection: The collected data for rendering.
|
|
53
|
+
:param projection: Which fields to render.
|
|
49
54
|
:param output_crs: The requested output projection.
|
|
50
55
|
"""
|
|
51
56
|
self.method = method
|
|
@@ -66,7 +71,7 @@ class OutputRenderer:
|
|
|
66
71
|
cls.validate(sub_collection.feature_type, **params)
|
|
67
72
|
|
|
68
73
|
queryset = cls.decorate_queryset(
|
|
69
|
-
sub_collection.
|
|
74
|
+
sub_collection.projection,
|
|
70
75
|
sub_collection.queryset,
|
|
71
76
|
output_crs,
|
|
72
77
|
**params,
|
|
@@ -84,54 +89,60 @@ class OutputRenderer:
|
|
|
84
89
|
and crs not in feature_type.supported_crs
|
|
85
90
|
):
|
|
86
91
|
raise InvalidParameterValue(
|
|
87
|
-
"srsName",
|
|
88
92
|
f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
|
|
93
|
+
locator="srsName",
|
|
89
94
|
)
|
|
90
95
|
|
|
91
96
|
@classmethod
|
|
92
97
|
def decorate_queryset(
|
|
93
98
|
cls,
|
|
94
|
-
|
|
99
|
+
projection: FeatureProjection,
|
|
95
100
|
queryset: models.QuerySet,
|
|
96
101
|
output_crs: CRS,
|
|
97
102
|
**params,
|
|
98
103
|
) -> models.QuerySet:
|
|
99
|
-
"""Apply presentation layer logic to the queryset.
|
|
104
|
+
"""Apply presentation layer logic to the queryset.
|
|
105
|
+
|
|
106
|
+
This allows fine-tuning the queryset for any special needs of the output rendering type.
|
|
107
|
+
|
|
108
|
+
:param feature_type: The feature that is being queried.
|
|
109
|
+
:param queryset: The constructed queryset so far.
|
|
110
|
+
:param output_crs: The projected output
|
|
111
|
+
:param params: All remaining request parameters (e.g. KVP parameters).
|
|
112
|
+
"""
|
|
100
113
|
# Avoid fetching relations, fetch these within the same query,
|
|
101
|
-
related = cls._get_prefetch_related(
|
|
114
|
+
related = cls._get_prefetch_related(projection, output_crs)
|
|
102
115
|
if related:
|
|
103
116
|
queryset = queryset.prefetch_related(*related)
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
# No need to request more data
|
|
107
|
-
fields = [
|
|
108
|
-
f.orm_field
|
|
109
|
-
for f in feature_type.xsd_type.elements
|
|
110
|
-
if not f.is_many or f.is_array # exclude M2M, but include ArrayField
|
|
111
|
-
]
|
|
112
|
-
return queryset.only("pk", *fields)
|
|
118
|
+
return queryset.only("pk", *projection.only_fields)
|
|
113
119
|
|
|
114
120
|
@classmethod
|
|
115
121
|
def _get_prefetch_related(
|
|
116
|
-
cls,
|
|
122
|
+
cls,
|
|
123
|
+
projection: FeatureProjection,
|
|
124
|
+
output_crs: CRS,
|
|
117
125
|
) -> list[models.Prefetch]:
|
|
118
126
|
"""Summarize which fields read data from relations.
|
|
119
127
|
|
|
120
128
|
This combines the input from flattened and complex fields,
|
|
121
129
|
in the unlikely case both variations are used in the same feature.
|
|
122
130
|
"""
|
|
131
|
+
# When PROPERTYNAME is used, determine all ORM paths (and levels) that this query will touch.
|
|
132
|
+
# The prefetch will only be applied when its field coincide with this list.
|
|
133
|
+
|
|
123
134
|
return [
|
|
124
135
|
models.Prefetch(
|
|
125
136
|
orm_relation.orm_path,
|
|
126
|
-
queryset=cls.get_prefetch_queryset(
|
|
137
|
+
queryset=cls.get_prefetch_queryset(projection, orm_relation, output_crs),
|
|
127
138
|
)
|
|
128
|
-
for orm_relation in
|
|
139
|
+
for orm_relation in projection.orm_relations
|
|
129
140
|
]
|
|
130
141
|
|
|
131
142
|
@classmethod
|
|
132
143
|
def get_prefetch_queryset(
|
|
133
144
|
cls,
|
|
134
|
-
|
|
145
|
+
projection: FeatureProjection,
|
|
135
146
|
feature_relation: FeatureRelation,
|
|
136
147
|
output_crs: CRS,
|
|
137
148
|
) -> models.QuerySet | None:
|
|
@@ -140,7 +151,8 @@ class OutputRenderer:
|
|
|
140
151
|
if feature_relation.related_model is None:
|
|
141
152
|
return None
|
|
142
153
|
|
|
143
|
-
|
|
154
|
+
# This will also apply .only() based on the projection's feature_relation
|
|
155
|
+
return projection.feature_type.get_related_queryset(feature_relation)
|
|
144
156
|
|
|
145
157
|
def get_response(self):
|
|
146
158
|
"""Render the output as streaming response."""
|
|
@@ -204,10 +216,14 @@ class OutputRenderer:
|
|
|
204
216
|
The actual exception is still raised and logged server-side.
|
|
205
217
|
"""
|
|
206
218
|
if settings.DEBUG:
|
|
207
|
-
return f"
|
|
219
|
+
return f"{exception.__class__.__name__}: {exception}"
|
|
208
220
|
else:
|
|
209
|
-
return f"
|
|
221
|
+
return f"{exception.__class__.__name__} during rendering!"
|
|
210
222
|
|
|
211
223
|
def render_stream(self):
|
|
212
224
|
"""Implement this in subclasses to implement a custom output format."""
|
|
213
225
|
raise NotImplementedError()
|
|
226
|
+
|
|
227
|
+
def get_projection(self, sub_collection: SimpleFeatureCollection) -> FeatureProjection:
|
|
228
|
+
"""Provide the projection clause for the given sub-collection."""
|
|
229
|
+
return self.source_query.get_projection(sub_collection.feature_type)
|