django-gisserver 1.5.0__py3-none-any.whl → 2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- 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.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/views.py
CHANGED
|
@@ -2,29 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import re
|
|
6
|
-
from urllib.parse import urlencode
|
|
7
|
+
from urllib.parse import unquote_plus, urlencode
|
|
7
8
|
|
|
8
9
|
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
|
9
10
|
from django.core.exceptions import PermissionDenied as Django_PermissionDenied
|
|
10
11
|
from django.shortcuts import render
|
|
11
12
|
from django.views import View
|
|
13
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
12
14
|
|
|
13
15
|
from gisserver import conf
|
|
14
16
|
from gisserver.exceptions import (
|
|
17
|
+
ExternalParsingError,
|
|
15
18
|
InvalidParameterValue,
|
|
16
|
-
MissingParameterValue,
|
|
17
19
|
OperationNotSupported,
|
|
20
|
+
OperationParsingFailed,
|
|
21
|
+
OperationProcessingFailed,
|
|
18
22
|
OWSException,
|
|
19
23
|
PermissionDenied,
|
|
20
24
|
)
|
|
21
25
|
from gisserver.features import FeatureType, ServiceDescription
|
|
22
26
|
from gisserver.operations import base, wfs20
|
|
27
|
+
from gisserver.parsers.ows import (
|
|
28
|
+
BaseOwsRequest,
|
|
29
|
+
KVPRequest,
|
|
30
|
+
resolve_kvp_parser_class,
|
|
31
|
+
resolve_xml_parser_class,
|
|
32
|
+
)
|
|
33
|
+
from gisserver.parsers.xml import parse_xml_from_string, split_ns
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
23
36
|
|
|
24
37
|
SAFE_VERSION = re.compile(r"\A[0-9.]+\Z")
|
|
25
38
|
|
|
26
39
|
|
|
27
|
-
class
|
|
40
|
+
class OWSView(View):
|
|
28
41
|
"""The base logic to implement OGC view like WFS.
|
|
29
42
|
|
|
30
43
|
Each subclass defines 'accept_operations' with the desired RPC operations.
|
|
@@ -33,6 +46,9 @@ class GISView(View):
|
|
|
33
46
|
#: Define the namespace to use in the XML
|
|
34
47
|
xml_namespace = "http://example.org/gisserver"
|
|
35
48
|
|
|
49
|
+
#: Define namespace aliases to use, default is {"app": self.xml_namespace}
|
|
50
|
+
xml_namespace_aliases = None
|
|
51
|
+
|
|
36
52
|
#: Default version to use
|
|
37
53
|
version = "2.0.0"
|
|
38
54
|
|
|
@@ -54,6 +70,10 @@ class GISView(View):
|
|
|
54
70
|
#: Whether to render GET HTML pages
|
|
55
71
|
use_html_templates = True
|
|
56
72
|
|
|
73
|
+
#: The OWS request, which contains the complete parsed request.
|
|
74
|
+
ows_request: BaseOwsRequest = None
|
|
75
|
+
|
|
76
|
+
@csrf_exempt
|
|
57
77
|
def dispatch(self, request, *args, **kwargs):
|
|
58
78
|
"""Render proper XML errors for exceptions on all request types."""
|
|
59
79
|
try:
|
|
@@ -78,6 +98,23 @@ class GISView(View):
|
|
|
78
98
|
else:
|
|
79
99
|
return None
|
|
80
100
|
|
|
101
|
+
@classmethod
|
|
102
|
+
def get_xml_namespace_aliases(cls) -> dict[str, str]:
|
|
103
|
+
"""Provide all namespaces aliases with a namespace.
|
|
104
|
+
This is most useful for parsing input.
|
|
105
|
+
"""
|
|
106
|
+
return cls.xml_namespace_aliases or {"app": cls.xml_namespace}
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def get_xml_namespaces_to_prefixes(cls) -> dict[str, str]:
|
|
110
|
+
"""Provide a mapping from namespace to prefix.
|
|
111
|
+
This is most useful for rendering output.
|
|
112
|
+
"""
|
|
113
|
+
return {
|
|
114
|
+
xml_namespace: prefix
|
|
115
|
+
for prefix, xml_namespace in cls.get_xml_namespace_aliases().items()
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
def get(self, request, *args, **kwargs):
|
|
82
119
|
"""Entry point to handle HTTP GET requests.
|
|
83
120
|
|
|
@@ -86,22 +123,78 @@ class GISView(View):
|
|
|
86
123
|
|
|
87
124
|
All query parameters are handled as case-insensitive.
|
|
88
125
|
"""
|
|
89
|
-
#
|
|
90
|
-
|
|
126
|
+
# Parse GET parameters in Key-Value-Pair syntax format.
|
|
127
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
128
|
+
logger.debug(
|
|
129
|
+
"Parsing GET parameters:\n%s",
|
|
130
|
+
unquote_plus(request.META["QUERY_STRING"].replace("&", "\n")),
|
|
131
|
+
)
|
|
132
|
+
self.kvp = kvp = KVPRequest(request.GET, ns_aliases=self.get_xml_namespace_aliases())
|
|
133
|
+
|
|
134
|
+
# Get service (only raises error when value is missing and "default" parameter is not given)
|
|
135
|
+
defaults = {"default": self.default_service} if self.default_service else {}
|
|
136
|
+
service = kvp.get_str("service", **defaults).upper()
|
|
91
137
|
|
|
92
138
|
# Perform early version parsing. The detailed validation happens by the operation
|
|
93
139
|
# Parameter objects, but by performing an early check, templates can use that version.
|
|
94
|
-
version =
|
|
95
|
-
|
|
96
|
-
self.set_version(version)
|
|
140
|
+
version = kvp.get_str("version", default=None)
|
|
141
|
+
self.set_version(service, version)
|
|
97
142
|
|
|
98
143
|
# Allow for a user-friendly opening page (hence the version check above)
|
|
99
144
|
if self.use_html_templates and self.is_index_request():
|
|
100
|
-
return self.render_index()
|
|
145
|
+
return self.render_index(service)
|
|
146
|
+
|
|
147
|
+
# Find the registered operation that handles the request
|
|
148
|
+
operation = kvp.get_str("request")
|
|
149
|
+
wfs_operation_cls = self.get_operation_class(service, operation)
|
|
101
150
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
151
|
+
# Parse the request syntax
|
|
152
|
+
request_cls = wfs_operation_cls.parser_class or resolve_kvp_parser_class(kvp)
|
|
153
|
+
self.ows_request = request_cls.from_kvp_request(kvp)
|
|
154
|
+
|
|
155
|
+
# Process the request!
|
|
156
|
+
return self.call_operation(wfs_operation_cls)
|
|
157
|
+
|
|
158
|
+
def post(self, request, *args, **kwargs):
|
|
159
|
+
"""Entry point to handle HTTP POST requests.
|
|
160
|
+
|
|
161
|
+
This parses the XML to get the correct service and operation,
|
|
162
|
+
to call the proper WFSMethod.
|
|
163
|
+
"""
|
|
164
|
+
# Parse the XML body
|
|
165
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
166
|
+
logger.debug("Parsing POST:\n%s", request.body.decode().rstrip())
|
|
167
|
+
try:
|
|
168
|
+
root = parse_xml_from_string(
|
|
169
|
+
request.body, extra_ns_aliases=self.get_xml_namespace_aliases()
|
|
170
|
+
)
|
|
171
|
+
except ExternalParsingError as e:
|
|
172
|
+
raise OperationParsingFailed(f"Unable to parse XML: {e}") from e
|
|
173
|
+
|
|
174
|
+
# Find the registered operation that handles the request
|
|
175
|
+
service = (
|
|
176
|
+
root.attrib.get("service", self.default_service)
|
|
177
|
+
if self.default_service
|
|
178
|
+
else root.get_str_attribute("service")
|
|
179
|
+
)
|
|
180
|
+
operation = split_ns(root.tag)[1]
|
|
181
|
+
wfs_operation_cls = self.get_operation_class(service, operation)
|
|
182
|
+
|
|
183
|
+
# Parse the request syntax
|
|
184
|
+
request_cls = wfs_operation_cls.parser_class or resolve_xml_parser_class(root)
|
|
185
|
+
try:
|
|
186
|
+
self.ows_request = request_cls.from_xml(root)
|
|
187
|
+
self.set_version(service, self.ows_request.version)
|
|
188
|
+
|
|
189
|
+
# Process the request!
|
|
190
|
+
return self.call_operation(wfs_operation_cls)
|
|
191
|
+
except (OperationParsingFailed, OperationProcessingFailed) as e:
|
|
192
|
+
# The WFS spec dictates that these exceptions
|
|
193
|
+
# (and ResponseCacheExpired, CannotLockAllFeatures, FeaturesNotLocked which we don't raise)
|
|
194
|
+
# should report the 'handle' in the 'locator' argument of the exception when it was provided.
|
|
195
|
+
if self.ows_request.handle:
|
|
196
|
+
e.locator = self.ows_request.handle
|
|
197
|
+
raise
|
|
105
198
|
|
|
106
199
|
def is_index_request(self):
|
|
107
200
|
"""Tell whether to index page should be shown."""
|
|
@@ -109,22 +202,29 @@ class GISView(View):
|
|
|
109
202
|
# (misspelled?) parameters on the query string, this is considered to be an index page.
|
|
110
203
|
# Minimal request has 2 parameters (SERVICE=WFS&REQUEST=GetCapabilities).
|
|
111
204
|
# Some servers also allow a default of SERVICE, thus needing even less.
|
|
112
|
-
return
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
205
|
+
return (
|
|
206
|
+
self.request.method == "GET"
|
|
207
|
+
and len(self.request.GET) < 2
|
|
208
|
+
and {
|
|
209
|
+
"REQUEST",
|
|
210
|
+
"SERVICE",
|
|
211
|
+
"VERSION",
|
|
212
|
+
"ACCEPTVERSIONS",
|
|
213
|
+
}.isdisjoint(key.upper() for key in self.request.GET)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def render_index(self, service: str | None = None):
|
|
120
217
|
"""Render the index page."""
|
|
121
218
|
# Not using the whole TemplateResponseMixin config with configurable parameters.
|
|
122
219
|
# If this really needs more configuration, overriding is likely just as easy.
|
|
123
|
-
return render(
|
|
220
|
+
return render(
|
|
221
|
+
self.request,
|
|
222
|
+
self.get_index_template_names(service),
|
|
223
|
+
self.get_index_context_data(service=service),
|
|
224
|
+
)
|
|
124
225
|
|
|
125
|
-
def get_index_context_data(self, **kwargs):
|
|
226
|
+
def get_index_context_data(self, service: str | None = None, **kwargs):
|
|
126
227
|
"""Provide the context data for the index page."""
|
|
127
|
-
service = self.KVP.get("SERVICE", self.default_service)
|
|
128
228
|
root_url = self.request.build_absolute_uri()
|
|
129
229
|
|
|
130
230
|
# Allow passing extra vendor parameters to the links generated in the template
|
|
@@ -138,6 +238,7 @@ class GISView(View):
|
|
|
138
238
|
|
|
139
239
|
return {
|
|
140
240
|
"view": self,
|
|
241
|
+
"xml_namespaces": self.get_xml_namespaces_to_prefixes(),
|
|
141
242
|
"service": service,
|
|
142
243
|
"root_url": root_url,
|
|
143
244
|
"base_query": base_query,
|
|
@@ -149,16 +250,11 @@ class GISView(View):
|
|
|
149
250
|
**kwargs,
|
|
150
251
|
}
|
|
151
252
|
|
|
152
|
-
def get_index_template_names(self):
|
|
253
|
+
def get_index_template_names(self, service: str | None = None):
|
|
153
254
|
"""Get the index page template name.
|
|
154
255
|
If no template is configured, some reasonable defaults are selected.
|
|
155
256
|
"""
|
|
156
|
-
|
|
157
|
-
if raw_service and raw_service in self.accept_operations:
|
|
158
|
-
service = raw_service.lower()
|
|
159
|
-
else:
|
|
160
|
-
service = "default"
|
|
161
|
-
|
|
257
|
+
service = service.lower() if service and service in self.accept_operations else "default"
|
|
162
258
|
if self.index_template_name:
|
|
163
259
|
# Allow the same substitutions for a manually configured template.
|
|
164
260
|
return [self.index_template_name.format(service=service, version=self.version)]
|
|
@@ -169,15 +265,13 @@ class GISView(View):
|
|
|
169
265
|
"gisserver/index.html",
|
|
170
266
|
]
|
|
171
267
|
|
|
172
|
-
def get_operation_class(self) -> type[base.
|
|
268
|
+
def get_operation_class(self, service: str, request: str) -> type[base.WFSOperation]:
|
|
173
269
|
"""Resolve the method that the client wants to call."""
|
|
174
270
|
if not self.accept_operations:
|
|
175
271
|
raise ImproperlyConfigured("View has no operations")
|
|
176
272
|
|
|
177
|
-
# The service is always WFS
|
|
178
|
-
service = self._get_required_arg("SERVICE", self.default_service).upper()
|
|
179
273
|
try:
|
|
180
|
-
operations = self.accept_operations[service]
|
|
274
|
+
operations = self.accept_operations[service.upper()]
|
|
181
275
|
except KeyError:
|
|
182
276
|
allowed = ", ".join(sorted(self.accept_operations.keys()))
|
|
183
277
|
raise InvalidParameterValue(
|
|
@@ -187,42 +281,42 @@ class GISView(View):
|
|
|
187
281
|
|
|
188
282
|
# Resolve the operation
|
|
189
283
|
# In mapserver, the operation name is case-insensitive.
|
|
190
|
-
operation = self._get_required_arg("REQUEST").upper()
|
|
191
284
|
uc_methods = {name.upper(): method for name, method in operations.items()}
|
|
192
|
-
|
|
193
285
|
try:
|
|
194
|
-
return uc_methods[
|
|
286
|
+
return uc_methods[request.upper()]
|
|
195
287
|
except KeyError:
|
|
196
288
|
allowed = ", ".join(operations.keys())
|
|
197
289
|
raise OperationNotSupported(
|
|
198
|
-
f"'{
|
|
290
|
+
f"'{request}' is not implemented, supported are: {allowed}.",
|
|
199
291
|
locator="request",
|
|
200
292
|
) from None
|
|
201
293
|
|
|
202
|
-
def call_operation(self,
|
|
294
|
+
def call_operation(self, wfs_operation_cls: type[base.WFSOperation]):
|
|
203
295
|
"""Call the resolved method."""
|
|
204
|
-
|
|
205
|
-
param_values = wfs_method.parse_request(self.KVP)
|
|
206
|
-
return wfs_method(**param_values) # goes into __call__()
|
|
296
|
+
self.request.ows_request = self.ows_request # for check_permissions()
|
|
207
297
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
except KeyError:
|
|
212
|
-
if default is not None:
|
|
213
|
-
return default
|
|
214
|
-
raise MissingParameterValue(locator=argname.lower()) from None
|
|
298
|
+
wfs_operation = wfs_operation_cls(self, self.ows_request)
|
|
299
|
+
wfs_operation.validate_request(self.ows_request)
|
|
300
|
+
return wfs_operation.process_request(self.ows_request)
|
|
215
301
|
|
|
216
|
-
def get_service_description(self, service: str) -> ServiceDescription:
|
|
302
|
+
def get_service_description(self, service: str | None = None) -> ServiceDescription:
|
|
217
303
|
"""Provide the (dynamically generated) service description."""
|
|
218
304
|
return self.service_description or ServiceDescription(title="Unnamed")
|
|
219
305
|
|
|
220
|
-
def set_version(self, version):
|
|
306
|
+
def set_version(self, service: str, version: str | None):
|
|
221
307
|
"""Enforce a particular version based on the request."""
|
|
308
|
+
if not version:
|
|
309
|
+
return
|
|
310
|
+
|
|
222
311
|
if not SAFE_VERSION.match(version):
|
|
223
312
|
# Make really sure we didn't mess things up, as this causes file includes.
|
|
224
313
|
raise SuspiciousOperation("Invalid/insecure version number parsed")
|
|
225
314
|
|
|
315
|
+
if version not in self.accept_versions:
|
|
316
|
+
raise InvalidParameterValue(
|
|
317
|
+
f"{service} Server does not support VERSION {version}.", locator="version"
|
|
318
|
+
)
|
|
319
|
+
|
|
226
320
|
# Enforce the requested version
|
|
227
321
|
self.version = version
|
|
228
322
|
|
|
@@ -232,7 +326,7 @@ class GISView(View):
|
|
|
232
326
|
return self.request.build_absolute_uri(self.request.path)
|
|
233
327
|
|
|
234
328
|
|
|
235
|
-
class WFSView(
|
|
329
|
+
class WFSView(OWSView):
|
|
236
330
|
"""A view for a single WFS server.
|
|
237
331
|
|
|
238
332
|
This view exposes multiple dataset,
|
|
@@ -246,6 +340,7 @@ class WFSView(GISView):
|
|
|
246
340
|
max_page_size = conf.GISSERVER_DEFAULT_MAX_PAGE_SIZE
|
|
247
341
|
|
|
248
342
|
#: Define the features (=tables) in this dataset.
|
|
343
|
+
#: For dynamic per-request logic, consider overwriting :meth:`get_feature_types` instead.
|
|
249
344
|
feature_types: list[FeatureType] = []
|
|
250
345
|
|
|
251
346
|
#: Internal configuration of all available RPC calls.
|
|
@@ -270,7 +365,7 @@ class WFSView(GISView):
|
|
|
270
365
|
"ImplementsTransactionalWFS": False, # only reads
|
|
271
366
|
"ImplementsLockingWFS": False, # only basic WFS
|
|
272
367
|
"KVPEncoding": True, # HTTP GET support
|
|
273
|
-
"XMLEncoding":
|
|
368
|
+
"XMLEncoding": True, # HTTP POST requests
|
|
274
369
|
"SOAPEncoding": False, # no SOAP requests
|
|
275
370
|
"ImplementsInheritance": False,
|
|
276
371
|
"ImplementsRemoteResolve": False,
|
|
@@ -305,20 +400,67 @@ class WFSView(GISView):
|
|
|
305
400
|
}
|
|
306
401
|
|
|
307
402
|
def get_feature_types(self) -> list[FeatureType]:
|
|
308
|
-
"""Return all available feature types this server exposes
|
|
403
|
+
"""Return all available feature types this server exposes.
|
|
404
|
+
|
|
405
|
+
This method may be overwritten to provide feature types dynamically,
|
|
406
|
+
for example to give them different elements based on user permissions.
|
|
407
|
+
"""
|
|
309
408
|
return self.feature_types
|
|
310
409
|
|
|
410
|
+
def get_bound_feature_types(self) -> list[FeatureType]:
|
|
411
|
+
"""Internal logic wrapping the FeatureType definitions provided by the developer.
|
|
412
|
+
This binds the XML namespace information from this view to the declared types.
|
|
413
|
+
"""
|
|
414
|
+
feature_types = self.get_feature_types()
|
|
415
|
+
for feature_type in feature_types:
|
|
416
|
+
# Make sure the feature type can advertise itself with an XML namespace.
|
|
417
|
+
feature_type.bind_namespace(default_xml_namespace=self.xml_namespace)
|
|
418
|
+
return feature_types
|
|
419
|
+
|
|
311
420
|
def get_index_context_data(self, **kwargs):
|
|
312
421
|
"""Add WFS specific metadata"""
|
|
313
|
-
|
|
422
|
+
get_feature_operation = self.accept_operations["WFS"]["GetFeature"]
|
|
423
|
+
operation = get_feature_operation(self, ows_request=None)
|
|
424
|
+
|
|
425
|
+
# Remove aliases
|
|
426
|
+
wfs_output_formats = []
|
|
427
|
+
seen = set()
|
|
428
|
+
for output_format in operation.get_output_formats():
|
|
429
|
+
if output_format.identifier not in seen:
|
|
430
|
+
wfs_output_formats.append(output_format)
|
|
431
|
+
seen.add(output_format.identifier)
|
|
314
432
|
|
|
315
433
|
context = super().get_index_context_data(**kwargs)
|
|
316
434
|
context.update(
|
|
317
435
|
{
|
|
318
|
-
"wfs_features": self.
|
|
436
|
+
"wfs_features": self.get_bound_feature_types(),
|
|
319
437
|
"wfs_output_formats": wfs_output_formats,
|
|
320
438
|
"wfs_filter_capabilities": self.wfs_filter_capabilities,
|
|
321
439
|
"wfs_service_constraints": self.wfs_service_constraints,
|
|
322
440
|
}
|
|
323
441
|
)
|
|
324
442
|
return context
|
|
443
|
+
|
|
444
|
+
def get_xml_schema_url(self, feature_types: list[FeatureType]) -> str:
|
|
445
|
+
"""Return the XML schema URL for the given feature types.
|
|
446
|
+
This is used in the GML output rendering.
|
|
447
|
+
"""
|
|
448
|
+
app_namespaces = self.get_xml_namespaces_to_prefixes()
|
|
449
|
+
type_names = ",".join(
|
|
450
|
+
f"{app_namespaces[ft.xml_namespace]}:{ft.name}" for ft in feature_types
|
|
451
|
+
)
|
|
452
|
+
return (
|
|
453
|
+
f"{self.server_url}?SERVICE=WFS&VERSION=2.0.0"
|
|
454
|
+
f"&REQUEST=DescribeFeatureType&TYPENAMES={type_names}"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def check_permissions(self, feature_type: FeatureType):
|
|
458
|
+
"""Hook that allows subclasses to reject access for datasets.
|
|
459
|
+
It may raise a Django PermissionDenied error.
|
|
460
|
+
|
|
461
|
+
This can access: ``self.request`` (the Django HTTPRequest) and ``self.ows_request``.
|
|
462
|
+
The latter contains a parsed request object
|
|
463
|
+
from the :mod:`gisserver.parsers.wfs20` package,
|
|
464
|
+
such as the parsed :class:`~gisserver.parsers.wfs20.GetFeature`
|
|
465
|
+
or :class:`~gisserver.parsers.wfs20.GetPropertyValue` request.
|
|
466
|
+
"""
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
gisserver/__init__.py,sha256=XtaIlx7Gik8RqIFDc1G0JAUfcmeNyTWjev1E1Oym_ms,40
|
|
2
|
-
gisserver/conf.py,sha256=3LSfeDRTdJCLuopAOD14Y5m2iPhovreIaNI9zbaukpo,2343
|
|
3
|
-
gisserver/db.py,sha256=-d6VQRkRWMSWKkA_6cVWhLzSI1cKmRoVktqA6hVwvuM,4928
|
|
4
|
-
gisserver/exceptions.py,sha256=wRsiKWhZtA8NmfMNncRtMeFgWDMdOWRp9Ey96iFAwMI,5194
|
|
5
|
-
gisserver/features.py,sha256=b3hEDs90xwEsANcRgipekZZrp4ucxRWwGeUutk2gRTw,31821
|
|
6
|
-
gisserver/geometries.py,sha256=HLZ3d8E5M2sfs2Kzz_GE3pZdynn-rYXPpuU9GVh0vsg,12398
|
|
7
|
-
gisserver/types.py,sha256=bsl3PezhGcHpUAtuV4AhdBmFLmtZZpDCT_HqpdVyLtM,36936
|
|
8
|
-
gisserver/views.py,sha256=xMHKezn4F1xZ2r78l9IzUlL5M3qZ78fjRJJtSzA8-Ww,12809
|
|
9
|
-
gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
gisserver/operations/base.py,sha256=Ls5i6ulMJqyhqu6o969rJ-mezW-T6OW5mr8oZ3BTBH8,17222
|
|
11
|
-
gisserver/operations/wfs20.py,sha256=bQeMJ4T6AisSBMABYa3li1u53w0zUxs02qBwjS4QbOw,18742
|
|
12
|
-
gisserver/output/__init__.py,sha256=C6hXIVIxBd2o_1Y7VhIU9vCg7pswI2tXsd74wgMdU4o,2134
|
|
13
|
-
gisserver/output/base.py,sha256=GygubFyhPO33fQw3jIuOerEH8yjbk0DJ6NhQdFkT7-g,8645
|
|
14
|
-
gisserver/output/csv.py,sha256=Mm7zYzUs9ffw9fqcKY9NT7qbMX-MzSFnqBg5I1wdRU0,6459
|
|
15
|
-
gisserver/output/geojson.py,sha256=bhRtNdMi1tMcSFBxDTNL8oysAMVhoolctKjZhxIncN8,11297
|
|
16
|
-
gisserver/output/gml32.py,sha256=KbhcBBVp4sEhvKs5e6fP4_GKKL6Kr2yyYaDLWePBQxs,31293
|
|
17
|
-
gisserver/output/results.py,sha256=VD-fJgWrMTwGaEGG_BSVhDv_m60dldBY6yAcTpRICT4,13949
|
|
18
|
-
gisserver/output/utils.py,sha256=FIlkHymA43RLZAfB6-trM3_IQKbIVb_4OUp5fxRbVGY,7379
|
|
19
|
-
gisserver/output/xmlschema.py,sha256=TdTMyP2_r98wkeffWAwrfm3l9XAmivu1KcYWNpgQKR4,4578
|
|
20
|
-
gisserver/parsers/__init__.py,sha256=g72D8EtR-QkhcemzY5-Qty-nhHPKza83Sz3KZm8bfq0,290
|
|
21
|
-
gisserver/parsers/base.py,sha256=9DE73wMphp7At3mGW0xwGU_TWPm55x8JEh4odpdRhlo,4833
|
|
22
|
-
gisserver/parsers/tags.py,sha256=NyJhbt-U2zTewfhghH4LRslTaDK2UOq9Vgk83pWfJh0,3553
|
|
23
|
-
gisserver/parsers/values.py,sha256=ypNPl4HduiVzexsL59ukZntlwXk5gHuSvQO6yhLeXD4,1016
|
|
24
|
-
gisserver/parsers/fes20/__init__.py,sha256=72RuFxAQp9hW0pgFUDyfQ0WsStNNk6J_y3NH-zYaPOA,1140
|
|
25
|
-
gisserver/parsers/fes20/expressions.py,sha256=A1-8JsGahXJPWLuTSVNAf0oSuyLfDMnFKM7viIt35Y8,9573
|
|
26
|
-
gisserver/parsers/fes20/filters.py,sha256=HdWguZvAT2L4Xm-bNa4XPh7MEymqTB6fo8r47Z-Bjuw,3511
|
|
27
|
-
gisserver/parsers/fes20/functions.py,sha256=mutPlbjOlg6R-9nNe8goaNdsAlZPkuckYlhMsQyp6NE,8723
|
|
28
|
-
gisserver/parsers/fes20/identifiers.py,sha256=vcPrMYcsemSlB9ziSUdVulhoI5PGFAh8vqeADBCWd60,2864
|
|
29
|
-
gisserver/parsers/fes20/operators.py,sha256=oQezU553Mts1DoExlH7_U29XXXdk6V84ihlBOozRzLg,23360
|
|
30
|
-
gisserver/parsers/fes20/query.py,sha256=_OxElAgeo34-00pNY5Apc6TSUKokSG9VWSMmIplN7AA,10638
|
|
31
|
-
gisserver/parsers/fes20/sorting.py,sha256=Uj8gsbBXumihWkrcj_pRPscwZAScUJSngAyjKri-Z8Y,2184
|
|
32
|
-
gisserver/parsers/gml/__init__.py,sha256=lNK0SXWkmTKdeJD9xdK6B-N-Y7wwGL4NRBI3dzoJDNI,1463
|
|
33
|
-
gisserver/parsers/gml/base.py,sha256=xWC5FrJyMjq7h5zmKwEvtRF0-CGVbCtvnO8Isvnf4mE,1065
|
|
34
|
-
gisserver/parsers/gml/geometries.py,sha256=cgV3WogA7LyC_H_NF7dGoGLvLdQRACG6AlThRkX_jhU,3171
|
|
35
|
-
gisserver/queries/__init__.py,sha256=yntEYRfaZ1Yjnr8lMvJt9Erh31WktikIf1af4TTuX74,1000
|
|
36
|
-
gisserver/queries/adhoc.py,sha256=Hhcxwt5C2OHqtLc-3QUqx8MZEnhoqkKWjBp56e6aljc,7428
|
|
37
|
-
gisserver/queries/base.py,sha256=EVJiwcLBjxGXJ8k012gGEiZ1q_gQZokSwdMl4H1SPus,7857
|
|
38
|
-
gisserver/queries/projection.py,sha256=KEz9UcZGVkYq7XTm_5nx8w5TnbSqht7rOGEuTGeJwm8,9922
|
|
39
|
-
gisserver/queries/stored.py,sha256=G258R9HVX3OooM5mhQF_N_JrFHLjM2y5iusrzPcN9TU,7125
|
|
40
|
-
gisserver/static/gisserver/index.css,sha256=fiQuCMuiWUhhrNQD6bbUJaX5vH5OpG9qgPdELPB8cpI,163
|
|
41
|
-
gisserver/templates/gisserver/index.html,sha256=yliku8jDqAOcenkbe1YnvJBnuS13MvzzsoDV8sQZ3GM,866
|
|
42
|
-
gisserver/templates/gisserver/service_description.html,sha256=3EHLnQx27_nTDZFD2RH6E10EWnTN90s1DGuEj9PeBrg,993
|
|
43
|
-
gisserver/templates/gisserver/wfs/feature_field.html,sha256=H5LsTAxGVGQqEpOZdEUTszQmFAZSpB9ggRJUsTdjYnw,538
|
|
44
|
-
gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
|
|
45
|
-
gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml,sha256=4Hxw1kCnxfWXTVPbHf8IW9_qiSeIEtnxHK6z6TS3-zA,1041
|
|
46
|
-
gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=Ej6ZXf-nGiPjiKdhNK5vAsX2iiiS12knjD0VaSzSltM,8717
|
|
47
|
-
gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml,sha256=CIBVQrKXmGMQ6BKbX4_0ru4lXtWnW5EIgHUbIi1V0Vc,636
|
|
48
|
-
gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
-
gisserver/templatetags/gisserver_tags.py,sha256=RFGqEj-EQi9FrJiFJ7HHAQqlU4fDKVKTtZrPNwURXgA,204
|
|
50
|
-
django_gisserver-1.5.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
51
|
-
django_gisserver-1.5.0.dist-info/METADATA,sha256=IWlSvP8d_4wsCVq4NWAE0T7Q8QZGqFVMvWUyLss85aY,5859
|
|
52
|
-
django_gisserver-1.5.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
53
|
-
django_gisserver-1.5.0.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
|
|
54
|
-
django_gisserver-1.5.0.dist-info/RECORD,,
|
gisserver/parsers/base.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from enum import Enum
|
|
4
|
-
from xml.etree.ElementTree import Element, QName
|
|
5
|
-
|
|
6
|
-
from gisserver.exceptions import ExternalParsingError
|
|
7
|
-
|
|
8
|
-
from .tags import split_ns
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class TagNameEnum(Enum):
|
|
12
|
-
"""An enumeration of tag names.
|
|
13
|
-
|
|
14
|
-
All enumerations that represent tag names inherit from this.
|
|
15
|
-
Each member name should be exactly the XML tag that it refers to.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
@classmethod
|
|
19
|
-
def from_xml(cls, element: Element):
|
|
20
|
-
"""Cast the element tag name into the enum member"""
|
|
21
|
-
ns, localname = split_ns(element.tag)
|
|
22
|
-
return cls[localname]
|
|
23
|
-
|
|
24
|
-
@classmethod
|
|
25
|
-
def _missing_(cls, value):
|
|
26
|
-
raise NotImplementedError(f"<{value}> is not registered as valid {cls.__name__}")
|
|
27
|
-
|
|
28
|
-
def __repr__(self):
|
|
29
|
-
# Make repr(filter) easier to copy-paste
|
|
30
|
-
return f"{self.__class__.__name__}.{self.name}"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class BaseNode:
|
|
34
|
-
"""The base node for all classes that represent an XML tag."""
|
|
35
|
-
|
|
36
|
-
xml_ns = None
|
|
37
|
-
xml_tags = []
|
|
38
|
-
|
|
39
|
-
def __init_subclass__(cls):
|
|
40
|
-
cls.xml_tags = []
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def from_xml(cls, element: Element):
|
|
44
|
-
raise NotImplementedError(
|
|
45
|
-
f"{cls.__name__}.from_xml() is not implemented to parse <{element.tag}>"
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
@classmethod
|
|
49
|
-
def from_child_xml(cls, element: Element) -> BaseNode:
|
|
50
|
-
"""By default, the node attempts to locate a child-class from the registry.
|
|
51
|
-
The individual tags should override `from_xml()` to return their own type.
|
|
52
|
-
"""
|
|
53
|
-
return tag_registry.from_child_xml(element, allowed_types=(cls,))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class TagRegistry:
|
|
57
|
-
"""Registration of all classes that can parse XML nodes.
|
|
58
|
-
|
|
59
|
-
The same class can be registered multiple times for different tag names.
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
parsers: dict[str, type[BaseNode]]
|
|
63
|
-
|
|
64
|
-
def __init__(self):
|
|
65
|
-
self.parsers = {}
|
|
66
|
-
|
|
67
|
-
def register(self, name=None, namespace=None, hidden=False):
|
|
68
|
-
"""Decorator to register a class as XML node parser.
|
|
69
|
-
|
|
70
|
-
This registers the decorated class as the designated parser
|
|
71
|
-
for a specific XML tag.
|
|
72
|
-
|
|
73
|
-
Usage:
|
|
74
|
-
|
|
75
|
-
@dataclass
|
|
76
|
-
@tag_registry.register()
|
|
77
|
-
class SomeXmlTag(BaseNode):
|
|
78
|
-
xml_ns = FES
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
def from_xml(cls, element: Element):
|
|
82
|
-
return cls(
|
|
83
|
-
...
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
def _dec(sub_class: type[BaseNode]) -> type[BaseNode]:
|
|
89
|
-
if sub_class.xml_ns is None:
|
|
90
|
-
raise RuntimeError(f"{sub_class.__name__}.xml_ns should be set")
|
|
91
|
-
|
|
92
|
-
localname = name or sub_class.__name__
|
|
93
|
-
qname = QName(namespace or sub_class.xml_ns, localname)
|
|
94
|
-
|
|
95
|
-
self.parsers[qname] = sub_class # Track this parser to resolve the tag.
|
|
96
|
-
if not hidden:
|
|
97
|
-
sub_class.xml_tags.append(name) # Allow fetching all names later
|
|
98
|
-
return sub_class # allow decorator usage
|
|
99
|
-
|
|
100
|
-
return _dec
|
|
101
|
-
|
|
102
|
-
def register_names(self, names: type[TagNameEnum], namespace=None):
|
|
103
|
-
"""Decorator to register the 'tag_class' as the
|
|
104
|
-
parser-backend for all member-names in this enum.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
def _dec(sub_class: type[BaseNode]):
|
|
108
|
-
# Looping over _member_names_ will skip aliased items (like BBOX/Within)
|
|
109
|
-
for member_name in names.__members__:
|
|
110
|
-
self.register(name=member_name, namespace=namespace)(sub_class)
|
|
111
|
-
return sub_class
|
|
112
|
-
|
|
113
|
-
return _dec
|
|
114
|
-
|
|
115
|
-
def from_child_xml(self, element: Element, allowed_types=None) -> BaseNode:
|
|
116
|
-
"""Convert the element into a Python class.
|
|
117
|
-
|
|
118
|
-
This locates the parser from the registered tag.
|
|
119
|
-
It's assumed that the tag has a "from_xml()" method too.
|
|
120
|
-
"""
|
|
121
|
-
try:
|
|
122
|
-
real_cls = self.resolve_class(element.tag)
|
|
123
|
-
except ExternalParsingError as e:
|
|
124
|
-
if allowed_types:
|
|
125
|
-
# Show better exception message
|
|
126
|
-
types = ", ".join(c.__name__ for c in allowed_types)
|
|
127
|
-
raise ExternalParsingError(f"{e}, expected one of: {types}") from None
|
|
128
|
-
|
|
129
|
-
raise
|
|
130
|
-
|
|
131
|
-
# Check whether the resolved class is indeed a valid option here.
|
|
132
|
-
if allowed_types is not None and not issubclass(real_cls, allowed_types):
|
|
133
|
-
types = ", ".join(c.__name__ for c in allowed_types)
|
|
134
|
-
raise ExternalParsingError(
|
|
135
|
-
f"Unexpected {real_cls.__name__} for <{element.tag}> node, "
|
|
136
|
-
f"expected one of: {types}"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
return real_cls.from_xml(element)
|
|
140
|
-
|
|
141
|
-
def resolve_class(self, tag_name) -> type[BaseNode]:
|
|
142
|
-
# Resolve the dataclass using the tag name
|
|
143
|
-
try:
|
|
144
|
-
return self.parsers[tag_name]
|
|
145
|
-
except KeyError:
|
|
146
|
-
raise ExternalParsingError(f"Unsupported tag: <{tag_name}>") from None
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
tag_registry = TagRegistry()
|