django-gisserver 2.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-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/crs.py +401 -0
- gisserver/db.py +71 -5
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +44 -36
- gisserver/geometries.py +64 -306
- gisserver/management/commands/loadgeojson.py +41 -21
- gisserver/operations/base.py +11 -7
- gisserver/operations/wfs20.py +31 -93
- gisserver/output/__init__.py +6 -2
- gisserver/output/base.py +28 -13
- gisserver/output/csv.py +18 -6
- gisserver/output/geojson.py +7 -6
- gisserver/output/gml32.py +43 -23
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +171 -65
- gisserver/parsers/fes20/__init__.py +76 -4
- gisserver/parsers/fes20/expressions.py +97 -27
- gisserver/parsers/fes20/filters.py +9 -6
- gisserver/parsers/fes20/identifiers.py +27 -7
- gisserver/parsers/fes20/lookups.py +8 -6
- gisserver/parsers/fes20/operators.py +101 -49
- gisserver/parsers/fes20/sorting.py +14 -6
- gisserver/parsers/gml/__init__.py +10 -19
- gisserver/parsers/gml/base.py +32 -14
- gisserver/parsers/gml/geometries.py +48 -21
- gisserver/parsers/ows/kvp.py +10 -2
- gisserver/parsers/ows/requests.py +6 -4
- gisserver/parsers/query.py +6 -2
- gisserver/parsers/values.py +61 -4
- gisserver/parsers/wfs20/__init__.py +2 -0
- gisserver/parsers/wfs20/adhoc.py +25 -17
- gisserver/parsers/wfs20/base.py +12 -7
- gisserver/parsers/wfs20/projection.py +3 -3
- gisserver/parsers/wfs20/requests.py +1 -0
- gisserver/parsers/wfs20/stored.py +3 -2
- gisserver/parsers/xml.py +12 -0
- gisserver/projection.py +17 -7
- gisserver/static/gisserver/index.css +8 -3
- 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/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/types.py +150 -81
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
gisserver/parsers/ows/kvp.py
CHANGED
|
@@ -105,7 +105,9 @@ class KVPRequest:
|
|
|
105
105
|
def split_parameter_lists(self) -> list[KVPRequest]:
|
|
106
106
|
"""Split the parameter lists into individual requests.
|
|
107
107
|
|
|
108
|
-
This translates a request such as
|
|
108
|
+
This translates a request such as:
|
|
109
|
+
|
|
110
|
+
.. code-block:: urlencoded
|
|
109
111
|
|
|
110
112
|
TYPENAMES=(ns1:F1,ns2:F2)(ns1:F1,ns1:F1)
|
|
111
113
|
&ALIASES=(A,B)(C,D)
|
|
@@ -113,15 +115,21 @@ class KVPRequest:
|
|
|
113
115
|
|
|
114
116
|
into separate pairs:
|
|
115
117
|
|
|
118
|
+
.. code-block:: urlencoded
|
|
119
|
+
|
|
116
120
|
TYPENAMES=ns1:F1,ns2:F2&ALIASES=A,B&FILTER=<Filter>…for A,B…</Filter>
|
|
117
121
|
TYPENAMES=ns1:F1,ns1:F1&ALIASES=C,D&FILTER=<Filter>…for C,D…</Filter>
|
|
118
122
|
|
|
119
123
|
It's both possible have some query parameters split and some shared.
|
|
120
124
|
For example to have two different bounding boxes:
|
|
121
125
|
|
|
126
|
+
.. code-block:: urlencoded
|
|
127
|
+
|
|
122
128
|
TYPENAMES=(INWATER_1M)(BuiltUpA_1M)&BBOX=(40.9821,...)(40.5874,...)
|
|
123
129
|
|
|
124
|
-
or have a single bounding box for both queries
|
|
130
|
+
or have a single bounding box for both queries:
|
|
131
|
+
|
|
132
|
+
.. code-block:: urlencoded
|
|
125
133
|
|
|
126
134
|
TYPENAMES=(INWATER_1M)(BuiltUpA_1M)&BBOX=40.9821,23.4948,41.0257,23.5525
|
|
127
135
|
"""
|
|
@@ -13,7 +13,7 @@ from gisserver.exceptions import (
|
|
|
13
13
|
OperationNotSupported,
|
|
14
14
|
OperationParsingFailed,
|
|
15
15
|
)
|
|
16
|
-
from gisserver.parsers.ast import
|
|
16
|
+
from gisserver.parsers.ast import AstNode, tag_registry
|
|
17
17
|
from gisserver.parsers.xml import NSElement, parse_xml_from_string, split_ns, xmlns
|
|
18
18
|
|
|
19
19
|
from .kvp import KVPRequest
|
|
@@ -28,7 +28,7 @@ __all__ = (
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@dataclass
|
|
31
|
-
class BaseOwsRequest(
|
|
31
|
+
class BaseOwsRequest(AstNode):
|
|
32
32
|
"""Base request data for all request types of the OWS standards.
|
|
33
33
|
This mirrors the ``<wfs:BaseRequestType>`` element from the WFS spec.
|
|
34
34
|
"""
|
|
@@ -80,7 +80,7 @@ class BaseOwsRequest(BaseNode):
|
|
|
80
80
|
return {
|
|
81
81
|
"SERVICE": self.service,
|
|
82
82
|
"VERSION": str(self.version),
|
|
83
|
-
"REQUEST": split_ns(self.
|
|
83
|
+
"REQUEST": split_ns(self.xml_name)[1],
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
|
|
@@ -118,7 +118,9 @@ def resolve_kvp_parser_class(kvp: KVPRequest) -> type[BaseOwsRequest]:
|
|
|
118
118
|
try:
|
|
119
119
|
request_cls = request_classes[request.upper()]
|
|
120
120
|
except KeyError:
|
|
121
|
-
allowed = ", ".join(
|
|
121
|
+
allowed = ", ".join(
|
|
122
|
+
xml_tag for node in request_classes.values() for xml_tag in node.get_tag_names()
|
|
123
|
+
)
|
|
122
124
|
raise OperationNotSupported(
|
|
123
125
|
f"'{request}' is not implemented, supported are: {allowed}.",
|
|
124
126
|
locator="request",
|
gisserver/parsers/query.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
import operator
|
|
7
7
|
from datetime import date, datetime
|
|
8
|
+
from decimal import Decimal as D
|
|
8
9
|
from functools import reduce
|
|
9
10
|
from typing import Union
|
|
10
11
|
|
|
@@ -16,14 +17,15 @@ from django.db.models.expressions import Combinable, Func
|
|
|
16
17
|
from gisserver.features import FeatureType
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
+
ScalarTypes = (bool, int, str, D, date, datetime) # Not Union for Python 3.9
|
|
21
|
+
RhsTypes = Union[Combinable, Func, Q, GEOSGeometry, bool, int, D, str, date, datetime, tuple]
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class CompiledQuery:
|
|
23
25
|
"""Intermediate data for translating FES queries to Django.
|
|
24
26
|
|
|
25
27
|
This class effectively contains all data from the ``<fes:Filter>`` object,
|
|
26
|
-
but using a format that can be translated to a
|
|
28
|
+
but using a format that can be translated to a Django QuerySet.
|
|
27
29
|
|
|
28
30
|
As the Abstract Syntax Tree of a FES-filter creates the ORM query,
|
|
29
31
|
it fills this object with all intermediate bits. This allows building
|
|
@@ -64,6 +66,7 @@ class CompiledQuery:
|
|
|
64
66
|
return name
|
|
65
67
|
|
|
66
68
|
def add_distinct(self):
|
|
69
|
+
"""Enforce "SELECT DISTINCT" on the query, used when joining 1-N or N-M relationships."""
|
|
67
70
|
self.distinct = True
|
|
68
71
|
|
|
69
72
|
def add_lookups(self, q_object: Q, type_name: str | None = None):
|
|
@@ -87,6 +90,7 @@ class CompiledQuery:
|
|
|
87
90
|
"""
|
|
88
91
|
if not isinstance(q_object, Q):
|
|
89
92
|
raise TypeError()
|
|
93
|
+
# Note the ORM also provides a FilteredRelation() option, that is not explored yet here.
|
|
90
94
|
self.extra_lookups.append(q_object)
|
|
91
95
|
|
|
92
96
|
def add_ordering(self, ordering: list[str]):
|
gisserver/parsers/values.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
"""Parsing of scalar values in the request."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import re
|
|
3
|
-
from datetime import datetime
|
|
5
|
+
from datetime import date, datetime, time
|
|
4
6
|
from decimal import Decimal as D
|
|
5
7
|
|
|
6
|
-
from django.utils.dateparse import parse_datetime
|
|
8
|
+
from django.utils.dateparse import parse_date, parse_datetime, parse_duration, parse_time
|
|
7
9
|
|
|
8
10
|
from gisserver.exceptions import ExternalParsingError
|
|
9
11
|
|
|
@@ -12,7 +14,15 @@ RE_FLOAT = re.compile(r"\A[0-9]+(\.[0-9]+)\Z")
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def auto_cast(value: str):
|
|
15
|
-
"""Automatically cast a value to a scalar.
|
|
17
|
+
"""Automatically cast a value to a scalar.
|
|
18
|
+
|
|
19
|
+
This recognizes integers, floats and ISO datetimes.
|
|
20
|
+
Booleans are not handled, as that leads to unpredictable behavior
|
|
21
|
+
(as seen in Yaml for example).
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(value, str):
|
|
24
|
+
return value
|
|
25
|
+
|
|
16
26
|
if value.isdigit():
|
|
17
27
|
return int(value)
|
|
18
28
|
elif RE_FLOAT.match(value):
|
|
@@ -26,14 +36,61 @@ def auto_cast(value: str):
|
|
|
26
36
|
return value
|
|
27
37
|
|
|
28
38
|
|
|
39
|
+
def parse_iso_date(raw_value: str) -> date:
|
|
40
|
+
"""Translate ISO date into a Python date value."""
|
|
41
|
+
try:
|
|
42
|
+
value = parse_date(raw_value)
|
|
43
|
+
except ValueError as e:
|
|
44
|
+
raise ExternalParsingError(str(e)) from e
|
|
45
|
+
|
|
46
|
+
if value is None:
|
|
47
|
+
raise ExternalParsingError("Date must be in YYYY-MM-DD format.")
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
def parse_iso_datetime(raw_value: str) -> datetime:
|
|
30
|
-
|
|
52
|
+
"""Translate ISO datetimes into a Python datetime value."""
|
|
53
|
+
try:
|
|
54
|
+
value = parse_datetime(raw_value)
|
|
55
|
+
except ValueError as e:
|
|
56
|
+
raise ExternalParsingError(str(e)) from e
|
|
57
|
+
|
|
31
58
|
if value is None:
|
|
32
59
|
raise ExternalParsingError("Date must be in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format.")
|
|
33
60
|
return value
|
|
34
61
|
|
|
35
62
|
|
|
63
|
+
def parse_iso_time(raw_value: str) -> time:
|
|
64
|
+
"""Translate ISO times into a Python time value."""
|
|
65
|
+
try:
|
|
66
|
+
value = parse_time(raw_value)
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
raise ExternalParsingError(str(e)) from e
|
|
69
|
+
|
|
70
|
+
if value is None:
|
|
71
|
+
raise ExternalParsingError("Time must be in HH:MM[:ss[.uuuuuu]][TZ] format.")
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_iso_duration(raw_value: str) -> time:
|
|
76
|
+
"""Translate ISO times into a Python time value."""
|
|
77
|
+
# The parse_duration() supports multiple formats, including ISO8601.
|
|
78
|
+
# Limit it to only one format.
|
|
79
|
+
if not raw_value.startswith(("P", "-P")):
|
|
80
|
+
raise ExternalParsingError("Duration must be in ISO8601 format (e.g. PT1H).")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
value = parse_duration(raw_value)
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
raise ExternalParsingError(str(e)) from e
|
|
86
|
+
|
|
87
|
+
if value is None:
|
|
88
|
+
raise ExternalParsingError("Duration must be in ISO8601 format (e.g. PT1H).")
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
36
92
|
def parse_bool(raw_value: str):
|
|
93
|
+
"""Translate XML notations of true/1 and false/0 into a boolean."""
|
|
37
94
|
if raw_value in ("true", "1"):
|
|
38
95
|
return True
|
|
39
96
|
elif raw_value in ("false", "0"):
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""WFS 2.0 element parsing.
|
|
2
2
|
|
|
3
|
+
These classes parse the XML request body.
|
|
4
|
+
|
|
3
5
|
The full spec can be found at: https://www.ogc.org/publications/standard/wfs/.
|
|
4
6
|
Secondly, using https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/wfs_xsd.html can be very
|
|
5
7
|
helpful to see which options each object type should support.
|
gisserver/parsers/wfs20/adhoc.py
CHANGED
|
@@ -14,12 +14,12 @@ from functools import cached_property
|
|
|
14
14
|
|
|
15
15
|
from django.db.models import Q
|
|
16
16
|
|
|
17
|
+
from gisserver.crs import CRS
|
|
17
18
|
from gisserver.exceptions import (
|
|
18
19
|
InvalidParameterValue,
|
|
19
20
|
MissingParameterValue,
|
|
20
21
|
OperationNotSupported,
|
|
21
22
|
)
|
|
22
|
-
from gisserver.geometries import CRS
|
|
23
23
|
from gisserver.parsers import fes20
|
|
24
24
|
from gisserver.parsers.ast import tag_registry
|
|
25
25
|
from gisserver.parsers.ows import KVPRequest
|
|
@@ -59,7 +59,7 @@ class AdhocQuery(QueryExpression):
|
|
|
59
59
|
|
|
60
60
|
The WFS Spec has 3 class levels for this:
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
- AdhocQueryExpression (types, projection, selection, sorting)
|
|
63
63
|
- Query (adds srsName, featureVersion)
|
|
64
64
|
- StoredQuery (adds storedQueryID)
|
|
65
65
|
|
|
@@ -71,25 +71,31 @@ class AdhocQuery(QueryExpression):
|
|
|
71
71
|
https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/query_xsd.html#AbstractAdhocQueryExpressionType
|
|
72
72
|
"""
|
|
73
73
|
|
|
74
|
-
# Tag attributes (fes:AbstractAdhocQueryExpression)
|
|
74
|
+
# Tag attributes (implements ``fes:AbstractAdhocQueryExpression``)
|
|
75
75
|
# WFS allows multiple names to construct JOIN queries.
|
|
76
76
|
# See https://docs.ogc.org/is/09-025r2/09-025r2.html#107
|
|
77
77
|
# and https://docs.ogc.org/is/09-025r2/09-025r2.html#190
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
|
|
79
|
+
#: The 'typeNames' value if the request provided them. use :meth:`get_type_names` instead.
|
|
80
|
+
typeNames: list[str]
|
|
81
|
+
#: Aliases for typeNames are used for joining the same table twice. (JOIN statements are not supported yet).
|
|
82
|
+
aliases: list[str] | None = None
|
|
83
|
+
#: For XML POST requests, this handle value is returned in the ``<ows:Exception>``.
|
|
84
|
+
handle: str = ""
|
|
85
|
+
|
|
86
|
+
# part of the <wfs:Query> tag attributes:
|
|
87
|
+
#: The Coordinate Reference System to render the tag in
|
|
82
88
|
srsName: CRS | None = None
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
#: Projection clause (implements ``fes:AbstractProjectionClause``)
|
|
85
91
|
property_names: list[PropertyName] | None = None
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
#: Selection clause (implements ``fes:AbstractSelectionClause``).
|
|
94
|
+
#: - for XML POST this is encoded in a <fes:Filter> tag.
|
|
95
|
+
#: - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
|
|
90
96
|
filter: fes20.Filter | None = None
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
#: Sorting Clause (implements ``fes:AbstractSortingClause``)
|
|
93
99
|
sortBy: fes20.SortBy | None = None
|
|
94
100
|
|
|
95
101
|
def __post_init__(self):
|
|
@@ -188,7 +194,9 @@ class AdhocQuery(QueryExpression):
|
|
|
188
194
|
return "filter"
|
|
189
195
|
|
|
190
196
|
def get_type_names(self) -> list[str]:
|
|
191
|
-
"""Tell which type names this query uses.
|
|
197
|
+
"""Tell which type names this query uses.
|
|
198
|
+
Multiple values means a JOIN is made (not supported yet).
|
|
199
|
+
"""
|
|
192
200
|
if not self.typeNames and self.filter is not None:
|
|
193
201
|
# Also make the behavior consistent, always supply the type name.
|
|
194
202
|
return self.filter.get_resource_id_types() or []
|
|
@@ -196,7 +204,7 @@ class AdhocQuery(QueryExpression):
|
|
|
196
204
|
return self.typeNames
|
|
197
205
|
|
|
198
206
|
def get_projection(self) -> FeatureProjection:
|
|
199
|
-
"""Tell how the
|
|
207
|
+
"""Tell how the ``<wfs:Query>`` element should be displayed."""
|
|
200
208
|
return FeatureProjection(
|
|
201
209
|
self.feature_types,
|
|
202
210
|
self.property_names,
|
|
@@ -205,7 +213,7 @@ class AdhocQuery(QueryExpression):
|
|
|
205
213
|
)
|
|
206
214
|
|
|
207
215
|
def bind(self, *args, **kwargs):
|
|
208
|
-
"""
|
|
216
|
+
"""Override to make sure the 'locator' points to the actual object that defined the type."""
|
|
209
217
|
try:
|
|
210
218
|
super().bind(*args, **kwargs)
|
|
211
219
|
except InvalidParameterValue as e:
|
|
@@ -219,7 +227,7 @@ class AdhocQuery(QueryExpression):
|
|
|
219
227
|
|
|
220
228
|
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
221
229
|
"""Apply our collected filter data to the compiler."""
|
|
222
|
-
# Add the
|
|
230
|
+
# Add the sorting
|
|
223
231
|
if self.sortBy is not None:
|
|
224
232
|
self.sortBy.build_ordering(compiler)
|
|
225
233
|
|
|
@@ -230,7 +238,7 @@ class AdhocQuery(QueryExpression):
|
|
|
230
238
|
else:
|
|
231
239
|
return None
|
|
232
240
|
|
|
233
|
-
def as_kvp(self):
|
|
241
|
+
def as_kvp(self) -> dict:
|
|
234
242
|
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
235
243
|
params = super().as_kvp()
|
|
236
244
|
params["TYPENAMES"] = ",".join(self.typeNames)
|
gisserver/parsers/wfs20/base.py
CHANGED
|
@@ -23,12 +23,12 @@ from gisserver.exceptions import (
|
|
|
23
23
|
)
|
|
24
24
|
from gisserver.features import FeatureType
|
|
25
25
|
from gisserver.parsers import fes20, wfs20
|
|
26
|
-
from gisserver.parsers.ast import
|
|
26
|
+
from gisserver.parsers.ast import AstNode
|
|
27
27
|
from gisserver.parsers.query import CompiledQuery
|
|
28
28
|
from gisserver.projection import FeatureProjection
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class QueryExpression(
|
|
31
|
+
class QueryExpression(AstNode):
|
|
32
32
|
"""WFS base class for all queries.
|
|
33
33
|
This object type is defined in the WFS spec (as ``<fes:AbstractQueryExpression>``).
|
|
34
34
|
|
|
@@ -47,16 +47,20 @@ class QueryExpression(BaseNode):
|
|
|
47
47
|
* :meth:`get_queryset` defines the full results.
|
|
48
48
|
"""
|
|
49
49
|
|
|
50
|
+
#: Configuration for the 'locator' argument in exceptions
|
|
50
51
|
query_locator: ClassVar[str] = None
|
|
51
52
|
|
|
52
53
|
# QueryExpression
|
|
54
|
+
#: The 'handle' that will be returned in exceptions.
|
|
53
55
|
handle: str = ""
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
#: Projection parameters (overwritten by subclasses)
|
|
58
|
+
#: In the WFS spec, this is only part of the operation/presentation.
|
|
59
|
+
#: For Django, we'd like to make this part of the query too.
|
|
58
60
|
property_names: list[wfs20.PropertyName] | None = None # PropertyName
|
|
59
|
-
|
|
61
|
+
|
|
62
|
+
#: The valueReference for the GetPropertyValue call, provided here for extra ORM filtering.
|
|
63
|
+
value_reference: fes20.ValueReference | None = None
|
|
60
64
|
|
|
61
65
|
def bind(
|
|
62
66
|
self,
|
|
@@ -120,6 +124,7 @@ class QueryExpression(BaseNode):
|
|
|
120
124
|
|
|
121
125
|
def get_type_names(self) -> list[str]:
|
|
122
126
|
"""Tell which type names this query applies to.
|
|
127
|
+
Multiple values means a JOIN is made (not supported yet).
|
|
123
128
|
|
|
124
129
|
This method needs to be defined in subclasses.
|
|
125
130
|
"""
|
|
@@ -135,7 +140,7 @@ class QueryExpression(BaseNode):
|
|
|
135
140
|
"""Define the compiled query that filters the queryset."""
|
|
136
141
|
raise NotImplementedError()
|
|
137
142
|
|
|
138
|
-
def as_kvp(self):
|
|
143
|
+
def as_kvp(self) -> dict:
|
|
139
144
|
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
140
145
|
params = {}
|
|
141
146
|
if self.property_names:
|
|
@@ -10,14 +10,14 @@ from gisserver.exceptions import (
|
|
|
10
10
|
OperationParsingFailed,
|
|
11
11
|
OperationProcessingFailed,
|
|
12
12
|
)
|
|
13
|
-
from gisserver.parsers.ast import
|
|
13
|
+
from gisserver.parsers.ast import AstNode, tag_registry
|
|
14
14
|
from gisserver.parsers.query import CompiledQuery
|
|
15
15
|
from gisserver.parsers.xml import NSElement, xmlns
|
|
16
16
|
from gisserver.types import XPathMatch
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ResolveValue(Enum):
|
|
20
|
-
"""The wfs:ResolveValueType enum, used by :class:`StandardResolveParameters`."""
|
|
20
|
+
"""The ``wfs:ResolveValueType`` enum, used by :class:`StandardResolveParameters`."""
|
|
21
21
|
|
|
22
22
|
local = "local"
|
|
23
23
|
remote = "remote"
|
|
@@ -31,7 +31,7 @@ class ResolveValue(Enum):
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|
|
33
33
|
@tag_registry.register("PropertyName", xmlns.wfs)
|
|
34
|
-
class PropertyName(
|
|
34
|
+
class PropertyName(AstNode):
|
|
35
35
|
"""The ``<wfs:PropertyName>`` element in the projection clause.
|
|
36
36
|
|
|
37
37
|
This parses and handles the syntax::
|
|
@@ -45,6 +45,7 @@ WFS_STORED_QUERY = xmlns.wfs20.qname("StoredQuery")
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
47
47
|
@tag_registry.register("GetCapabilities", xmlns.wfs20)
|
|
48
|
+
@tag_registry.register("GetCapabilities", xmlns.wfs1, hidden=True) # to give negotiation error.
|
|
48
49
|
class GetCapabilities(BaseOwsRequest):
|
|
49
50
|
"""Request parsing for GetCapabilities.
|
|
50
51
|
|
|
@@ -60,7 +60,7 @@ class StoredQuery(QueryExpression):
|
|
|
60
60
|
Note that the base class logic (such as ``<wfs:PropertyName>`` elements) are still applicable.
|
|
61
61
|
|
|
62
62
|
This element resolves the stored query using the
|
|
63
|
-
:class:`~gisserver.
|
|
63
|
+
:class:`~gisserver.extensions.queries.StoredQueryRegistry`,
|
|
64
64
|
and passes the execution to this custom function.
|
|
65
65
|
"""
|
|
66
66
|
|
|
@@ -165,7 +165,8 @@ class StoredQuery(QueryExpression):
|
|
|
165
165
|
"""Forward queryset creation to the implementation class."""
|
|
166
166
|
return self.implementation.build_query(compiler)
|
|
167
167
|
|
|
168
|
-
def as_kvp(self):
|
|
168
|
+
def as_kvp(self) -> dict:
|
|
169
|
+
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
169
170
|
# As this is such edge case, only support the minimal for CITE tests.
|
|
170
171
|
params = super().as_kvp()
|
|
171
172
|
params["STOREDQUERY_ID"] = self.id
|
gisserver/parsers/xml.py
CHANGED
|
@@ -50,8 +50,10 @@ class xmlns(Enum):
|
|
|
50
50
|
ows20 = "http://www.opengis.net/ows/2.0"
|
|
51
51
|
wms = "http://www.opengis.net/wms" # Web Map Service (WMS)
|
|
52
52
|
wcs = "http://www.opengis.net/wcs" # Web Coverage Service (WCS)
|
|
53
|
+
wcs20 = "http://www.opengis.net/wcs/2.0"
|
|
53
54
|
wps = "http://www.opengis.net/wps/1.0.0" # Web Processing Service (WPS)
|
|
54
55
|
wmts = "http://www.opengis.net/wmts/1.0" # Web Map Tile Service (WMTS)
|
|
56
|
+
wfs1 = "http://www.opengis.net/wfs"
|
|
55
57
|
wfs20 = "http://www.opengis.net/wfs/2.0" # Web Feature Service (WFS)
|
|
56
58
|
fes20 = "http://www.opengis.net/fes/2.0" # Filter Encoding Standard (FES)
|
|
57
59
|
gml21 = "http://www.opengis.net/gml"
|
|
@@ -108,6 +110,16 @@ class NSElement(Element):
|
|
|
108
110
|
"""Resolve an aliased QName value to its fully qualified name."""
|
|
109
111
|
return parse_qname(qname, self.ns_aliases)
|
|
110
112
|
|
|
113
|
+
@property
|
|
114
|
+
def qname(self) -> str:
|
|
115
|
+
"""Provde the tag name in its original short format"""
|
|
116
|
+
ns, localname = split_ns(self.tag)
|
|
117
|
+
if ns:
|
|
118
|
+
for prefix, full_ns in self.ns_aliases.items():
|
|
119
|
+
if full_ns == ns:
|
|
120
|
+
return f"{prefix}:{localname}" if prefix else localname
|
|
121
|
+
return localname
|
|
122
|
+
|
|
111
123
|
def get_str_attribute(self, name: str) -> str:
|
|
112
124
|
"""Resolve an attribute, raise an error when it's missing."""
|
|
113
125
|
try:
|
gisserver/projection.py
CHANGED
|
@@ -29,8 +29,8 @@ if typing.TYPE_CHECKING:
|
|
|
29
29
|
from django.contrib.gis.geos import GEOSGeometry
|
|
30
30
|
from django.db import models
|
|
31
31
|
|
|
32
|
+
from gisserver.crs import CRS
|
|
32
33
|
from gisserver.features import FeatureType
|
|
33
|
-
from gisserver.geometries import CRS
|
|
34
34
|
from gisserver.parsers import fes20, wfs20
|
|
35
35
|
|
|
36
36
|
__all__ = (
|
|
@@ -47,14 +47,25 @@ class FeatureProjection:
|
|
|
47
47
|
|
|
48
48
|
Instead of walking over the full XSD object tree,
|
|
49
49
|
this object wraps that and makes sure only the actual requested fields are used.
|
|
50
|
-
When a PROPERTYNAME is used in the request,
|
|
51
|
-
which fields to retrieve, which to prefetch, and which to render.
|
|
50
|
+
When a ``PROPERTYNAME`` (or ``<wfs:PropertyName>``) is used in the request,
|
|
51
|
+
this will limit which fields to retrieve, which to prefetch, and which to render.
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
|
+
#: Referencing the Feature that is rendered.
|
|
54
55
|
feature_type: FeatureType
|
|
56
|
+
|
|
57
|
+
#: The list of root element to render for this feature.
|
|
55
58
|
xsd_root_elements: list[XsdElement]
|
|
59
|
+
|
|
60
|
+
#: The subset of child nodes to render for a given element.
|
|
56
61
|
xsd_child_nodes: dict[XsdElement | None, list[XsdElement]]
|
|
57
62
|
|
|
63
|
+
#: The output Coordinate Reference System
|
|
64
|
+
output_crs: CRS
|
|
65
|
+
|
|
66
|
+
#: Whether the output should be rendered without wrapper tags (for GetFeatureById).
|
|
67
|
+
output_standalone: bool
|
|
68
|
+
|
|
58
69
|
def __init__(
|
|
59
70
|
self,
|
|
60
71
|
feature_types: list[FeatureType],
|
|
@@ -69,7 +80,8 @@ class FeatureProjection:
|
|
|
69
80
|
:param property_names: Limited list of fields to render only.
|
|
70
81
|
:param value_reference: Single element to display fo GetPropertyValue
|
|
71
82
|
:param output_crs: Which coordinate reference system to use for geometry data.
|
|
72
|
-
:param output_standalone:
|
|
83
|
+
:param output_standalone: Used for the ``GetFeatureById`` stored query.
|
|
84
|
+
This removes the ``wfs:FeatureCollection><wfs:member>`` wrapper elements from the output.
|
|
73
85
|
"""
|
|
74
86
|
self.feature_types = feature_types
|
|
75
87
|
self.feature_type = feature_types[0] # JOIN still not supported.
|
|
@@ -302,9 +314,7 @@ class FeatureProjection:
|
|
|
302
314
|
|
|
303
315
|
@dataclass
|
|
304
316
|
class FeatureRelation:
|
|
305
|
-
"""Tell which related fields are queried by the feature.
|
|
306
|
-
Each dict holds an ORM-path, with the relevant sub-elements.
|
|
307
|
-
"""
|
|
317
|
+
"""Tell which related fields are queried by the feature."""
|
|
308
318
|
|
|
309
319
|
#: The ORM path that is queried for this particular relation
|
|
310
320
|
orm_path: str
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* general CSS that can be disabled by removing the .meta-page in the overwritten templates */
|
|
2
|
+
|
|
1
3
|
.meta-page {
|
|
2
4
|
font-family: Arial, sans-serif;
|
|
3
5
|
line-height: 1.5;
|
|
@@ -8,8 +10,11 @@
|
|
|
8
10
|
.meta-page h3 { margin: 1rem 0 0.5rem 0; }
|
|
9
11
|
.meta-page p { margin: 0 0 1rem 0; }
|
|
10
12
|
|
|
13
|
+
/* application-specific */
|
|
14
|
+
.connect-url { margin-left: 4rem; }
|
|
15
|
+
|
|
11
16
|
tbody th { text-align: left; padding-right: 2rem; }
|
|
12
17
|
tbody td { padding-right: 2rem; }
|
|
13
|
-
.complex-level-1 th { padding-left:
|
|
14
|
-
.complex-level-2 th { padding-left:
|
|
15
|
-
.complex-level-3 th { padding-left:
|
|
18
|
+
.table > tbody > tr.complex-level-1 > th { padding-left: 20px; }
|
|
19
|
+
.table > tbody > tr.complex-level-2 > th { padding-left: 40px; }
|
|
20
|
+
.table > tbody > tr.complex-level-3 > th { padding-left: 60px; }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>{% load i18n static %}{# This is a very basic template that can be overwritten in projects #}
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
|
5
|
+
<title>{% block title %}{{ service_description.title }} {{ accept_operations|dictsort:0|join:"/" }}{% endblock %}</title>
|
|
6
|
+
{% block link %}<link rel="stylesheet" type="text/css" href="{% static 'gisserver/index.css' %}">{% endblock %}
|
|
7
|
+
{% block extrahead %}{% endblock %}
|
|
8
|
+
</head>
|
|
9
|
+
<body class="{% block body-class %}meta-page{% endblock %}">
|
|
10
|
+
{% block content %}{% endblock %}
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{% block extrahead %}{% endblock %}
|
|
8
|
-
</head>
|
|
9
|
-
<body class="{% block body-class %}meta-page{% endblock %}">
|
|
10
|
-
<header>
|
|
1
|
+
|
|
2
|
+
{% extends "gisserver/base.html" %}{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{{ service_description.title }} {{ accept_operations|dictsort:0|join:"/" }}{% endblock %}
|
|
5
|
+
{% block content %}
|
|
6
|
+
<header id="wfs-header">
|
|
11
7
|
{% block header %}{% include "gisserver/service_description.html" %}{% endblock %}
|
|
12
8
|
</header>
|
|
13
9
|
|
|
14
|
-
<main>
|
|
10
|
+
<main id="wfs-main">
|
|
15
11
|
{% block main %}
|
|
16
12
|
{% if wfs_features %}
|
|
17
|
-
<h2>WFS Feature Types</h2>
|
|
13
|
+
<h2>{% translate "WFS Feature Types" %}</h2>
|
|
18
14
|
{% for feature_type in wfs_features %}
|
|
19
15
|
<article>{% include "gisserver/wfs/feature_type.html" %}</article>
|
|
20
16
|
{% endfor %}
|
|
21
17
|
{% endif %}
|
|
22
18
|
{% endblock %}
|
|
23
19
|
</main>
|
|
24
|
-
|
|
25
|
-
</body>
|
|
26
|
-
</html>
|
|
20
|
+
{% endblock %}
|
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
<h1>{{ service_description.title }} {{ accept_operations|dictsort:0|join:"/" }}</h1>
|
|
3
3
|
{% if service_description.abstract %}{{ service_description.abstract|linebreaks }}{% endif %}
|
|
4
4
|
|
|
5
|
+
{% if service_description.keywords or service_description.provider_name %}
|
|
6
|
+
<dl>
|
|
5
7
|
{% if service_description.keywords %}
|
|
6
|
-
<
|
|
8
|
+
<dt>{% translate "Keywords" %}:</dt><dd>{{ service_description.keywords|join:", " }}</dd>
|
|
7
9
|
{% endif %}
|
|
8
10
|
{% if service_description.provider_name %}
|
|
9
|
-
<
|
|
10
|
-
{% if service_description.
|
|
11
|
+
<dt>{% translate "Provider" %}:</dt>
|
|
12
|
+
<dd>{% if service_description.provider_site %}<a href="{{ service_description.provider_site }}">{% endif %}{{ service_description.provider_name }}{% if service_description.provider_site %}</a></dd>{% endif %}
|
|
13
|
+
{% if service_description.contact_person %}<dt>{% translate "Contact" %}:</dt><dd>{{ service_description.contact_person }}</dd>{% endif %}
|
|
11
14
|
{% endif %}
|
|
15
|
+
</dl>
|
|
16
|
+
{% endif %}
|
|
17
|
+
|
|
12
18
|
{% if connect_url %}
|
|
13
|
-
<h2>{%
|
|
14
|
-
<p>{%
|
|
15
|
-
<
|
|
19
|
+
<h2>{% translate "Using This WFS" %}</h2>
|
|
20
|
+
<p>{% translate "Add the following URL to your GIS application:" %}</p>
|
|
21
|
+
<p class="connect-url"><code>{{ connect_url }}</code></p>
|
|
16
22
|
{% endif %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{% load gisserver_tags %}<tr{% if level %} class="complex-field-member complex-level-{{ level }}"{% endif %}>
|
|
2
|
-
<th><code>{{ field.name }}</code></th>
|
|
2
|
+
<th><code>{% if level %}/{% endif %}{{ field.name }}</code></th>
|
|
3
3
|
<td>{{ field.xsd_element.type|to_qname:xml_namespaces }}{% if field.xsd_element.is_many %} <em>(maxOccurs={{ field.xsd_element.max_occurs }})</em>{% endif %}</td>
|
|
4
4
|
<td>{{ field.abstract|default:'' }}</td>
|
|
5
5
|
</tr>
|