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,37 +1,111 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import AnyStr, Union
4
- from xml.etree.ElementTree import Element, QName
3
+ from dataclasses import dataclass, field
4
+ from typing import AnyStr, ClassVar, Union
5
5
 
6
- from defusedxml.ElementTree import ParseError, fromstring
6
+ from django.db.models import Q
7
7
 
8
- from gisserver.exceptions import ExternalParsingError
9
- from gisserver.parsers.base import tag_registry
10
- from gisserver.parsers.tags import expect_tag
11
- from gisserver.types import FES20, GML32
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, query
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
- class Filter:
22
+
23
+ @dataclass
24
+ @tag_registry.register("Filter", xmlns.fes20)
25
+ class Filter(BaseNode):
19
26
  """The <fes:Filter> element.
20
27
 
21
- As this is a wrapper, it only contains a "predicate" element with the contents.
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
- def __init__(self, predicate: FilterPredicates, source: AnyStr | None = None):
30
- self.predicate = predicate
31
- self.source = source
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="{FES20}" xmlns:gml="{GML32}"{text[end_first:]}'
124
+ text = f'{first_tag} xmlns="{xmlns.fes20}" xmlns:gml="{xmlns.gml32}"{text[end_first:]}'
51
125
 
52
- try:
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(FES20, "Filter")
61
- def from_xml(cls, element: Element, source: AnyStr | None = None) -> Filter:
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 == QName(FES20, "ResourceId"):
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.from_child_xml(child) for child in element]
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
- predicate=tag_registry.from_child_xml(
74
- element[0], allowed_types=(expressions.Function, operators.Operator)
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 compile_query(self, feature_type=None, using=None) -> query.CompiledQuery:
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
- q_object = self.predicate.build_query(compiler)
85
- if q_object is not None:
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 __repr__(self):
91
- return f"Filter(predicate={self.predicate!r}, source={self.source})"
92
-
93
- def __eq__(self, other):
94
- if isinstance(other, Filter):
95
- return self.predicate == other.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 NotImplemented
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.base import BaseNode, tag_registry
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.types import FES20
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 = FES20
35
+ xml_ns = xmlns.fes20
35
36
 
36
- #: Tell which the type this ID belongs to, needs to be overwritten.
37
- type_name = ... # need to be defined by subclass!
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
- try:
55
- self.type_name, self.id = self.rid.rsplit(".", 1)
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
- # This should end in a 404 instead.
61
- self.type_name = None
62
- self.id = None
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(FES20, "ResourceId", leaf=True)
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=get_attribute(element, "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=None) -> Q:
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
- lookup = Q(pk=self.id or self.rid)
89
- if compiler is not None:
90
- # When the
91
- # NOTE: type_name is currently read by the IdOperator that contains this object,
92
- # This code path only happens for stand-alone KVP invocation.
93
- compiler.add_lookups(lookup, type_name=self.type_name)
94
- return lookup
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
+ )