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
|
@@ -1,37 +1,111 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import AnyStr, ClassVar, Union
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from django.db.models import Q
|
|
7
7
|
|
|
8
|
-
from gisserver.exceptions import
|
|
9
|
-
from gisserver.parsers.
|
|
10
|
-
from gisserver.parsers.
|
|
11
|
-
from gisserver.
|
|
8
|
+
from gisserver.exceptions import InvalidParameterValue
|
|
9
|
+
from gisserver.parsers.ast import BaseNode, expect_tag, tag_registry
|
|
10
|
+
from gisserver.parsers.gml import GEOSGMLGeometry
|
|
11
|
+
from gisserver.parsers.ows import KVPRequest
|
|
12
|
+
from gisserver.parsers.query import CompiledQuery
|
|
13
|
+
from gisserver.parsers.xml import NSElement, parse_xml_from_string, xmlns
|
|
12
14
|
|
|
13
|
-
from . import expressions, identifiers, operators
|
|
15
|
+
from . import expressions, identifiers, operators
|
|
14
16
|
|
|
15
17
|
FilterPredicates = Union[expressions.Function, operators.Operator]
|
|
16
18
|
|
|
19
|
+
# Fully qualified tag names
|
|
20
|
+
FES_RESOURCE_ID = xmlns.fes20.qname("ResourceId")
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
@tag_registry.register("Filter", xmlns.fes20)
|
|
25
|
+
class Filter(BaseNode):
|
|
19
26
|
"""The <fes:Filter> element.
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
This parses and handles the syntax::
|
|
29
|
+
|
|
30
|
+
<fes:Filter>
|
|
31
|
+
<fes:SomeOperator>
|
|
32
|
+
...
|
|
33
|
+
</fes:SomeOperator>
|
|
34
|
+
</fes:Filter>
|
|
35
|
+
|
|
36
|
+
The :meth:`build_query` will convert the parsed tree
|
|
37
|
+
into a format that can build a Django ORM QuerySet.
|
|
38
|
+
|
|
39
|
+
.. seealso:: https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/filter_xsd.html#Filter
|
|
22
40
|
"""
|
|
23
41
|
|
|
24
|
-
query_language = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
|
|
42
|
+
query_language: ClassVar[str] = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
|
|
25
43
|
|
|
26
44
|
predicate: FilterPredicates
|
|
27
|
-
source: AnyStr | None
|
|
45
|
+
source: AnyStr | None = field(default=None, compare=False)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_kvp_request(cls, kvp: KVPRequest) -> Filter | None:
|
|
49
|
+
"""Parse the filter from the GET request."""
|
|
50
|
+
|
|
51
|
+
# Check filter language
|
|
52
|
+
filter_language = kvp.get_str("FILTER_LANGUAGE", default=cls.query_language)
|
|
53
|
+
if filter_language != cls.query_language:
|
|
54
|
+
raise InvalidParameterValue(
|
|
55
|
+
f"Invalid value for filterLanguage: {filter_language}", locator="filterLanguage"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Parse filter
|
|
59
|
+
filter = kvp.get_custom(
|
|
60
|
+
"filter", default=None, parser=lambda x: cls.from_string(x, kvp.ns_aliases)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Parse alternatives to filter
|
|
64
|
+
resource_ids = [
|
|
65
|
+
identifiers.ResourceId.from_string(rid, kvp.ns_aliases)
|
|
66
|
+
for rid in kvp.get_list("resourceID", default=[])
|
|
67
|
+
]
|
|
68
|
+
bbox = kvp.get_custom("bbox", default=None, parser=GEOSGMLGeometry.from_bbox)
|
|
69
|
+
|
|
70
|
+
# Make sure the various query options are not mixed.
|
|
71
|
+
cls.validate_kvp_exclusions(filter, bbox, resource_ids)
|
|
72
|
+
|
|
73
|
+
if filter is None:
|
|
74
|
+
# See if the other KVP parameters still provide a basic filter.
|
|
75
|
+
# Instead of implementing these parameters separately in the AdhocQueryExpression,
|
|
76
|
+
# they are implemented by constructing the filter AST internally.
|
|
77
|
+
if resource_ids:
|
|
78
|
+
filter = Filter(predicate=operators.IdOperator(resource_ids))
|
|
79
|
+
elif bbox is not None:
|
|
80
|
+
filter = Filter(
|
|
81
|
+
predicate=operators.BinarySpatialOperator(
|
|
82
|
+
operatorType=operators.SpatialOperatorName.BBOX,
|
|
83
|
+
operand1=None,
|
|
84
|
+
operand2=bbox,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return filter
|
|
28
89
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
90
|
+
@classmethod
|
|
91
|
+
def validate_kvp_exclusions(
|
|
92
|
+
cls, filter: Filter | None, bbox: GEOSGMLGeometry | None, resource_ids: list
|
|
93
|
+
):
|
|
94
|
+
"""Validate mutually exclusive parameters"""
|
|
95
|
+
if filter is not None and (bbox is not None or resource_ids):
|
|
96
|
+
raise InvalidParameterValue(
|
|
97
|
+
"The FILTER parameter is mutually exclusive with BBOX and RESOURCEID",
|
|
98
|
+
locator="filter",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if resource_ids and (bbox is not None or filter is not None):
|
|
102
|
+
raise InvalidParameterValue(
|
|
103
|
+
"The RESOURCEID parameter is mutually exclusive with BBOX and FILTER",
|
|
104
|
+
locator="resourceId",
|
|
105
|
+
)
|
|
32
106
|
|
|
33
107
|
@classmethod
|
|
34
|
-
def from_string(cls, text: AnyStr) -> Filter:
|
|
108
|
+
def from_string(cls, text: AnyStr, ns_aliases: dict[str, str] | None = None) -> Filter:
|
|
35
109
|
"""Parse an XML <fes20:Filter> string.
|
|
36
110
|
|
|
37
111
|
This uses defusedxml by default, to avoid various XML injection attacks.
|
|
@@ -47,51 +121,45 @@ class Filter:
|
|
|
47
121
|
if "xmlns" not in first_tag and (
|
|
48
122
|
first_tag == "<Filter" or first_tag.startswith("<Filter ")
|
|
49
123
|
):
|
|
50
|
-
text = f'{first_tag} xmlns="{
|
|
124
|
+
text = f'{first_tag} xmlns="{xmlns.fes20}" xmlns:gml="{xmlns.gml32}"{text[end_first:]}'
|
|
51
125
|
|
|
52
|
-
|
|
53
|
-
root_element = fromstring(text)
|
|
54
|
-
except ParseError as e:
|
|
55
|
-
# Offer consistent results for callers to check for invalid data.
|
|
56
|
-
raise ExternalParsingError(str(e)) from e
|
|
126
|
+
root_element = parse_xml_from_string(text, extra_ns_aliases=ns_aliases)
|
|
57
127
|
return Filter.from_xml(root_element, source=text)
|
|
58
128
|
|
|
59
129
|
@classmethod
|
|
60
|
-
@expect_tag(
|
|
61
|
-
def from_xml(cls, element:
|
|
130
|
+
@expect_tag(xmlns.fes20, "Filter")
|
|
131
|
+
def from_xml(cls, element: NSElement, source: AnyStr | None = None) -> Filter:
|
|
62
132
|
"""Parse the <fes20:Filter> element."""
|
|
63
|
-
if len(element) > 1 or element[0].tag ==
|
|
133
|
+
if len(element) > 1 or element[0].tag == FES_RESOURCE_ID:
|
|
64
134
|
# fes20:ResourceId is the only element that may appear multiple times.
|
|
135
|
+
# Wrap it in an IdOperator so this class can have a single element as predicate.
|
|
65
136
|
return Filter(
|
|
66
137
|
predicate=operators.IdOperator(
|
|
67
|
-
[identifiers.Id.
|
|
138
|
+
[identifiers.Id.child_from_xml(child) for child in element]
|
|
68
139
|
),
|
|
69
140
|
source=source,
|
|
70
141
|
)
|
|
71
142
|
else:
|
|
72
143
|
return Filter(
|
|
73
|
-
|
|
74
|
-
|
|
144
|
+
# Can be Function or Operator (e.g. BinaryComparisonOperator),
|
|
145
|
+
# but not Literal or ValueReference.
|
|
146
|
+
predicate=tag_registry.node_from_xml(
|
|
147
|
+
element[0], allowed_types=FilterPredicates.__args__
|
|
75
148
|
),
|
|
76
149
|
source=source,
|
|
77
150
|
)
|
|
78
151
|
|
|
79
|
-
def
|
|
152
|
+
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
80
153
|
"""Collect the data to perform a Django ORM query."""
|
|
81
|
-
compiler = query.CompiledQuery(feature_type=feature_type, using=using)
|
|
82
|
-
|
|
83
154
|
# Function, Operator, IdList
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
compiler.add_lookups(q_object)
|
|
87
|
-
|
|
88
|
-
return compiler
|
|
155
|
+
# The operators may add the logic themselves, or return a Q object.
|
|
156
|
+
return self.predicate.build_query(compiler)
|
|
89
157
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if isinstance(
|
|
95
|
-
return self.predicate
|
|
158
|
+
def get_resource_id_types(self) -> list[str] | None:
|
|
159
|
+
"""When the filter parses ResourceId objects, return those.
|
|
160
|
+
This can return an empty list in case a ResourceId object doesn't define a type.
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(self.predicate, operators.IdOperator):
|
|
163
|
+
return self.predicate.get_type_names()
|
|
96
164
|
else:
|
|
97
|
-
return
|
|
165
|
+
return None
|
|
@@ -11,16 +11,17 @@ from enum import Enum
|
|
|
11
11
|
from django.db.models import Q
|
|
12
12
|
|
|
13
13
|
from gisserver import conf
|
|
14
|
-
from gisserver.exceptions import ExternalValueError
|
|
15
|
-
from gisserver.parsers.
|
|
16
|
-
from gisserver.parsers.tags import expect_tag, get_attribute
|
|
14
|
+
from gisserver.exceptions import ExternalValueError, InvalidParameterValue
|
|
15
|
+
from gisserver.parsers.ast import BaseNode, expect_no_children, expect_tag, tag_registry
|
|
17
16
|
from gisserver.parsers.values import auto_cast, parse_iso_datetime
|
|
18
|
-
from gisserver.
|
|
17
|
+
from gisserver.parsers.xml import parse_qname, xmlns
|
|
19
18
|
|
|
20
19
|
NoneType = type(None)
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class VersionActionTokens(Enum):
|
|
23
|
+
"""Values for the 'version' attribute of the ResourceId node."""
|
|
24
|
+
|
|
24
25
|
FIRST = "FIRST"
|
|
25
26
|
LAST = "LAST"
|
|
26
27
|
ALL = "ALL"
|
|
@@ -31,10 +32,10 @@ class VersionActionTokens(Enum):
|
|
|
31
32
|
class Id(BaseNode):
|
|
32
33
|
"""Abstract base class, as defined by FES spec."""
|
|
33
34
|
|
|
34
|
-
xml_ns =
|
|
35
|
+
xml_ns = xmlns.fes20
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
def get_type_name(self):
|
|
38
|
+
raise NotImplementedError()
|
|
38
39
|
|
|
39
40
|
def build_query(self, compiler) -> Q:
|
|
40
41
|
raise NotImplementedError()
|
|
@@ -43,26 +44,36 @@ class Id(BaseNode):
|
|
|
43
44
|
@dataclass
|
|
44
45
|
@tag_registry.register("ResourceId")
|
|
45
46
|
class ResourceId(Id):
|
|
46
|
-
"""The <fes:ResourceId> element.
|
|
47
|
+
"""The <fes:ResourceId> element.
|
|
48
|
+
This element allow queries to retrieve a resource by their identifier.
|
|
49
|
+
"""
|
|
47
50
|
|
|
51
|
+
# A raw "resource identifier". Needs to encode the object name somehow,
|
|
52
|
+
# and it's completely unrelated to XML namespacing.
|
|
48
53
|
rid: str
|
|
54
|
+
type_name: str | None
|
|
49
55
|
version: int | datetime | VersionActionTokens | NoneType = None
|
|
50
56
|
startTime: datetime | None = None
|
|
51
57
|
endTime: datetime | None = None
|
|
52
58
|
|
|
59
|
+
def get_type_name(self):
|
|
60
|
+
return self.type_name
|
|
61
|
+
|
|
53
62
|
def __post_init__(self):
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
except ValueError:
|
|
57
|
-
if conf.GISSERVER_WFS_STRICT_STANDARD:
|
|
58
|
-
raise ExternalValueError("Expected typename.id format") from None
|
|
63
|
+
if conf.GISSERVER_WFS_STRICT_STANDARD and "." not in self.rid:
|
|
64
|
+
raise ExternalValueError("Expected typename.id format") from None
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_string(cls, rid, ns_aliases: dict[str, str]):
|
|
68
|
+
# Like GeoServer, assume the "name" part of the "resource id" is a QName.
|
|
69
|
+
return cls(
|
|
70
|
+
rid=rid,
|
|
71
|
+
type_name=parse_qname(rid.rpartition(".")[0], ns_aliases),
|
|
72
|
+
)
|
|
63
73
|
|
|
64
74
|
@classmethod
|
|
65
|
-
@expect_tag(
|
|
75
|
+
@expect_tag(xmlns.fes20, "ResourceId")
|
|
76
|
+
@expect_no_children
|
|
66
77
|
def from_xml(cls, element):
|
|
67
78
|
version = element.get("version")
|
|
68
79
|
startTime = element.get("startTime")
|
|
@@ -71,24 +82,31 @@ class ResourceId(Id):
|
|
|
71
82
|
if version:
|
|
72
83
|
version = auto_cast(version)
|
|
73
84
|
|
|
85
|
+
rid = element.get_str_attribute("rid")
|
|
74
86
|
return cls(
|
|
75
|
-
rid=
|
|
87
|
+
rid=rid,
|
|
88
|
+
type_name=element.parse_qname(rid.rpartition(".")[0]),
|
|
76
89
|
version=version,
|
|
77
90
|
startTime=parse_iso_datetime(startTime) if startTime else None,
|
|
78
91
|
endTime=parse_iso_datetime(endTime) if endTime else None,
|
|
79
92
|
)
|
|
80
93
|
|
|
81
|
-
def build_query(self, compiler
|
|
94
|
+
def build_query(self, compiler) -> Q:
|
|
82
95
|
"""Render the SQL filter"""
|
|
83
96
|
if self.startTime or self.endTime or self.version:
|
|
84
97
|
raise NotImplementedError(
|
|
85
98
|
"No support for <fes:ResourceId> startTime/endTime/version attributes"
|
|
86
99
|
)
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
compiler.
|
|
94
|
-
|
|
101
|
+
object_id = self.rid.rpartition(".")[2]
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# The 'ID' parameter is typed as string, but here we can check
|
|
105
|
+
# whether the database model needs an integer instead.
|
|
106
|
+
compiler.feature_types[0].model._meta.pk.get_prep_value(object_id)
|
|
107
|
+
except (TypeError, ValueError) as e:
|
|
108
|
+
raise InvalidParameterValue(
|
|
109
|
+
f"Invalid resourceId value: {e}", locator="resourceId"
|
|
110
|
+
) from e
|
|
111
|
+
|
|
112
|
+
return Q(pk=object_id)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Additional ORM lookups used by the fes-filter code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.contrib.gis.db.models.fields import BaseSpatialField
|
|
6
|
+
from django.contrib.gis.db.models.lookups import DWithinLookup
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.db.models import lookups
|
|
9
|
+
|
|
10
|
+
from gisserver.compat import ArrayField
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@models.CharField.register_lookup
|
|
14
|
+
@models.TextField.register_lookup
|
|
15
|
+
@models.ForeignObject.register_lookup
|
|
16
|
+
class FesLike(lookups.Lookup):
|
|
17
|
+
"""Allow fieldname__fes_like=... lookups in querysets."""
|
|
18
|
+
|
|
19
|
+
lookup_name = "fes_like"
|
|
20
|
+
|
|
21
|
+
def as_sql(self, compiler, connection):
|
|
22
|
+
"""Generate the required SQL."""
|
|
23
|
+
# lhs = "table"."field"
|
|
24
|
+
# rhs = %s
|
|
25
|
+
# lhs_params = []
|
|
26
|
+
# lhs_params = ["prep-value"]
|
|
27
|
+
lhs, lhs_params = self.process_lhs(compiler, connection)
|
|
28
|
+
rhs, rhs_params = self.process_rhs(compiler, connection)
|
|
29
|
+
return f"{lhs} LIKE {rhs}", lhs_params + rhs_params
|
|
30
|
+
|
|
31
|
+
def get_db_prep_lookup(self, value, connection):
|
|
32
|
+
"""This expects that the right-hand-side already has wildcard characters."""
|
|
33
|
+
return "%s", [value]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@models.Field.register_lookup
|
|
37
|
+
@models.ForeignObject.register_lookup
|
|
38
|
+
class FesNotEqual(lookups.Lookup):
|
|
39
|
+
"""Allow fieldname__fes_notequal=... lookups in querysets."""
|
|
40
|
+
|
|
41
|
+
lookup_name = "fes_notequal"
|
|
42
|
+
|
|
43
|
+
def as_sql(self, compiler, connection):
|
|
44
|
+
"""Generate the required SQL."""
|
|
45
|
+
lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
|
|
46
|
+
rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
|
|
47
|
+
return f"{lhs} != {rhs}", (lhs_params + rhs_params)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@BaseSpatialField.register_lookup
|
|
51
|
+
class FesBeyondLookup(DWithinLookup):
|
|
52
|
+
"""Based on the FES 2.0.3 corrigendum:
|
|
53
|
+
|
|
54
|
+
DWithin(A,B,d) = Distance(A,B) < d
|
|
55
|
+
Beyond(A,B,d) = Distance(A,B) > d
|
|
56
|
+
|
|
57
|
+
See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
lookup_name = "fes_beyond"
|
|
61
|
+
sql_template = "NOT %(func)s(%(lhs)s, %(rhs)s, %(value)s)"
|
|
62
|
+
|
|
63
|
+
def get_rhs_op(self, connection, rhs):
|
|
64
|
+
# Allow the SQL $(func)s to be different from the ORM lookup name.
|
|
65
|
+
# This uses ST_DWithin() on PostGIS
|
|
66
|
+
return connection.ops.gis_operators["dwithin"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if ArrayField is None:
|
|
70
|
+
ARRAY_LOOKUPS = {}
|
|
71
|
+
else:
|
|
72
|
+
# Comparisons with array fields go through a separate ORM lookup expression,
|
|
73
|
+
# so these can check whether ANY element matches in the array.
|
|
74
|
+
# This gives consistency between other repeated elements (e.g. M2M, reverse FK)
|
|
75
|
+
# where the whole object is returned when one of the sub-objects match.
|
|
76
|
+
ARRAY_LOOKUPS = {
|
|
77
|
+
"exact": "fes_anyexact",
|
|
78
|
+
"fes_notequal": "fes_anynotequal",
|
|
79
|
+
"fes_like": "fes_anylike",
|
|
80
|
+
"lt": "fes_anylt",
|
|
81
|
+
"lte": "fes_anylte",
|
|
82
|
+
"gt": "fes_anygt",
|
|
83
|
+
"gte": "fes_anygte",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class ArrayAnyMixin:
|
|
87
|
+
any_operators = {
|
|
88
|
+
"exact": "= ANY(%s)",
|
|
89
|
+
"ne": "!= ANY(%s)",
|
|
90
|
+
"gt": "< ANY(%s)",
|
|
91
|
+
"gte": "<= ANY(%s)",
|
|
92
|
+
"lt": "> ANY(%s)",
|
|
93
|
+
"lte": ">= ANY(%s)",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def as_sql(self, compiler, connection):
|
|
97
|
+
# For the ANY() comparison, the filter operands need to be reversed.
|
|
98
|
+
# So instead of "field < value", it becomes "value > ANY(field)
|
|
99
|
+
lhs_sql, lhs_params = self.process_lhs(compiler, connection)
|
|
100
|
+
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
|
|
101
|
+
lhs_sql = self.get_rhs_op(connection, lhs_sql)
|
|
102
|
+
return f"{rhs_sql} {lhs_sql}", (rhs_params + lhs_params)
|
|
103
|
+
|
|
104
|
+
def get_rhs_op(self, connection, rhs):
|
|
105
|
+
return self.any_operators[self.lookup_name] % rhs
|
|
106
|
+
|
|
107
|
+
def _register_any_lookup(base: type[lookups.BuiltinLookup]):
|
|
108
|
+
"""Register array lookups under a different name."""
|
|
109
|
+
cls = type(f"FesArrayAny{base.__name__}", (ArrayAnyMixin, base), {})
|
|
110
|
+
ArrayField.register_lookup(cls, lookup_name=f"fes_any{base.lookup_name}")
|
|
111
|
+
|
|
112
|
+
_register_any_lookup(lookups.Exact)
|
|
113
|
+
_register_any_lookup(lookups.Exact)
|
|
114
|
+
_register_any_lookup(lookups.GreaterThan)
|
|
115
|
+
_register_any_lookup(lookups.GreaterThanOrEqual)
|
|
116
|
+
_register_any_lookup(lookups.LessThan)
|
|
117
|
+
_register_any_lookup(lookups.LessThanOrEqual)
|
|
118
|
+
|
|
119
|
+
@ArrayField.register_lookup
|
|
120
|
+
class FesArrayAnyNotEqual(lookups.Lookup):
|
|
121
|
+
"""Inequality test for a single item in the array"""
|
|
122
|
+
|
|
123
|
+
lookup_name = "fes_anynotequal"
|
|
124
|
+
|
|
125
|
+
def as_sql(self, compiler, connection):
|
|
126
|
+
"""Generate the required SQL."""
|
|
127
|
+
lhs, lhs_params = self.process_lhs(compiler, connection)
|
|
128
|
+
rhs, rhs_params = self.process_rhs(compiler, connection)
|
|
129
|
+
return f"{rhs} != ANY({lhs})", (rhs_params + lhs_params)
|
|
130
|
+
|
|
131
|
+
@ArrayField.register_lookup
|
|
132
|
+
class FesArrayLike(FesLike):
|
|
133
|
+
"""Allow fieldname__fes_like=... lookups in querysets."""
|
|
134
|
+
|
|
135
|
+
lookup_name = "fes_anylike"
|
|
136
|
+
|
|
137
|
+
def as_sql(self, compiler, connection):
|
|
138
|
+
"""Generate the required SQL."""
|
|
139
|
+
lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
|
|
140
|
+
rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
|
|
141
|
+
return (
|
|
142
|
+
f"EXISTS(SELECT 1 FROM unnest({lhs}) AS item WHERE item LIKE {rhs})", # noqa: S608
|
|
143
|
+
(lhs_params + rhs_params),
|
|
144
|
+
)
|