django-gisserver 1.4.1__py3-none-any.whl → 2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.4.1.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 +63 -60
  8. gisserver/exceptions.py +47 -9
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +267 -240
  13. gisserver/geometries.py +34 -39
  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 +129 -305
  18. gisserver/operations/wfs20.py +428 -336
  19. gisserver/output/__init__.py +10 -48
  20. gisserver/output/base.py +198 -143
  21. gisserver/output/csv.py +81 -85
  22. gisserver/output/geojson.py +63 -72
  23. gisserver/output/gml32.py +310 -281
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +71 -30
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -154
  28. gisserver/output/xmlschema.py +86 -47
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +15 -11
  32. gisserver/parsers/fes20/expressions.py +89 -50
  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 +336 -128
  37. gisserver/parsers/fes20/sorting.py +107 -34
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +6 -3
  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 +11 -11
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +375 -258
  61. gisserver/views.py +206 -75
  62. django_gisserver-1.4.1.dist-info/RECORD +0 -53
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -275
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -34
  67. gisserver/queries/adhoc.py +0 -181
  68. gisserver/queries/base.py +0 -146
  69. gisserver/queries/stored.py +0 -205
  70. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  71. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  72. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  73. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
@@ -1,275 +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(self, value_reference: expressions.ValueReference) -> str:
88
- """Add a reference that should be returned by the query.
89
-
90
- This includes the XPath expression to the query, in case that adds
91
- extra lookups. The name (or alias) is returned that can be used in the
92
- ``queryset.values()`` result. This is needed to support cases like
93
- these in the future: ``addresses/Address[street="Oxfordstrasse"]/number``
94
- """
95
- return value_reference.build_rhs(self)
96
-
97
- def apply_extra_lookups(self, comparison: Q) -> Q:
98
- """Combine stashed lookups with the provided Q object.
99
-
100
- This is called for functions that compile a "Q" object.
101
- In case a node added extra lookups (for attributes), these are combined here
102
- with the actual comparison.
103
- """
104
- if not self.extra_lookups:
105
- return comparison
106
-
107
- # The extra lookups are used for XPath queries such as "/node[@attr=..]/foo".
108
- # A <ValueReference> with such lookup also requires to limit the filtered results,
109
- # in addition to the comparison operator code that is wrapped up here.
110
- result = reduce(operator.and_, [comparison] + self.extra_lookups)
111
- self.extra_lookups.clear()
112
- return result
113
-
114
- def mark_empty(self):
115
- """Mark as returning no results."""
116
- self.is_empty = True
117
-
118
- def filter_queryset(self, queryset: QuerySet, feature_type: FeatureType) -> QuerySet:
119
- """Apply the filters and lookups to the queryset.
120
-
121
- :param queryset: The queryset to filter.
122
- :param feature_type: The feature type that the queryset originated from.
123
- """
124
- if self.is_empty:
125
- return queryset.none()
126
-
127
- if self.extra_lookups:
128
- # Each time an expression node calls add_extra_lookup(),
129
- # the parent should have used apply_extra_lookups()
130
- raise RuntimeError("apply_extra_lookups() was not called")
131
-
132
- # All are applied at once.
133
- if self.annotations:
134
- queryset = queryset.annotate(**self.annotations)
135
-
136
- lookups = self.lookups
137
- try:
138
- lookups += self.typed_lookups[feature_type.name]
139
- except KeyError:
140
- pass
141
-
142
- if lookups:
143
- queryset = queryset.filter(*lookups)
144
-
145
- if self.ordering:
146
- queryset = queryset.order_by(*self.ordering)
147
-
148
- if self.distinct:
149
- queryset = queryset.distinct()
150
-
151
- return queryset
152
-
153
- def __repr__(self):
154
- return (
155
- "<CompiledQuery"
156
- f" annotations={self.annotations!r},"
157
- f" lookups={self.lookups!r},"
158
- f" typed_lookups={self.typed_lookups!r}>"
159
- )
160
-
161
- def __eq__(self, other):
162
- """For pytest comparisons."""
163
- if isinstance(other, CompiledQuery):
164
- return (
165
- other.lookups == self.lookups
166
- and other.typed_lookups == self.typed_lookups
167
- and other.annotations == self.annotations
168
- )
169
- else:
170
- return NotImplemented
171
-
172
-
173
- @models.CharField.register_lookup
174
- @models.TextField.register_lookup
175
- @models.ForeignObject.register_lookup
176
- class FesLike(lookups.Lookup):
177
- """Allow fieldname__fes_like=... lookups in querysets."""
178
-
179
- lookup_name = "fes_like"
180
-
181
- def as_sql(self, compiler, connection):
182
- """Generate the required SQL."""
183
- # lhs = "table"."field"
184
- # rhs = %s
185
- # lhs_params = []
186
- # lhs_params = ["prep-value"]
187
- lhs, lhs_params = self.process_lhs(compiler, connection)
188
- rhs, rhs_params = self.process_rhs(compiler, connection)
189
- return f"{lhs} LIKE {rhs}", lhs_params + rhs_params
190
-
191
- def get_db_prep_lookup(self, value, connection):
192
- """This expects that the right-hand-side already has wildcard characters."""
193
- return "%s", [value]
194
-
195
-
196
- @models.Field.register_lookup
197
- @models.ForeignObject.register_lookup
198
- class FesNotEqual(lookups.Lookup):
199
- """Allow fieldname__fes_notequal=... lookups in querysets."""
200
-
201
- lookup_name = "fes_notequal"
202
-
203
- def as_sql(self, compiler, connection):
204
- """Generate the required SQL."""
205
- lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
206
- rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
207
- return f"{lhs} != {rhs}", (lhs_params + rhs_params)
208
-
209
-
210
- @BaseSpatialField.register_lookup
211
- class FesBeyondLookup(DWithinLookup):
212
- """Based on the FES 2.0.3 corrigendum:
213
-
214
- DWithin(A,B,d) = Distance(A,B) < d
215
- Beyond(A,B,d) = Distance(A,B) > d
216
-
217
- See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
218
- """
219
-
220
- lookup_name = "fes_beyond"
221
- sql_template = "NOT %(func)s(%(lhs)s, %(rhs)s, %(value)s)"
222
-
223
- def get_rhs_op(self, connection, rhs):
224
- # Allow the SQL $(func)s to be different from the ORM lookup name.
225
- # This uses ST_DWithin() on PostGIS
226
- return connection.ops.gis_operators["dwithin"]
227
-
228
-
229
- if "django.contrib.postgres" in settings.INSTALLED_APPS:
230
- from django.contrib.postgres.fields import ArrayField
231
-
232
- class ArrayAnyMixin:
233
- any_operators = {
234
- "exact": "= ANY(%s)",
235
- "ne": "!= ANY(%s)",
236
- "gt": "< ANY(%s)",
237
- "gte": "<= ANY(%s)",
238
- "lt": "> ANY(%s)",
239
- "lte": ">= ANY(%s)",
240
- }
241
-
242
- def as_sql(self, compiler, connection):
243
- # For the ANY() comparison, the filter operands need to be reversed.
244
- # So instead of "field < value", it becomes "value > ANY(field)
245
- lhs_sql, lhs_params = self.process_lhs(compiler, connection)
246
- rhs_sql, rhs_params = self.process_rhs(compiler, connection)
247
- lhs_sql = self.get_rhs_op(connection, lhs_sql)
248
- return f"{rhs_sql} {lhs_sql}", (rhs_params + lhs_params)
249
-
250
- def get_rhs_op(self, connection, rhs):
251
- return self.any_operators[self.lookup_name] % rhs
252
-
253
- def _register_any_lookup(base: type[lookups.BuiltinLookup]):
254
- """Register array lookups under a different name."""
255
- cls = type(f"FesArrayAny{base.__name__}", (ArrayAnyMixin, base), {})
256
- ArrayField.register_lookup(cls, lookup_name=f"fes_any{base.lookup_name}")
257
-
258
- _register_any_lookup(lookups.Exact)
259
- _register_any_lookup(lookups.Exact)
260
- _register_any_lookup(lookups.GreaterThan)
261
- _register_any_lookup(lookups.GreaterThanOrEqual)
262
- _register_any_lookup(lookups.LessThan)
263
- _register_any_lookup(lookups.LessThanOrEqual)
264
-
265
- @ArrayField.register_lookup
266
- class FesArrayAnyNotEqual(FesNotEqual):
267
- """Inequality test for a single item in the array"""
268
-
269
- lookup_name = "fes_anynotequal"
270
-
271
- def as_sql(self, compiler, connection):
272
- """Generate the required SQL."""
273
- lhs, lhs_params = self.process_lhs(compiler, connection)
274
- rhs, rhs_params = self.process_rhs(compiler, connection)
275
- 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,34 +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 .stored import (
17
- GetFeatureById,
18
- QueryExpressionText,
19
- StoredQuery,
20
- StoredQueryDescription,
21
- StoredQueryParameter,
22
- stored_query_registry,
23
- )
24
-
25
- __all__ = (
26
- "QueryExpression",
27
- "AdhocQuery",
28
- "QueryExpressionText",
29
- "StoredQueryDescription",
30
- "StoredQuery",
31
- "stored_query_registry",
32
- "StoredQueryParameter",
33
- "GetFeatureById",
34
- )
@@ -1,181 +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 a 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
-
44
- typeNames: list[FeatureType] # typeNames in WFS/FES spec
45
- # aliases: Optional[List[str]] = None
46
- handle: str = "" # only for XML POST requests
47
-
48
- # Projection clause:
49
- # propertyName
50
-
51
- # Selection clause:
52
- # - for XML POST this is encoded in a <fes:Query>
53
- # - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
54
- filter: fes20.Filter | None = None
55
- filter_language: str = fes20.Filter.query_language
56
- bbox: BoundingBox | None = None
57
-
58
- # Sorting Clause
59
- sortBy: fes20.SortBy | None = None
60
-
61
- # Officially part of the GetFeature/GetPropertyValue request object,
62
- # but included here for ease of query implementation.
63
- resourceId: fes20.IdOperator | None = None
64
-
65
- # GetPropertyValue:
66
- # In the WFS spec, this is only part of the operation/presentation.
67
- # For Django, we'd like to make this part of the query too.
68
- value_reference: fes20.ValueReference | None = None
69
-
70
- @classmethod
71
- def from_kvp_request(cls, **params):
72
- """Build this object from a HTTP GET (key-value-pair) request."""
73
- # Validate optionally required parameters
74
- if not params["typeNames"] and not params["resourceID"]:
75
- raise MissingParameterValue("typeNames", "Empty TYPENAMES parameter")
76
-
77
- # Validate mutually exclusive parameters
78
- if params["filter"] and (params["bbox"] or params["resourceID"]):
79
- raise InvalidParameterValue(
80
- "filter",
81
- "The FILTER parameter is mutually exclusive with BBOX and RESOURCEID",
82
- )
83
-
84
- # Validate mutually exclusive parameters
85
- if params["resourceID"]:
86
- if params["bbox"] or params["filter"]:
87
- raise InvalidParameterValue(
88
- "resourceID",
89
- "The RESOURCEID parameter is mutually exclusive with BBOX and FILTER",
90
- )
91
-
92
- # When ResourceId + typenames is defined, it should be a value from typenames
93
- # see WFS spec 7.9.2.4.1
94
- if params["typeNames"]:
95
- id_type_names = params["resourceID"].type_names
96
- if id_type_names:
97
- # Only test when the RESOURCEID has a typename.id format
98
- # Otherwise, this breaks the CITE RESOURCEID=test-UUID parameter.
99
- kvp_type_names = {feature_type.name for feature_type in params["typeNames"]}
100
- if not kvp_type_names.issuperset(id_type_names):
101
- raise InvalidParameterValue(
102
- "resourceID",
103
- "When TYPENAMES and RESOURCEID are combined, "
104
- "the RESOURCEID type should be included in TYPENAMES.",
105
- )
106
-
107
- return AdhocQuery(
108
- typeNames=params["typeNames"],
109
- filter=params["filter"],
110
- filter_language=params["filter_language"],
111
- bbox=params["bbox"],
112
- sortBy=params["sortBy"],
113
- resourceId=params["resourceID"],
114
- value_reference=params.get("valueReference"),
115
- )
116
-
117
- def bind(self, *args, **kwargs):
118
- """Inform this quey object of the available feature types"""
119
- super().bind(*args, **kwargs)
120
-
121
- if self.resourceId:
122
- # Early validation whether the selected resourceID type exists.
123
- feature_types = [
124
- self.resolve_type_name(type_name, locator="resourceID")
125
- for type_name in self.resourceId.type_names
126
- ]
127
-
128
- # Also make the behavior consistent, always supply the type name.
129
- if not self.typeNames:
130
- self.typeNames = feature_types
131
-
132
- def get_type_names(self):
133
- return self.typeNames
134
-
135
- def compile_query(self, feature_type: FeatureType, using=None) -> fes20.CompiledQuery:
136
- """Return our internal CompiledQuery object that can be applied to the queryset."""
137
- if self.filter:
138
- # Generate the internal query object from the <fes:Filter>
139
- return self.filter.compile_query(feature_type, using=using)
140
- else:
141
- # Generate the internal query object from the BBOX and sortBy args.
142
- return self._compile_non_filter_query(feature_type, using=using)
143
-
144
- def _compile_non_filter_query(self, feature_type: FeatureType, using=None):
145
- """Generate the query based on the remaining parameters.
146
-
147
- This is slightly more efficient then generating the fes Filter object
148
- from these KVP parameters (which could also be done within the request method).
149
- """
150
- compiler = fes20.CompiledQuery(feature_type=feature_type, using=using)
151
-
152
- if self.bbox:
153
- # Validate whether the provided SRID is supported.
154
- # While PostGIS would support many more ID's,
155
- # it would crash when an unsupported ID is given.
156
- crs = self.bbox.crs
157
- if (
158
- conf.GISSERVER_SUPPORTED_CRS_ONLY
159
- and crs is not None
160
- and crs not in feature_type.supported_crs
161
- ):
162
- raise InvalidParameterValue(
163
- "bbox",
164
- f"Feature '{feature_type.name}' does not support SRID {crs.srid}.",
165
- )
166
-
167
- # Using __within does not work with geometries
168
- # that only partially exist within the bbox
169
- lookup = operators.SpatialOperatorName.BBOX.value # "intersects"
170
- filters = {
171
- f"{feature_type.geometry_field.name}__{lookup}": self.bbox.as_polygon(),
172
- }
173
- compiler.add_lookups(Q(**filters))
174
-
175
- if self.resourceId:
176
- self.resourceId.build_query(compiler=compiler)
177
-
178
- if self.sortBy:
179
- compiler.add_sort_by(self.sortBy)
180
-
181
- return compiler