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