django-gisserver 1.4.1__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.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.4.1.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 +63 -60
- gisserver/exceptions.py +47 -9
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +267 -240
- gisserver/geometries.py +34 -39
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +129 -305
- gisserver/operations/wfs20.py +428 -336
- gisserver/output/__init__.py +10 -48
- gisserver/output/base.py +198 -143
- gisserver/output/csv.py +81 -85
- gisserver/output/geojson.py +63 -72
- gisserver/output/gml32.py +310 -281
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +71 -30
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -154
- gisserver/output/xmlschema.py +86 -47
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +15 -11
- gisserver/parsers/fes20/expressions.py +89 -50
- 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 +336 -128
- gisserver/parsers/fes20/sorting.py +107 -34
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +6 -3
- 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 +11 -11
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +375 -258
- gisserver/views.py +206 -75
- django_gisserver-1.4.1.dist-info/RECORD +0 -53
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -275
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -34
- gisserver/queries/adhoc.py +0 -181
- gisserver/queries/base.py +0 -146
- gisserver/queries/stored.py +0 -205
- 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.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/output/__init__.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
"""All supported output formats"""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from .results import FeatureCollection, SimpleFeatureCollection # isort: skip (fixes import loops)
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from .base import OutputRenderer
|
|
5
|
+
from .base import CollectionOutputRenderer, OutputRenderer
|
|
8
6
|
from .csv import CSVRenderer, DBCSVRenderer
|
|
9
7
|
from .geojson import DBGeoJsonRenderer, GeoJsonRenderer
|
|
10
8
|
from .gml32 import (
|
|
@@ -13,11 +11,13 @@ from .gml32 import (
|
|
|
13
11
|
GML32Renderer,
|
|
14
12
|
GML32ValueRenderer,
|
|
15
13
|
)
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
14
|
+
from .stored import DescribeStoredQueriesRenderer, ListStoredQueriesRenderer
|
|
15
|
+
from .utils import to_qname
|
|
16
|
+
from .xmlschema import XmlSchemaRenderer
|
|
18
17
|
|
|
19
18
|
__all__ = [
|
|
20
19
|
"OutputRenderer",
|
|
20
|
+
"CollectionOutputRenderer",
|
|
21
21
|
"FeatureCollection",
|
|
22
22
|
"SimpleFeatureCollection",
|
|
23
23
|
"DBCSVRenderer",
|
|
@@ -28,46 +28,8 @@ __all__ = [
|
|
|
28
28
|
"GeoJsonRenderer",
|
|
29
29
|
"GML32Renderer",
|
|
30
30
|
"GML32ValueRenderer",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
31
|
+
"XmlSchemaRenderer",
|
|
32
|
+
"ListStoredQueriesRenderer",
|
|
33
|
+
"DescribeStoredQueriesRenderer",
|
|
34
|
+
"to_qname",
|
|
35
35
|
]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def select_renderer(native_renderer_class, db_renderer_class):
|
|
39
|
-
"""Dynamically select the preferred renderer.
|
|
40
|
-
This allows changing the settings within the app.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
class SelectRenderer:
|
|
44
|
-
"""A proxy to the actual rendering class.
|
|
45
|
-
Its structure still allows accessing class-properties of the actual class.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
@classproperty
|
|
49
|
-
def real_class(self):
|
|
50
|
-
if conf.GISSERVER_USE_DB_RENDERING:
|
|
51
|
-
return db_renderer_class
|
|
52
|
-
else:
|
|
53
|
-
return native_renderer_class
|
|
54
|
-
|
|
55
|
-
def __new__(cls, *args, **kwargs):
|
|
56
|
-
# Return the actual class instead
|
|
57
|
-
return cls.real_class(*args, **kwargs)
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
def decorate_collection(cls, *args, **kwargs):
|
|
61
|
-
return cls.real_class.decorate_collection(*args, **kwargs)
|
|
62
|
-
|
|
63
|
-
@classproperty
|
|
64
|
-
def max_page_size(cls):
|
|
65
|
-
return cls.real_class.max_page_size
|
|
66
|
-
|
|
67
|
-
return SelectRenderer
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
csv_renderer = select_renderer(CSVRenderer, DBCSVRenderer)
|
|
71
|
-
gml32_renderer = select_renderer(GML32Renderer, DBGML32Renderer)
|
|
72
|
-
gml32_value_renderer = select_renderer(GML32ValueRenderer, DBGML32ValueRenderer)
|
|
73
|
-
geojson_renderer = select_renderer(GeoJsonRenderer, DBGeoJsonRenderer)
|
gisserver/output/base.py
CHANGED
|
@@ -1,22 +1,141 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import math
|
|
5
|
+
import typing
|
|
6
|
+
from io import BytesIO, StringIO
|
|
4
7
|
|
|
5
8
|
from django.conf import settings
|
|
6
9
|
from django.db import models
|
|
7
10
|
from django.http import HttpResponse, StreamingHttpResponse
|
|
8
|
-
from django.
|
|
11
|
+
from django.http.response import HttpResponseBase # Django 3.2 import location
|
|
9
12
|
|
|
10
|
-
from gisserver import
|
|
11
|
-
from gisserver.
|
|
12
|
-
from gisserver.
|
|
13
|
-
from gisserver.
|
|
14
|
-
from gisserver.operations.base import WFSMethod
|
|
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
|
|
15
17
|
|
|
16
|
-
from .
|
|
18
|
+
from .utils import render_xmlns_attributes, to_qname
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
if typing.TYPE_CHECKING:
|
|
23
|
+
from gisserver.operations.base import WFSOperation
|
|
24
|
+
from gisserver.projection import FeatureProjection, FeatureRelation
|
|
25
|
+
|
|
26
|
+
from .results import FeatureCollection
|
|
17
27
|
|
|
18
28
|
|
|
19
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):
|
|
20
139
|
"""Base class to create streaming responses.
|
|
21
140
|
|
|
22
141
|
It receives the collected 'context' data of the WFSMethod.
|
|
@@ -26,188 +145,124 @@ class OutputRenderer:
|
|
|
26
145
|
#: This value can be 'math.inf' to support endless pages by default.
|
|
27
146
|
max_page_size = None
|
|
28
147
|
|
|
29
|
-
#: Define the content type for rendering the output
|
|
30
|
-
content_type = "application/octet-stream"
|
|
31
|
-
|
|
32
148
|
#: An optional content-disposition header to output
|
|
33
149
|
content_disposition = None
|
|
34
150
|
|
|
35
|
-
def __init__(
|
|
36
|
-
self,
|
|
37
|
-
method: WFSMethod,
|
|
38
|
-
source_query,
|
|
39
|
-
collection: FeatureCollection,
|
|
40
|
-
output_crs: CRS,
|
|
41
|
-
):
|
|
151
|
+
def __init__(self, operation: WFSOperation, collection: FeatureCollection):
|
|
42
152
|
"""
|
|
43
153
|
Receive the collected data to render.
|
|
44
154
|
|
|
45
|
-
:param
|
|
46
|
-
:param
|
|
47
|
-
:type source_query: gisserver.queries.QueryExpression
|
|
48
|
-
:param collection: The collected data for rendering
|
|
49
|
-
:param output_crs: The requested output projection.
|
|
155
|
+
:param operation: The calling WFS Method (e.g. GetFeature class)
|
|
156
|
+
:param collection: The collected data for rendering.
|
|
50
157
|
"""
|
|
51
|
-
|
|
52
|
-
self.source_query = source_query
|
|
158
|
+
super().__init__(operation)
|
|
53
159
|
self.collection = collection
|
|
54
|
-
self.
|
|
55
|
-
self.xml_srs_name = escape(str(self.output_crs))
|
|
56
|
-
|
|
57
|
-
# Common elements for output rendering:
|
|
58
|
-
self.server_url = method.view.server_url
|
|
59
|
-
self.app_xml_namespace = method.view.xml_namespace
|
|
160
|
+
self.apply_projection()
|
|
60
161
|
|
|
61
|
-
|
|
62
|
-
def decorate_collection(cls, collection: FeatureCollection, output_crs: CRS, **params):
|
|
162
|
+
def apply_projection(self):
|
|
63
163
|
"""Perform presentation-layer logic enhancements on the queryset."""
|
|
64
|
-
for sub_collection in collection.results:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
queryset = cls.decorate_queryset(
|
|
69
|
-
sub_collection.feature_type,
|
|
70
|
-
sub_collection.queryset,
|
|
71
|
-
output_crs,
|
|
72
|
-
**params,
|
|
164
|
+
for sub_collection in self.collection.results:
|
|
165
|
+
queryset = self.decorate_queryset(
|
|
166
|
+
projection=sub_collection.projection,
|
|
167
|
+
queryset=sub_collection.queryset,
|
|
73
168
|
)
|
|
169
|
+
if sub_collection._result_cache is not None:
|
|
170
|
+
raise RuntimeError(
|
|
171
|
+
"SimpleFeatureCollection QuerySet was already processed in output rendering."
|
|
172
|
+
)
|
|
74
173
|
if queryset is not None:
|
|
75
174
|
sub_collection.queryset = queryset
|
|
76
175
|
|
|
77
|
-
@classmethod
|
|
78
|
-
def validate(cls, feature_type: FeatureType, **params):
|
|
79
|
-
"""Validate the presentation parameters"""
|
|
80
|
-
crs = params["srsName"]
|
|
81
|
-
if (
|
|
82
|
-
conf.GISSERVER_SUPPORTED_CRS_ONLY
|
|
83
|
-
and crs is not None
|
|
84
|
-
and crs not in feature_type.supported_crs
|
|
85
|
-
):
|
|
86
|
-
raise InvalidParameterValue(
|
|
87
|
-
"srsName",
|
|
88
|
-
f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
@classmethod
|
|
92
176
|
def decorate_queryset(
|
|
93
|
-
|
|
94
|
-
feature_type: FeatureType,
|
|
95
|
-
queryset: models.QuerySet,
|
|
96
|
-
output_crs: CRS,
|
|
97
|
-
**params,
|
|
177
|
+
self, projection: FeatureProjection, queryset: models.QuerySet
|
|
98
178
|
) -> models.QuerySet:
|
|
99
|
-
"""Apply presentation layer logic to the queryset.
|
|
179
|
+
"""Apply presentation layer logic to the queryset.
|
|
180
|
+
|
|
181
|
+
This allows fine-tuning the queryset for any special needs of the output rendering type.
|
|
182
|
+
|
|
183
|
+
:param feature_type: The feature that is being queried.
|
|
184
|
+
:param queryset: The constructed queryset so far.
|
|
185
|
+
:param output_crs: The projected output
|
|
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
|
+
|
|
100
192
|
# Avoid fetching relations, fetch these within the same query,
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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)
|
|
201
|
+
|
|
202
|
+
logger.debug(
|
|
203
|
+
"QuerySet for %s only retrieves: %r",
|
|
204
|
+
queryset.model._meta.label,
|
|
205
|
+
projection.only_fields,
|
|
206
|
+
)
|
|
207
|
+
return queryset.only("pk", *projection.only_fields)
|
|
113
208
|
|
|
114
|
-
|
|
115
|
-
def _get_prefetch_related(
|
|
116
|
-
cls, feature_type: FeatureType, output_crs: CRS
|
|
117
|
-
) -> list[models.Prefetch]:
|
|
209
|
+
def _get_prefetch_related(self, projection: FeatureProjection) -> list[models.Prefetch]:
|
|
118
210
|
"""Summarize which fields read data from relations.
|
|
119
211
|
|
|
120
212
|
This combines the input from flattened and complex fields,
|
|
121
213
|
in the unlikely case both variations are used in the same feature.
|
|
122
214
|
"""
|
|
215
|
+
# When PROPERTYNAME is used, determine all ORM paths (and levels) that this query will touch.
|
|
216
|
+
# The prefetch will only be applied when its field coincide with this list.
|
|
217
|
+
|
|
123
218
|
return [
|
|
124
219
|
models.Prefetch(
|
|
125
220
|
orm_relation.orm_path,
|
|
126
|
-
queryset=
|
|
221
|
+
queryset=self.get_prefetch_queryset(projection, orm_relation),
|
|
127
222
|
)
|
|
128
|
-
for orm_relation in
|
|
223
|
+
for orm_relation in projection.orm_relations
|
|
129
224
|
]
|
|
130
225
|
|
|
131
|
-
@classmethod
|
|
132
226
|
def get_prefetch_queryset(
|
|
133
|
-
|
|
134
|
-
feature_type: FeatureType,
|
|
135
|
-
feature_relation: FeatureRelation,
|
|
136
|
-
output_crs: CRS,
|
|
227
|
+
self, projection: FeatureProjection, feature_relation: FeatureRelation
|
|
137
228
|
) -> models.QuerySet | None:
|
|
138
229
|
"""Generate a custom queryset that's used to prefetch a relation."""
|
|
139
230
|
# Multiple elements could be referencing the same model, just take first that is filled in.
|
|
140
231
|
if feature_relation.related_model is None:
|
|
141
232
|
return None
|
|
142
233
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def get_response(self):
|
|
146
|
-
"""Render the output as streaming response."""
|
|
147
|
-
stream = self.render_stream()
|
|
148
|
-
if isinstance(stream, (str, bytes)):
|
|
149
|
-
# Not a real stream, output anyway as regular HTTP response.
|
|
150
|
-
response = HttpResponse(content=stream, content_type=self.content_type)
|
|
151
|
-
else:
|
|
152
|
-
# A actual generator.
|
|
153
|
-
stream = self._trap_exceptions(stream)
|
|
154
|
-
response = StreamingHttpResponse(
|
|
155
|
-
streaming_content=stream,
|
|
156
|
-
content_type=self.content_type,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Add HTTP headers
|
|
160
|
-
for name, value in self.get_headers().items():
|
|
161
|
-
response[name] = value
|
|
162
|
-
|
|
163
|
-
# Handover to WSGI server (starts streaming when reading the contents)
|
|
164
|
-
return response
|
|
234
|
+
# This will also apply .only() based on the projection's feature_relation
|
|
235
|
+
return projection.feature_type.get_related_queryset(feature_relation)
|
|
165
236
|
|
|
166
237
|
def get_headers(self):
|
|
167
238
|
"""Return the response headers"""
|
|
168
239
|
if self.content_disposition:
|
|
169
240
|
# Offer a common quick content-disposition logic that works for all possible queries.
|
|
170
|
-
sub_collection = self.collection.results[0]
|
|
171
|
-
if sub_collection.stop == math.inf:
|
|
172
|
-
page = f"{sub_collection.start}-end" if sub_collection.start else "all"
|
|
173
|
-
elif sub_collection.stop:
|
|
174
|
-
page = f"{sub_collection.start}-{sub_collection.stop - 1}"
|
|
175
|
-
else:
|
|
176
|
-
page = "results"
|
|
177
|
-
|
|
178
241
|
return {
|
|
179
242
|
"Content-Disposition": self.content_disposition.format(
|
|
180
|
-
|
|
181
|
-
page=page,
|
|
182
|
-
date=self.collection.date.strftime("%Y-%m-%d %H.%M.%S%z"),
|
|
183
|
-
timestamp=self.collection.timestamp,
|
|
243
|
+
**self.get_content_disposition_kwargs()
|
|
184
244
|
)
|
|
185
245
|
}
|
|
186
246
|
|
|
187
247
|
return {}
|
|
188
248
|
|
|
189
|
-
def
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
yield self.render_exception(e)
|
|
197
|
-
raise
|
|
198
|
-
|
|
199
|
-
def render_exception(self, exception: Exception):
|
|
200
|
-
"""Inform the client that the stream processing was interrupted with an exception.
|
|
201
|
-
The exception can be rendered in the format fits with the output.
|
|
202
|
-
|
|
203
|
-
Purposefully, not much information is given, so avoid informing clients.
|
|
204
|
-
The actual exception is still raised and logged server-side.
|
|
205
|
-
"""
|
|
206
|
-
if settings.DEBUG:
|
|
207
|
-
return f"<!-- {exception.__class__.__name__}: {exception} -->\n"
|
|
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}"
|
|
208
256
|
else:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
}
|