django-gisserver 1.4.1__tar.gz → 1.5.0__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.
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/CHANGES.md +11 -1
- {django-gisserver-1.4.1/django_gisserver.egg-info → django_gisserver-1.5.0}/PKG-INFO +12 -2
- {django-gisserver-1.4.1 → django_gisserver-1.5.0/django_gisserver.egg-info}/PKG-INFO +12 -2
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/SOURCES.txt +1 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/requires.txt +2 -2
- django_gisserver-1.5.0/gisserver/__init__.py +1 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/db.py +14 -20
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/exceptions.py +23 -9
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/features.py +62 -99
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/geometries.py +2 -2
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/operations/base.py +31 -21
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/operations/wfs20.py +44 -38
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/__init__.py +2 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/base.py +43 -27
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/csv.py +38 -33
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/geojson.py +43 -51
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/gml32.py +88 -67
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/results.py +23 -8
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/utils.py +18 -2
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/xmlschema.py +1 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/base.py +2 -2
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/__init__.py +18 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/expressions.py +7 -12
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/functions.py +1 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/operators.py +7 -3
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/query.py +11 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/sorting.py +3 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/gml/base.py +1 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/__init__.py +3 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/adhoc.py +16 -12
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/base.py +76 -36
- django_gisserver-1.5.0/gisserver/queries/projection.py +240 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/stored.py +7 -6
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/types.py +78 -24
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/views.py +9 -20
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/pyproject.toml +1 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/setup.py +2 -2
- django-gisserver-1.4.1/gisserver/__init__.py +0 -1
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/LICENSE +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/MANIFEST.in +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/README.md +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/dependency_links.txt +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/not-zip-safe +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/top_level.txt +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/conf.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/operations/__init__.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/__init__.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/filters.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/identifiers.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/gml/__init__.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/gml/geometries.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/tags.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/values.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/static/gisserver/index.css +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/index.html +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/service_description.html +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/feature_type.html +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templatetags/__init__.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templatetags/gisserver_tags.py +0 -0
- {django-gisserver-1.4.1 → django_gisserver-1.5.0}/setup.cfg +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# 2024-11-25 (1.5.0)
|
|
2
|
+
|
|
3
|
+
* Added `PROPERTYNAME` support
|
|
4
|
+
* Added rendering of `<wfs:truncatedResponse>` when errors happen during output streaming.
|
|
5
|
+
* Make sure Django 4.1+ won't do double prefetches on relations that we prefetch.
|
|
6
|
+
* Hide the erroneous output formats in `GetCapabilities` that are supported for FME.
|
|
7
|
+
* Bump requirements to non-vulnerable versions (of lxml and orjson).
|
|
8
|
+
* Cleaned up code, removing some internal methods.
|
|
9
|
+
* Cleaned up leftover Python 3.7 compat code.
|
|
10
|
+
|
|
1
11
|
# 2024-08-29 (1.4.1)
|
|
2
12
|
|
|
3
13
|
* Fix 500 error when `model_attribute` points to a dotted-path.
|
|
@@ -197,7 +207,7 @@ for their work on this release.
|
|
|
197
207
|
# 2020-06-29 (0.8.1)
|
|
198
208
|
|
|
199
209
|
* Added unpaginated GeoJSON support (performance is good enough).
|
|
200
|
-
* Added basic support for non-namespaced `FILTER` queries, and `PropertyName` tags (though being
|
|
210
|
+
* Added basic support for non-namespaced `FILTER` queries, and `PropertyName` tags (though being WFS1 attributes, clients still use them).
|
|
201
211
|
* Added extra strict check that `ValueReference`/`Literal`/`ResourceId` nodes don't have child nodes.
|
|
202
212
|
* Fixed allowing filtering on unknown or undefined fields (XPaths are now resolved to known elements).
|
|
203
213
|
* Optimized results streaming by automatically using a queryset-iterator if possible.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
|
@@ -29,8 +29,18 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
29
29
|
Requires: Django (>=3.2)
|
|
30
30
|
Requires-Python: >=3.8
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
|
-
Provides-Extra: tests
|
|
33
32
|
License-File: LICENSE
|
|
33
|
+
Requires-Dist: Django>=3.2
|
|
34
|
+
Requires-Dist: defusedxml>=0.6.0
|
|
35
|
+
Requires-Dist: lru_dict>=1.1.7
|
|
36
|
+
Requires-Dist: orjson>=3.9.15
|
|
37
|
+
Provides-Extra: tests
|
|
38
|
+
Requires-Dist: django-environ>=0.4.5; extra == "tests"
|
|
39
|
+
Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
|
|
40
|
+
Requires-Dist: lxml>=4.9.1; extra == "tests"
|
|
41
|
+
Requires-Dist: pytest>=6.2.3; extra == "tests"
|
|
42
|
+
Requires-Dist: pytest-django>=4.1.0; extra == "tests"
|
|
43
|
+
Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
|
|
34
44
|
|
|
35
45
|
[](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
|
|
36
46
|
[](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
|
@@ -29,8 +29,18 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
29
29
|
Requires: Django (>=3.2)
|
|
30
30
|
Requires-Python: >=3.8
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
|
-
Provides-Extra: tests
|
|
33
32
|
License-File: LICENSE
|
|
33
|
+
Requires-Dist: Django>=3.2
|
|
34
|
+
Requires-Dist: defusedxml>=0.6.0
|
|
35
|
+
Requires-Dist: lru_dict>=1.1.7
|
|
36
|
+
Requires-Dist: orjson>=3.9.15
|
|
37
|
+
Provides-Extra: tests
|
|
38
|
+
Requires-Dist: django-environ>=0.4.5; extra == "tests"
|
|
39
|
+
Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
|
|
40
|
+
Requires-Dist: lxml>=4.9.1; extra == "tests"
|
|
41
|
+
Requires-Dist: pytest>=6.2.3; extra == "tests"
|
|
42
|
+
Requires-Dist: pytest-django>=4.1.0; extra == "tests"
|
|
43
|
+
Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
|
|
34
44
|
|
|
35
45
|
[](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
|
|
36
46
|
[](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml)
|
|
@@ -47,6 +47,7 @@ gisserver/parsers/gml/geometries.py
|
|
|
47
47
|
gisserver/queries/__init__.py
|
|
48
48
|
gisserver/queries/adhoc.py
|
|
49
49
|
gisserver/queries/base.py
|
|
50
|
+
gisserver/queries/projection.py
|
|
50
51
|
gisserver/queries/stored.py
|
|
51
52
|
gisserver/static/gisserver/index.css
|
|
52
53
|
gisserver/templates/gisserver/index.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.5.0" # follows PEP440
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from functools import lru_cache, reduce
|
|
6
|
-
from typing import cast
|
|
7
6
|
|
|
8
|
-
from django.contrib.gis.db.models import
|
|
7
|
+
from django.contrib.gis.db.models import functions
|
|
9
8
|
from django.db import connection, connections, models
|
|
10
9
|
|
|
11
10
|
from gisserver import conf
|
|
12
11
|
from gisserver.geometries import CRS
|
|
13
|
-
from gisserver.types import
|
|
12
|
+
from gisserver.types import GmlElement
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class AsEWKT(functions.GeoFunc):
|
|
@@ -54,9 +53,9 @@ class ST_Union(functions.Union):
|
|
|
54
53
|
arity = None
|
|
55
54
|
|
|
56
55
|
def as_postgresql(self, compiler, connection, **extra_context):
|
|
57
|
-
# PostgreSQL can handle ST_Union(ARRAY
|
|
56
|
+
# PostgreSQL can handle ST_Union(ARRAY[field names]), other databases don't.
|
|
58
57
|
if len(self.source_expressions) > 2:
|
|
59
|
-
extra_context["template"] = "%(function)s(ARRAY
|
|
58
|
+
extra_context["template"] = "%(function)s(ARRAY[%(expressions)s])"
|
|
60
59
|
return self.as_sql(compiler, connection, **extra_context)
|
|
61
60
|
|
|
62
61
|
|
|
@@ -65,7 +64,7 @@ def get_geometries_union(
|
|
|
65
64
|
) -> str | functions.Union:
|
|
66
65
|
"""Generate a union of multiple geometry fields."""
|
|
67
66
|
if not expressions:
|
|
68
|
-
raise ValueError("Missing
|
|
67
|
+
raise ValueError("Missing geometry fields for get_geometries_union()")
|
|
69
68
|
|
|
70
69
|
if len(expressions) == 1:
|
|
71
70
|
return next(iter(expressions)) # fastest in set data type
|
|
@@ -73,7 +72,7 @@ def get_geometries_union(
|
|
|
73
72
|
return functions.Union(*expressions)
|
|
74
73
|
elif connections[using].vendor == "postgresql":
|
|
75
74
|
# postgres can handle multiple field names
|
|
76
|
-
return ST_Union(expressions)
|
|
75
|
+
return ST_Union(*expressions)
|
|
77
76
|
else:
|
|
78
77
|
# other databases do Union(Union(1, 2), 3)
|
|
79
78
|
return reduce(functions.Union, expressions)
|
|
@@ -122,25 +121,20 @@ def escape_xml_name(name: str, template="{name}") -> str:
|
|
|
122
121
|
|
|
123
122
|
|
|
124
123
|
def get_db_geometry_selects(
|
|
125
|
-
gml_elements: list[
|
|
124
|
+
gml_elements: list[GmlElement], output_crs: CRS
|
|
126
125
|
) -> dict[str, str | functions.Transform]:
|
|
127
126
|
"""Utility to generate select clauses for the geometry fields of a type.
|
|
128
127
|
Key is the xsd element name, value is the database select expression.
|
|
129
128
|
"""
|
|
130
129
|
return {
|
|
131
|
-
|
|
132
|
-
for
|
|
133
|
-
if
|
|
130
|
+
gml_element.name: get_db_geometry_target(gml_element, output_crs)
|
|
131
|
+
for gml_element in gml_elements
|
|
132
|
+
if gml_element.source is not None
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
|
|
137
|
-
def
|
|
136
|
+
def get_db_geometry_target(gml_element: GmlElement, output_crs: CRS) -> str | functions.Transform:
|
|
138
137
|
"""Wrap the selection of a geometry field in a CRS Transform if needed."""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def get_db_geometry_target(xpath_match: XPathMatch, output_crs: CRS):
|
|
144
|
-
"""Based on a resolved element, build the proper geometry field select clause."""
|
|
145
|
-
field = cast(GeometryField, xpath_match.child.source)
|
|
146
|
-
return conditional_transform(xpath_match.orm_path, field.srid, output_srid=output_crs.srid)
|
|
138
|
+
return conditional_transform(
|
|
139
|
+
gml_element.orm_path, gml_element.source.srid, output_srid=output_crs.srid
|
|
140
|
+
)
|
|
@@ -9,13 +9,14 @@ https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#411
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from django.conf import settings
|
|
12
|
+
from django.http import HttpResponse
|
|
12
13
|
from django.utils.html import format_html
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class ExternalValueError(ValueError):
|
|
16
17
|
"""Raise a ValueError for external input.
|
|
17
18
|
This helps to distinguish between internal bugs
|
|
18
|
-
(e.g. unpacking values) and
|
|
19
|
+
(e.g. unpacking values) and malformed external input.
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
|
|
@@ -32,18 +33,29 @@ class OWSException(Exception):
|
|
|
32
33
|
version = "2.0.0"
|
|
33
34
|
code = None
|
|
34
35
|
text_template = None
|
|
36
|
+
debug_hint = True
|
|
35
37
|
|
|
36
|
-
def __init__(self,
|
|
38
|
+
def __init__(self, text=None, code=None, locator=None, status_code=None):
|
|
37
39
|
text = text or self.text_template.format(code=self.code, locator=locator)
|
|
40
|
+
if (code and len(text) < len(code)) or (locator and len(text) < len(locator)):
|
|
41
|
+
raise ValueError("text/locator arguments are switched")
|
|
42
|
+
|
|
38
43
|
super().__init__(text)
|
|
39
44
|
self.locator = locator
|
|
40
45
|
self.text = text
|
|
41
|
-
self.code = code or self.code
|
|
46
|
+
self.code = code or self.code or self.__class__.__name__
|
|
42
47
|
self.status_code = status_code or self.status_code
|
|
43
48
|
|
|
49
|
+
def as_response(self):
|
|
50
|
+
return HttpResponse(
|
|
51
|
+
b'<?xml version="1.0" encoding="UTF-8"?>\n%b' % self.as_xml().encode("utf-8"),
|
|
52
|
+
content_type="text/xml; charset=utf-8",
|
|
53
|
+
status=self.status_code,
|
|
54
|
+
reason=self.reason,
|
|
55
|
+
)
|
|
56
|
+
|
|
44
57
|
def as_xml(self):
|
|
45
58
|
return format_html(
|
|
46
|
-
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
47
59
|
"<ows:ExceptionReport"
|
|
48
60
|
' xmlns:ows="http://www.opengis.net/ows/1.1"'
|
|
49
61
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
@@ -51,16 +63,18 @@ class OWSException(Exception):
|
|
|
51
63
|
' http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd"'
|
|
52
64
|
' xml:lang="en-US"'
|
|
53
65
|
' version="2.0.0">\n'
|
|
54
|
-
' <ows:Exception exceptionCode="{code}"
|
|
55
|
-
" <ows:ExceptionText>{text}{debug}</ows:ExceptionText>\n"
|
|
66
|
+
' <ows:Exception exceptionCode="{code}"{locator_attr}>\n\n'
|
|
67
|
+
" <ows:ExceptionText>{text}{debug}</ows:ExceptionText>\n\n"
|
|
56
68
|
" </ows:Exception>\n"
|
|
57
|
-
"</ows:ExceptionReport
|
|
69
|
+
"</ows:ExceptionReport>\n",
|
|
58
70
|
code=self.code,
|
|
59
|
-
|
|
71
|
+
locator_attr=(
|
|
72
|
+
format_html(' locator="{locator}"', locator=self.locator) if self.locator else ""
|
|
73
|
+
),
|
|
60
74
|
text=self.text,
|
|
61
75
|
debug=(
|
|
62
76
|
".\n\n(set GISSERVER_WRAP_FILTER_DB_ERRORS=False to see the Django error page)"
|
|
63
|
-
if settings.DEBUG and self.status_code >= 500
|
|
77
|
+
if settings.DEBUG and self.status_code >= 500 and self.debug_hint
|
|
64
78
|
else ""
|
|
65
79
|
),
|
|
66
80
|
)
|
|
@@ -18,10 +18,9 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import html
|
|
20
20
|
import operator
|
|
21
|
-
from collections import defaultdict
|
|
22
21
|
from dataclasses import dataclass
|
|
23
22
|
from functools import cached_property, lru_cache, reduce
|
|
24
|
-
from typing import Union
|
|
23
|
+
from typing import TYPE_CHECKING, Literal, Union
|
|
25
24
|
|
|
26
25
|
from django.conf import settings
|
|
27
26
|
from django.contrib.gis.db import models as gis_models
|
|
@@ -35,6 +34,7 @@ from gisserver.exceptions import ExternalValueError
|
|
|
35
34
|
from gisserver.geometries import CRS, WGS84, BoundingBox
|
|
36
35
|
from gisserver.types import (
|
|
37
36
|
GmlBoundedByElement,
|
|
37
|
+
GmlElement,
|
|
38
38
|
GmlIdAttribute,
|
|
39
39
|
GmlNameElement,
|
|
40
40
|
XPathMatch,
|
|
@@ -49,13 +49,10 @@ if "django.contrib.postgres" in settings.INSTALLED_APPS:
|
|
|
49
49
|
else:
|
|
50
50
|
ArrayField = None
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
from
|
|
54
|
-
|
|
55
|
-
_all_ = Literal["__all__"]
|
|
56
|
-
except ImportError:
|
|
57
|
-
_all_ = str
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from gisserver.queries import FeatureRelation
|
|
58
54
|
|
|
55
|
+
_all_ = Literal["__all__"]
|
|
59
56
|
|
|
60
57
|
__all__ = [
|
|
61
58
|
"FeatureType",
|
|
@@ -125,7 +122,12 @@ def get_basic_field_type(
|
|
|
125
122
|
return DEFAULT_XSD_TYPE
|
|
126
123
|
|
|
127
124
|
|
|
128
|
-
def _get_model_fields(
|
|
125
|
+
def _get_model_fields(
|
|
126
|
+
model: type[models.Model],
|
|
127
|
+
fields: _all_ | list[str],
|
|
128
|
+
parent: ComplexFeatureField | None = None,
|
|
129
|
+
feature_type: FeatureType | None = None,
|
|
130
|
+
):
|
|
129
131
|
if fields == "__all__":
|
|
130
132
|
# All regular fields
|
|
131
133
|
# Relationships will not be expanded since it can expose so many other fields.
|
|
@@ -136,6 +138,7 @@ def _get_model_fields(model, fields, parent=None):
|
|
|
136
138
|
# .bind() is called directly by providing these arguments via init:
|
|
137
139
|
model=model,
|
|
138
140
|
parent=parent,
|
|
141
|
+
feature_type=feature_type,
|
|
139
142
|
)
|
|
140
143
|
for f in model._meta.get_fields()
|
|
141
144
|
if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey # OneToOneField
|
|
@@ -144,7 +147,7 @@ def _get_model_fields(model, fields, parent=None):
|
|
|
144
147
|
# Only defined fields
|
|
145
148
|
fields = [f if isinstance(f, FeatureField) else FeatureField(f) for f in fields]
|
|
146
149
|
for field in fields:
|
|
147
|
-
field.bind(model, parent=parent)
|
|
150
|
+
field.bind(model, parent=parent, feature_type=feature_type)
|
|
148
151
|
return fields
|
|
149
152
|
|
|
150
153
|
|
|
@@ -180,15 +183,30 @@ class FeatureField:
|
|
|
180
183
|
model_attribute=None,
|
|
181
184
|
model=None,
|
|
182
185
|
parent: ComplexFeatureField | None = None,
|
|
186
|
+
feature_type: FeatureType | None = None,
|
|
183
187
|
abstract=None,
|
|
184
188
|
xsd_class: type[XsdElement] | None = None,
|
|
185
189
|
):
|
|
190
|
+
"""
|
|
191
|
+
Initialize a single field of the feature.
|
|
192
|
+
|
|
193
|
+
:param name: Name of the model field.
|
|
194
|
+
:param model_attribute: Which model attribute to access. This can be a dotted field path.
|
|
195
|
+
:param model: Which model is accessed. Usually this is passed via :meth:`bind`.
|
|
196
|
+
:param parent: The parent field of this element. Usually this is passed via :meth:`bind`.
|
|
197
|
+
:param feature_type: The feature this field is a part of. Usually this is passed via :meth:`bind`.
|
|
198
|
+
:param abstract: The "help text" or short abstract/description for this field.
|
|
199
|
+
:param xsd_class: Override which class is used to construct the internal XsdElement.
|
|
200
|
+
This controls the schema rendering, value retrieval and rendering of the element.
|
|
201
|
+
"""
|
|
186
202
|
self.name = name
|
|
187
203
|
self.model_attribute = model_attribute
|
|
188
204
|
self.model = None
|
|
189
205
|
self.model_field = None
|
|
190
206
|
self.parent = parent
|
|
207
|
+
self.feature_type = feature_type
|
|
191
208
|
self.abstract = abstract
|
|
209
|
+
|
|
192
210
|
# Allow to override the class attribute on 'self',
|
|
193
211
|
# which avoids having to subclass this field class as well.
|
|
194
212
|
if xsd_class is not None:
|
|
@@ -196,7 +214,7 @@ class FeatureField:
|
|
|
196
214
|
|
|
197
215
|
self._nillable_relation = False
|
|
198
216
|
if model is not None:
|
|
199
|
-
self.bind(model)
|
|
217
|
+
self.bind(model, parent=parent, feature_type=feature_type)
|
|
200
218
|
|
|
201
219
|
def __repr__(self):
|
|
202
220
|
return f"<{self.__class__.__name__}: {self.name}>"
|
|
@@ -208,6 +226,7 @@ class FeatureField:
|
|
|
208
226
|
self,
|
|
209
227
|
model: type[models.Model],
|
|
210
228
|
parent: ComplexFeatureField | None = None,
|
|
229
|
+
feature_type: FeatureType | None = None,
|
|
211
230
|
):
|
|
212
231
|
"""Late-binding for the model.
|
|
213
232
|
|
|
@@ -220,11 +239,13 @@ class FeatureField:
|
|
|
220
239
|
:param model: The model is field is linked to.
|
|
221
240
|
:param parent: When this element is part of a complex feature,
|
|
222
241
|
this links to the parent field.
|
|
242
|
+
:param feature_type: The original feature type that his element was mentioned in.
|
|
223
243
|
"""
|
|
224
244
|
if self.model is not None:
|
|
225
245
|
raise RuntimeError(f"Feature field '{self.name}' cannot be reused")
|
|
226
246
|
self.model = model
|
|
227
247
|
self.parent = parent
|
|
248
|
+
self.feature_type = feature_type
|
|
228
249
|
|
|
229
250
|
if self.model_attribute:
|
|
230
251
|
# Support dot based field traversal.
|
|
@@ -284,6 +305,7 @@ class FeatureField:
|
|
|
284
305
|
max_occurs=max_occurs,
|
|
285
306
|
model_attribute=self.model_attribute,
|
|
286
307
|
source=self.model_field,
|
|
308
|
+
feature_type=self.feature_type,
|
|
287
309
|
)
|
|
288
310
|
|
|
289
311
|
|
|
@@ -311,9 +333,14 @@ class ComplexFeatureField(FeatureField):
|
|
|
311
333
|
):
|
|
312
334
|
"""
|
|
313
335
|
:param name: Name of the model field.
|
|
314
|
-
:param fields:
|
|
315
|
-
be a list of :
|
|
336
|
+
:param fields: If the field exposes a foreign key, provide its child element names.
|
|
337
|
+
This can be a list of :func:`field` elements, or plain field names.
|
|
316
338
|
Using ``__all__`` also works but is not recommended outside testing.
|
|
339
|
+
:param model_attribute: Which model attribute to access. This can be a dotted field path.
|
|
340
|
+
:param abstract: The "help text" or short abstract/description for this field.
|
|
341
|
+
:param xsd_class: Override which class is used to construct the internal XsdElement.
|
|
342
|
+
This controls the schema rendering, value retrieval and rendering of the element.
|
|
343
|
+
:param xsd_base_type: Override which class is the base class for the element.
|
|
317
344
|
"""
|
|
318
345
|
super().__init__(
|
|
319
346
|
name,
|
|
@@ -343,6 +370,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
343
370
|
type_name=self.name,
|
|
344
371
|
source=pk_field,
|
|
345
372
|
model_attribute=pk_field.name,
|
|
373
|
+
feature_type=self.feature_type,
|
|
346
374
|
)
|
|
347
375
|
],
|
|
348
376
|
base=self.xsd_base_type,
|
|
@@ -377,9 +405,13 @@ def field(
|
|
|
377
405
|
so little knowledge is needed about the internal working.
|
|
378
406
|
|
|
379
407
|
:param name: Name of the model field.
|
|
380
|
-
:param
|
|
408
|
+
:param model_attribute: Which model attribute to access. This can be a dotted field path.
|
|
409
|
+
:param abstract: The "help text" or short abstract/description for this field.
|
|
410
|
+
:param fields: If the field exposes a foreign key, provide its child element names.
|
|
381
411
|
This can be a list of :func:`field` elements, or plain field names.
|
|
382
412
|
Using ``__all__`` also works but is not recommended outside testing.
|
|
413
|
+
:param xsd_class: Override which class is used to construct the internal XsdElement.
|
|
414
|
+
This controls the schema rendering, value retrieval and rendering of the element.
|
|
383
415
|
"""
|
|
384
416
|
if fields is not None:
|
|
385
417
|
return ComplexFeatureField(
|
|
@@ -398,42 +430,6 @@ def field(
|
|
|
398
430
|
)
|
|
399
431
|
|
|
400
432
|
|
|
401
|
-
@dataclass
|
|
402
|
-
class FeatureRelation:
|
|
403
|
-
"""Tell which related fields are queried by the feature.
|
|
404
|
-
Each dict holds an ORM-path, with the relevant sub-elements.
|
|
405
|
-
"""
|
|
406
|
-
|
|
407
|
-
#: The ORM path that is queried for this particular relation
|
|
408
|
-
orm_path: str
|
|
409
|
-
#: The fields that will be retrieved for that path
|
|
410
|
-
orm_fields: set[str]
|
|
411
|
-
#: The model that is accessed for this relation (if set)
|
|
412
|
-
related_model: type[models.Model] | None
|
|
413
|
-
#: The source elements that access this relation.
|
|
414
|
-
xsd_elements: list[XsdElement]
|
|
415
|
-
|
|
416
|
-
@cached_property
|
|
417
|
-
def _local_model_field_names(self) -> list[str]:
|
|
418
|
-
"""Tell which local fields of the model will be accessed by this feature."""
|
|
419
|
-
|
|
420
|
-
meta = self.related_model._meta
|
|
421
|
-
result = []
|
|
422
|
-
for name in self.orm_fields:
|
|
423
|
-
model_field = meta.get_field(name)
|
|
424
|
-
if not model_field.many_to_many and not model_field.one_to_many:
|
|
425
|
-
result.append(name)
|
|
426
|
-
|
|
427
|
-
# When this relation is retrieved through a ManyToOneRel (reverse FK),
|
|
428
|
-
# the prefetch_related() also needs to have the original foreign key
|
|
429
|
-
# in order to link all prefetches to the proper parent instance.
|
|
430
|
-
for xsd_element in self.xsd_elements:
|
|
431
|
-
if xsd_element.source is not None and xsd_element.source.one_to_many:
|
|
432
|
-
result.append(xsd_element.source.field.name)
|
|
433
|
-
|
|
434
|
-
return result
|
|
435
|
-
|
|
436
|
-
|
|
437
433
|
class FeatureType:
|
|
438
434
|
"""Declare a feature that is exposed on the map.
|
|
439
435
|
|
|
@@ -471,7 +467,7 @@ class FeatureType:
|
|
|
471
467
|
:param fields: Define which fields to show in the WFS data.
|
|
472
468
|
This can be a list of field names, or :class:`FeatureField` objects.
|
|
473
469
|
:param display_field_name: Name of the field that's used as general string representation.
|
|
474
|
-
:param
|
|
470
|
+
:param geometry_field_name: Name of the geometry field to expose (default = auto-detect).
|
|
475
471
|
:param name: Name, also used as XML tag name.
|
|
476
472
|
:param title: Used in WFS metadata.
|
|
477
473
|
:param abstract: Used in WFS metadata.
|
|
@@ -529,6 +525,11 @@ class FeatureType:
|
|
|
529
525
|
"""Return all spatial reference system ID's that this feature supports."""
|
|
530
526
|
return [self.crs] + self.other_crs
|
|
531
527
|
|
|
528
|
+
@cached_property
|
|
529
|
+
def geometry_elements(self) -> list[GmlElement]:
|
|
530
|
+
"""Tell which fields of the XSD have a geometry field."""
|
|
531
|
+
return [ff.xsd_element for ff in self.fields if ff.xsd_element.is_geometry]
|
|
532
|
+
|
|
532
533
|
@cached_property
|
|
533
534
|
def geometry_fields(self) -> list[GeometryField]:
|
|
534
535
|
"""Tell which fields of the model have a geometry field.
|
|
@@ -540,52 +541,6 @@ class FeatureType:
|
|
|
540
541
|
if ff.xsd_element.is_geometry
|
|
541
542
|
]
|
|
542
543
|
|
|
543
|
-
@cached_property
|
|
544
|
-
def orm_relations(self) -> list[FeatureRelation]:
|
|
545
|
-
"""Tell which fields will be retrieved from related fields.
|
|
546
|
-
|
|
547
|
-
This gives an object layout based on the XSD elements,
|
|
548
|
-
that can be used for prefetching data.
|
|
549
|
-
"""
|
|
550
|
-
models = {}
|
|
551
|
-
fields = defaultdict(set)
|
|
552
|
-
elements = defaultdict(list)
|
|
553
|
-
|
|
554
|
-
# Check all elements that render as "dotted" flattened relation
|
|
555
|
-
for xsd_element in self.xsd_type.flattened_elements:
|
|
556
|
-
if xsd_element.source is not None:
|
|
557
|
-
# Split "relation.field" notation into path, and take the field as child attribute.
|
|
558
|
-
obj_path, field = xsd_element.orm_relation
|
|
559
|
-
elements[obj_path].append(xsd_element)
|
|
560
|
-
fields[obj_path].add(field)
|
|
561
|
-
# field is already on relation:
|
|
562
|
-
models[obj_path] = xsd_element.source.model
|
|
563
|
-
|
|
564
|
-
# Check all elements that render as "nested" complex type:
|
|
565
|
-
for xsd_element in self.xsd_type.complex_elements:
|
|
566
|
-
# The complex element itself points to the root of the path,
|
|
567
|
-
# all sub elements become the child attributes.
|
|
568
|
-
obj_path = xsd_element.orm_path
|
|
569
|
-
elements[obj_path].append(xsd_element)
|
|
570
|
-
fields[obj_path] = {
|
|
571
|
-
f.orm_path
|
|
572
|
-
for f in xsd_element.type.elements
|
|
573
|
-
if not f.is_many or f.is_array # exclude M2M, but include ArrayField
|
|
574
|
-
}
|
|
575
|
-
if xsd_element.source:
|
|
576
|
-
# field references a related object:
|
|
577
|
-
models[obj_path] = xsd_element.source.related_model
|
|
578
|
-
|
|
579
|
-
return [
|
|
580
|
-
FeatureRelation(
|
|
581
|
-
orm_path=obj_path,
|
|
582
|
-
orm_fields=sub_fields,
|
|
583
|
-
related_model=models.get(obj_path),
|
|
584
|
-
xsd_elements=elements[obj_path],
|
|
585
|
-
)
|
|
586
|
-
for obj_path, sub_fields in fields.items()
|
|
587
|
-
]
|
|
588
|
-
|
|
589
544
|
@cached_property
|
|
590
545
|
def _local_model_field_names(self) -> list[str]:
|
|
591
546
|
"""Tell which local fields of the model will be accessed by this feature."""
|
|
@@ -610,9 +565,9 @@ class FeatureType:
|
|
|
610
565
|
if isinstance(f, gis_models.GeometryField)
|
|
611
566
|
)
|
|
612
567
|
|
|
613
|
-
return [FeatureField(self._geometry_field_name, model=self.model)]
|
|
568
|
+
return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
|
|
614
569
|
else:
|
|
615
|
-
return _get_model_fields(self.model, self._fields)
|
|
570
|
+
return _get_model_fields(self.model, self._fields, feature_type=self)
|
|
616
571
|
|
|
617
572
|
@cached_property
|
|
618
573
|
def display_field(self) -> models.Field | None:
|
|
@@ -653,6 +608,13 @@ class FeatureType:
|
|
|
653
608
|
# Default: take the first geometry
|
|
654
609
|
return self.geometry_fields[0]
|
|
655
610
|
|
|
611
|
+
@cached_property
|
|
612
|
+
def main_geometry_element(self) -> GmlElement:
|
|
613
|
+
"""Give access to the main geometry element."""
|
|
614
|
+
# NOTE: this property should make it easier to move away from having a main geometry field
|
|
615
|
+
# at the model level, and allow a geometry field in any depth of the data model.
|
|
616
|
+
return next((e for e in self.geometry_elements if e.source is self.geometry_field), None)
|
|
617
|
+
|
|
656
618
|
@cached_property
|
|
657
619
|
def geometry_field_name(self) -> str:
|
|
658
620
|
"""Tell which field is the geometry field."""
|
|
@@ -772,6 +734,7 @@ class FeatureType:
|
|
|
772
734
|
type_name=self.name,
|
|
773
735
|
source=pk_field,
|
|
774
736
|
model_attribute=pk_field.name,
|
|
737
|
+
feature_type=self,
|
|
775
738
|
)
|
|
776
739
|
],
|
|
777
740
|
base=XsdTypes.gmlAbstractGMLType,
|
|
@@ -123,7 +123,7 @@ class CRS:
|
|
|
123
123
|
|
|
124
124
|
@classmethod
|
|
125
125
|
def from_srid(cls, srid: int, backend=None):
|
|
126
|
-
"""Instantiate this class using
|
|
126
|
+
"""Instantiate this class using a numeric spatial reference ID
|
|
127
127
|
|
|
128
128
|
This is logically identical to calling::
|
|
129
129
|
|
|
@@ -142,7 +142,7 @@ class CRS:
|
|
|
142
142
|
|
|
143
143
|
@classmethod
|
|
144
144
|
def _from_urn(cls, urn, backend=None): # noqa: C901
|
|
145
|
-
"""Instantiate this class using
|
|
145
|
+
"""Instantiate this class using a URN format."""
|
|
146
146
|
urn_match = CRS_URN_REGEX.match(urn)
|
|
147
147
|
if not urn_match:
|
|
148
148
|
raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
|