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.
Files changed (74) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/db.py +56 -47
  8. gisserver/exceptions.py +26 -2
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +220 -156
  13. gisserver/geometries.py +32 -37
  14. gisserver/management/__init__.py +0 -0
  15. gisserver/management/commands/__init__.py +0 -0
  16. gisserver/management/commands/loadgeojson.py +291 -0
  17. gisserver/operations/base.py +122 -308
  18. gisserver/operations/wfs20.py +423 -337
  19. gisserver/output/__init__.py +9 -48
  20. gisserver/output/base.py +178 -139
  21. gisserver/output/csv.py +65 -74
  22. gisserver/output/geojson.py +34 -35
  23. gisserver/output/gml32.py +254 -246
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +52 -26
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -170
  28. gisserver/output/xmlschema.py +85 -46
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +13 -27
  32. gisserver/parsers/fes20/expressions.py +82 -38
  33. gisserver/parsers/fes20/filters.py +111 -43
  34. gisserver/parsers/fes20/identifiers.py +44 -26
  35. gisserver/parsers/fes20/lookups.py +144 -0
  36. gisserver/parsers/fes20/operators.py +331 -127
  37. gisserver/parsers/fes20/sorting.py +104 -33
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +5 -2
  40. gisserver/parsers/gml/geometries.py +69 -35
  41. gisserver/parsers/ows/__init__.py +25 -0
  42. gisserver/parsers/ows/kvp.py +190 -0
  43. gisserver/parsers/ows/requests.py +158 -0
  44. gisserver/parsers/query.py +175 -0
  45. gisserver/parsers/values.py +26 -0
  46. gisserver/parsers/wfs20/__init__.py +37 -0
  47. gisserver/parsers/wfs20/adhoc.py +245 -0
  48. gisserver/parsers/wfs20/base.py +143 -0
  49. gisserver/parsers/wfs20/projection.py +103 -0
  50. gisserver/parsers/wfs20/requests.py +482 -0
  51. gisserver/parsers/wfs20/stored.py +192 -0
  52. gisserver/parsers/xml.py +249 -0
  53. gisserver/projection.py +357 -0
  54. gisserver/static/gisserver/index.css +12 -1
  55. gisserver/templates/gisserver/index.html +1 -1
  56. gisserver/templates/gisserver/service_description.html +2 -2
  57. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +322 -259
  61. gisserver/views.py +198 -56
  62. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -285
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -37
  67. gisserver/queries/adhoc.py +0 -185
  68. gisserver/queries/base.py +0 -186
  69. gisserver/queries/projection.py +0 -240
  70. gisserver/queries/stored.py +0 -206
  71. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  72. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  73. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  74. {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()
@@ -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"]
@@ -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>