django-gisserver 1.4.0__tar.gz → 1.4.1__tar.gz

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 (64) hide show
  1. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/CHANGES.md +6 -0
  2. {django-gisserver-1.4.0/django_gisserver.egg-info → django-gisserver-1.4.1}/PKG-INFO +5 -3
  3. {django-gisserver-1.4.0 → django-gisserver-1.4.1/django_gisserver.egg-info}/PKG-INFO +5 -3
  4. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/SOURCES.txt +0 -1
  5. django-gisserver-1.4.1/gisserver/__init__.py +1 -0
  6. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/features.py +3 -2
  7. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/pyproject.toml +2 -2
  8. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/setup.py +4 -2
  9. django-gisserver-1.4.0/gisserver/__init__.py +0 -1
  10. django-gisserver-1.4.0/gisserver/output/gml32_lxml.py +0 -612
  11. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/LICENSE +0 -0
  12. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/MANIFEST.in +0 -0
  13. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/README.md +0 -0
  14. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/dependency_links.txt +0 -0
  15. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/not-zip-safe +0 -0
  16. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/requires.txt +0 -0
  17. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/top_level.txt +0 -0
  18. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/conf.py +0 -0
  19. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/db.py +0 -0
  20. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/exceptions.py +0 -0
  21. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/geometries.py +0 -0
  22. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/operations/__init__.py +0 -0
  23. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/operations/base.py +0 -0
  24. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/operations/wfs20.py +0 -0
  25. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/__init__.py +0 -0
  26. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/base.py +0 -0
  27. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/csv.py +0 -0
  28. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/geojson.py +0 -0
  29. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/gml32.py +0 -0
  30. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/results.py +0 -0
  31. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/utils.py +0 -0
  32. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/output/xmlschema.py +0 -0
  33. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/__init__.py +0 -0
  34. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/base.py +0 -0
  35. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/__init__.py +0 -0
  36. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/expressions.py +0 -0
  37. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/filters.py +0 -0
  38. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/functions.py +0 -0
  39. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/identifiers.py +0 -0
  40. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/operators.py +0 -0
  41. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/query.py +0 -0
  42. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/sorting.py +0 -0
  43. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/gml/__init__.py +0 -0
  44. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/gml/base.py +0 -0
  45. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/gml/geometries.py +0 -0
  46. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/tags.py +0 -0
  47. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/parsers/values.py +0 -0
  48. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/queries/__init__.py +0 -0
  49. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/queries/adhoc.py +0 -0
  50. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/queries/base.py +0 -0
  51. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/queries/stored.py +0 -0
  52. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/static/gisserver/index.css +0 -0
  53. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/index.html +0 -0
  54. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/service_description.html +0 -0
  55. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -0
  56. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
  57. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -0
  58. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
  59. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/feature_type.html +0 -0
  60. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templatetags/__init__.py +0 -0
  61. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/templatetags/gisserver_tags.py +0 -0
  62. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/types.py +0 -0
  63. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/gisserver/views.py +0 -0
  64. {django-gisserver-1.4.0 → django-gisserver-1.4.1}/setup.cfg +0 -0
@@ -1,3 +1,9 @@
1
+ # 2024-08-29 (1.4.1)
2
+
3
+ * Fix 500 error when `model_attribute` points to a dotted-path.
4
+ PR thanks to [sposs](https://github.com/sposs).
5
+ * Updated pre-commit configuration and test matrix.
6
+
1
7
  # 2024-07-01 (1.4.0)
2
8
 
3
9
  * Added `GISSERVER_COUNT_NUMBER_MATCHED` setting to allow disabling "numberReturned" counting.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
@@ -12,20 +12,22 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Framework :: Django
20
20
  Classifier: Framework :: Django :: 3.2
21
21
  Classifier: Framework :: Django :: 4.0
22
22
  Classifier: Framework :: Django :: 4.2
23
+ Classifier: Framework :: Django :: 5.0
24
+ Classifier: Framework :: Django :: 5.1
23
25
  Classifier: Topic :: Internet :: WWW/HTTP
24
26
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
25
27
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
26
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
29
  Requires: Django (>=3.2)
28
- Requires-Python: >=3.6
30
+ Requires-Python: >=3.8
29
31
  Description-Content-Type: text/markdown
30
32
  Provides-Extra: tests
31
33
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
@@ -12,20 +12,22 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Framework :: Django
20
20
  Classifier: Framework :: Django :: 3.2
21
21
  Classifier: Framework :: Django :: 4.0
22
22
  Classifier: Framework :: Django :: 4.2
23
+ Classifier: Framework :: Django :: 5.0
24
+ Classifier: Framework :: Django :: 5.1
23
25
  Classifier: Topic :: Internet :: WWW/HTTP
24
26
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
25
27
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
26
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
29
  Requires: Django (>=3.2)
28
- Requires-Python: >=3.6
30
+ Requires-Python: >=3.8
29
31
  Description-Content-Type: text/markdown
30
32
  Provides-Extra: tests
31
33
  License-File: LICENSE
@@ -26,7 +26,6 @@ gisserver/output/base.py
26
26
  gisserver/output/csv.py
27
27
  gisserver/output/geojson.py
28
28
  gisserver/output/gml32.py
29
- gisserver/output/gml32_lxml.py
30
29
  gisserver/output/results.py
31
30
  gisserver/output/utils.py
32
31
  gisserver/output/xmlschema.py
@@ -0,0 +1 @@
1
+ __version__ = "1.4.1" # follows PEP440
@@ -189,7 +189,6 @@ class FeatureField:
189
189
  self.model_field = None
190
190
  self.parent = parent
191
191
  self.abstract = abstract
192
-
193
192
  # Allow to override the class attribute on 'self',
194
193
  # which avoids having to subclass this field class as well.
195
194
  if xsd_class is not None:
@@ -593,7 +592,9 @@ class FeatureType:
593
592
  return [
594
593
  (ff.model_field.name if ff.model_field else ff.model_attribute)
595
594
  for ff in self.fields
596
- if not ff.model_field.many_to_many and not ff.model_field.one_to_many
595
+ if not ff.model_field.many_to_many
596
+ and not ff.model_field.one_to_many
597
+ and not (ff.model_attribute and "." in ff.model_attribute)
597
598
  ]
598
599
 
599
600
  @cached_property
@@ -22,12 +22,12 @@ plugins = ["django_coverage_plugin"]
22
22
  # ==== black ====
23
23
  [tool.black]
24
24
  line-length = 99
25
- target-version = ['py38']
25
+ target-version = ['py39']
26
26
 
27
27
  # ==== ruff ====
28
28
  [tool.ruff]
29
29
  line-length = 99
30
- target-version = "py38"
30
+ target-version = "py39"
31
31
 
32
32
  [tool.ruff.lint]
33
33
  select = [
@@ -60,18 +60,20 @@ setup(
60
60
  "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
61
61
  "Operating System :: OS Independent",
62
62
  "Programming Language :: Python",
63
- "Programming Language :: Python :: 3.8",
64
63
  "Programming Language :: Python :: 3.9",
65
64
  "Programming Language :: Python :: 3.10",
66
65
  "Programming Language :: Python :: 3.11",
66
+ "Programming Language :: Python :: 3.12",
67
67
  "Framework :: Django",
68
68
  "Framework :: Django :: 3.2",
69
69
  "Framework :: Django :: 4.0",
70
70
  "Framework :: Django :: 4.2",
71
+ "Framework :: Django :: 5.0",
72
+ "Framework :: Django :: 5.1",
71
73
  "Topic :: Internet :: WWW/HTTP",
72
74
  "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
73
75
  "Topic :: Software Development :: Libraries :: Application Frameworks",
74
76
  "Topic :: Software Development :: Libraries :: Python Modules",
75
77
  ],
76
- python_requires=">=3.6",
78
+ python_requires=">=3.8",
77
79
  )
@@ -1 +0,0 @@
1
- __version__ = "1.4.0" # follows PEP440
@@ -1,612 +0,0 @@
1
- """Output rendering logic.
2
-
3
- Note that the Django format_html() / mark_safe() logic is not used here,
4
- as it's quite a performance improvement to just use html.escape().
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import re
10
- from datetime import date, datetime, time, timezone
11
- from io import BytesIO
12
- from typing import cast
13
-
14
- from django.contrib.gis import geos
15
- from django.db import models
16
- from django.http import HttpResponse, StreamingHttpResponse
17
- from lxml import etree
18
-
19
- from gisserver.db import (
20
- AsGML,
21
- build_db_annotations,
22
- conditional_transform,
23
- get_db_annotation,
24
- get_db_geometry_selects,
25
- get_db_geometry_target,
26
- get_geometries_union,
27
- )
28
- from gisserver.exceptions import NotFound
29
- from gisserver.features import FeatureRelation, FeatureType
30
- from gisserver.geometries import CRS
31
- from gisserver.parsers.fes20 import ValueReference
32
- from gisserver.types import XsdComplexType, XsdElement
33
-
34
- from .base import OutputRenderer
35
- from .results import SimpleFeatureCollection
36
-
37
- GML_RENDER_FUNCTIONS = {}
38
- RE_GML_ID = re.compile(r'gml:id="[^"]+"')
39
-
40
- WFS_NS = "http://www.opengis.net/wfs/2.0"
41
- GML32_NS = "http://www.opengis.net/gml/3.2"
42
- XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
43
-
44
- FIX_GML_NS = f' xmlns:gml="{GML32_NS}">'
45
-
46
- # Avoid string concatenations in code:
47
- XSI_NIL_ATTRIB = {f"{{{XSI_NS}}}nil": "true"}
48
- GML_ID_ATTR = f"{{{GML32_NS}}}id" # gml:id="..."
49
- WFS_MEMBER = f"{{{WFS_NS}}}member" # <wfs:member>
50
- GML_BOUNDED_BY = f"{{{GML32_NS}}}boundedBy" # <gml:boundedBy>
51
- GML_ENVELOPE = f"{{{GML32_NS}}}Envelope" # <gml:Envelope>
52
- GML_LOWER_CORNER = f"{{{GML32_NS}}}lowerCorner" # <gml:lowerCorner>
53
- GML_UPPER_CORNER = f"{{{GML32_NS}}}upperCorner" # <gml:upperCorner>
54
-
55
- # register common namespaces globally, that's fine here I guess... *fingers crossed*
56
- # etree.register_namespace("wfs", WFS_NS)
57
- # etree.register_namespace("gml", GML32_NS)
58
- NS_MAP = {
59
- "wfs": WFS_NS,
60
- "gml": GML32_NS,
61
- }
62
-
63
-
64
- def default_if_none(value, default):
65
- if value is None:
66
- return default
67
- else:
68
- return value
69
-
70
-
71
- def register_geos_type(geos_type):
72
- def _inc(func):
73
- GML_RENDER_FUNCTIONS[geos_type] = func
74
- return func
75
-
76
- return _inc
77
-
78
-
79
- class GML32Renderer(OutputRenderer):
80
- """Render the GetFeature XML output in GML 3.2 format"""
81
-
82
- content_type = "text/xml; charset=utf-8"
83
- xml_collection_tag = "FeatureCollection"
84
- chunk_size = 40_000
85
-
86
- def __init__(self, *args, **kwargs):
87
- super().__init__(*args, **kwargs)
88
- self.nsmap = {**NS_MAP, "app": self.app_xml_namespace}
89
-
90
- def _make_element(self, node: XsdElement, attrib=None) -> etree.Element:
91
- tag = f"{{{self.nsmap[node.prefix]}}}{node.name}" # node.xml_name is prefix:name format.
92
- return etree.Element(tag, attrib=attrib, nsmap=self.nsmap)
93
-
94
- def _make_feature_element(self, feature_type: FeatureType, attrib=None) -> etree.Element:
95
- tag = f"{{{self.nsmap[feature_type.xml_prefix]}}}{feature_type.name}" # node.xml_name is prefix:name format.
96
- return etree.Element(tag, attrib=attrib, nsmap=self.nsmap)
97
-
98
- def get_response(self):
99
- """Render the output as streaming response."""
100
- from gisserver.queries import GetFeatureById
101
-
102
- if isinstance(self.source_query, GetFeatureById):
103
- # WFS spec requires that GetFeatureById output only returns the contents.
104
- # The streaming response is avoided here, to allow returning a 404.
105
- return self.get_standalone_response()
106
- else:
107
- # Use default streaming response, with render_stream()
108
- response = super().get_response()
109
- if isinstance(response, StreamingHttpResponse):
110
- response["Content-Encoding"] = "gzip" # lxml.xmlfile compression
111
- return response
112
-
113
- def get_standalone_response(self):
114
- """Render a standalone item, for GetFeatureById"""
115
- sub_collection = self.collection.results[0]
116
- self.start_collection(sub_collection)
117
- instance = sub_collection.first()
118
- if instance is None:
119
- raise NotFound("Feature not found.")
120
-
121
- body = self.render_feature(
122
- feature_type=sub_collection.feature_type,
123
- instance=instance,
124
- )
125
-
126
- if isinstance(body, str):
127
- # Best guess for GetFeatureById combined with
128
- # GetPropertyValue&VALUEREFERENCE=@gml:id
129
- return HttpResponse(body, content_type="text/plain")
130
- else:
131
- return HttpResponse(
132
- etree.tostring(body, xml_declaration=True),
133
- content_type=self.content_type,
134
- )
135
-
136
- def get_xsi_schema_location(self):
137
- xsd_typenames = ",".join(
138
- sub_collection.feature_type.name for sub_collection in self.collection.results
139
- )
140
-
141
- # These are pairs of "namespace location"
142
- return (
143
- f"{self.app_xml_namespace}"
144
- f" {self.server_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES={xsd_typenames}"
145
- " http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd"
146
- " http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd"
147
- )
148
-
149
- def render_stream(self):
150
- """Render the XML as streaming content.
151
- This renders the standard <wfs:FeatureCollection> / <wfs:ValueCollection>
152
- """
153
- collection = self.collection
154
- output = BytesIO()
155
- number_matched = collection.number_matched
156
- number_returned = collection.number_returned
157
-
158
- root_attrib = {
159
- f"{{{XSI_NS}}}schemaLocation": self.get_xsi_schema_location(),
160
- "timestamp": collection.timestamp,
161
- "numberMatched": (
162
- str(int(number_matched)) if number_matched is not None else "unknown"
163
- ),
164
- "numberReturned": str(number_returned),
165
- }
166
- if collection.next:
167
- root_attrib["next"] = collection.next
168
- if collection.previous:
169
- root_attrib["previous"] = collection.previous
170
-
171
- with etree.xmlfile(output) as xml_file:
172
- xml_file.write_declaration()
173
-
174
- # Either FeatureCollection / ValueCollection
175
- with xml_file.element(
176
- f"{{{WFS_NS}}}{self.xml_collection_tag}",
177
- attrib=root_attrib,
178
- nsmap=self.nsmap,
179
- ):
180
- # no results in all sub collections, means not writing any <wfs:member> tags at all.
181
- if number_returned:
182
- has_multiple_collections = len(collection.results) > 1
183
-
184
- for sub_collection in collection.results:
185
- self.start_collection(sub_collection) # hook for subclasses
186
-
187
- if has_multiple_collections:
188
- xml_file.write(
189
- (
190
- f"<wfs:member>\n"
191
- f"<wfs:{self.xml_collection_tag}"
192
- f' timeStamp="{collection.timestamp}"'
193
- f' numberMatched="{int(sub_collection.number_matched)}"'
194
- f' numberReturned="{int(sub_collection.number_returned)}">\n'
195
- ).encode()
196
- )
197
-
198
- for i, instance in enumerate(sub_collection):
199
- xml_file.write(
200
- self.render_wfs_member(sub_collection.feature_type, instance)
201
- )
202
-
203
- # Only perform a 'yield' every once in a while,
204
- # as it goes back-and-forth for writing it to the client.
205
- if output.tell() > self.chunk_size:
206
- xml_file.flush()
207
- xml_chunk = output.getvalue()
208
- output.seek(0)
209
- output.truncate(0)
210
- yield xml_chunk
211
-
212
- if has_multiple_collections:
213
- xml_file.write(
214
- f"</wfs:{self.xml_collection_tag}>\n</wfs:member>\n".encode()
215
- )
216
-
217
- # xml_file.flush()
218
- yield output.getvalue()
219
-
220
- def start_collection(self, sub_collection: SimpleFeatureCollection):
221
- """Hook to allow initialization per feature type"""
222
- pass
223
-
224
- def render_wfs_member(
225
- self, feature_type: FeatureType, instance: models.Model
226
- ) -> etree.Element:
227
- """Write the full <wfs:member> block."""
228
- wfs_member_tag = etree.Element(WFS_MEMBER, nsmap=self.nsmap)
229
- wfs_member_tag.append(self.render_feature(feature_type, instance))
230
- return wfs_member_tag
231
-
232
- def render_feature(
233
- self, feature_type: FeatureType, instance: models.Model, nsmap=None
234
- ) -> etree.Element:
235
- """Write the contents of the object value.
236
-
237
- This output is typically wrapped in <wfs:member> tags
238
- unless it's used for a GetPropertyById response.
239
- """
240
- self.gml_seq = 0 # need to increment this between write_xml_field calls
241
-
242
- # Write <app:FeatureTypeName> start node
243
- feature_tag = self._make_feature_element(
244
- feature_type.xml_name,
245
- attrib={GML_ID_ATTR: f"{feature_type.name}.{instance.pk}"},
246
- )
247
-
248
- # Add all base class members, in their correct ordering
249
- # By having these as XsdElement objects instead of hard-coded writes,
250
- # the query/filter logic also works for these elements.
251
- if feature_type.xsd_type.base.is_complex_type:
252
- for xsd_element in feature_type.xsd_type.base.elements:
253
- if xsd_element.xml_name == "gml:boundedBy":
254
- # Special case for <gml:boundedBy>, so it will render with
255
- # the output CRS and can be overwritten with DB-rendered GML.
256
- gml = self.render_bounds(feature_type, instance)
257
- if gml is not None:
258
- feature_tag.append(gml)
259
- elif xsd_element.is_many:
260
- # some <app:...> node that has multiple values
261
- feature_tag.extend(self.render_many(feature_type, xsd_element, instance))
262
- else:
263
- # e.g. <gml:name>, or all other <app:...> nodes.
264
- feature_tag.append(self.render_xml_field(feature_type, xsd_element, instance))
265
-
266
- # Add all members
267
- for xsd_element in feature_type.xsd_type.elements:
268
- if xsd_element.is_many:
269
- # some <app:...> node that has multiple values
270
- feature_tag.extend(self.render_many(feature_type, xsd_element, instance))
271
- else:
272
- # e.g. <gml:name>, or all other <app:...> nodes.
273
- feature_tag.append(self.render_xml_field(feature_type, xsd_element, instance))
274
-
275
- return feature_tag
276
-
277
- def render_bounds(self, feature_type, instance) -> etree.Element | None:
278
- """Render the GML bounds for the complete instance"""
279
- envelope = feature_type.get_envelope(instance, self.output_crs)
280
- if envelope is None:
281
- return None
282
-
283
- bounded_by = etree.Element(GML_BOUNDED_BY, nsmap=self.nsmap)
284
- envelope = etree.SubElement(
285
- bounded_by,
286
- GML_ENVELOPE,
287
- attrib={"srsDimension": "2", "srsName": self.xml_srs_name},
288
- nsmap=self.nsmap,
289
- )
290
- lower = " ".join(map(str, envelope.lower_corner))
291
- upper = " ".join(map(str, envelope.upper_corner))
292
- etree.SubElement(envelope, GML_LOWER_CORNER, nsmap=self.nsmap).text = lower
293
- etree.SubElement(envelope, GML_UPPER_CORNER, nsmap=self.nsmap).text = upper
294
- return bounded_by
295
-
296
- def render_many(
297
- self, feature_type, xsd_element: XsdElement, instance: models.Model
298
- ) -> list[etree.Element]:
299
- """Render a single field, multiple times."""
300
- value = xsd_element.get_value(instance)
301
- if value is None:
302
- # No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
303
- if xsd_element.min_occurs == 0:
304
- return []
305
- else:
306
- # <app:field xsi:nil="true"/>
307
- return [self._make_element(xsd_element, attrib=XSI_NIL_ATTRIB)]
308
- else:
309
- # Render the tag multiple times
310
- if xsd_element.type.is_complex_type:
311
- # If the retrieved QuerySet was not filtered yet, do so now. This can't
312
- # be done in get_value() because the FeatureType is not known there.
313
- value = feature_type.filter_related_queryset(value)
314
-
315
- return [
316
- self.render_xml_field(feature_type, xsd_element, instance=item) for item in value
317
- ]
318
-
319
- def render_xml_field(
320
- self, feature_type: FeatureType, xsd_element: XsdElement, instance: models.Model
321
- ) -> etree.Element:
322
- """Write the value of a single field."""
323
- if xsd_element.is_geometry:
324
- # Short-cirquit, allow overriding:
325
- return self.render_gml_field(feature_type, xsd_element, instance)
326
-
327
- value = xsd_element.get_value(instance)
328
- if value is None:
329
- return self._make_element(xsd_element, attrib=XSI_NIL_ATTRIB)
330
- elif xsd_element.type.is_complex_type:
331
- # Expanded foreign relation / dictionary
332
- return self.render_xml_complex_type(feature_type, xsd_element, value)
333
- else:
334
- xml_field = self._make_element(xsd_element)
335
- xml_field.text = self._value_to_string(value)
336
- return xml_field # <app:field>{value}</app:field>
337
-
338
- def _value_to_string(self, value):
339
- # TODO: can this be a lookup on the XsdElement?
340
- if isinstance(value, str):
341
- return value
342
- elif isinstance(value, datetime):
343
- return value.astimezone(timezone.utc).isoformat()
344
- elif isinstance(value, (date, time)):
345
- return value.isoformat()
346
- elif isinstance(value, bool):
347
- return "true" if value else "false"
348
- else:
349
- return str(value)
350
-
351
- def render_xml_complex_type(self, feature_type, xsd_element, value) -> etree.Element:
352
- """Write a single field, that consists of sub elements"""
353
- xsd_type = cast(XsdComplexType, xsd_element.type)
354
- field = self._make_element(xsd_element)
355
- for sub_element in xsd_type.elements:
356
- if sub_element.is_many:
357
- field.extend(self.render_many(feature_type, sub_element, instance=value))
358
- else:
359
- field.append(self.render_xml_field(feature_type, sub_element, instance=value))
360
-
361
- return field
362
-
363
- def render_gml_field(
364
- self,
365
- feature_type: FeatureType,
366
- xsd_element: XsdElement,
367
- instance,
368
- ) -> str:
369
- """Write the field that holds an GML tag.
370
- This is a separate function on purpose, so it can be optimized for database-rendering.
371
- """
372
- value: geos.GEOSGeometry = xsd_element.get_value(instance) # take from DB field
373
- if value is None:
374
- return self._make_element(xsd_element.xml_name, attrib=XSI_NIL_ATTRIB)
375
-
376
- self.gml_seq += 1
377
- gml_id = self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq)
378
-
379
- # Reparsing XML is faster than rendering it ourselves,
380
- # because it avoids a C-API call for every coordinate
381
- self.output_crs.apply_to(value)
382
- gml_parsed = etree.XML(value.ogr.gml)
383
- gml_parsed.attrib[GML_ID_ATTR] = gml_id
384
- gml_parsed.attrib["srsName"] = self.xml_srs_name # TODO: needed?
385
-
386
- node = self._make_element(xsd_element)
387
- node.append(gml_parsed)
388
- return node
389
-
390
- def get_gml_id(self, feature_type: FeatureType, object_id, seq) -> str:
391
- """Generate the gml:id value, which is required for GML 3.2 objects."""
392
- return f"{feature_type.name}.{object_id}.{seq}"
393
-
394
-
395
- class DBGML32Renderer(GML32Renderer):
396
- """Faster GetFeature renderer that uses the database to render GML 3.2"""
397
-
398
- @classmethod
399
- def decorate_queryset(
400
- cls,
401
- feature_type: FeatureType,
402
- queryset: models.QuerySet,
403
- output_crs: CRS,
404
- **params,
405
- ):
406
- """Update the queryset to let the database render the GML output.
407
- This is far more efficient then GeoDjango's logic, which performs a
408
- C-API call for every single coordinate of a geometry.
409
- """
410
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
411
-
412
- # Retrieve geometries as pre-rendered instead.
413
- gml_elements = feature_type.xsd_type.geometry_elements
414
- geo_selects = get_db_geometry_selects(gml_elements, output_crs)
415
- if geo_selects:
416
- queryset = queryset.defer(*geo_selects.keys()).annotate(
417
- _as_envelope_gml=cls.get_db_envelope_as_gml(feature_type, queryset, output_crs),
418
- **build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
419
- )
420
-
421
- return queryset
422
-
423
- @classmethod
424
- def get_prefetch_queryset(
425
- cls,
426
- feature_type: FeatureType,
427
- feature_relation: FeatureRelation,
428
- output_crs: CRS,
429
- ) -> models.QuerySet | None:
430
- """Perform DB annotations for prefetched relations too."""
431
- base = super().get_prefetch_queryset(feature_type, feature_relation, output_crs)
432
- if base is None:
433
- return None
434
-
435
- # Find which fields are GML elements
436
- gml_elements = []
437
- for e in feature_relation.xsd_elements:
438
- if e.is_geometry:
439
- # Prefetching a flattened relation
440
- gml_elements.append(e)
441
- elif e.type.is_complex_type:
442
- # Prefetching a complex type
443
- xsd_type: XsdComplexType = cast(XsdComplexType, e.type)
444
- gml_elements.extend(xsd_type.geometry_elements)
445
-
446
- geometries = get_db_geometry_selects(gml_elements, output_crs)
447
- if geometries:
448
- # Exclude geometries from the fields, fetch them as pre-rendered annotations instead.
449
- return base.defer(geometries.keys()).annotate(
450
- **build_db_annotations(geometries, "_as_gml_{name}", AsGML),
451
- )
452
- else:
453
- return base
454
-
455
- @classmethod
456
- def get_db_envelope_as_gml(cls, feature_type, queryset, output_crs) -> AsGML:
457
- """Offload the GML rendering of the envelope to the database.
458
-
459
- This also avoids offloads the geometry union calculation to the DB.
460
- """
461
- geo_fields_union = cls._get_geometries_union(feature_type, queryset, output_crs)
462
- return AsGML(geo_fields_union, envelope=True)
463
-
464
- @classmethod
465
- def _get_geometries_union(cls, feature_type: FeatureType, queryset, output_crs):
466
- """Combine all geometries of the model in a single SQL function."""
467
- # Apply transforms where needed, in case some geometries use a different SRID.
468
- return get_geometries_union(
469
- [
470
- conditional_transform(
471
- model_field.name,
472
- model_field.srid,
473
- output_srid=output_crs.srid,
474
- )
475
- for model_field in feature_type.geometry_fields
476
- ],
477
- using=queryset.db,
478
- )
479
-
480
- def render_gml_field(
481
- self,
482
- feature_type: FeatureType,
483
- xsd_element: XsdElement,
484
- instance,
485
- gml_id,
486
- ) -> str:
487
- # Optimized path, pre-rendered GML
488
- # Take from DB annotation instead of DB field.
489
- gml_text = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
490
- if gml_text is None:
491
- return self._make_element(xsd_element, attrib=XSI_NIL_ATTRIB)
492
- else:
493
- # Extract DB annotation, it needs an gml:id, and have namespaces following ours.
494
- gml_text = gml_text.replace(">", FIX_GML_NS, 1) # fix parsing issues
495
- self.gml_seq += 1
496
- gml_id = self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq)
497
- gml_parsed = etree.XML(gml_text) # TODO: this means re-parsing the string..
498
- gml_parsed.attrib[GML_ID_ATTR] = gml_id
499
-
500
- node = self._make_element(xsd_element)
501
- node.append(gml_parsed)
502
- return node
503
-
504
- def render_bounds(self, feature_type, instance):
505
- """Generate the <gml:boundedBy> from DB prerendering."""
506
- gml_text = instance._as_envelope_gml
507
- if gml_text is not None:
508
- # TODO: need to reparse here, but can that be avoided??
509
- gml_text = gml_text.replace(">", FIX_GML_NS, 1) # fix parsing issues
510
- bounded_by = etree.Element(GML_BOUNDED_BY, nsmap=self.nsmap)
511
- bounded_by.append(etree.XML(gml_text))
512
- return bounded_by
513
- # return f"<gml:boundedBy>{gml}</gml:boundedBy>\n"
514
-
515
-
516
- class GML32ValueRenderer(GML32Renderer):
517
- """Render the GetPropertyValue XML output in GML 3.2 format"""
518
-
519
- content_type = "text/xml; charset=utf-8"
520
- xml_collection_tag = "ValueCollection"
521
-
522
- def __init__(self, *args, value_reference: ValueReference, **kwargs):
523
- self.value_reference = value_reference
524
- super().__init__(*args, **kwargs)
525
- self.xsd_node = None
526
-
527
- @classmethod
528
- def decorate_queryset(
529
- cls,
530
- feature_type: FeatureType,
531
- queryset: models.QuerySet,
532
- output_crs: CRS,
533
- **params,
534
- ):
535
- # Don't optimize queryset, it only retrieves one value
536
- return queryset
537
-
538
- def start_collection(self, sub_collection: SimpleFeatureCollection):
539
- # Resolve which XsdNode is being rendered
540
- match = sub_collection.feature_type.resolve_element(self.value_reference.xpath)
541
- self.xsd_node = match.child
542
-
543
- def render_wfs_member(
544
- self, feature_type: FeatureType, instance: dict, extra_xmlns=""
545
- ) -> etree.Element:
546
- """Overwritten to handle attribute support."""
547
- wsf_member = etree.Element(WFS_MEMBER, nsmap=self.nsmap)
548
- if self.xsd_node.is_attribute:
549
- # <wfs:member>{value}</wfs:member}
550
- # When GetPropertyValue selects an attribute, it's value is rendered
551
- # as plain-text (without spaces!) inside a <wfs:member> element.
552
- # The format_value() is needed for @gml:id
553
- body = self.xsd_node.format_raw_value(instance["member"])
554
- wsf_member.text = body
555
- else:
556
- # <wfs:member><app:field>...</app:field></wfs:member>
557
- # The call to GetPropertyValue selected an element.
558
- # Render this single element tag inside the <wfs:member> parent.
559
- wsf_member.append(self.render_feature(feature_type, instance))
560
- return wsf_member
561
-
562
- def render_feature(
563
- self, feature_type: FeatureType, instance: dict, extra_xmlns=""
564
- ) -> etree.ElementTree | str:
565
- """Write the XML for a single object.
566
- In this case, it's only a single XML tag.
567
- """
568
- value = instance["member"]
569
- if self.xsd_node.is_geometry:
570
- gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
571
- return self.render_gml_field(
572
- feature_type, self.xsd_node, instance=instance, gml_id=gml_id
573
- )
574
- else:
575
- # The xsd_element is needed so write_xml_field() can render complex types.
576
- if self.xsd_node.is_attribute:
577
- # For GetFeatureById, allow returning raw values
578
- value = self.xsd_node.format_raw_value(value) # needed for @gml:id
579
- return str(value)
580
- else:
581
- return self.render_xml_field(feature_type, cast(XsdElement, self.xsd_node), value)
582
-
583
-
584
- class DBGML32ValueRenderer(DBGML32Renderer, GML32ValueRenderer):
585
- """Faster GetPropertyValue renderer that uses the database to render GML 3.2"""
586
-
587
- @classmethod
588
- def decorate_queryset(cls, feature_type: FeatureType, queryset, output_crs, **params):
589
- """Update the queryset to let the database render the GML output."""
590
- value_reference = params["valueReference"]
591
- match = feature_type.resolve_element(value_reference.xpath)
592
- if match.child.is_geometry:
593
- # Add 'gml_member' to point to the pre-rendered GML version.
594
- return queryset.values(
595
- "pk", gml_member=AsGML(get_db_geometry_target(match, output_crs))
596
- )
597
- else:
598
- return queryset
599
-
600
- def render_wfs_member(self, feature_type: FeatureType, instance: dict, extra_xmlns="") -> str:
601
- """Write the XML for a single object."""
602
- if "gml_member" in instance:
603
- gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
604
- body = self.render_db_gml_field(
605
- feature_type,
606
- self.xsd_node,
607
- instance["gml_member"],
608
- gml_id=gml_id,
609
- )
610
- return f"<wfs:member>\n{body}</wfs:member>\n"
611
- else:
612
- return super().render_wfs_member(feature_type, instance, extra_xmlns=extra_xmlns)