django-gisserver 1.5.0__py3-none-any.whl → 2.1__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.1.dist-info}/METADATA +34 -8
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/crs.py +401 -0
- gisserver/db.py +126 -51
- gisserver/exceptions.py +132 -4
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
- gisserver/extensions/queries.py +266 -0
- gisserver/features.py +253 -181
- gisserver/geometries.py +64 -311
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +311 -0
- gisserver/operations/base.py +130 -312
- gisserver/operations/wfs20.py +399 -375
- gisserver/output/__init__.py +14 -49
- gisserver/output/base.py +198 -144
- gisserver/output/csv.py +78 -75
- gisserver/output/geojson.py +37 -37
- gisserver/output/gml32.py +287 -259
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +73 -61
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +81 -169
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +426 -0
- gisserver/parsers/fes20/__init__.py +89 -31
- gisserver/parsers/fes20/expressions.py +172 -58
- gisserver/parsers/fes20/filters.py +116 -45
- gisserver/parsers/fes20/identifiers.py +66 -28
- gisserver/parsers/fes20/lookups.py +146 -0
- gisserver/parsers/fes20/operators.py +417 -161
- gisserver/parsers/fes20/sorting.py +113 -34
- gisserver/parsers/gml/__init__.py +17 -25
- gisserver/parsers/gml/base.py +36 -15
- gisserver/parsers/gml/geometries.py +105 -44
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +198 -0
- gisserver/parsers/ows/requests.py +160 -0
- gisserver/parsers/query.py +179 -0
- gisserver/parsers/values.py +87 -4
- gisserver/parsers/wfs20/__init__.py +39 -0
- gisserver/parsers/wfs20/adhoc.py +253 -0
- gisserver/parsers/wfs20/base.py +148 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +483 -0
- gisserver/parsers/wfs20/stored.py +193 -0
- gisserver/parsers/xml.py +261 -0
- gisserver/projection.py +367 -0
- gisserver/static/gisserver/index.css +20 -4
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +3 -3
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +445 -313
- gisserver/views.py +227 -62
- 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.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""These classes map to the FES 2.0 specification for identifiers.
|
|
2
2
|
The class names are identical to those in the FES spec.
|
|
3
|
+
|
|
4
|
+
Inheritance structure:
|
|
5
|
+
|
|
6
|
+
* :class:`Id`
|
|
7
|
+
|
|
8
|
+
* :class:`ResourceId`
|
|
3
9
|
"""
|
|
4
10
|
|
|
5
11
|
from __future__ import annotations
|
|
@@ -11,16 +17,17 @@ from enum import Enum
|
|
|
11
17
|
from django.db.models import Q
|
|
12
18
|
|
|
13
19
|
from gisserver import conf
|
|
14
|
-
from gisserver.exceptions import ExternalValueError
|
|
15
|
-
from gisserver.parsers.
|
|
16
|
-
from gisserver.parsers.tags import expect_tag, get_attribute
|
|
20
|
+
from gisserver.exceptions import ExternalValueError, InvalidParameterValue
|
|
21
|
+
from gisserver.parsers.ast import AstNode, expect_no_children, expect_tag, tag_registry
|
|
17
22
|
from gisserver.parsers.values import auto_cast, parse_iso_datetime
|
|
18
|
-
from gisserver.
|
|
23
|
+
from gisserver.parsers.xml import parse_qname, xmlns
|
|
19
24
|
|
|
20
25
|
NoneType = type(None)
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
class VersionActionTokens(Enum):
|
|
29
|
+
"""Values for the 'version' attribute of the :class:`ResourceId` node."""
|
|
30
|
+
|
|
24
31
|
FIRST = "FIRST"
|
|
25
32
|
LAST = "LAST"
|
|
26
33
|
ALL = "ALL"
|
|
@@ -28,13 +35,16 @@ class VersionActionTokens(Enum):
|
|
|
28
35
|
PREVIOUS = "PREVIOUS"
|
|
29
36
|
|
|
30
37
|
|
|
31
|
-
class Id(
|
|
32
|
-
"""Abstract base class, as defined by FES spec.
|
|
38
|
+
class Id(AstNode):
|
|
39
|
+
"""Abstract base class, as defined by FES spec.
|
|
40
|
+
Any custom identifier-element needs to extend from this node.
|
|
41
|
+
By default, the :class:`ResourceId` element is supported.
|
|
42
|
+
"""
|
|
33
43
|
|
|
34
|
-
xml_ns =
|
|
44
|
+
xml_ns = xmlns.fes20
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
def get_type_name(self):
|
|
47
|
+
raise NotImplementedError()
|
|
38
48
|
|
|
39
49
|
def build_query(self, compiler) -> Q:
|
|
40
50
|
raise NotImplementedError()
|
|
@@ -43,26 +53,47 @@ class Id(BaseNode):
|
|
|
43
53
|
@dataclass
|
|
44
54
|
@tag_registry.register("ResourceId")
|
|
45
55
|
class ResourceId(Id):
|
|
46
|
-
"""The
|
|
56
|
+
"""The ``<fes:ResourceId>`` element.
|
|
57
|
+
This element allow queries to retrieve a resource by their identifier.
|
|
47
58
|
|
|
59
|
+
This parses the syntax::
|
|
60
|
+
|
|
61
|
+
<fes:ResourceId rid="typename.123" />
|
|
62
|
+
|
|
63
|
+
This element is placed inside a :class:`~gisserver.parsers.fes20.filters.Filter`.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
#: A raw "resource identifier". It typically includes the object name,
|
|
67
|
+
#: which is completely unrelated to XML namespacing.
|
|
48
68
|
rid: str
|
|
69
|
+
|
|
70
|
+
#: Internal extra attribute, referencing the inferred typename from the :attr:`rid`.
|
|
71
|
+
type_name: str | None
|
|
72
|
+
|
|
73
|
+
#: Unused, this is part of additional conformance classes.
|
|
49
74
|
version: int | datetime | VersionActionTokens | NoneType = None
|
|
50
75
|
startTime: datetime | None = None
|
|
51
76
|
endTime: datetime | None = None
|
|
52
77
|
|
|
78
|
+
def get_type_name(self):
|
|
79
|
+
"""Implemented/override to expose the inferred type name."""
|
|
80
|
+
return self.type_name
|
|
81
|
+
|
|
53
82
|
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
|
|
83
|
+
if conf.GISSERVER_WFS_STRICT_STANDARD and "." not in self.rid:
|
|
84
|
+
raise ExternalValueError("Expected typename.id format") from None
|
|
59
85
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_string(cls, rid, ns_aliases: dict[str, str]):
|
|
88
|
+
# Like GeoServer, assume the "name" part of the "resource id" is a QName.
|
|
89
|
+
return cls(
|
|
90
|
+
rid=rid,
|
|
91
|
+
type_name=parse_qname(rid.rpartition(".")[0], ns_aliases),
|
|
92
|
+
)
|
|
63
93
|
|
|
64
94
|
@classmethod
|
|
65
|
-
@expect_tag(
|
|
95
|
+
@expect_tag(xmlns.fes20, "ResourceId")
|
|
96
|
+
@expect_no_children
|
|
66
97
|
def from_xml(cls, element):
|
|
67
98
|
version = element.get("version")
|
|
68
99
|
startTime = element.get("startTime")
|
|
@@ -71,24 +102,31 @@ class ResourceId(Id):
|
|
|
71
102
|
if version:
|
|
72
103
|
version = auto_cast(version)
|
|
73
104
|
|
|
105
|
+
rid = element.get_str_attribute("rid")
|
|
74
106
|
return cls(
|
|
75
|
-
rid=
|
|
107
|
+
rid=rid,
|
|
108
|
+
type_name=element.parse_qname(rid.rpartition(".")[0]),
|
|
76
109
|
version=version,
|
|
77
110
|
startTime=parse_iso_datetime(startTime) if startTime else None,
|
|
78
111
|
endTime=parse_iso_datetime(endTime) if endTime else None,
|
|
79
112
|
)
|
|
80
113
|
|
|
81
|
-
def build_query(self, compiler
|
|
114
|
+
def build_query(self, compiler) -> Q:
|
|
82
115
|
"""Render the SQL filter"""
|
|
83
116
|
if self.startTime or self.endTime or self.version:
|
|
84
117
|
raise NotImplementedError(
|
|
85
118
|
"No support for <fes:ResourceId> startTime/endTime/version attributes"
|
|
86
119
|
)
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
compiler.
|
|
94
|
-
|
|
121
|
+
object_id = self.rid.rpartition(".")[2]
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# The 'ID' parameter is typed as string, but here we can check
|
|
125
|
+
# whether the database model needs an integer instead.
|
|
126
|
+
compiler.feature_types[0].model._meta.pk.get_prep_value(object_id)
|
|
127
|
+
except (TypeError, ValueError) as e:
|
|
128
|
+
raise InvalidParameterValue(
|
|
129
|
+
f"Invalid resourceId value: {e}", locator="resourceId"
|
|
130
|
+
) from e
|
|
131
|
+
|
|
132
|
+
return Q(pk=object_id)
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
"""Allow ``fieldname__fes_beyond=...`` lookups in querysets.
|
|
53
|
+
|
|
54
|
+
Based on the FES 2.0.3 corrigendum:
|
|
55
|
+
|
|
56
|
+
* ``DWithin(A,B,d) = Distance(A,B) < d``
|
|
57
|
+
* ``Beyond(A,B,d) = Distance(A,B) > d``
|
|
58
|
+
|
|
59
|
+
See: https://docs.opengeospatial.org/is/09-026r2/09-026r2.html#61
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
lookup_name = "fes_beyond"
|
|
63
|
+
sql_template = "NOT %(func)s(%(lhs)s, %(rhs)s, %(value)s)"
|
|
64
|
+
|
|
65
|
+
def get_rhs_op(self, connection, rhs):
|
|
66
|
+
# Allow the SQL $(func)s to be different from the ORM lookup name.
|
|
67
|
+
# This uses ST_DWithin() on PostGIS
|
|
68
|
+
return connection.ops.gis_operators["dwithin"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if ArrayField is None:
|
|
72
|
+
ARRAY_LOOKUPS = {}
|
|
73
|
+
else:
|
|
74
|
+
# Comparisons with array fields go through a separate ORM lookup expression,
|
|
75
|
+
# so these can check whether ANY element matches in the array.
|
|
76
|
+
# This gives consistency between other repeated elements (e.g. M2M, reverse FK)
|
|
77
|
+
# where the whole object is returned when one of the sub-objects match.
|
|
78
|
+
ARRAY_LOOKUPS = {
|
|
79
|
+
"exact": "fes_anyexact",
|
|
80
|
+
"fes_notequal": "fes_anynotequal",
|
|
81
|
+
"fes_like": "fes_anylike",
|
|
82
|
+
"lt": "fes_anylt",
|
|
83
|
+
"lte": "fes_anylte",
|
|
84
|
+
"gt": "fes_anygt",
|
|
85
|
+
"gte": "fes_anygte",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class ArrayAnyMixin:
|
|
89
|
+
any_operators = {
|
|
90
|
+
"exact": "= ANY(%s)",
|
|
91
|
+
"ne": "!= ANY(%s)",
|
|
92
|
+
"gt": "< ANY(%s)",
|
|
93
|
+
"gte": "<= ANY(%s)",
|
|
94
|
+
"lt": "> ANY(%s)",
|
|
95
|
+
"lte": ">= ANY(%s)",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def as_sql(self, compiler, connection):
|
|
99
|
+
# For the ANY() comparison, the filter operands need to be reversed.
|
|
100
|
+
# So instead of "field < value", it becomes "value > ANY(field)
|
|
101
|
+
lhs_sql, lhs_params = self.process_lhs(compiler, connection)
|
|
102
|
+
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
|
|
103
|
+
lhs_sql = self.get_rhs_op(connection, lhs_sql)
|
|
104
|
+
return f"{rhs_sql} {lhs_sql}", (rhs_params + lhs_params)
|
|
105
|
+
|
|
106
|
+
def get_rhs_op(self, connection, rhs):
|
|
107
|
+
return self.any_operators[self.lookup_name] % rhs
|
|
108
|
+
|
|
109
|
+
def _register_any_lookup(base: type[lookups.BuiltinLookup]):
|
|
110
|
+
"""Register array lookups under a different name."""
|
|
111
|
+
cls = type(f"FesArrayAny{base.__name__}", (ArrayAnyMixin, base), {})
|
|
112
|
+
ArrayField.register_lookup(cls, lookup_name=f"fes_any{base.lookup_name}")
|
|
113
|
+
|
|
114
|
+
_register_any_lookup(lookups.Exact)
|
|
115
|
+
_register_any_lookup(lookups.Exact)
|
|
116
|
+
_register_any_lookup(lookups.GreaterThan)
|
|
117
|
+
_register_any_lookup(lookups.GreaterThanOrEqual)
|
|
118
|
+
_register_any_lookup(lookups.LessThan)
|
|
119
|
+
_register_any_lookup(lookups.LessThanOrEqual)
|
|
120
|
+
|
|
121
|
+
@ArrayField.register_lookup
|
|
122
|
+
class FesArrayAnyNotEqual(lookups.Lookup):
|
|
123
|
+
"""Inequality test for a single item in the array"""
|
|
124
|
+
|
|
125
|
+
lookup_name = "fes_anynotequal"
|
|
126
|
+
|
|
127
|
+
def as_sql(self, compiler, connection):
|
|
128
|
+
"""Generate the required SQL."""
|
|
129
|
+
lhs, lhs_params = self.process_lhs(compiler, connection)
|
|
130
|
+
rhs, rhs_params = self.process_rhs(compiler, connection)
|
|
131
|
+
return f"{rhs} != ANY({lhs})", (rhs_params + lhs_params)
|
|
132
|
+
|
|
133
|
+
@ArrayField.register_lookup
|
|
134
|
+
class FesArrayLike(FesLike):
|
|
135
|
+
"""Allow like lookups for array fields."""
|
|
136
|
+
|
|
137
|
+
lookup_name = "fes_anylike"
|
|
138
|
+
|
|
139
|
+
def as_sql(self, compiler, connection):
|
|
140
|
+
"""Generate the required SQL."""
|
|
141
|
+
lhs, lhs_params = self.process_lhs(compiler, connection) # = (table.field, %s)
|
|
142
|
+
rhs, rhs_params = self.process_rhs(compiler, connection) # = ("prep-value", [])
|
|
143
|
+
return (
|
|
144
|
+
f"EXISTS(SELECT 1 FROM unnest({lhs}) AS item WHERE item LIKE {rhs})", # noqa: S608
|
|
145
|
+
(lhs_params + rhs_params),
|
|
146
|
+
)
|