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
@@ -1,285 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import operator
4
- from functools import reduce
5
-
6
- from django.conf import settings
7
- from django.contrib.gis.db.models.fields import BaseSpatialField
8
- from django.contrib.gis.db.models.lookups import DWithinLookup
9
- from django.db import models
10
- from django.db.models import Q, QuerySet, lookups
11
- from django.db.models.expressions import Combinable
12
-
13
- from gisserver.features import FeatureType
14
-
15
- from . import expressions, sorting
16
-
17
-
18
- class CompiledQuery:
19
- """Intermediate data for translating FES queries to Django.
20
-
21
- This class contains all data from the ``<fes:Filter>`` object in a model
22
- that can be translated to a django QuerySet.
23
-
24
- This object is passed though all build_...() methods,
25
- so it can be used to add extra lookups and annotations.
26
- """
27
-
28
- def __init__(
29
- self,
30
- feature_type: FeatureType | None = None,
31
- using: str | None = None,
32
- lookups: list[Q] | None = None,
33
- typed_lookups: dict[str, list[Q]] | None = None,
34
- annotations: dict[str, Combinable | Q] | None = None,
35
- ):
36
- """The init method is typically used only in unit tests."""
37
- self.feature_type = feature_type
38
- self.using = using
39
- self.lookups = lookups or []
40
- self.typed_lookups = typed_lookups or {}
41
- self.annotations = annotations or {}
42
- self.aliases = 0
43
- self.extra_lookups: list[Q] = []
44
- self.ordering: list[str] = []
45
- self.is_empty = False
46
- self.distinct = False
47
-
48
- def add_annotation(self, value: Combinable | Q) -> str:
49
- """Create a named-alias for a function/Q object.
50
- This alias can be used in a comparison, where expressions are used as left-hand-side.
51
- """
52
- self.aliases += 1
53
- name = f"a{self.aliases}"
54
- self.annotations[name] = value
55
- return name
56
-
57
- def add_distinct(self):
58
- self.distinct = True
59
-
60
- def add_lookups(self, q_object: Q, type_name: str | None = None):
61
- """Register an extra 'WHERE' clause of the query.
62
- This is used for comparisons, ID selectors and other query types.
63
- """
64
- if not isinstance(q_object, Q):
65
- raise TypeError()
66
-
67
- if type_name is not None:
68
- if type_name not in self.typed_lookups:
69
- self.typed_lookups[type_name] = []
70
- self.typed_lookups[type_name].append(q_object)
71
- else:
72
- self.lookups.append(q_object)
73
-
74
- def add_extra_lookup(self, q_object: Q):
75
- """Temporary stash an extra lookup that the expression can't return yet.
76
- This is used for XPath selectors that also filter on attributes,
77
- e.g. "element[@attr=..]/child". The attribute lookup is processed as another filter.
78
- """
79
- if not isinstance(q_object, Q):
80
- raise TypeError()
81
- self.extra_lookups.append(q_object)
82
-
83
- def add_sort_by(self, sort_by: sorting.SortBy):
84
- """Read the desired result ordering from a ``<fes:SortBy>`` element."""
85
- self.ordering += sort_by.build_ordering(self.feature_type)
86
-
87
- def add_value_reference(
88
- self, value_reference: expressions.ValueReference
89
- ) -> expressions.RhsTypes:
90
- """Add a reference that should be returned by the query.
91
-
92
- This includes the XPath expression to the query, in case that adds
93
- extra lookups. The name (or alias) is returned that can be used in the
94
- ``queryset.values()`` result. This is needed to support cases like
95
- these in the future: ``addresses/Address[street="Oxfordstrasse"]/number``
96
- """
97
- # The actual limiting of fields happens inside the decorate_queryset() of the renderer.
98
- return value_reference.build_rhs(self)
99
-
100
- def add_property_name(self, property_name: expressions.ValueReference) -> expressions.RhsTypes:
101
- """Define which field should be returned by the query."""
102
- # Make sure any xpath [attr=value] lookups work.
103
- # This will also validate the name because it resolves the ORM path.
104
- # The actual limiting of fields happens inside the decorate_queryset() of the renderer.
105
- return property_name.build_rhs(self)
106
-
107
- def apply_extra_lookups(self, comparison: Q) -> Q:
108
- """Combine stashed lookups with the provided Q object.
109
-
110
- This is called for functions that compile a "Q" object.
111
- In case a node added extra lookups (for attributes), these are combined here
112
- with the actual comparison.
113
- """
114
- if not self.extra_lookups:
115
- return comparison
116
-
117
- # The extra lookups are used for XPath queries such as "/node[@attr=..]/foo".
118
- # A <ValueReference> with such lookup also requires to limit the filtered results,
119
- # in addition to the comparison operator code that is wrapped up here.
120
- result = reduce(operator.and_, [comparison] + self.extra_lookups)
121
- self.extra_lookups.clear()
122
- return result
123
-
124
- def mark_empty(self):
125
- """Mark as returning no results."""
126
- self.is_empty = True
127
-
128
- def filter_queryset(self, queryset: QuerySet, feature_type: FeatureType) -> QuerySet:
129
- """Apply the filters and lookups to the queryset.
130
-
131
- :param queryset: The queryset to filter.
132
- :param feature_type: The feature type that the queryset originated from.
133
- """
134
- if self.is_empty:
135
- return queryset.none()
136
-
137
- if self.extra_lookups:
138
- # Each time an expression node calls add_extra_lookup(),
139
- # the parent should have used apply_extra_lookups()
140
- raise RuntimeError("apply_extra_lookups() was not called")
141
-
142
- # All are applied at once.
143
- if self.annotations:
144
- queryset = queryset.annotate(**self.annotations)
145
-
146
- lookups = self.lookups
147
- try:
148
- lookups += self.typed_lookups[feature_type.name]
149
- except KeyError:
150
- pass
151
-
152
- if lookups:
153
- queryset = queryset.filter(*lookups)
154
-
155
- if self.ordering:
156
- queryset = queryset.order_by(*self.ordering)
157
-
158
- if self.distinct:
159
- queryset = queryset.distinct()
160
-
161
- return queryset
162
-
163
- def __repr__(self):
164
- return (
165
- "<CompiledQuery"
166
- f" annotations={self.annotations!r},"
167
- f" lookups={self.lookups!r},"
168
- f" typed_lookups={self.typed_lookups!r}>"
169
- )
170
-
171
- def __eq__(self, other):
172
- """For pytest comparisons."""
173
- if isinstance(other, CompiledQuery):
174
- return (
175
- other.lookups == self.lookups
176
- and other.typed_lookups == self.typed_lookups
177
- and other.annotations == self.annotations
178
- )
179
- else:
180
- return NotImplemented
181
-
182
-
183
- @models.CharField.register_lookup
184
- @models.TextField.register_lookup
185
- @models.ForeignObject.register_lookup
186
- class FesLike(lookups.Lookup):
187
- """Allow fieldname__fes_like=... lookups in querysets."""
188
-
189
- lookup_name = "fes_like"
190
-
191
- def as_sql(self, compiler, connection):
192
- """Generate the required SQL."""
193
- # lhs = "table"."field"
194
- # rhs = %s
195
- # lhs_params = []
196
- # lhs_params = ["prep-value"]
197
- lhs, lhs_params = self.process_lhs(compiler, connection)
198
- rhs, rhs_params = self.process_rhs(compiler, connection)
199
- return f"{lhs} LIKE {rhs}", lhs_params + rhs_params
200
-
201
- def get_db_prep_lookup(self, value, connection):
202
- """This expects that the right-hand-side already has wildcard characters."""
203
- return "%s", [value]
204
-
205
-
206
- @models.Field.register_lookup
207
- @models.ForeignObject.register_lookup
208
- class FesNotEqual(lookups.Lookup):
209
- """Allow fieldname__fes_notequal=... lookups in querysets."""
210
-
211
- lookup_name = "fes_notequal"
212
-
213
- def as_sql(self, compiler, connection):
214
- """Generate the required SQL."""
215
- lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
216
- rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
217
- return f"{lhs} != {rhs}", (lhs_params + rhs_params)
218
-
219
-
220
- @BaseSpatialField.register_lookup
221
- class FesBeyondLookup(DWithinLookup):
222
- """Based on the FES 2.0.3 corrigendum:
223
-
224
- DWithin(A,B,d) = Distance(A,B) < d
225
- Beyond(A,B,d) = Distance(A,B) > d
226
-
227
- See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
228
- """
229
-
230
- lookup_name = "fes_beyond"
231
- sql_template = "NOT %(func)s(%(lhs)s, %(rhs)s, %(value)s)"
232
-
233
- def get_rhs_op(self, connection, rhs):
234
- # Allow the SQL $(func)s to be different from the ORM lookup name.
235
- # This uses ST_DWithin() on PostGIS
236
- return connection.ops.gis_operators["dwithin"]
237
-
238
-
239
- if "django.contrib.postgres" in settings.INSTALLED_APPS:
240
- from django.contrib.postgres.fields import ArrayField
241
-
242
- class ArrayAnyMixin:
243
- any_operators = {
244
- "exact": "= ANY(%s)",
245
- "ne": "!= ANY(%s)",
246
- "gt": "< ANY(%s)",
247
- "gte": "<= ANY(%s)",
248
- "lt": "> ANY(%s)",
249
- "lte": ">= ANY(%s)",
250
- }
251
-
252
- def as_sql(self, compiler, connection):
253
- # For the ANY() comparison, the filter operands need to be reversed.
254
- # So instead of "field < value", it becomes "value > ANY(field)
255
- lhs_sql, lhs_params = self.process_lhs(compiler, connection)
256
- rhs_sql, rhs_params = self.process_rhs(compiler, connection)
257
- lhs_sql = self.get_rhs_op(connection, lhs_sql)
258
- return f"{rhs_sql} {lhs_sql}", (rhs_params + lhs_params)
259
-
260
- def get_rhs_op(self, connection, rhs):
261
- return self.any_operators[self.lookup_name] % rhs
262
-
263
- def _register_any_lookup(base: type[lookups.BuiltinLookup]):
264
- """Register array lookups under a different name."""
265
- cls = type(f"FesArrayAny{base.__name__}", (ArrayAnyMixin, base), {})
266
- ArrayField.register_lookup(cls, lookup_name=f"fes_any{base.lookup_name}")
267
-
268
- _register_any_lookup(lookups.Exact)
269
- _register_any_lookup(lookups.Exact)
270
- _register_any_lookup(lookups.GreaterThan)
271
- _register_any_lookup(lookups.GreaterThanOrEqual)
272
- _register_any_lookup(lookups.LessThan)
273
- _register_any_lookup(lookups.LessThanOrEqual)
274
-
275
- @ArrayField.register_lookup
276
- class FesArrayAnyNotEqual(FesNotEqual):
277
- """Inequality test for a single item in the array"""
278
-
279
- lookup_name = "fes_anynotequal"
280
-
281
- def as_sql(self, compiler, connection):
282
- """Generate the required SQL."""
283
- lhs, lhs_params = self.process_lhs(compiler, connection)
284
- rhs, rhs_params = self.process_rhs(compiler, connection)
285
- return f"{rhs} != ANY({lhs})", (rhs_params + lhs_params)
gisserver/parsers/tags.py DELETED
@@ -1,102 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from functools import wraps
4
- from itertools import chain
5
- from typing import TYPE_CHECKING
6
- from xml.etree.ElementTree import Element, QName
7
-
8
- from gisserver.exceptions import ExternalParsingError
9
-
10
-
11
- def expect_tag(namespace: str, *tag_names: str, leaf=False):
12
- """Validate whether a given tag is need"""
13
- valid_tags = {str(QName(namespace, name)) for name in tag_names}
14
- expect0 = str(QName(namespace, tag_names[0]))
15
-
16
- def _wrapper(func):
17
- @wraps(func)
18
- def _expect_tag_decorator(cls, element: Element, *args, **kwargs):
19
- if element.tag not in valid_tags:
20
- raise ExternalParsingError(
21
- f"{cls.__name__} parser expects an <{expect0}> node, got <{element.tag}>"
22
- )
23
- if leaf and len(element):
24
- raise ExternalParsingError(
25
- f"Unsupported child element for {element.tag} element: {element[0].tag}."
26
- )
27
-
28
- return func(cls, element, *args, **kwargs)
29
-
30
- return _expect_tag_decorator
31
-
32
- return _wrapper
33
-
34
-
35
- def expect_children(min_child_nodes, *expect_types: str | type[BaseNode]):
36
- def _wrapper(func):
37
- @wraps(func)
38
- def _expect_children_decorator(cls, element: Element, *args, **kwargs):
39
- if len(element) < min_child_nodes:
40
- type_names = ", ".join(
41
- sorted(
42
- set(
43
- chain.from_iterable(
44
- (
45
- [child_type]
46
- if isinstance(child_type, str)
47
- else chain.from_iterable(
48
- sub_type.xml_tags
49
- for sub_type in child_type.__subclasses__()
50
- )
51
- )
52
- for child_type in expect_types
53
- )
54
- )
55
- )
56
- )
57
- suffix = f" (possible tags: {type_names})" if type_names else ""
58
- raise ExternalParsingError(
59
- f"<{element.tag}> should have {min_child_nodes} child nodes, "
60
- f"got {len(element)}{suffix}"
61
- )
62
-
63
- return func(cls, element, *args, **kwargs)
64
-
65
- return _expect_children_decorator
66
-
67
- return _wrapper
68
-
69
-
70
- def get_child(root, namespace, localname) -> Element:
71
- """Find the element using a fully qualified name."""
72
- return root.find(QName(namespace, localname).text)
73
-
74
-
75
- def get_children(root, namespace, localname) -> list[Element]:
76
- """Find the element using a fully qualified name."""
77
- return root.findall(QName(namespace, localname).text)
78
-
79
-
80
- def get_attribute(element: Element, name) -> str:
81
- """Resolve an attribute, raise an error when it's missing."""
82
- try:
83
- return element.attrib[name]
84
- except KeyError:
85
- raise ExternalParsingError(
86
- f"Element {element.tag} misses required attribute '{name}'"
87
- ) from None
88
-
89
-
90
- def split_ns(tag_name: str) -> tuple[str | None, str]:
91
- """Split the element tag into the namespace and local name.
92
- The stdlib etree doesn't have the properties for this (lxml does).
93
- """
94
- if tag_name.startswith("{"):
95
- end = tag_name.index("}")
96
- return tag_name[1:end], tag_name[end + 1 :]
97
- else:
98
- return None, tag_name
99
-
100
-
101
- if TYPE_CHECKING:
102
- from gisserver.parsers.base import BaseNode
@@ -1,37 +0,0 @@
1
- """Entry point to handle queries.
2
-
3
- WFS defines 2 query types:
4
- - Adhoc queries are constructed directly from request parameters.
5
- - Stored queries are defined first, and executed later.
6
-
7
- Both use the FES (Filter Encoding Syntax) filtering logic internally.
8
-
9
- The objects in this module closely follow the WFS spec.
10
- By using the same type definitions, a lot of code logic follows naturally.
11
- The "GetFeatureById" is a mandatory built-in stored query.
12
- """
13
-
14
- from .adhoc import AdhocQuery
15
- from .base import QueryExpression
16
- from .projection import FeatureProjection, FeatureRelation
17
- from .stored import (
18
- GetFeatureById,
19
- QueryExpressionText,
20
- StoredQuery,
21
- StoredQueryDescription,
22
- StoredQueryParameter,
23
- stored_query_registry,
24
- )
25
-
26
- __all__ = (
27
- "QueryExpression",
28
- "AdhocQuery",
29
- "QueryExpressionText",
30
- "StoredQueryDescription",
31
- "StoredQuery",
32
- "stored_query_registry",
33
- "StoredQueryParameter",
34
- "GetFeatureById",
35
- "FeatureProjection",
36
- "FeatureRelation",
37
- )
@@ -1,185 +0,0 @@
1
- """Handle adhoc-query objects.
2
-
3
- The adhoc query is based on incoming request parameters,
4
- such as the "FILTER", "BBOX" and "RESOURCEID" parameters.
5
-
6
- These definitions follow the WFS spec.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import logging
12
- from dataclasses import dataclass
13
-
14
- from django.db.models import Q
15
-
16
- from gisserver import conf
17
- from gisserver.exceptions import InvalidParameterValue, MissingParameterValue
18
- from gisserver.features import FeatureType
19
- from gisserver.geometries import BoundingBox
20
- from gisserver.parsers import fes20
21
- from gisserver.parsers.fes20 import operators
22
-
23
- from .base import QueryExpression
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- @dataclass
29
- class AdhocQuery(QueryExpression):
30
- """The Ad hoc query expression parameters.
31
-
32
- This represents all dynamic queries received as request (hence "adhoc"),
33
- such as the "FILTER" and "BBOX" arguments from an HTTP GET.
34
-
35
- The WFS Spec has 3 class levels for this:
36
- - AdhocQueryExpression (types, projection, selection, sorting)
37
- - Query (adds srsName, featureVersion)
38
-
39
- For KVP requests, this dataclass is almost identical to **params.
40
- However, it allows combining the filter parameters. These become
41
- one single XML request for HTTP POST requests later.
42
-
43
- .. seealso::
44
- https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/query_xsd.html#AbstractAdhocQueryExpressionType
45
- """
46
-
47
- typeNames: list[FeatureType] # typeNames in WFS/FES spec
48
- # aliases: Optional[List[str]] = None
49
- handle: str = "" # only for XML POST requests
50
-
51
- # Projection clause (fes:AbstractProjectionClause)
52
- property_names: list[fes20.ValueReference] | None = None
53
-
54
- # Selection clause (fes:AbstractSelectionClause):
55
- # - for XML POST this is encoded in a <fes:Query>
56
- # - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
57
- filter: fes20.Filter | None = None
58
- filter_language: str = fes20.Filter.query_language
59
- bbox: BoundingBox | None = None
60
-
61
- # Sorting Clause (fes:AbstractSortingClause)
62
- sortBy: fes20.SortBy | None = None
63
-
64
- # Officially part of the GetFeature/GetPropertyValue request object,
65
- # but included here for ease of query implementation.
66
- resourceId: fes20.IdOperator | None = None
67
-
68
- # GetPropertyValue:
69
- # In the WFS spec, this is only part of the operation/presentation.
70
- # For Django, we'd like to make this part of the query too.
71
- value_reference: fes20.ValueReference | None = None
72
-
73
- @classmethod
74
- def from_kvp_request(cls, **params):
75
- """Build this object from an HTTP GET (key-value-pair) request."""
76
- # Validate optionally required parameters
77
- if not params["typeNames"] and not params["resourceID"]:
78
- raise MissingParameterValue("Empty TYPENAMES parameter", locator="typeNames")
79
-
80
- # Validate mutually exclusive parameters
81
- if params["filter"] and (params["bbox"] or params["resourceID"]):
82
- raise InvalidParameterValue(
83
- "The FILTER parameter is mutually exclusive with BBOX and RESOURCEID",
84
- locator="filter",
85
- )
86
-
87
- # Validate mutually exclusive parameters
88
- if params["resourceID"]:
89
- if params["bbox"] or params["filter"]:
90
- raise InvalidParameterValue(
91
- "The RESOURCEID parameter is mutually exclusive with BBOX and FILTER",
92
- locator="resourceID",
93
- )
94
-
95
- # When ResourceId + typenames is defined, it should be a value from typenames
96
- # see WFS spec 7.9.2.4.1
97
- if params["typeNames"]:
98
- id_type_names = params["resourceID"].type_names
99
- if id_type_names:
100
- # Only test when the RESOURCEID has a typename.id format
101
- # Otherwise, this breaks the CITE RESOURCEID=test-UUID parameter.
102
- kvp_type_names = {feature_type.name for feature_type in params["typeNames"]}
103
- if not kvp_type_names.issuperset(id_type_names):
104
- raise InvalidParameterValue(
105
- "When TYPENAMES and RESOURCEID are combined, "
106
- "the RESOURCEID type should be included in TYPENAMES.",
107
- locator="resourceID",
108
- )
109
-
110
- return AdhocQuery(
111
- typeNames=params["typeNames"],
112
- property_names=params["propertyName"],
113
- filter=params["filter"],
114
- filter_language=params["filter_language"],
115
- bbox=params["bbox"],
116
- sortBy=params["sortBy"],
117
- resourceId=params["resourceID"],
118
- value_reference=params.get("valueReference"),
119
- )
120
-
121
- def bind(self, *args, **kwargs):
122
- """Inform this query object of the available feature types"""
123
- super().bind(*args, **kwargs)
124
-
125
- if self.resourceId:
126
- # Early validation whether the selected resourceID type exists.
127
- feature_types = [
128
- self.resolve_type_name(type_name, locator="resourceID")
129
- for type_name in self.resourceId.type_names
130
- ]
131
-
132
- # Also make the behavior consistent, always supply the type name.
133
- if not self.typeNames:
134
- self.typeNames = feature_types
135
-
136
- def get_type_names(self):
137
- return self.typeNames
138
-
139
- def compile_query(self, feature_type: FeatureType, using=None) -> fes20.CompiledQuery:
140
- """Return our internal CompiledQuery object that can be applied to the queryset."""
141
- if self.filter:
142
- # Generate the internal query object from the <fes:Filter>
143
- return self.filter.compile_query(feature_type, using=using)
144
- else:
145
- # Generate the internal query object from the BBOX and sortBy args.
146
- return self._compile_non_filter_query(feature_type, using=using)
147
-
148
- def _compile_non_filter_query(self, feature_type: FeatureType, using=None):
149
- """Generate the query based on the remaining parameters.
150
-
151
- This is slightly more efficient than generating the fes Filter object
152
- from these KVP parameters (which could also be done within the request method).
153
- """
154
- compiler = fes20.CompiledQuery(feature_type=feature_type, using=using)
155
-
156
- if self.bbox:
157
- # Validate whether the provided SRID is supported.
158
- # While PostGIS would support many more ID's,
159
- # it would crash when an unsupported ID is given.
160
- crs = self.bbox.crs
161
- if (
162
- conf.GISSERVER_SUPPORTED_CRS_ONLY
163
- and crs is not None
164
- and crs not in feature_type.supported_crs
165
- ):
166
- raise InvalidParameterValue(
167
- "bbox",
168
- f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
169
- )
170
-
171
- # Using __within does not work with geometries
172
- # that only partially exist within the bbox
173
- lookup = operators.SpatialOperatorName.BBOX.value # "intersects"
174
- filters = {
175
- f"{feature_type.geometry_field.name}__{lookup}": self.bbox.as_polygon(),
176
- }
177
- compiler.add_lookups(Q(**filters))
178
-
179
- if self.resourceId:
180
- self.resourceId.build_query(compiler=compiler)
181
-
182
- if self.sortBy:
183
- compiler.add_sort_by(self.sortBy)
184
-
185
- return compiler