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/queries/base.py
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from django.db.models import QuerySet
|
|
4
|
-
|
|
5
|
-
from gisserver.exceptions import InvalidParameterValue
|
|
6
|
-
from gisserver.features import FeatureType
|
|
7
|
-
from gisserver.output import FeatureCollection, SimpleFeatureCollection
|
|
8
|
-
from gisserver.parsers import fes20
|
|
9
|
-
from gisserver.types import split_xml_name
|
|
10
|
-
|
|
11
|
-
from .projection import FeatureProjection
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class QueryExpression:
|
|
15
|
-
"""WFS base class for all queries.
|
|
16
|
-
This object type is defined in the WFS spec (as ``<fes:AbstractQueryExpression>``).
|
|
17
|
-
|
|
18
|
-
The WFS server can initiate queries in multiple ways.
|
|
19
|
-
This class provides the common interface for all these query types;
|
|
20
|
-
whether the request provided "ad-hoc" parameters or called a stored procedure.
|
|
21
|
-
Each query type has its own way of generating the actual database statement to perform.
|
|
22
|
-
|
|
23
|
-
The subclasses can override the following logic:
|
|
24
|
-
|
|
25
|
-
* :meth:`get_type_names` defines which types this query applies to.
|
|
26
|
-
* :meth:`compile_query` defines how to filter the queryset.
|
|
27
|
-
|
|
28
|
-
For full control, these methods can also be overwritten instead:
|
|
29
|
-
|
|
30
|
-
* :meth:`get_queryset` defines the full results.
|
|
31
|
-
* :meth:`get_hits` to return the collection for ``RESULTTYPE=hits``.
|
|
32
|
-
* :meth:`get_results` to return the collection for ``RESULTTYPE=results``.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
handle = ""
|
|
36
|
-
value_reference: fes20.ValueReference | None = None
|
|
37
|
-
property_names: list[fes20.ValueReference] | None = None
|
|
38
|
-
projections: dict[FeatureType, FeatureProjection] | None = None
|
|
39
|
-
|
|
40
|
-
def bind(
|
|
41
|
-
self,
|
|
42
|
-
all_feature_types: dict[str, FeatureType],
|
|
43
|
-
value_reference: fes20.ValueReference | None = None,
|
|
44
|
-
property_names: list[fes20.ValueReference] | None = None,
|
|
45
|
-
):
|
|
46
|
-
"""Bind the query to presentation-layer logic (e.g. request parameters).
|
|
47
|
-
|
|
48
|
-
:param all_feature_types: Which features are queried.
|
|
49
|
-
:param value_reference: Which field is returned (by ``GetPropertyValue``)
|
|
50
|
-
:param property_name: Which field is returned (by ``GetFeature`` + propertyName parameter)
|
|
51
|
-
"""
|
|
52
|
-
self.projections = {}
|
|
53
|
-
self.all_feature_types = all_feature_types
|
|
54
|
-
if value_reference is not None:
|
|
55
|
-
self.value_reference = value_reference
|
|
56
|
-
if property_names is not None:
|
|
57
|
-
self.property_names = property_names
|
|
58
|
-
|
|
59
|
-
def check_permissions(self, request):
|
|
60
|
-
"""Verify whether the user has access to view these data sources"""
|
|
61
|
-
for feature_type in self.get_type_names():
|
|
62
|
-
feature_type.check_permissions(request)
|
|
63
|
-
|
|
64
|
-
def resolve_type_name(self, type_name, locator="typename") -> FeatureType:
|
|
65
|
-
"""Find the feature type for a given name.
|
|
66
|
-
This is a utility that custom subclasses can use.
|
|
67
|
-
"""
|
|
68
|
-
# Strip the namespace prefix. The Python ElementTree parser does
|
|
69
|
-
# not expose the used namespace prefixes, so text-values can't be
|
|
70
|
-
# mapped against it. As we expose just one namespace, just strip it.
|
|
71
|
-
xmlns, type_name = split_xml_name(type_name)
|
|
72
|
-
|
|
73
|
-
try:
|
|
74
|
-
return self.all_feature_types[type_name]
|
|
75
|
-
except KeyError:
|
|
76
|
-
raise InvalidParameterValue(
|
|
77
|
-
locator, f"Typename '{type_name}' doesn't exist in this server."
|
|
78
|
-
) from None
|
|
79
|
-
|
|
80
|
-
def get_hits(self) -> FeatureCollection:
|
|
81
|
-
"""Run the query, return the number of hits only.
|
|
82
|
-
|
|
83
|
-
Override this method in case you need full control over the response data.
|
|
84
|
-
Otherwise, override :meth:`compile_query` or :meth:`get_queryset`.
|
|
85
|
-
"""
|
|
86
|
-
results = []
|
|
87
|
-
number_matched = 0
|
|
88
|
-
for feature_type in self.get_type_names():
|
|
89
|
-
queryset = self.get_queryset(feature_type)
|
|
90
|
-
number_matched += queryset.count()
|
|
91
|
-
|
|
92
|
-
# Include empty feature collections,
|
|
93
|
-
# so the selected feature types are still known.
|
|
94
|
-
results.append(
|
|
95
|
-
SimpleFeatureCollection(
|
|
96
|
-
self, feature_type, queryset=queryset.none(), start=0, stop=0
|
|
97
|
-
)
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
return FeatureCollection(results=results, number_matched=number_matched)
|
|
101
|
-
|
|
102
|
-
def get_results(self, start_index=0, count=100) -> FeatureCollection:
|
|
103
|
-
"""Run the query, return the full paginated results.
|
|
104
|
-
|
|
105
|
-
Override this method in case you need full control over the response data.
|
|
106
|
-
Otherwise, override :meth:`compile_query` or :meth:`get_queryset`.
|
|
107
|
-
"""
|
|
108
|
-
stop = start_index + count
|
|
109
|
-
results = [
|
|
110
|
-
# The querysets are not executed yet, until the output is reading them.
|
|
111
|
-
SimpleFeatureCollection(
|
|
112
|
-
self,
|
|
113
|
-
feature_type,
|
|
114
|
-
queryset=self.get_queryset(feature_type),
|
|
115
|
-
start=start_index,
|
|
116
|
-
stop=stop,
|
|
117
|
-
)
|
|
118
|
-
for feature_type in self.get_type_names()
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
# number_matched is not given here, so some rendering formats can count it instead.
|
|
122
|
-
# For GML it need to be printed at the start, but for GeoJSON it can be rendered
|
|
123
|
-
# as the last bit of the response. That avoids performing an expensive COUNT query.
|
|
124
|
-
return FeatureCollection(results=results)
|
|
125
|
-
|
|
126
|
-
def get_queryset(self, feature_type: FeatureType) -> QuerySet:
|
|
127
|
-
"""Generate the queryset for the specific feature type.
|
|
128
|
-
|
|
129
|
-
This method can be overwritten in subclasses to define the returned data.
|
|
130
|
-
However, consider overwriting :meth:`compile_query` instead of simple data.
|
|
131
|
-
"""
|
|
132
|
-
queryset = feature_type.get_queryset()
|
|
133
|
-
|
|
134
|
-
# Apply filters
|
|
135
|
-
compiler = self.compile_query(feature_type, using=queryset.db)
|
|
136
|
-
|
|
137
|
-
# If defined, limit which fields will be queried.
|
|
138
|
-
if self.property_names:
|
|
139
|
-
for property_name in self.property_names:
|
|
140
|
-
compiler.add_property_name(property_name)
|
|
141
|
-
|
|
142
|
-
if self.value_reference is not None:
|
|
143
|
-
if feature_type.resolve_element(self.value_reference.xpath) is None:
|
|
144
|
-
raise InvalidParameterValue(
|
|
145
|
-
"valueReference",
|
|
146
|
-
f"Field '{self.value_reference.xpath}' does not exist.",
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
# For GetPropertyValue, adjust the query so only that value is requested.
|
|
150
|
-
# This makes sure XPath attribute selectors are already handled by the
|
|
151
|
-
# database query, instead of being a presentation-layer handling.
|
|
152
|
-
field = compiler.add_value_reference(self.value_reference)
|
|
153
|
-
queryset = compiler.filter_queryset(queryset, feature_type=feature_type)
|
|
154
|
-
return queryset.values("pk", member=field)
|
|
155
|
-
else:
|
|
156
|
-
return compiler.filter_queryset(queryset, feature_type=feature_type)
|
|
157
|
-
|
|
158
|
-
def get_type_names(self) -> list[FeatureType]:
|
|
159
|
-
"""Tell which type names this query applies to.
|
|
160
|
-
|
|
161
|
-
This method needs to be defined in subclasses.
|
|
162
|
-
"""
|
|
163
|
-
raise NotImplementedError(
|
|
164
|
-
f"{self.__class__.__name__}.get_type_names() should be implemented."
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
def get_projection(self, feature_type: FeatureType) -> FeatureProjection:
|
|
168
|
-
"""Provide the projection of this query for a given feature.
|
|
169
|
-
|
|
170
|
-
NOTE: as the AdhocQuery has a typeNames (plural!) argument,
|
|
171
|
-
this class still needs to check per feature type which fields to apply to.
|
|
172
|
-
"""
|
|
173
|
-
try:
|
|
174
|
-
return self.projections[feature_type]
|
|
175
|
-
except KeyError:
|
|
176
|
-
projection = FeatureProjection(feature_type, self.property_names)
|
|
177
|
-
self.projections[feature_type] = projection
|
|
178
|
-
return projection
|
|
179
|
-
|
|
180
|
-
def compile_query(self, feature_type: FeatureType, using=None) -> fes20.CompiledQuery:
|
|
181
|
-
"""Define the compiled query that filters the queryset.
|
|
182
|
-
|
|
183
|
-
Subclasses need to define this method, unless
|
|
184
|
-
:meth:`get_queryset` is completely overwritten.
|
|
185
|
-
"""
|
|
186
|
-
raise NotImplementedError()
|
gisserver/queries/projection.py
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections import defaultdict
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from functools import cached_property
|
|
6
|
-
from typing import cast
|
|
7
|
-
|
|
8
|
-
from django.db import models
|
|
9
|
-
|
|
10
|
-
from gisserver.features import FeatureType
|
|
11
|
-
from gisserver.parsers import fes20
|
|
12
|
-
from gisserver.types import GmlElement, XPathMatch, XsdElement, _XsdElement_WithComplexType
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class FeatureProjection:
|
|
16
|
-
"""Tell which fields to access and render for a single feature.
|
|
17
|
-
This is inspired on 'fes:AbstractProjectionClause'.
|
|
18
|
-
|
|
19
|
-
Instead of walking over the full XSD object tree,
|
|
20
|
-
this object wraps that and makes sure only the actual requested fields are used.
|
|
21
|
-
When a PROPERTYNAME is used in the request, this will limit
|
|
22
|
-
which fields to retrieve, which to prefetch, and which to render.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
feature_type: FeatureType
|
|
26
|
-
|
|
27
|
-
def __init__(self, feature_type: FeatureType, property_names: list[fes20.ValueReference]):
|
|
28
|
-
self.feature_type = feature_type
|
|
29
|
-
self.property_names = property_names
|
|
30
|
-
|
|
31
|
-
if property_names:
|
|
32
|
-
# Discover which elements should be rendered.
|
|
33
|
-
child_nodes = self._get_child_nodes_subset()
|
|
34
|
-
self.xsd_root_elements = child_nodes.pop(None) # Pop it to avoid recursion risks
|
|
35
|
-
self.xsd_child_nodes = child_nodes
|
|
36
|
-
else:
|
|
37
|
-
# Retrieve all elements.
|
|
38
|
-
self.xsd_root_elements = feature_type.xsd_type.all_elements
|
|
39
|
-
self.xsd_child_nodes = feature_type.xsd_type.elements_with_children
|
|
40
|
-
|
|
41
|
-
def _get_child_nodes_subset(self) -> dict[XsdElement | None, list[XsdElement]]:
|
|
42
|
-
"""Translate the PROPERTYNAME into a dictionary of nodes to render."""
|
|
43
|
-
child_nodes = defaultdict(list)
|
|
44
|
-
for xpath_match in self.xpath_matches:
|
|
45
|
-
# Make sure the tree is known beforehand, and it has no duplicate parent nodes.
|
|
46
|
-
# (e.g. PROPERTYNAME=node/child1,node/child2)
|
|
47
|
-
parent = None
|
|
48
|
-
for xsd_node in xpath_match.nodes:
|
|
49
|
-
if xsd_node not in child_nodes[parent]:
|
|
50
|
-
child_nodes[parent].append(xsd_node)
|
|
51
|
-
parent = xsd_node
|
|
52
|
-
|
|
53
|
-
return dict(child_nodes)
|
|
54
|
-
|
|
55
|
-
def add_field(self, xsd_element: XsdElement):
|
|
56
|
-
"""Restore the retrieval of a field that was not asked for in the original query."""
|
|
57
|
-
if not self.property_names:
|
|
58
|
-
return
|
|
59
|
-
if xsd_element.type.is_complex_type:
|
|
60
|
-
raise NotImplementedError("Can't restore nested elements right now")
|
|
61
|
-
|
|
62
|
-
# For now, add to all cached properties:
|
|
63
|
-
# TODO: have a better approach?
|
|
64
|
-
self.xsd_root_elements.append(xsd_element)
|
|
65
|
-
if xsd_element.is_geometry:
|
|
66
|
-
self.geometry_elements.append(xsd_element)
|
|
67
|
-
|
|
68
|
-
if xsd_element.orm_path not in self.only_fields:
|
|
69
|
-
self.only_fields.append(xsd_element.orm_path)
|
|
70
|
-
|
|
71
|
-
if xsd_element.is_flattened:
|
|
72
|
-
self.flattened_elements.append(xsd_element)
|
|
73
|
-
|
|
74
|
-
@cached_property
|
|
75
|
-
def xpath_matches(self) -> list[XPathMatch]:
|
|
76
|
-
"""Resolve which elements the property names point to"""
|
|
77
|
-
if not self.property_names:
|
|
78
|
-
raise RuntimeError("This method is only useful for propertyname projections.")
|
|
79
|
-
|
|
80
|
-
return [
|
|
81
|
-
self.feature_type.resolve_element(property_name.xpath)
|
|
82
|
-
for property_name in self.property_names
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
@cached_property
|
|
86
|
-
def geometry_elements(self) -> list[GmlElement]:
|
|
87
|
-
"""Tell which GML elements will be hit."""
|
|
88
|
-
gml_elements = []
|
|
89
|
-
for e in self.xsd_root_elements:
|
|
90
|
-
if e.is_geometry and e.xml_name != "gml:boundedBy":
|
|
91
|
-
# Prefetching a flattened relation
|
|
92
|
-
gml_elements.append(e)
|
|
93
|
-
|
|
94
|
-
for xsd_children in self.xsd_child_nodes.values():
|
|
95
|
-
for e in xsd_children:
|
|
96
|
-
if e.is_geometry and e.xml_name != "gml:boundedBy":
|
|
97
|
-
gml_elements.append(e)
|
|
98
|
-
|
|
99
|
-
return gml_elements
|
|
100
|
-
|
|
101
|
-
@cached_property
|
|
102
|
-
def complex_elements(self) -> list[_XsdElement_WithComplexType]:
|
|
103
|
-
"""Shortcut to get all elements with a complex type"""
|
|
104
|
-
if not self.property_names:
|
|
105
|
-
return self.feature_type.xsd_type.complex_elements
|
|
106
|
-
else:
|
|
107
|
-
return [e for e in self.xsd_root_elements if e.type.is_complex_type]
|
|
108
|
-
|
|
109
|
-
@cached_property
|
|
110
|
-
def flattened_elements(self) -> list[XsdElement]:
|
|
111
|
-
"""Shortcut to get all elements with a flattened model attribute"""
|
|
112
|
-
if not self.property_names:
|
|
113
|
-
return self.feature_type.xsd_type.flattened_elements
|
|
114
|
-
else:
|
|
115
|
-
return [e for e in self.xsd_root_elements if e.is_flattened]
|
|
116
|
-
|
|
117
|
-
@cached_property
|
|
118
|
-
def main_geometry_element(self) -> GmlElement | None:
|
|
119
|
-
"""Return the field used to describe the geometry of the feature.
|
|
120
|
-
When the projection excludes the geometry, ``None`` is returned.
|
|
121
|
-
"""
|
|
122
|
-
main_field = self.feature_type.geometry_field
|
|
123
|
-
if self.property_names and main_field not in self.xsd_root_elements:
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
xpath_match = self.feature_type.resolve_element(main_field)
|
|
127
|
-
return cast(GmlElement, xpath_match.child)
|
|
128
|
-
|
|
129
|
-
@cached_property
|
|
130
|
-
def orm_relations(self) -> list[FeatureRelation]:
|
|
131
|
-
"""Tell which fields will be retrieved from related fields.
|
|
132
|
-
|
|
133
|
-
This gives an object layout based on the XSD elements,
|
|
134
|
-
that can be used for prefetching data.
|
|
135
|
-
"""
|
|
136
|
-
related_models: dict[str, type[models.Model]] = {}
|
|
137
|
-
fields: dict[str, set[XsdElement]] = defaultdict(set)
|
|
138
|
-
elements = defaultdict(list)
|
|
139
|
-
|
|
140
|
-
# Check all elements that render as "dotted" flattened relation
|
|
141
|
-
for xsd_element in self.flattened_elements:
|
|
142
|
-
if xsd_element.source is not None:
|
|
143
|
-
# Split "relation.field" notation into path, and take the field as child attribute.
|
|
144
|
-
obj_path, field = xsd_element.orm_relation
|
|
145
|
-
elements[obj_path].append(xsd_element)
|
|
146
|
-
fields[obj_path].add(xsd_element)
|
|
147
|
-
# field is already on relation:
|
|
148
|
-
related_models[obj_path] = xsd_element.source.model
|
|
149
|
-
|
|
150
|
-
# Check all elements that render as "nested" complex type:
|
|
151
|
-
for xsd_element in self.complex_elements:
|
|
152
|
-
# The complex element itself points to the root of the path,
|
|
153
|
-
# all sub elements become the child attributes.
|
|
154
|
-
obj_path = xsd_element.orm_path
|
|
155
|
-
elements[obj_path].append(xsd_element)
|
|
156
|
-
fields[obj_path] = {
|
|
157
|
-
f
|
|
158
|
-
for f in self.xsd_child_nodes[xsd_element]
|
|
159
|
-
if not f.is_many or f.is_array # exclude M2M, but include ArrayField
|
|
160
|
-
}
|
|
161
|
-
if xsd_element.source:
|
|
162
|
-
# field references a related object:
|
|
163
|
-
related_models[obj_path] = xsd_element.source.related_model
|
|
164
|
-
|
|
165
|
-
return [
|
|
166
|
-
FeatureRelation(
|
|
167
|
-
orm_path=obj_path,
|
|
168
|
-
sub_fields=sub_fields,
|
|
169
|
-
related_model=related_models.get(obj_path),
|
|
170
|
-
xsd_elements=elements[obj_path],
|
|
171
|
-
)
|
|
172
|
-
for obj_path, sub_fields in fields.items()
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
@cached_property
|
|
176
|
-
def only_fields(self) -> list[str]:
|
|
177
|
-
"""Tell which fields to limit the queryset to.
|
|
178
|
-
This excludes M2M fields because those are not part of the local model data.
|
|
179
|
-
"""
|
|
180
|
-
if self.property_names is not None:
|
|
181
|
-
return [
|
|
182
|
-
# TODO: While ORM rel__child paths can be passed to .only(),
|
|
183
|
-
# these may not be accurately applied to foreign keys yet.
|
|
184
|
-
# Also, this is bypassed by our generated Prefetch() objects.
|
|
185
|
-
xpath_match.orm_path
|
|
186
|
-
for xpath_match in self.xpath_matches
|
|
187
|
-
if not xpath_match.is_many or xpath_match.child.is_array
|
|
188
|
-
]
|
|
189
|
-
else:
|
|
190
|
-
# Also limit the queryset to the actual fields that are shown.
|
|
191
|
-
# No need to request more data
|
|
192
|
-
return [
|
|
193
|
-
f.orm_field
|
|
194
|
-
for f in self.feature_type.xsd_type.elements
|
|
195
|
-
if not f.is_many or f.is_array # avoid M2M fields for .only(), but keep ArrayField
|
|
196
|
-
]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
@dataclass
|
|
200
|
-
class FeatureRelation:
|
|
201
|
-
"""Tell which related fields are queried by the feature.
|
|
202
|
-
Each dict holds an ORM-path, with the relevant sub-elements.
|
|
203
|
-
"""
|
|
204
|
-
|
|
205
|
-
#: The ORM path that is queried for this particular relation
|
|
206
|
-
orm_path: str
|
|
207
|
-
#: The fields that will be retrieved for that path (limited by the projection)
|
|
208
|
-
sub_fields: set[XsdElement]
|
|
209
|
-
#: The model that is accessed for this relation (if set)
|
|
210
|
-
related_model: type[models.Model] | None
|
|
211
|
-
#: The source elements that access this relation.
|
|
212
|
-
xsd_elements: list[XsdElement]
|
|
213
|
-
|
|
214
|
-
@cached_property
|
|
215
|
-
def _local_model_field_names(self) -> list[str]:
|
|
216
|
-
"""Tell which local fields of the model will be accessed by this feature."""
|
|
217
|
-
result = []
|
|
218
|
-
for field in self.sub_fields:
|
|
219
|
-
model_field = field.source
|
|
220
|
-
if not model_field.many_to_many and not model_field.one_to_many:
|
|
221
|
-
result.append(model_field.name)
|
|
222
|
-
|
|
223
|
-
result.extend(self._local_backlink_field_names)
|
|
224
|
-
return result
|
|
225
|
-
|
|
226
|
-
@property
|
|
227
|
-
def _local_backlink_field_names(self) -> list[str]:
|
|
228
|
-
# When this relation is retrieved through a ManyToOneRel (reverse FK),
|
|
229
|
-
# the prefetch_related() also needs to have the original foreign key
|
|
230
|
-
# in order to link all prefetches to the proper parent instance.
|
|
231
|
-
return [
|
|
232
|
-
xsd_element.source.field.name
|
|
233
|
-
for xsd_element in self.xsd_elements
|
|
234
|
-
if xsd_element.source is not None and xsd_element.source.one_to_many
|
|
235
|
-
]
|
|
236
|
-
|
|
237
|
-
@property
|
|
238
|
-
def geometry_elements(self) -> list[GmlElement]:
|
|
239
|
-
"""Tell which geometry elements this relation will access."""
|
|
240
|
-
return [f for f in self.sub_fields if f.is_geometry and f.name != "gml:boundedBy"]
|
gisserver/queries/stored.py
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
"""Handle (stored)query objects.
|
|
2
|
-
|
|
3
|
-
These definitions follow the WFS spec.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
|
|
10
|
-
from django.db.models import Q, QuerySet
|
|
11
|
-
|
|
12
|
-
from gisserver.exceptions import InvalidParameterValue, MissingParameterValue, NotFound
|
|
13
|
-
from gisserver.features import FeatureType
|
|
14
|
-
from gisserver.operations.base import Parameter
|
|
15
|
-
from gisserver.output import FeatureCollection
|
|
16
|
-
from gisserver.parsers import fes20
|
|
17
|
-
from gisserver.types import XsdTypes
|
|
18
|
-
|
|
19
|
-
from .base import QueryExpression
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class QueryExpressionText:
|
|
23
|
-
"""Define the body of a stored query.
|
|
24
|
-
|
|
25
|
-
This object type is defined in the WFS spec.
|
|
26
|
-
It may contain a wfs:Query or wfs:StoredQuery element.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
return_feature_types: list[str] | None = None
|
|
30
|
-
language: str = fes20.Filter.query_language
|
|
31
|
-
is_private: bool = False
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@dataclass
|
|
35
|
-
class StoredQueryDescription:
|
|
36
|
-
"""WFS metadata of a stored query.
|
|
37
|
-
This object type is defined in the WFS spec.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
id: str = None
|
|
41
|
-
title: str | None = None
|
|
42
|
-
abstract: str | None = None
|
|
43
|
-
parameters: dict[str, XsdTypes] | None = None
|
|
44
|
-
expressions: list = None # TODO: support multiple body expressions
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class StoredQuery(QueryExpression):
|
|
48
|
-
"""Base class for stored queries.
|
|
49
|
-
|
|
50
|
-
This represents all predefined queries on the server.
|
|
51
|
-
A good description can be found at:
|
|
52
|
-
https://mapserver.org/ogc/wfs_server.html#stored-queries-wfs-2-0
|
|
53
|
-
|
|
54
|
-
The implementation logic is fully defined by the :class:`QueryExpression`
|
|
55
|
-
base class. For a simple implementation, the following data should be
|
|
56
|
-
overwritten:
|
|
57
|
-
|
|
58
|
-
* :meth:`get_type_names` to define which type this query references.
|
|
59
|
-
* :meth:`compile_query` to define the queryset filter.
|
|
60
|
-
|
|
61
|
-
For advanced overriding, see the :class:`QueryExpression` base class,
|
|
62
|
-
or the :class:`GetFeatureById` implementation.
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
# Official WFS docs have an 'id' and 'parameters' property for the
|
|
66
|
-
# StoredQuery class, but these are avoided here to give subclasses full
|
|
67
|
-
# control over which properties to store. E.g. "id" conflicts with Django
|
|
68
|
-
# model subclasses that stores the query.
|
|
69
|
-
meta: StoredQueryDescription
|
|
70
|
-
|
|
71
|
-
def __init__(self, **parameters):
|
|
72
|
-
self.parameters = parameters
|
|
73
|
-
|
|
74
|
-
@classmethod
|
|
75
|
-
def extract_parameters(cls, KVP) -> dict[str, str]:
|
|
76
|
-
"""Extract the arguments from the key-value-pair (=HTTP GET) request."""
|
|
77
|
-
args = {}
|
|
78
|
-
for name, _xsd_type in cls.meta.parameters.items():
|
|
79
|
-
try:
|
|
80
|
-
args[name] = KVP[name]
|
|
81
|
-
except KeyError:
|
|
82
|
-
raise MissingParameterValue(
|
|
83
|
-
f"Stored query {cls.meta.id} requires an '{name}' parameter", locator=name
|
|
84
|
-
) from None
|
|
85
|
-
|
|
86
|
-
# Avoid unexpected behavior, check whether the client also sends adhoc query parameters
|
|
87
|
-
for name in ("filter", "bbox", "resourceID"):
|
|
88
|
-
if name not in args and KVP.get(name.upper()):
|
|
89
|
-
raise InvalidParameterValue(
|
|
90
|
-
"Stored query can't be combined with adhoc-query parameters", locator=name
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
return args
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class StoredQueryRegistry:
|
|
97
|
-
"""Registry of functions to be callable by <fes:Query>."""
|
|
98
|
-
|
|
99
|
-
def __init__(self):
|
|
100
|
-
self.stored_queries = {}
|
|
101
|
-
|
|
102
|
-
def __bool__(self):
|
|
103
|
-
return bool(self.stored_queries)
|
|
104
|
-
|
|
105
|
-
def __iter__(self):
|
|
106
|
-
return iter(self.stored_queries.values())
|
|
107
|
-
|
|
108
|
-
def register(self, meta: StoredQueryDescription | None = None, **meta_kwargs):
|
|
109
|
-
"""Register a custom class that handles a stored query"""
|
|
110
|
-
|
|
111
|
-
def _metadata_dec(query: type[StoredQuery]):
|
|
112
|
-
query.meta = meta or StoredQueryDescription(**meta_kwargs)
|
|
113
|
-
self.stored_queries[query.meta.id] = query
|
|
114
|
-
return query
|
|
115
|
-
|
|
116
|
-
return _metadata_dec
|
|
117
|
-
|
|
118
|
-
def resolve_query(self, query_id) -> type[StoredQuery]:
|
|
119
|
-
"""Find the stored procedure using the ID."""
|
|
120
|
-
try:
|
|
121
|
-
return self.stored_queries[query_id]
|
|
122
|
-
except KeyError:
|
|
123
|
-
raise InvalidParameterValue(
|
|
124
|
-
f"Stored query does not exist: {query_id}",
|
|
125
|
-
locator="STOREDQUERY_ID",
|
|
126
|
-
) from None
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
stored_query_registry = StoredQueryRegistry()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class StoredQueryParameter(Parameter):
|
|
133
|
-
"""Special parameter parsing for the 'STOREDQUERY_ID' parameter"""
|
|
134
|
-
|
|
135
|
-
def __init__(self):
|
|
136
|
-
super().__init__(name="STOREDQUERY_ID", parser=stored_query_registry.resolve_query)
|
|
137
|
-
|
|
138
|
-
def value_from_query(self, KVP: dict):
|
|
139
|
-
"""Customize the request parsing to read custom parameters too."""
|
|
140
|
-
stored_query_class = super().value_from_query(KVP)
|
|
141
|
-
if stored_query_class is None:
|
|
142
|
-
return None
|
|
143
|
-
|
|
144
|
-
parameters = stored_query_class.extract_parameters(KVP)
|
|
145
|
-
return stored_query_class(**parameters)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@stored_query_registry.register(
|
|
149
|
-
id="urn:ogc:def:query:OGC-WFS::GetFeatureById",
|
|
150
|
-
title="Get feature by identifier",
|
|
151
|
-
abstract="Returns the single feature that corresponds with the ID argument",
|
|
152
|
-
parameters={"ID": XsdTypes.string},
|
|
153
|
-
# expressions=[QueryExpressionText],
|
|
154
|
-
)
|
|
155
|
-
class GetFeatureById(StoredQuery):
|
|
156
|
-
"""The stored query for GetFeatureById.
|
|
157
|
-
|
|
158
|
-
This is essentially the same as:
|
|
159
|
-
|
|
160
|
-
<wfs:Query xmlns:wfs='..." xmlns:fes='...'>
|
|
161
|
-
<fes:Filter><fes:ResourceId rid='{ID}'/></fes:Filter>
|
|
162
|
-
</wfs:Query>
|
|
163
|
-
|
|
164
|
-
Except that the response is supposed to contain only the item itself.
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
def __init__(self, ID):
|
|
168
|
-
super().__init__(ID=ID)
|
|
169
|
-
try:
|
|
170
|
-
type_name, id = ID.rsplit(".", 1)
|
|
171
|
-
except ValueError:
|
|
172
|
-
# Always report this as 404
|
|
173
|
-
raise NotFound("Expected typeName.id for ID parameter", locator="ID") from None
|
|
174
|
-
|
|
175
|
-
self.type_name = type_name
|
|
176
|
-
self.id = id
|
|
177
|
-
|
|
178
|
-
def get_type_names(self) -> list[FeatureType]:
|
|
179
|
-
"""Tell which type names this query applies to."""
|
|
180
|
-
feature_type = self.all_feature_types[self.type_name]
|
|
181
|
-
return [feature_type]
|
|
182
|
-
|
|
183
|
-
def get_queryset(self, feature_type: FeatureType) -> QuerySet:
|
|
184
|
-
"""Override to implement ID type checking."""
|
|
185
|
-
try:
|
|
186
|
-
return super().get_queryset(feature_type)
|
|
187
|
-
except (ValueError, TypeError) as e:
|
|
188
|
-
raise InvalidParameterValue(f"Invalid ID value: {e}", locator="ID") from e
|
|
189
|
-
|
|
190
|
-
def get_results(self, *args, **kwargs) -> FeatureCollection:
|
|
191
|
-
"""Override to implement 404 checking."""
|
|
192
|
-
collection = super().get_results(*args, **kwargs)
|
|
193
|
-
|
|
194
|
-
# Directly attempt to collect the data.
|
|
195
|
-
# Avoid having to do that in the output renderer.
|
|
196
|
-
if collection.results[0].first() is None:
|
|
197
|
-
# WFS 2.0.2: Return NotFound instead of InvalidParameterValue
|
|
198
|
-
raise NotFound(f"Feature not found with ID {self.id}.", locator="ID")
|
|
199
|
-
|
|
200
|
-
return collection
|
|
201
|
-
|
|
202
|
-
def compile_query(self, feature_type: FeatureType, using=None) -> fes20.CompiledQuery:
|
|
203
|
-
"""Create the internal query object that will be applied to the queryset."""
|
|
204
|
-
compiler = fes20.CompiledQuery(feature_type=feature_type)
|
|
205
|
-
compiler.add_lookups(Q(pk=self.id), type_name=self.type_name)
|
|
206
|
-
return compiler
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<wfs:DescribeStoredQueriesResponse
|
|
3
|
-
xmlns="http://www.opengis.net/wfs/2.0"
|
|
4
|
-
xmlns:app="{{ app_xml_namespace }}"
|
|
5
|
-
xmlns:wfs="http://www.opengis.net/wfs/2.0"
|
|
6
|
-
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
7
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
8
|
-
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd">
|
|
9
|
-
|
|
10
|
-
{% for query_meta in stored_queries %}
|
|
11
|
-
<StoredQueryDescription id="{{ query_meta.id }}">
|
|
12
|
-
<Title>{{ query_meta.title }}</Title>
|
|
13
|
-
<Abstract>{{ query_meta.abstract }}</Abstract>
|
|
14
|
-
{% for name, type in query_meta.parameters.items %}
|
|
15
|
-
<Parameter name="{{ name }}" type="{{ type.with_prefix }}"/>{% endfor %}
|
|
16
|
-
<QueryExpressionText isPrivate="true" language="urn:ogc:def:queryLanguage:OGC-WFS::WFS_QueryExpression" returnFeatureTypes="{% for feature in feature_types %}{% if not forloop.first %} {% endif %}{{ feature.name }}{% endfor %}"/>
|
|
17
|
-
</StoredQueryDescription>
|
|
18
|
-
{% endfor %}
|
|
19
|
-
|
|
20
|
-
</wfs:DescribeStoredQueriesResponse>
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<wfs:ListStoredQueriesResponse
|
|
3
|
-
xmlns="http://www.opengis.net/wfs/2.0"
|
|
4
|
-
xmlns:app="{{ app_xml_namespace }}"
|
|
5
|
-
xmlns:wfs="http://www.opengis.net/wfs/2.0"
|
|
6
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
7
|
-
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd">
|
|
8
|
-
|
|
9
|
-
<StoredQuery id="urn:ogc:def:query:OGC-WFS::GetFeatureById">
|
|
10
|
-
<Title>Get feature by identifier</Title>{% for feature_type in feature_types %}
|
|
11
|
-
<ReturnFeatureType>{{ feature_type.name }}</ReturnFeatureType>{% endfor %}
|
|
12
|
-
</StoredQuery>
|
|
13
|
-
|
|
14
|
-
</wfs:ListStoredQueriesResponse>
|
|
File without changes
|
|
File without changes
|