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.
Files changed (55) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/crs.py +401 -0
  6. gisserver/db.py +71 -5
  7. gisserver/exceptions.py +106 -2
  8. gisserver/extensions/functions.py +122 -28
  9. gisserver/extensions/queries.py +15 -10
  10. gisserver/features.py +44 -36
  11. gisserver/geometries.py +64 -306
  12. gisserver/management/commands/loadgeojson.py +41 -21
  13. gisserver/operations/base.py +11 -7
  14. gisserver/operations/wfs20.py +31 -93
  15. gisserver/output/__init__.py +6 -2
  16. gisserver/output/base.py +28 -13
  17. gisserver/output/csv.py +18 -6
  18. gisserver/output/geojson.py +7 -6
  19. gisserver/output/gml32.py +43 -23
  20. gisserver/output/results.py +25 -39
  21. gisserver/output/utils.py +9 -2
  22. gisserver/parsers/ast.py +171 -65
  23. gisserver/parsers/fes20/__init__.py +76 -4
  24. gisserver/parsers/fes20/expressions.py +97 -27
  25. gisserver/parsers/fes20/filters.py +9 -6
  26. gisserver/parsers/fes20/identifiers.py +27 -7
  27. gisserver/parsers/fes20/lookups.py +8 -6
  28. gisserver/parsers/fes20/operators.py +101 -49
  29. gisserver/parsers/fes20/sorting.py +14 -6
  30. gisserver/parsers/gml/__init__.py +10 -19
  31. gisserver/parsers/gml/base.py +32 -14
  32. gisserver/parsers/gml/geometries.py +48 -21
  33. gisserver/parsers/ows/kvp.py +10 -2
  34. gisserver/parsers/ows/requests.py +6 -4
  35. gisserver/parsers/query.py +6 -2
  36. gisserver/parsers/values.py +61 -4
  37. gisserver/parsers/wfs20/__init__.py +2 -0
  38. gisserver/parsers/wfs20/adhoc.py +25 -17
  39. gisserver/parsers/wfs20/base.py +12 -7
  40. gisserver/parsers/wfs20/projection.py +3 -3
  41. gisserver/parsers/wfs20/requests.py +1 -0
  42. gisserver/parsers/wfs20/stored.py +3 -2
  43. gisserver/parsers/xml.py +12 -0
  44. gisserver/projection.py +17 -7
  45. gisserver/static/gisserver/index.css +8 -3
  46. gisserver/templates/gisserver/base.html +12 -0
  47. gisserver/templates/gisserver/index.html +9 -15
  48. gisserver/templates/gisserver/service_description.html +12 -6
  49. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  50. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  51. gisserver/types.py +150 -81
  52. gisserver/views.py +47 -24
  53. django_gisserver-2.0.dist-info/RECORD +0 -66
  54. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  55. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
@@ -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 BaseNode, tag_registry
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(BaseNode):
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.xml_tags[0])[1],
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(node.xml_tags[0] for node in request_classes.values())
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",
@@ -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
- RhsTypes = Union[Combinable, Func, Q, GEOSGeometry, bool, int, str, date, datetime, tuple]
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 django QuerySet.
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]):
@@ -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
- value = parse_datetime(raw_value)
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.
@@ -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
- - AdhocQueryExpression (types, projection, selection, sorting)
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
- typeNames: list[str] # typeNames in WFS/FES spec, multiple values means a JOIN is made.
79
- aliases: list[str] | None = None # aliases for typeNames for joining the same table twice.
80
- handle: str = "" # only for XML POST requests, is returned in ows:Exception
81
- # Query tag attributes
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
- # Projection clause (fes:AbstractProjectionClause)
90
+ #: Projection clause (implements ``fes:AbstractProjectionClause``)
85
91
  property_names: list[PropertyName] | None = None
86
92
 
87
- # Selection clause (fes:AbstractSelectionClause):
88
- # - for XML POST this is encoded in a <fes:Query>
89
- # - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
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
- # Sorting Clause (fes:AbstractSortingClause)
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 <wfs:Query> element should be displayed."""
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
- """Make sure the 'locator' points to the actual object that defined the type."""
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)
@@ -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 BaseNode
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(BaseNode):
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
- # Projection parameters (overwritten by subclasses)
56
- # In the WFS spec, this is only part of the operation/presentation.
57
- # For Django, we'd like to make this part of the query too.
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
- value_reference: fes20.ValueReference | None = None # GetPropertyValue call
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 BaseNode, tag_registry
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(BaseNode):
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.projection.storage.StoredQueryRegistry`,
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, this will limit
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: Whether the ``<wfs:
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: 16px; }
14
- .complex-level-2 th { padding-left: 32px; }
15
- .complex-level-3 th { padding-left: 48px; }
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
- <!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>{{ service_description.title }} {{ accept_operations|dictsort:0|join:"/" }}</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
- <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
- <p>{% trans "Keywords" %}: {{ service_description.keywords|join:", " }}</p>
8
+ <dt>{% translate "Keywords" %}:</dt><dd>{{ service_description.keywords|join:", " }}</dd>
7
9
  {% endif %}
8
10
  {% if service_description.provider_name %}
9
- <p>{% trans "Provider" %}: {% if service_description.provider_site %}<a href="{{ service_description.provider_site }}">{% endif %}{{ service_description.provider_name }}{% if service_description.provider_site %}</a></p>{% endif %}
10
- {% if service_description.contact_person %}<p>{% trans "Contact" %}: {{ service_description.contact_person }}</p>{% endif %}
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>{% trans "Using This WFS" %}</h2>
14
- <p>{% trans "Add the following URL to your GIS application:" %}</p>
15
- <blockquote><code>{{ connect_url }}</code></blockquote>
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>