django-gisserver 1.5.0__py3-none-any.whl → 2.1__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.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/crs.py +401 -0
- gisserver/db.py +126 -51
- gisserver/exceptions.py +132 -4
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
- gisserver/extensions/queries.py +266 -0
- gisserver/features.py +253 -181
- gisserver/geometries.py +64 -311
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +311 -0
- gisserver/operations/base.py +130 -312
- gisserver/operations/wfs20.py +399 -375
- gisserver/output/__init__.py +14 -49
- gisserver/output/base.py +198 -144
- gisserver/output/csv.py +78 -75
- gisserver/output/geojson.py +37 -37
- gisserver/output/gml32.py +287 -259
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +73 -61
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +81 -169
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +426 -0
- gisserver/parsers/fes20/__init__.py +89 -31
- gisserver/parsers/fes20/expressions.py +172 -58
- gisserver/parsers/fes20/filters.py +116 -45
- gisserver/parsers/fes20/identifiers.py +66 -28
- gisserver/parsers/fes20/lookups.py +146 -0
- gisserver/parsers/fes20/operators.py +417 -161
- gisserver/parsers/fes20/sorting.py +113 -34
- gisserver/parsers/gml/__init__.py +17 -25
- gisserver/parsers/gml/base.py +36 -15
- gisserver/parsers/gml/geometries.py +105 -44
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +198 -0
- gisserver/parsers/ows/requests.py +160 -0
- gisserver/parsers/query.py +179 -0
- gisserver/parsers/values.py +87 -4
- gisserver/parsers/wfs20/__init__.py +39 -0
- gisserver/parsers/wfs20/adhoc.py +253 -0
- gisserver/parsers/wfs20/base.py +148 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +483 -0
- gisserver/parsers/wfs20/stored.py +193 -0
- gisserver/parsers/xml.py +261 -0
- gisserver/projection.py +367 -0
- gisserver/static/gisserver/index.css +20 -4
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +3 -3
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +445 -313
- gisserver/views.py +227 -62
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
gisserver/operations/base.py
CHANGED
|
@@ -1,141 +1,78 @@
|
|
|
1
1
|
"""The base protocol to implement an operation.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The request itself is parsed by :mod:`gisserver.parsers.wfs20`, and handled here.
|
|
4
|
+
It can be seen as the "controller" that handles the actual request type.
|
|
5
|
+
|
|
6
|
+
All operations extend from an WFSOperation class.
|
|
7
|
+
This defines the metadata for the ``GetCapabilities`` call and possible output formats.
|
|
8
|
+
|
|
9
|
+
Each :class:`WFSOperation` can define a :attr:`~WFSOperation.parser_class`, or let it autodetect.
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
from __future__ import annotations
|
|
9
13
|
|
|
14
|
+
import logging
|
|
10
15
|
import math
|
|
11
16
|
import re
|
|
12
|
-
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
from
|
|
17
|
+
import typing
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from functools import cached_property
|
|
15
20
|
|
|
16
|
-
from django.core.exceptions import
|
|
21
|
+
from django.core.exceptions import SuspiciousOperation
|
|
17
22
|
from django.http import HttpResponse
|
|
18
23
|
from django.template.loader import render_to_string
|
|
19
24
|
|
|
20
|
-
from gisserver.exceptions import
|
|
21
|
-
ExternalParsingError,
|
|
22
|
-
InvalidParameterValue,
|
|
23
|
-
MissingParameterValue,
|
|
24
|
-
OperationParsingFailed,
|
|
25
|
-
)
|
|
25
|
+
from gisserver.exceptions import InvalidParameterValue
|
|
26
26
|
from gisserver.features import FeatureType
|
|
27
|
+
from gisserver.output.base import OutputRenderer
|
|
28
|
+
from gisserver.parsers import ows
|
|
29
|
+
from gisserver.parsers.values import fix_type_name
|
|
27
30
|
|
|
31
|
+
if typing.TYPE_CHECKING:
|
|
32
|
+
from gisserver.views import WFSView
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
28
35
|
NoneType = type(None)
|
|
36
|
+
R = typing.TypeVar("R", bound=OutputRenderer)
|
|
29
37
|
|
|
30
38
|
RE_SAFE_FILENAME = re.compile(r"\A[A-Za-z0-9]+[A-Za-z0-9.]*") # no dot at the start.
|
|
31
39
|
|
|
40
|
+
__all__ = (
|
|
41
|
+
"Parameter",
|
|
42
|
+
"OutputFormat",
|
|
43
|
+
"WFSOperation",
|
|
44
|
+
"OutputFormatMixin",
|
|
45
|
+
"XmlTemplateMixin",
|
|
46
|
+
)
|
|
47
|
+
|
|
32
48
|
|
|
33
49
|
@dataclass(frozen=True)
|
|
34
50
|
class Parameter:
|
|
35
|
-
"""The
|
|
51
|
+
"""The ``<ows:Parameter>`` tag to output in ``GetCapabilities``."""
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
WFSMethod parameters attribute / get_parameters().
|
|
39
|
-
|
|
40
|
-
These fields are used to parse the incoming request.
|
|
41
|
-
The GetCapabilities method also reads the metadata of this value.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
#: Name of the parameter (using the XML casing style)
|
|
53
|
+
#: The name of the parameter
|
|
45
54
|
name: str
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
alias: str | None = None
|
|
49
|
-
|
|
50
|
-
#: Whether the parameter is required
|
|
51
|
-
required: bool = False
|
|
52
|
-
|
|
53
|
-
#: Optional parser callback to convert the parameter into a Python type
|
|
54
|
-
parser: Callable[[str], Any] = None
|
|
55
|
-
|
|
56
|
-
#: Whether to list this parameter in GetCapabilities
|
|
57
|
-
in_capabilities: bool = False
|
|
58
|
-
|
|
59
|
-
#: List of allowed values (also shown in GetCapabilities)
|
|
60
|
-
allowed_values: list | tuple | set | NoneType = None
|
|
61
|
-
|
|
62
|
-
#: Default value if it's not given
|
|
63
|
-
default: Any = None
|
|
64
|
-
|
|
65
|
-
#: Overridable dict for error messages
|
|
66
|
-
error_messages: dict = field(default_factory=dict)
|
|
67
|
-
|
|
68
|
-
def value_from_query(self, KVP: dict): # noqa: C901
|
|
69
|
-
"""Parse a request variable using the type definition.
|
|
70
|
-
|
|
71
|
-
This uses the dataclass settings to parse the incoming request value.
|
|
72
|
-
"""
|
|
73
|
-
# URL-based key-value-pair parameters use uppercase.
|
|
74
|
-
kvp_name = self.name.upper()
|
|
75
|
-
value = KVP.get(kvp_name)
|
|
76
|
-
if not value and self.alias:
|
|
77
|
-
value = KVP.get(self.alias.upper())
|
|
78
|
-
|
|
79
|
-
# Check required field settings, both empty and missing value are treated the same.
|
|
80
|
-
if not value:
|
|
81
|
-
if not self.required:
|
|
82
|
-
return self.default
|
|
83
|
-
elif value is None:
|
|
84
|
-
raise MissingParameterValue(f"Missing {kvp_name} parameter", locator=self.name)
|
|
85
|
-
else:
|
|
86
|
-
raise InvalidParameterValue(f"Empty {kvp_name} parameter", locator=self.name)
|
|
87
|
-
|
|
88
|
-
# Allow conversion into a python object
|
|
89
|
-
if self.parser is not None:
|
|
90
|
-
try:
|
|
91
|
-
value = self.parser(value)
|
|
92
|
-
except ExternalParsingError as e:
|
|
93
|
-
raise OperationParsingFailed(
|
|
94
|
-
f"Unable to parse {kvp_name} argument: {e}",
|
|
95
|
-
locator=self.name,
|
|
96
|
-
) from None
|
|
97
|
-
except (TypeError, ValueError, NotImplementedError) as e:
|
|
98
|
-
# TypeError/ValueError are raised by most handlers for unexpected data
|
|
99
|
-
# The NotImplementedError can be raised by fes parsing.
|
|
100
|
-
raise InvalidParameterValue(
|
|
101
|
-
f"Invalid {kvp_name} argument: {e}",
|
|
102
|
-
locator=self.name,
|
|
103
|
-
) from None
|
|
104
|
-
|
|
105
|
-
# Validate against value choices
|
|
106
|
-
self.validate_value(value)
|
|
107
|
-
|
|
108
|
-
return value
|
|
109
|
-
|
|
110
|
-
def validate_value(self, value):
|
|
111
|
-
"""Validate the parsed value.
|
|
112
|
-
This method can be overwritten by a subclass if needed.
|
|
113
|
-
"""
|
|
114
|
-
if self.allowed_values is not None and value not in self.allowed_values:
|
|
115
|
-
msg = self.error_messages.get("invalid", "Invalid value for {name}: {value}")
|
|
116
|
-
raise InvalidParameterValue(msg.format(name=self.name, value=value), locator=self.name)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class UnsupportedParameter(Parameter):
|
|
120
|
-
def value_from_query(self, KVP: dict):
|
|
121
|
-
kvp_name = self.name.upper()
|
|
122
|
-
if kvp_name in KVP:
|
|
123
|
-
raise InvalidParameterValue(
|
|
124
|
-
f"Support for {self.name} is not implemented!", locator=self.name
|
|
125
|
-
)
|
|
126
|
-
return None
|
|
55
|
+
#: The values to report in ``<ows:AllowedValues><ows:Value>``.
|
|
56
|
+
allowed_values: list[str]
|
|
127
57
|
|
|
128
58
|
|
|
129
|
-
class OutputFormat:
|
|
59
|
+
class OutputFormat(typing.Generic[R]):
|
|
130
60
|
"""Declare an output format for the method.
|
|
131
61
|
|
|
132
|
-
These formats
|
|
62
|
+
These formats are used in the :meth:`get_output_formats` for
|
|
63
|
+
any :class:`WFSOperation` that implements :class:`OutputFormatMixin`.
|
|
64
|
+
This also connects the output format with a :attr:`renderer_class`.
|
|
133
65
|
"""
|
|
134
66
|
|
|
67
|
+
#: The class that performs the output rendering.
|
|
68
|
+
renderer_class: type[R] | None = None
|
|
69
|
+
|
|
135
70
|
def __init__(
|
|
136
71
|
self,
|
|
137
72
|
content_type,
|
|
138
|
-
|
|
73
|
+
*,
|
|
74
|
+
subtype=None,
|
|
75
|
+
renderer_class: type[R] | None = None, # None is special case for GetCapabilities
|
|
139
76
|
max_page_size=None,
|
|
140
77
|
title=None,
|
|
141
78
|
in_capabilities=True,
|
|
@@ -143,17 +80,16 @@ class OutputFormat:
|
|
|
143
80
|
):
|
|
144
81
|
"""
|
|
145
82
|
:param content_type: The MIME-type used in the request to select this type.
|
|
83
|
+
:param subtype: Shorter alias for the MIME-type.
|
|
146
84
|
:param renderer_class: The class that performs the output rendering.
|
|
147
|
-
If it's not given, the operation renders it's output using an XML template.
|
|
148
85
|
:param max_page_size: Used to override the ``max_page_size`` of the renderer_class.
|
|
149
86
|
:param title: A human-friendly name for an HTML overview page.
|
|
150
87
|
:param in_capabilities: Whether this format needs to be advertised in GetCapabilities.
|
|
151
88
|
:param extra: Any additional key-value pairs for the definition.
|
|
152
|
-
Could include ``subtype`` as a shorter alias for the MIME-type.
|
|
153
89
|
"""
|
|
154
90
|
self.content_type = content_type
|
|
91
|
+
self.subtype = subtype
|
|
155
92
|
self.extra = extra
|
|
156
|
-
self.subtype = self.extra.get("subtype")
|
|
157
93
|
self.renderer_class = renderer_class
|
|
158
94
|
self.title = title
|
|
159
95
|
self.in_capabilities = in_capabilities
|
|
@@ -189,177 +125,100 @@ class OutputFormat:
|
|
|
189
125
|
|
|
190
126
|
def __str__(self):
|
|
191
127
|
extra = "".join(f"; {name}={value}" for name, value in self.extra.items())
|
|
128
|
+
if self.subtype:
|
|
129
|
+
extra = f"; subtype={self.subtype}{extra}"
|
|
192
130
|
return f"{self.content_type}{extra}"
|
|
193
131
|
|
|
194
132
|
def __repr__(self):
|
|
195
133
|
return f"<OutputFormat: {self}>"
|
|
196
134
|
|
|
197
135
|
|
|
198
|
-
class
|
|
136
|
+
class WFSOperation:
|
|
199
137
|
"""Basic interface to implement an WFS method.
|
|
200
138
|
|
|
201
139
|
Each operation in this GIS-server extends from this base class. This class
|
|
202
140
|
also exposes all requires metadata for the GetCapabilities request.
|
|
203
141
|
"""
|
|
204
142
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
parameters: list[Parameter] = []
|
|
209
|
-
|
|
210
|
-
#: List the supported output formats for this method.
|
|
211
|
-
output_formats: list[OutputFormat] = []
|
|
212
|
-
|
|
213
|
-
#: Default template to use for rendering
|
|
214
|
-
xml_template_name = None
|
|
143
|
+
#: Optionally, mention explicitly what the parser class should be used.
|
|
144
|
+
#: Otherwise, it's automatically resolved from the registered types.
|
|
145
|
+
parser_class: type[ows.BaseOwsRequest] = None
|
|
215
146
|
|
|
216
|
-
|
|
217
|
-
|
|
147
|
+
def __init__(self, view: WFSView, ows_request: ows.BaseOwsRequest):
|
|
148
|
+
self.view = view
|
|
149
|
+
self.ows_request = ows_request
|
|
218
150
|
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
#
|
|
223
|
-
"
|
|
224
|
-
"
|
|
225
|
-
|
|
226
|
-
self.view.xml_namespace: "app",
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
def get_parameters(self):
|
|
230
|
-
"""Dynamically return the supported parameters for this method."""
|
|
231
|
-
parameters = [
|
|
232
|
-
# Always add SERVICE and VERSION as required parameters
|
|
233
|
-
# Part of BaseRequest:
|
|
234
|
-
Parameter(
|
|
235
|
-
"service",
|
|
236
|
-
required=not bool(self.view.default_service),
|
|
237
|
-
in_capabilities=True,
|
|
238
|
-
allowed_values=list(self.view.accept_operations.keys()),
|
|
239
|
-
default=self.view.default_service, # can be None or e.g. "WFS"
|
|
240
|
-
error_messages={"invalid": "Unsupported service type: {value}."},
|
|
241
|
-
),
|
|
242
|
-
Parameter(
|
|
243
|
-
"version",
|
|
244
|
-
required=True, # Mandatory except for GetCapabilities
|
|
245
|
-
allowed_values=self.view.accept_versions,
|
|
246
|
-
error_messages={
|
|
247
|
-
"invalid": "WFS Server does not support VERSION {value}.",
|
|
248
|
-
},
|
|
249
|
-
),
|
|
250
|
-
] + self.parameters
|
|
251
|
-
|
|
252
|
-
if self.output_formats:
|
|
253
|
-
parameters += [
|
|
254
|
-
# Part of StandardPresentationParameters:
|
|
255
|
-
Parameter(
|
|
256
|
-
"outputFormat",
|
|
257
|
-
in_capabilities=True,
|
|
258
|
-
allowed_values=self.output_formats,
|
|
259
|
-
parser=self._parse_output_format,
|
|
260
|
-
default=self.output_formats[0],
|
|
261
|
-
)
|
|
262
|
-
]
|
|
151
|
+
def get_parameters(self) -> list[Parameter]:
|
|
152
|
+
"""Parameters to advertise in the capabilities for this method."""
|
|
153
|
+
return [
|
|
154
|
+
# Always advertise SERVICE and VERSION as required parameters:
|
|
155
|
+
Parameter("service", allowed_values=list(self.view.accept_operations.keys())),
|
|
156
|
+
Parameter("version", allowed_values=self.view.accept_versions),
|
|
157
|
+
]
|
|
263
158
|
|
|
264
|
-
|
|
159
|
+
def validate_request(self, ows_request: ows.BaseOwsRequest):
|
|
160
|
+
"""Validate the request."""
|
|
265
161
|
|
|
266
|
-
def
|
|
267
|
-
"""
|
|
268
|
-
|
|
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".
|
|
271
|
-
for v in {value, value.replace(" ", "+")}:
|
|
272
|
-
for o in self.output_formats:
|
|
273
|
-
if o.matches(v):
|
|
274
|
-
return o
|
|
275
|
-
raise InvalidParameterValue(
|
|
276
|
-
f"'{value}' is not a permitted output format for this operation.",
|
|
277
|
-
locator="outputformat",
|
|
278
|
-
) from None
|
|
279
|
-
|
|
280
|
-
def _parse_namespaces(self, value) -> dict[str, str]:
|
|
281
|
-
"""Parse the 'namespaces' definition.
|
|
162
|
+
def process_request(self, ows_request: ows.BaseOwsRequest):
|
|
163
|
+
"""Default call implementation: render an XML template."""
|
|
164
|
+
raise NotImplementedError()
|
|
282
165
|
|
|
283
|
-
|
|
284
|
-
|
|
166
|
+
@cached_property
|
|
167
|
+
def all_feature_types_by_name(self) -> dict[str, FeatureType]:
|
|
168
|
+
"""Create a lookup for feature types by name.
|
|
169
|
+
This can be cached as the :class:`WFSOperation` is instantiated with each request.
|
|
285
170
|
"""
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
171
|
+
return {ft.xml_name: ft for ft in self.view.get_bound_feature_types()}
|
|
172
|
+
|
|
173
|
+
def resolve_feature_type(self, type_name: str, locator: str = "typeNames") -> FeatureType:
|
|
174
|
+
"""Find the FeatureType defined in the application that corresponds with the XML type name."""
|
|
175
|
+
alt_type_name = fix_type_name(type_name, self.view.xml_namespace)
|
|
176
|
+
try:
|
|
177
|
+
return self.all_feature_types_by_name[alt_type_name]
|
|
178
|
+
except KeyError:
|
|
179
|
+
if alt_type_name:
|
|
180
|
+
logger.debug(
|
|
181
|
+
"Unable to locate '%s' (nor %s) in the server, options are: %r",
|
|
182
|
+
type_name,
|
|
183
|
+
alt_type_name,
|
|
184
|
+
list(self.all_feature_types_by_name),
|
|
299
185
|
)
|
|
300
|
-
if prefix.endswith(")"):
|
|
301
|
-
# xmlns(http://...)
|
|
302
|
-
prefix = ""
|
|
303
|
-
uri = prefix[6:-1]
|
|
304
186
|
else:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
namespaces[uri] = prefix
|
|
315
|
-
|
|
316
|
-
return namespaces
|
|
317
|
-
|
|
318
|
-
def parse_request(self, KVP: dict) -> dict[str, Any]:
|
|
319
|
-
"""Parse the parameters of the request"""
|
|
320
|
-
self.namespaces.update(self._parse_namespaces(KVP.get("NAMESPACES")))
|
|
321
|
-
param_values = {param.name: param.value_from_query(KVP) for param in self.get_parameters()}
|
|
322
|
-
param_values["NAMESPACES"] = self.namespaces
|
|
323
|
-
|
|
324
|
-
for param in self.get_parameters():
|
|
325
|
-
param_values[param.name] = param.value_from_query(KVP)
|
|
326
|
-
|
|
327
|
-
# Update version if requested.
|
|
328
|
-
# This is stored on the view, so exceptions also use it.
|
|
329
|
-
if param_values.get("version"):
|
|
330
|
-
self.view.set_version(param_values["version"])
|
|
187
|
+
logger.debug(
|
|
188
|
+
"Unable to locate '%s' in the server, options are: %r",
|
|
189
|
+
type_name,
|
|
190
|
+
list(self.all_feature_types_by_name),
|
|
191
|
+
)
|
|
192
|
+
raise InvalidParameterValue(
|
|
193
|
+
f"Typename '{type_name}' doesn't exist in this server.",
|
|
194
|
+
locator=locator or "typeNames",
|
|
195
|
+
) from None
|
|
331
196
|
|
|
332
|
-
self.validate(**param_values)
|
|
333
|
-
return param_values
|
|
334
197
|
|
|
335
|
-
|
|
336
|
-
|
|
198
|
+
class XmlTemplateMixin:
|
|
199
|
+
"""Mixin to support methods that render using a template."""
|
|
337
200
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if "outputFormat" in params:
|
|
342
|
-
output_format: OutputFormat = params["outputFormat"]
|
|
201
|
+
#: Default template to use for rendering
|
|
202
|
+
#: This is resolved as :samp:`gisserver/{service}/{version}/{xml_template_name}`.
|
|
203
|
+
xml_template_name = None
|
|
343
204
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
renderer = output_format.renderer_class(self, **context)
|
|
347
|
-
return renderer.get_response()
|
|
205
|
+
#: The content-type to render.
|
|
206
|
+
xml_content_type = "text/xml; charset=utf-8"
|
|
348
207
|
|
|
349
|
-
|
|
208
|
+
def process_request(self, ows_request: ows.BaseOwsRequest):
|
|
209
|
+
"""Process the request by rendering a Django template."""
|
|
210
|
+
context = self.get_context_data()
|
|
211
|
+
return self.render_xml(context, ows_request)
|
|
350
212
|
|
|
351
|
-
def get_context_data(self
|
|
213
|
+
def get_context_data(self):
|
|
352
214
|
"""Collect all arguments to use for rendering the XML template"""
|
|
353
215
|
return {}
|
|
354
216
|
|
|
355
|
-
def render_xml(self, context,
|
|
356
|
-
"""
|
|
357
|
-
|
|
358
|
-
This is the default method when the OutputFormat class doesn't have a renderer_class
|
|
359
|
-
"""
|
|
217
|
+
def render_xml(self, context, ows_request: ows.BaseOwsRequest):
|
|
218
|
+
"""Render the response using a template."""
|
|
360
219
|
return HttpResponse(
|
|
361
220
|
render_to_string(
|
|
362
|
-
self._get_xml_template_name(),
|
|
221
|
+
self._get_xml_template_name(ows_request),
|
|
363
222
|
context={
|
|
364
223
|
"view": self.view,
|
|
365
224
|
"app_xml_namespace": self.view.xml_namespace,
|
|
@@ -370,9 +229,9 @@ class WFSMethod:
|
|
|
370
229
|
content_type=self.xml_content_type,
|
|
371
230
|
)
|
|
372
231
|
|
|
373
|
-
def _get_xml_template_name(self):
|
|
232
|
+
def _get_xml_template_name(self, ows_request: ows.BaseOwsRequest) -> str:
|
|
374
233
|
"""Generate the XML template name for this operation, and check its file pattern"""
|
|
375
|
-
service =
|
|
234
|
+
service = ows_request.service.lower()
|
|
376
235
|
template_name = f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
|
|
377
236
|
|
|
378
237
|
# Since 'service' and 'version' are based on external input,
|
|
@@ -383,66 +242,25 @@ class WFSMethod:
|
|
|
383
242
|
return template_name
|
|
384
243
|
|
|
385
244
|
|
|
386
|
-
class
|
|
387
|
-
"""
|
|
388
|
-
|
|
389
|
-
def __init__(self, *args, **kwargs):
|
|
390
|
-
super().__init__(*args, **kwargs)
|
|
391
|
-
|
|
392
|
-
# Retrieve the feature types directly, as these are needed during parsing.
|
|
393
|
-
# This is not delayed until _parse_type_names() as that wraps any TypeError
|
|
394
|
-
# from view.get_feature_types() as an InvalidParameterValue exception.
|
|
395
|
-
self.all_feature_types = self.view.get_feature_types()
|
|
396
|
-
self.all_feature_types_by_name = _get_feature_types_by_name(self.all_feature_types)
|
|
245
|
+
class OutputFormatMixin:
|
|
246
|
+
"""Mixin to support methods that handle different output formats."""
|
|
397
247
|
|
|
398
|
-
def
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"typeNames",
|
|
404
|
-
alias="typeName", # WFS 1.0 name, but still needed for CITE tests
|
|
405
|
-
required=False, # sometimes required, depends on other parameters
|
|
406
|
-
parser=self._parse_type_names,
|
|
407
|
-
),
|
|
408
|
-
]
|
|
409
|
-
|
|
410
|
-
def _parse_type_names(self, type_names) -> list[FeatureType]:
|
|
411
|
-
"""Find the requested feature types by name"""
|
|
412
|
-
if "(" in type_names:
|
|
413
|
-
# This allows to perform multiple queries in a single request:
|
|
414
|
-
# TYPENAMES=(A)(B)&FILTER=(filter for A)(filter for B)
|
|
415
|
-
raise OperationParsingFailed(
|
|
416
|
-
"Parameter lists to perform multiple queries are not supported yet.",
|
|
417
|
-
locator="typenames",
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
return [self._parse_type_name(name, locator="typenames") for name in type_names.split(",")]
|
|
421
|
-
|
|
422
|
-
def _parse_type_name(self, name, locator="typename") -> FeatureType:
|
|
423
|
-
"""Find the requested feature type for a type name"""
|
|
424
|
-
app_prefix = self.namespaces[self.view.xml_namespace]
|
|
425
|
-
# strip our XML prefix
|
|
426
|
-
local_name = name[len(app_prefix) + 1 :] if name.startswith(f"{app_prefix}:") else name
|
|
427
|
-
|
|
428
|
-
try:
|
|
429
|
-
return self.all_feature_types_by_name[local_name]
|
|
430
|
-
except KeyError:
|
|
431
|
-
raise InvalidParameterValue(
|
|
432
|
-
f"Typename '{name}' doesn't exist in this server. "
|
|
433
|
-
f"Please check the capabilities and reformulate your request.",
|
|
434
|
-
locator=locator,
|
|
435
|
-
) from None
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def _get_feature_types_by_name(feature_types) -> dict[str, FeatureType]:
|
|
439
|
-
"""Create a lookup for feature types by name."""
|
|
440
|
-
features_by_name = {ft.name: ft for ft in feature_types}
|
|
441
|
-
|
|
442
|
-
# Check against bad configuration
|
|
443
|
-
if len(features_by_name) != len(feature_types):
|
|
444
|
-
all_names = [ft.name for ft in feature_types]
|
|
445
|
-
duplicates = ", ".join(sorted({n for n in all_names if all_names.count(n) > 1}))
|
|
446
|
-
raise ImproperlyConfigured(f"FeatureType names should be unique: {duplicates}")
|
|
248
|
+
def get_output_formats(self) -> list[OutputFormat]:
|
|
249
|
+
"""List all output formats. This is exposed in GetCapabilities,
|
|
250
|
+
and used for internal rendering.
|
|
251
|
+
"""
|
|
252
|
+
raise NotImplementedError()
|
|
447
253
|
|
|
448
|
-
|
|
254
|
+
def resolve_output_format(self, value, locator="outputFormat") -> OutputFormat:
|
|
255
|
+
"""Select the proper OutputFormat object based on the input value"""
|
|
256
|
+
# When using ?OUTPUTFORMAT=application/gml+xml", it is actually a URL-encoded space
|
|
257
|
+
# character. Hence, spaces are replaced back to a '+' character to allow such notation
|
|
258
|
+
# instead of forcing it to be ?OUTPUTFORMAT=application/gml%2bxml".
|
|
259
|
+
for v in {value, value.replace(" ", "+")}:
|
|
260
|
+
for o in self.get_output_formats():
|
|
261
|
+
if o.matches(v):
|
|
262
|
+
return o
|
|
263
|
+
raise InvalidParameterValue(
|
|
264
|
+
f"'{value}' is not a permitted output format for this operation.",
|
|
265
|
+
locator=locator or "outputFormat",
|
|
266
|
+
) from None
|