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/output/__init__.py
CHANGED
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .results import FeatureCollection, SimpleFeatureCollection # isort: skip (fixes import loops)
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from gisserver import conf
|
|
8
|
-
|
|
9
|
-
from .base import OutputRenderer
|
|
5
|
+
from .base import CollectionOutputRenderer, OutputRenderer
|
|
10
6
|
from .csv import CSVRenderer, DBCSVRenderer
|
|
11
7
|
from .geojson import DBGeoJsonRenderer, GeoJsonRenderer
|
|
12
8
|
from .gml32 import (
|
|
@@ -15,10 +11,13 @@ from .gml32 import (
|
|
|
15
11
|
GML32Renderer,
|
|
16
12
|
GML32ValueRenderer,
|
|
17
13
|
)
|
|
18
|
-
from .
|
|
14
|
+
from .stored import DescribeStoredQueriesRenderer, ListStoredQueriesRenderer
|
|
15
|
+
from .utils import to_qname
|
|
16
|
+
from .xmlschema import XmlSchemaRenderer
|
|
19
17
|
|
|
20
18
|
__all__ = [
|
|
21
19
|
"OutputRenderer",
|
|
20
|
+
"CollectionOutputRenderer",
|
|
22
21
|
"FeatureCollection",
|
|
23
22
|
"SimpleFeatureCollection",
|
|
24
23
|
"DBCSVRenderer",
|
|
@@ -29,46 +28,8 @@ __all__ = [
|
|
|
29
28
|
"GeoJsonRenderer",
|
|
30
29
|
"GML32Renderer",
|
|
31
30
|
"GML32ValueRenderer",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
31
|
+
"XmlSchemaRenderer",
|
|
32
|
+
"ListStoredQueriesRenderer",
|
|
33
|
+
"DescribeStoredQueriesRenderer",
|
|
34
|
+
"to_qname",
|
|
36
35
|
]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def select_renderer(native_renderer_class, db_renderer_class):
|
|
40
|
-
"""Dynamically select the preferred renderer.
|
|
41
|
-
This allows changing the settings within the app.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
class SelectRenderer:
|
|
45
|
-
"""A proxy to the actual rendering class.
|
|
46
|
-
Its structure still allows accessing class-properties of the actual class.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
@classproperty
|
|
50
|
-
def real_class(self):
|
|
51
|
-
if conf.GISSERVER_USE_DB_RENDERING:
|
|
52
|
-
return db_renderer_class
|
|
53
|
-
else:
|
|
54
|
-
return native_renderer_class
|
|
55
|
-
|
|
56
|
-
def __new__(cls, *args, **kwargs):
|
|
57
|
-
# Return the actual class instead
|
|
58
|
-
return cls.real_class(*args, **kwargs)
|
|
59
|
-
|
|
60
|
-
@classmethod
|
|
61
|
-
def decorate_collection(cls, *args, **kwargs):
|
|
62
|
-
return cls.real_class.decorate_collection(*args, **kwargs)
|
|
63
|
-
|
|
64
|
-
@classproperty
|
|
65
|
-
def max_page_size(cls):
|
|
66
|
-
return cls.real_class.max_page_size
|
|
67
|
-
|
|
68
|
-
return SelectRenderer
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
csv_renderer = select_renderer(CSVRenderer, DBCSVRenderer)
|
|
72
|
-
gml32_renderer = select_renderer(GML32Renderer, DBGML32Renderer)
|
|
73
|
-
gml32_value_renderer = select_renderer(GML32ValueRenderer, DBGML32ValueRenderer)
|
|
74
|
-
geojson_renderer = select_renderer(GeoJsonRenderer, DBGeoJsonRenderer)
|
gisserver/output/base.py
CHANGED
|
@@ -1,26 +1,141 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import math
|
|
4
5
|
import typing
|
|
6
|
+
from io import BytesIO, StringIO
|
|
5
7
|
|
|
6
8
|
from django.conf import settings
|
|
7
9
|
from django.db import models
|
|
8
10
|
from django.http import HttpResponse, StreamingHttpResponse
|
|
9
|
-
from django.
|
|
11
|
+
from django.http.response import HttpResponseBase # Django 3.2 import location
|
|
10
12
|
|
|
11
|
-
from gisserver import
|
|
12
|
-
from gisserver.
|
|
13
|
+
from gisserver.features import FeatureType
|
|
14
|
+
from gisserver.parsers.values import fix_type_name
|
|
15
|
+
from gisserver.parsers.xml import split_ns
|
|
16
|
+
from gisserver.types import XsdAnyType, XsdNode
|
|
17
|
+
|
|
18
|
+
from .utils import render_xmlns_attributes, to_qname
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
13
21
|
|
|
14
22
|
if typing.TYPE_CHECKING:
|
|
15
|
-
from gisserver.
|
|
16
|
-
from gisserver.
|
|
17
|
-
from gisserver.operations.base import WFSMethod
|
|
18
|
-
from gisserver.queries import FeatureProjection, FeatureRelation, QueryExpression
|
|
23
|
+
from gisserver.operations.base import WFSOperation
|
|
24
|
+
from gisserver.projection import FeatureProjection, FeatureRelation
|
|
19
25
|
|
|
20
|
-
from .results import FeatureCollection
|
|
26
|
+
from .results import FeatureCollection
|
|
21
27
|
|
|
22
28
|
|
|
23
29
|
class OutputRenderer:
|
|
30
|
+
"""Base class for rendering content.
|
|
31
|
+
|
|
32
|
+
Note most rendering logic will generally
|
|
33
|
+
need to use :class:`XmlOutputRenderer` or :class:`ConnectionOutputRenderer`
|
|
34
|
+
as their base class
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#: Default content type for the HTTP response
|
|
38
|
+
content_type = "application/octet-stream"
|
|
39
|
+
|
|
40
|
+
def __init__(self, operation: WFSOperation):
|
|
41
|
+
"""Base method for all output rendering."""
|
|
42
|
+
self.operation = operation
|
|
43
|
+
|
|
44
|
+
def get_response(self) -> HttpResponseBase:
|
|
45
|
+
"""Render the output as regular or streaming response."""
|
|
46
|
+
stream = self.render_stream()
|
|
47
|
+
if isinstance(stream, (str, bytes, StringIO, BytesIO)):
|
|
48
|
+
# Not a real stream, output anyway as regular HTTP response.
|
|
49
|
+
return HttpResponse(
|
|
50
|
+
content=stream,
|
|
51
|
+
content_type=self.content_type,
|
|
52
|
+
headers=self.get_headers(),
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
# An actual generator.
|
|
56
|
+
# Handover to WSGI server (starts streaming when reading the contents)
|
|
57
|
+
stream = self._trap_exceptions(stream)
|
|
58
|
+
return StreamingHttpResponse(
|
|
59
|
+
streaming_content=stream,
|
|
60
|
+
content_type=self.content_type,
|
|
61
|
+
headers=self.get_headers(),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def get_headers(self):
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
def _trap_exceptions(self, stream):
|
|
68
|
+
"""Decorate the generator to show exceptions"""
|
|
69
|
+
try:
|
|
70
|
+
yield from stream
|
|
71
|
+
except Exception as e:
|
|
72
|
+
# Can't return 500 at this point,
|
|
73
|
+
# but can still tell the client what happened.
|
|
74
|
+
yield self.render_exception(e)
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
def render_exception(self, exception: Exception):
|
|
78
|
+
"""Inform the client that the stream processing was interrupted with an exception.
|
|
79
|
+
The exception can be rendered in the format fits with the output.
|
|
80
|
+
|
|
81
|
+
Purposefully, not much information is given, so avoid informing clients.
|
|
82
|
+
The actual exception is still raised and logged server-side.
|
|
83
|
+
"""
|
|
84
|
+
if settings.DEBUG:
|
|
85
|
+
return f"{exception.__class__.__name__}: {exception}"
|
|
86
|
+
else:
|
|
87
|
+
return f"{exception.__class__.__name__} during rendering!"
|
|
88
|
+
|
|
89
|
+
def render_stream(self):
|
|
90
|
+
"""Implement this in subclasses to implement a custom output format.
|
|
91
|
+
|
|
92
|
+
The implementation may return a ``str``/``bytes`` object, which becomes
|
|
93
|
+
a normal ``HttpResponse`` object **OR** return a generator
|
|
94
|
+
that emits chunks. Such generator is wrapped in a ``StreamingHttpResponse``.
|
|
95
|
+
"""
|
|
96
|
+
raise NotImplementedError()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class XmlOutputRenderer(OutputRenderer):
|
|
100
|
+
"""Base class/mixin for XML-based rendering.
|
|
101
|
+
|
|
102
|
+
This provides the logic to translate XML elements into QName aliases.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
#: Default content type for the HTTP response
|
|
106
|
+
content_type = "text/xml; charset=utf-8"
|
|
107
|
+
|
|
108
|
+
#: Default extra namespaces to include in the xmlns="..." attributes, and use for to_qname().
|
|
109
|
+
xml_namespaces = {}
|
|
110
|
+
|
|
111
|
+
def __init__(self, operation: WFSOperation):
|
|
112
|
+
"""Base method for all output rendering."""
|
|
113
|
+
super().__init__(operation)
|
|
114
|
+
self.app_namespaces = {
|
|
115
|
+
**self.xml_namespaces,
|
|
116
|
+
**operation.view.get_xml_namespaces_to_prefixes(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def render_xmlns_attributes(self):
|
|
120
|
+
"""Render XML Namespace declaration attributes"""
|
|
121
|
+
return render_xmlns_attributes(self.app_namespaces)
|
|
122
|
+
|
|
123
|
+
def to_qname(self, xsd_type: XsdNode | XsdAnyType, namespaces=None) -> str:
|
|
124
|
+
"""Generate the aliased name for the element or type."""
|
|
125
|
+
return to_qname(xsd_type.namespace, xsd_type.name, namespaces or self.app_namespaces)
|
|
126
|
+
|
|
127
|
+
def feature_to_qname(self, feature_type: FeatureType | str) -> str:
|
|
128
|
+
"""Convert the FeatureType name to a QName"""
|
|
129
|
+
if isinstance(feature_type, FeatureType):
|
|
130
|
+
return to_qname(feature_type.xml_namespace, feature_type.name, self.app_namespaces)
|
|
131
|
+
else:
|
|
132
|
+
# e.g. "return_type" for StoredQueryDescription/QueryExpressionText
|
|
133
|
+
type_name = fix_type_name(feature_type, self.operation.view.xml_namespace)
|
|
134
|
+
ns, localname = split_ns(type_name)
|
|
135
|
+
return to_qname(ns, localname, self.app_namespaces)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CollectionOutputRenderer(OutputRenderer):
|
|
24
139
|
"""Base class to create streaming responses.
|
|
25
140
|
|
|
26
141
|
It receives the collected 'context' data of the WFSMethod.
|
|
@@ -30,76 +145,36 @@ class OutputRenderer:
|
|
|
30
145
|
#: This value can be 'math.inf' to support endless pages by default.
|
|
31
146
|
max_page_size = None
|
|
32
147
|
|
|
33
|
-
#: Define the content type for rendering the output
|
|
34
|
-
content_type = "application/octet-stream"
|
|
35
|
-
|
|
36
148
|
#: An optional content-disposition header to output
|
|
37
149
|
content_disposition = None
|
|
38
150
|
|
|
39
|
-
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
method: WFSMethod,
|
|
42
|
-
source_query: QueryExpression,
|
|
43
|
-
collection: FeatureCollection,
|
|
44
|
-
output_crs: CRS,
|
|
45
|
-
):
|
|
151
|
+
def __init__(self, operation: WFSOperation, collection: FeatureCollection):
|
|
46
152
|
"""
|
|
47
153
|
Receive the collected data to render.
|
|
48
|
-
These parameters are received from the ``get_context_data()`` method of the view.
|
|
49
154
|
|
|
50
|
-
:param
|
|
51
|
-
:param source_query: The query that generated this output.
|
|
155
|
+
:param operation: The calling WFS Method (e.g. GetFeature class)
|
|
52
156
|
:param collection: The collected data for rendering.
|
|
53
|
-
:param projection: Which fields to render.
|
|
54
|
-
:param output_crs: The requested output projection.
|
|
55
157
|
"""
|
|
56
|
-
|
|
57
|
-
self.source_query = source_query
|
|
158
|
+
super().__init__(operation)
|
|
58
159
|
self.collection = collection
|
|
59
|
-
self.
|
|
60
|
-
self.xml_srs_name = escape(str(self.output_crs))
|
|
160
|
+
self.apply_projection()
|
|
61
161
|
|
|
62
|
-
|
|
63
|
-
self.server_url = method.view.server_url
|
|
64
|
-
self.app_xml_namespace = method.view.xml_namespace
|
|
65
|
-
|
|
66
|
-
@classmethod
|
|
67
|
-
def decorate_collection(cls, collection: FeatureCollection, output_crs: CRS, **params):
|
|
162
|
+
def apply_projection(self):
|
|
68
163
|
"""Perform presentation-layer logic enhancements on the queryset."""
|
|
69
|
-
for sub_collection in collection.results:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
queryset = cls.decorate_queryset(
|
|
74
|
-
sub_collection.projection,
|
|
75
|
-
sub_collection.queryset,
|
|
76
|
-
output_crs,
|
|
77
|
-
**params,
|
|
164
|
+
for sub_collection in self.collection.results:
|
|
165
|
+
queryset = self.decorate_queryset(
|
|
166
|
+
projection=sub_collection.projection,
|
|
167
|
+
queryset=sub_collection.queryset,
|
|
78
168
|
)
|
|
169
|
+
if sub_collection._result_cache is not None:
|
|
170
|
+
raise RuntimeError(
|
|
171
|
+
"SimpleFeatureCollection QuerySet was already processed in output rendering."
|
|
172
|
+
)
|
|
79
173
|
if queryset is not None:
|
|
80
174
|
sub_collection.queryset = queryset
|
|
81
175
|
|
|
82
|
-
@classmethod
|
|
83
|
-
def validate(cls, feature_type: FeatureType, **params):
|
|
84
|
-
"""Validate the presentation parameters"""
|
|
85
|
-
crs = params["srsName"]
|
|
86
|
-
if (
|
|
87
|
-
conf.GISSERVER_SUPPORTED_CRS_ONLY
|
|
88
|
-
and crs is not None
|
|
89
|
-
and crs not in feature_type.supported_crs
|
|
90
|
-
):
|
|
91
|
-
raise InvalidParameterValue(
|
|
92
|
-
f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
|
|
93
|
-
locator="srsName",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
@classmethod
|
|
97
176
|
def decorate_queryset(
|
|
98
|
-
|
|
99
|
-
projection: FeatureProjection,
|
|
100
|
-
queryset: models.QuerySet,
|
|
101
|
-
output_crs: CRS,
|
|
102
|
-
**params,
|
|
177
|
+
self, projection: FeatureProjection, queryset: models.QuerySet
|
|
103
178
|
) -> models.QuerySet:
|
|
104
179
|
"""Apply presentation layer logic to the queryset.
|
|
105
180
|
|
|
@@ -108,21 +183,30 @@ class OutputRenderer:
|
|
|
108
183
|
:param feature_type: The feature that is being queried.
|
|
109
184
|
:param queryset: The constructed queryset so far.
|
|
110
185
|
:param output_crs: The projected output
|
|
111
|
-
:param params: All remaining request parameters (e.g. KVP parameters).
|
|
112
186
|
"""
|
|
187
|
+
if queryset._result_cache is not None:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"QuerySet was already processed before passing to the output rendering."
|
|
190
|
+
)
|
|
191
|
+
|
|
113
192
|
# Avoid fetching relations, fetch these within the same query,
|
|
114
|
-
|
|
115
|
-
if
|
|
116
|
-
|
|
193
|
+
prefetches = self._get_prefetch_related(projection)
|
|
194
|
+
if prefetches:
|
|
195
|
+
logger.debug(
|
|
196
|
+
"QuerySet for %s prefetches: %r",
|
|
197
|
+
queryset.model._meta.label,
|
|
198
|
+
[p.prefetch_through for p in prefetches],
|
|
199
|
+
)
|
|
200
|
+
queryset = queryset.prefetch_related(*prefetches)
|
|
117
201
|
|
|
202
|
+
logger.debug(
|
|
203
|
+
"QuerySet for %s only retrieves: %r",
|
|
204
|
+
queryset.model._meta.label,
|
|
205
|
+
projection.only_fields,
|
|
206
|
+
)
|
|
118
207
|
return queryset.only("pk", *projection.only_fields)
|
|
119
208
|
|
|
120
|
-
|
|
121
|
-
def _get_prefetch_related(
|
|
122
|
-
cls,
|
|
123
|
-
projection: FeatureProjection,
|
|
124
|
-
output_crs: CRS,
|
|
125
|
-
) -> list[models.Prefetch]:
|
|
209
|
+
def _get_prefetch_related(self, projection: FeatureProjection) -> list[models.Prefetch]:
|
|
126
210
|
"""Summarize which fields read data from relations.
|
|
127
211
|
|
|
128
212
|
This combines the input from flattened and complex fields,
|
|
@@ -134,17 +218,13 @@ class OutputRenderer:
|
|
|
134
218
|
return [
|
|
135
219
|
models.Prefetch(
|
|
136
220
|
orm_relation.orm_path,
|
|
137
|
-
queryset=
|
|
221
|
+
queryset=self.get_prefetch_queryset(projection, orm_relation),
|
|
138
222
|
)
|
|
139
223
|
for orm_relation in projection.orm_relations
|
|
140
224
|
]
|
|
141
225
|
|
|
142
|
-
@classmethod
|
|
143
226
|
def get_prefetch_queryset(
|
|
144
|
-
|
|
145
|
-
projection: FeatureProjection,
|
|
146
|
-
feature_relation: FeatureRelation,
|
|
147
|
-
output_crs: CRS,
|
|
227
|
+
self, projection: FeatureProjection, feature_relation: FeatureRelation
|
|
148
228
|
) -> models.QuerySet | None:
|
|
149
229
|
"""Generate a custom queryset that's used to prefetch a relation."""
|
|
150
230
|
# Multiple elements could be referencing the same model, just take first that is filled in.
|
|
@@ -154,76 +234,35 @@ class OutputRenderer:
|
|
|
154
234
|
# This will also apply .only() based on the projection's feature_relation
|
|
155
235
|
return projection.feature_type.get_related_queryset(feature_relation)
|
|
156
236
|
|
|
157
|
-
def get_response(self):
|
|
158
|
-
"""Render the output as streaming response."""
|
|
159
|
-
stream = self.render_stream()
|
|
160
|
-
if isinstance(stream, (str, bytes)):
|
|
161
|
-
# Not a real stream, output anyway as regular HTTP response.
|
|
162
|
-
response = HttpResponse(content=stream, content_type=self.content_type)
|
|
163
|
-
else:
|
|
164
|
-
# A actual generator.
|
|
165
|
-
stream = self._trap_exceptions(stream)
|
|
166
|
-
response = StreamingHttpResponse(
|
|
167
|
-
streaming_content=stream,
|
|
168
|
-
content_type=self.content_type,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Add HTTP headers
|
|
172
|
-
for name, value in self.get_headers().items():
|
|
173
|
-
response[name] = value
|
|
174
|
-
|
|
175
|
-
# Handover to WSGI server (starts streaming when reading the contents)
|
|
176
|
-
return response
|
|
177
|
-
|
|
178
237
|
def get_headers(self):
|
|
179
238
|
"""Return the response headers"""
|
|
180
239
|
if self.content_disposition:
|
|
181
240
|
# Offer a common quick content-disposition logic that works for all possible queries.
|
|
182
|
-
sub_collection = self.collection.results[0]
|
|
183
|
-
if sub_collection.stop == math.inf:
|
|
184
|
-
page = f"{sub_collection.start}-end" if sub_collection.start else "all"
|
|
185
|
-
elif sub_collection.stop:
|
|
186
|
-
page = f"{sub_collection.start}-{sub_collection.stop - 1}"
|
|
187
|
-
else:
|
|
188
|
-
page = "results"
|
|
189
|
-
|
|
190
241
|
return {
|
|
191
242
|
"Content-Disposition": self.content_disposition.format(
|
|
192
|
-
|
|
193
|
-
page=page,
|
|
194
|
-
date=self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
|
|
195
|
-
timestamp=self.collection.timestamp,
|
|
243
|
+
**self.get_content_disposition_kwargs()
|
|
196
244
|
)
|
|
197
245
|
}
|
|
198
246
|
|
|
199
247
|
return {}
|
|
200
248
|
|
|
201
|
-
def
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
yield self.render_exception(e)
|
|
209
|
-
raise
|
|
210
|
-
|
|
211
|
-
def render_exception(self, exception: Exception):
|
|
212
|
-
"""Inform the client that the stream processing was interrupted with an exception.
|
|
213
|
-
The exception can be rendered in the format fits with the output.
|
|
214
|
-
|
|
215
|
-
Purposefully, not much information is given, so avoid informing clients.
|
|
216
|
-
The actual exception is still raised and logged server-side.
|
|
217
|
-
"""
|
|
218
|
-
if settings.DEBUG:
|
|
219
|
-
return f"{exception.__class__.__name__}: {exception}"
|
|
249
|
+
def get_content_disposition_kwargs(self) -> dict:
|
|
250
|
+
"""Offer a common quick content-disposition logic that works for all possible queries."""
|
|
251
|
+
sub_collection = self.collection.results[0]
|
|
252
|
+
if sub_collection.stop == math.inf:
|
|
253
|
+
page = f"{sub_collection.start}-end" if sub_collection.start else "all"
|
|
254
|
+
elif sub_collection.stop:
|
|
255
|
+
page = f"{sub_collection.start}-{sub_collection.stop - 1}"
|
|
220
256
|
else:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
257
|
+
page = "results"
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"typenames": "+".join(
|
|
261
|
+
feature_type.name
|
|
262
|
+
for sub in self.collection.results
|
|
263
|
+
for feature_type in sub.feature_types
|
|
264
|
+
),
|
|
265
|
+
"page": page,
|
|
266
|
+
"date": self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
|
|
267
|
+
"timestamp": self.collection.timestamp,
|
|
268
|
+
}
|