django-gisserver 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/METADATA +15 -13
- django_gisserver-1.5.0.dist-info/RECORD +54 -0
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/db.py +14 -20
- gisserver/exceptions.py +23 -9
- gisserver/features.py +64 -100
- gisserver/geometries.py +2 -2
- gisserver/operations/base.py +31 -21
- gisserver/operations/wfs20.py +44 -38
- gisserver/output/__init__.py +2 -1
- gisserver/output/base.py +43 -27
- gisserver/output/csv.py +38 -33
- gisserver/output/geojson.py +43 -51
- gisserver/output/gml32.py +88 -67
- gisserver/output/results.py +23 -8
- gisserver/output/utils.py +18 -2
- gisserver/output/xmlschema.py +1 -1
- gisserver/parsers/base.py +2 -2
- gisserver/parsers/fes20/__init__.py +18 -0
- gisserver/parsers/fes20/expressions.py +7 -12
- gisserver/parsers/fes20/functions.py +1 -1
- gisserver/parsers/fes20/operators.py +7 -3
- gisserver/parsers/fes20/query.py +11 -1
- gisserver/parsers/fes20/sorting.py +3 -1
- gisserver/parsers/gml/base.py +1 -1
- gisserver/queries/__init__.py +3 -0
- gisserver/queries/adhoc.py +16 -12
- gisserver/queries/base.py +76 -36
- gisserver/queries/projection.py +240 -0
- gisserver/queries/stored.py +7 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
- gisserver/types.py +78 -24
- gisserver/views.py +9 -20
- django_gisserver-1.4.0.dist-info/RECORD +0 -54
- gisserver/output/gml32_lxml.py +0 -612
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -12,33 +12,35 @@ 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.
|
|
30
|
+
Requires-Python: >=3.8
|
|
29
31
|
Description-Content-Type: text/markdown
|
|
30
32
|
License-File: LICENSE
|
|
31
|
-
Requires-Dist: Django
|
|
32
|
-
Requires-Dist: defusedxml
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist: orjson
|
|
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
|
|
35
37
|
Provides-Extra: tests
|
|
36
|
-
Requires-Dist: django-environ
|
|
37
|
-
Requires-Dist: psycopg2-binary
|
|
38
|
-
Requires-Dist: lxml
|
|
39
|
-
Requires-Dist: pytest
|
|
40
|
-
Requires-Dist: pytest-django
|
|
41
|
-
Requires-Dist: pytest-cov
|
|
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"
|
|
42
44
|
|
|
43
45
|
[](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
|
|
44
46
|
[](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
gisserver/__init__.py,sha256=XtaIlx7Gik8RqIFDc1G0JAUfcmeNyTWjev1E1Oym_ms,40
|
|
2
|
+
gisserver/conf.py,sha256=3LSfeDRTdJCLuopAOD14Y5m2iPhovreIaNI9zbaukpo,2343
|
|
3
|
+
gisserver/db.py,sha256=-d6VQRkRWMSWKkA_6cVWhLzSI1cKmRoVktqA6hVwvuM,4928
|
|
4
|
+
gisserver/exceptions.py,sha256=wRsiKWhZtA8NmfMNncRtMeFgWDMdOWRp9Ey96iFAwMI,5194
|
|
5
|
+
gisserver/features.py,sha256=b3hEDs90xwEsANcRgipekZZrp4ucxRWwGeUutk2gRTw,31821
|
|
6
|
+
gisserver/geometries.py,sha256=HLZ3d8E5M2sfs2Kzz_GE3pZdynn-rYXPpuU9GVh0vsg,12398
|
|
7
|
+
gisserver/types.py,sha256=bsl3PezhGcHpUAtuV4AhdBmFLmtZZpDCT_HqpdVyLtM,36936
|
|
8
|
+
gisserver/views.py,sha256=xMHKezn4F1xZ2r78l9IzUlL5M3qZ78fjRJJtSzA8-Ww,12809
|
|
9
|
+
gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
gisserver/operations/base.py,sha256=Ls5i6ulMJqyhqu6o969rJ-mezW-T6OW5mr8oZ3BTBH8,17222
|
|
11
|
+
gisserver/operations/wfs20.py,sha256=bQeMJ4T6AisSBMABYa3li1u53w0zUxs02qBwjS4QbOw,18742
|
|
12
|
+
gisserver/output/__init__.py,sha256=C6hXIVIxBd2o_1Y7VhIU9vCg7pswI2tXsd74wgMdU4o,2134
|
|
13
|
+
gisserver/output/base.py,sha256=GygubFyhPO33fQw3jIuOerEH8yjbk0DJ6NhQdFkT7-g,8645
|
|
14
|
+
gisserver/output/csv.py,sha256=Mm7zYzUs9ffw9fqcKY9NT7qbMX-MzSFnqBg5I1wdRU0,6459
|
|
15
|
+
gisserver/output/geojson.py,sha256=bhRtNdMi1tMcSFBxDTNL8oysAMVhoolctKjZhxIncN8,11297
|
|
16
|
+
gisserver/output/gml32.py,sha256=KbhcBBVp4sEhvKs5e6fP4_GKKL6Kr2yyYaDLWePBQxs,31293
|
|
17
|
+
gisserver/output/results.py,sha256=VD-fJgWrMTwGaEGG_BSVhDv_m60dldBY6yAcTpRICT4,13949
|
|
18
|
+
gisserver/output/utils.py,sha256=FIlkHymA43RLZAfB6-trM3_IQKbIVb_4OUp5fxRbVGY,7379
|
|
19
|
+
gisserver/output/xmlschema.py,sha256=TdTMyP2_r98wkeffWAwrfm3l9XAmivu1KcYWNpgQKR4,4578
|
|
20
|
+
gisserver/parsers/__init__.py,sha256=g72D8EtR-QkhcemzY5-Qty-nhHPKza83Sz3KZm8bfq0,290
|
|
21
|
+
gisserver/parsers/base.py,sha256=9DE73wMphp7At3mGW0xwGU_TWPm55x8JEh4odpdRhlo,4833
|
|
22
|
+
gisserver/parsers/tags.py,sha256=NyJhbt-U2zTewfhghH4LRslTaDK2UOq9Vgk83pWfJh0,3553
|
|
23
|
+
gisserver/parsers/values.py,sha256=ypNPl4HduiVzexsL59ukZntlwXk5gHuSvQO6yhLeXD4,1016
|
|
24
|
+
gisserver/parsers/fes20/__init__.py,sha256=72RuFxAQp9hW0pgFUDyfQ0WsStNNk6J_y3NH-zYaPOA,1140
|
|
25
|
+
gisserver/parsers/fes20/expressions.py,sha256=A1-8JsGahXJPWLuTSVNAf0oSuyLfDMnFKM7viIt35Y8,9573
|
|
26
|
+
gisserver/parsers/fes20/filters.py,sha256=HdWguZvAT2L4Xm-bNa4XPh7MEymqTB6fo8r47Z-Bjuw,3511
|
|
27
|
+
gisserver/parsers/fes20/functions.py,sha256=mutPlbjOlg6R-9nNe8goaNdsAlZPkuckYlhMsQyp6NE,8723
|
|
28
|
+
gisserver/parsers/fes20/identifiers.py,sha256=vcPrMYcsemSlB9ziSUdVulhoI5PGFAh8vqeADBCWd60,2864
|
|
29
|
+
gisserver/parsers/fes20/operators.py,sha256=oQezU553Mts1DoExlH7_U29XXXdk6V84ihlBOozRzLg,23360
|
|
30
|
+
gisserver/parsers/fes20/query.py,sha256=_OxElAgeo34-00pNY5Apc6TSUKokSG9VWSMmIplN7AA,10638
|
|
31
|
+
gisserver/parsers/fes20/sorting.py,sha256=Uj8gsbBXumihWkrcj_pRPscwZAScUJSngAyjKri-Z8Y,2184
|
|
32
|
+
gisserver/parsers/gml/__init__.py,sha256=lNK0SXWkmTKdeJD9xdK6B-N-Y7wwGL4NRBI3dzoJDNI,1463
|
|
33
|
+
gisserver/parsers/gml/base.py,sha256=xWC5FrJyMjq7h5zmKwEvtRF0-CGVbCtvnO8Isvnf4mE,1065
|
|
34
|
+
gisserver/parsers/gml/geometries.py,sha256=cgV3WogA7LyC_H_NF7dGoGLvLdQRACG6AlThRkX_jhU,3171
|
|
35
|
+
gisserver/queries/__init__.py,sha256=yntEYRfaZ1Yjnr8lMvJt9Erh31WktikIf1af4TTuX74,1000
|
|
36
|
+
gisserver/queries/adhoc.py,sha256=Hhcxwt5C2OHqtLc-3QUqx8MZEnhoqkKWjBp56e6aljc,7428
|
|
37
|
+
gisserver/queries/base.py,sha256=EVJiwcLBjxGXJ8k012gGEiZ1q_gQZokSwdMl4H1SPus,7857
|
|
38
|
+
gisserver/queries/projection.py,sha256=KEz9UcZGVkYq7XTm_5nx8w5TnbSqht7rOGEuTGeJwm8,9922
|
|
39
|
+
gisserver/queries/stored.py,sha256=G258R9HVX3OooM5mhQF_N_JrFHLjM2y5iusrzPcN9TU,7125
|
|
40
|
+
gisserver/static/gisserver/index.css,sha256=fiQuCMuiWUhhrNQD6bbUJaX5vH5OpG9qgPdELPB8cpI,163
|
|
41
|
+
gisserver/templates/gisserver/index.html,sha256=yliku8jDqAOcenkbe1YnvJBnuS13MvzzsoDV8sQZ3GM,866
|
|
42
|
+
gisserver/templates/gisserver/service_description.html,sha256=3EHLnQx27_nTDZFD2RH6E10EWnTN90s1DGuEj9PeBrg,993
|
|
43
|
+
gisserver/templates/gisserver/wfs/feature_field.html,sha256=H5LsTAxGVGQqEpOZdEUTszQmFAZSpB9ggRJUsTdjYnw,538
|
|
44
|
+
gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
|
|
45
|
+
gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml,sha256=4Hxw1kCnxfWXTVPbHf8IW9_qiSeIEtnxHK6z6TS3-zA,1041
|
|
46
|
+
gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=Ej6ZXf-nGiPjiKdhNK5vAsX2iiiS12knjD0VaSzSltM,8717
|
|
47
|
+
gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml,sha256=CIBVQrKXmGMQ6BKbX4_0ru4lXtWnW5EIgHUbIi1V0Vc,636
|
|
48
|
+
gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
gisserver/templatetags/gisserver_tags.py,sha256=RFGqEj-EQi9FrJiFJ7HHAQqlU4fDKVKTtZrPNwURXgA,204
|
|
50
|
+
django_gisserver-1.5.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
51
|
+
django_gisserver-1.5.0.dist-info/METADATA,sha256=IWlSvP8d_4wsCVq4NWAE0T7Q8QZGqFVMvWUyLss85aY,5859
|
|
52
|
+
django_gisserver-1.5.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
53
|
+
django_gisserver-1.5.0.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
|
|
54
|
+
django_gisserver-1.5.0.dist-info/RECORD,,
|
gisserver/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.5.0" # follows PEP440
|
gisserver/db.py
CHANGED
|
@@ -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
|
+
)
|
gisserver/exceptions.py
CHANGED
|
@@ -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
|
)
|
gisserver/features.py
CHANGED
|
@@ -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,14 +183,28 @@ 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
|
|
192
209
|
|
|
193
210
|
# Allow to override the class attribute on 'self',
|
|
@@ -197,7 +214,7 @@ class FeatureField:
|
|
|
197
214
|
|
|
198
215
|
self._nillable_relation = False
|
|
199
216
|
if model is not None:
|
|
200
|
-
self.bind(model)
|
|
217
|
+
self.bind(model, parent=parent, feature_type=feature_type)
|
|
201
218
|
|
|
202
219
|
def __repr__(self):
|
|
203
220
|
return f"<{self.__class__.__name__}: {self.name}>"
|
|
@@ -209,6 +226,7 @@ class FeatureField:
|
|
|
209
226
|
self,
|
|
210
227
|
model: type[models.Model],
|
|
211
228
|
parent: ComplexFeatureField | None = None,
|
|
229
|
+
feature_type: FeatureType | None = None,
|
|
212
230
|
):
|
|
213
231
|
"""Late-binding for the model.
|
|
214
232
|
|
|
@@ -221,11 +239,13 @@ class FeatureField:
|
|
|
221
239
|
:param model: The model is field is linked to.
|
|
222
240
|
:param parent: When this element is part of a complex feature,
|
|
223
241
|
this links to the parent field.
|
|
242
|
+
:param feature_type: The original feature type that his element was mentioned in.
|
|
224
243
|
"""
|
|
225
244
|
if self.model is not None:
|
|
226
245
|
raise RuntimeError(f"Feature field '{self.name}' cannot be reused")
|
|
227
246
|
self.model = model
|
|
228
247
|
self.parent = parent
|
|
248
|
+
self.feature_type = feature_type
|
|
229
249
|
|
|
230
250
|
if self.model_attribute:
|
|
231
251
|
# Support dot based field traversal.
|
|
@@ -285,6 +305,7 @@ class FeatureField:
|
|
|
285
305
|
max_occurs=max_occurs,
|
|
286
306
|
model_attribute=self.model_attribute,
|
|
287
307
|
source=self.model_field,
|
|
308
|
+
feature_type=self.feature_type,
|
|
288
309
|
)
|
|
289
310
|
|
|
290
311
|
|
|
@@ -312,9 +333,14 @@ class ComplexFeatureField(FeatureField):
|
|
|
312
333
|
):
|
|
313
334
|
"""
|
|
314
335
|
:param name: Name of the model field.
|
|
315
|
-
:param fields:
|
|
316
|
-
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.
|
|
317
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.
|
|
318
344
|
"""
|
|
319
345
|
super().__init__(
|
|
320
346
|
name,
|
|
@@ -344,6 +370,7 @@ class ComplexFeatureField(FeatureField):
|
|
|
344
370
|
type_name=self.name,
|
|
345
371
|
source=pk_field,
|
|
346
372
|
model_attribute=pk_field.name,
|
|
373
|
+
feature_type=self.feature_type,
|
|
347
374
|
)
|
|
348
375
|
],
|
|
349
376
|
base=self.xsd_base_type,
|
|
@@ -378,9 +405,13 @@ def field(
|
|
|
378
405
|
so little knowledge is needed about the internal working.
|
|
379
406
|
|
|
380
407
|
:param name: Name of the model field.
|
|
381
|
-
: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.
|
|
382
411
|
This can be a list of :func:`field` elements, or plain field names.
|
|
383
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.
|
|
384
415
|
"""
|
|
385
416
|
if fields is not None:
|
|
386
417
|
return ComplexFeatureField(
|
|
@@ -399,42 +430,6 @@ def field(
|
|
|
399
430
|
)
|
|
400
431
|
|
|
401
432
|
|
|
402
|
-
@dataclass
|
|
403
|
-
class FeatureRelation:
|
|
404
|
-
"""Tell which related fields are queried by the feature.
|
|
405
|
-
Each dict holds an ORM-path, with the relevant sub-elements.
|
|
406
|
-
"""
|
|
407
|
-
|
|
408
|
-
#: The ORM path that is queried for this particular relation
|
|
409
|
-
orm_path: str
|
|
410
|
-
#: The fields that will be retrieved for that path
|
|
411
|
-
orm_fields: set[str]
|
|
412
|
-
#: The model that is accessed for this relation (if set)
|
|
413
|
-
related_model: type[models.Model] | None
|
|
414
|
-
#: The source elements that access this relation.
|
|
415
|
-
xsd_elements: list[XsdElement]
|
|
416
|
-
|
|
417
|
-
@cached_property
|
|
418
|
-
def _local_model_field_names(self) -> list[str]:
|
|
419
|
-
"""Tell which local fields of the model will be accessed by this feature."""
|
|
420
|
-
|
|
421
|
-
meta = self.related_model._meta
|
|
422
|
-
result = []
|
|
423
|
-
for name in self.orm_fields:
|
|
424
|
-
model_field = meta.get_field(name)
|
|
425
|
-
if not model_field.many_to_many and not model_field.one_to_many:
|
|
426
|
-
result.append(name)
|
|
427
|
-
|
|
428
|
-
# When this relation is retrieved through a ManyToOneRel (reverse FK),
|
|
429
|
-
# the prefetch_related() also needs to have the original foreign key
|
|
430
|
-
# in order to link all prefetches to the proper parent instance.
|
|
431
|
-
for xsd_element in self.xsd_elements:
|
|
432
|
-
if xsd_element.source is not None and xsd_element.source.one_to_many:
|
|
433
|
-
result.append(xsd_element.source.field.name)
|
|
434
|
-
|
|
435
|
-
return result
|
|
436
|
-
|
|
437
|
-
|
|
438
433
|
class FeatureType:
|
|
439
434
|
"""Declare a feature that is exposed on the map.
|
|
440
435
|
|
|
@@ -472,7 +467,7 @@ class FeatureType:
|
|
|
472
467
|
:param fields: Define which fields to show in the WFS data.
|
|
473
468
|
This can be a list of field names, or :class:`FeatureField` objects.
|
|
474
469
|
:param display_field_name: Name of the field that's used as general string representation.
|
|
475
|
-
:param
|
|
470
|
+
:param geometry_field_name: Name of the geometry field to expose (default = auto-detect).
|
|
476
471
|
:param name: Name, also used as XML tag name.
|
|
477
472
|
:param title: Used in WFS metadata.
|
|
478
473
|
:param abstract: Used in WFS metadata.
|
|
@@ -530,6 +525,11 @@ class FeatureType:
|
|
|
530
525
|
"""Return all spatial reference system ID's that this feature supports."""
|
|
531
526
|
return [self.crs] + self.other_crs
|
|
532
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
|
+
|
|
533
533
|
@cached_property
|
|
534
534
|
def geometry_fields(self) -> list[GeometryField]:
|
|
535
535
|
"""Tell which fields of the model have a geometry field.
|
|
@@ -541,59 +541,15 @@ class FeatureType:
|
|
|
541
541
|
if ff.xsd_element.is_geometry
|
|
542
542
|
]
|
|
543
543
|
|
|
544
|
-
@cached_property
|
|
545
|
-
def orm_relations(self) -> list[FeatureRelation]:
|
|
546
|
-
"""Tell which fields will be retrieved from related fields.
|
|
547
|
-
|
|
548
|
-
This gives an object layout based on the XSD elements,
|
|
549
|
-
that can be used for prefetching data.
|
|
550
|
-
"""
|
|
551
|
-
models = {}
|
|
552
|
-
fields = defaultdict(set)
|
|
553
|
-
elements = defaultdict(list)
|
|
554
|
-
|
|
555
|
-
# Check all elements that render as "dotted" flattened relation
|
|
556
|
-
for xsd_element in self.xsd_type.flattened_elements:
|
|
557
|
-
if xsd_element.source is not None:
|
|
558
|
-
# Split "relation.field" notation into path, and take the field as child attribute.
|
|
559
|
-
obj_path, field = xsd_element.orm_relation
|
|
560
|
-
elements[obj_path].append(xsd_element)
|
|
561
|
-
fields[obj_path].add(field)
|
|
562
|
-
# field is already on relation:
|
|
563
|
-
models[obj_path] = xsd_element.source.model
|
|
564
|
-
|
|
565
|
-
# Check all elements that render as "nested" complex type:
|
|
566
|
-
for xsd_element in self.xsd_type.complex_elements:
|
|
567
|
-
# The complex element itself points to the root of the path,
|
|
568
|
-
# all sub elements become the child attributes.
|
|
569
|
-
obj_path = xsd_element.orm_path
|
|
570
|
-
elements[obj_path].append(xsd_element)
|
|
571
|
-
fields[obj_path] = {
|
|
572
|
-
f.orm_path
|
|
573
|
-
for f in xsd_element.type.elements
|
|
574
|
-
if not f.is_many or f.is_array # exclude M2M, but include ArrayField
|
|
575
|
-
}
|
|
576
|
-
if xsd_element.source:
|
|
577
|
-
# field references a related object:
|
|
578
|
-
models[obj_path] = xsd_element.source.related_model
|
|
579
|
-
|
|
580
|
-
return [
|
|
581
|
-
FeatureRelation(
|
|
582
|
-
orm_path=obj_path,
|
|
583
|
-
orm_fields=sub_fields,
|
|
584
|
-
related_model=models.get(obj_path),
|
|
585
|
-
xsd_elements=elements[obj_path],
|
|
586
|
-
)
|
|
587
|
-
for obj_path, sub_fields in fields.items()
|
|
588
|
-
]
|
|
589
|
-
|
|
590
544
|
@cached_property
|
|
591
545
|
def _local_model_field_names(self) -> list[str]:
|
|
592
546
|
"""Tell which local fields of the model will be accessed by this feature."""
|
|
593
547
|
return [
|
|
594
548
|
(ff.model_field.name if ff.model_field else ff.model_attribute)
|
|
595
549
|
for ff in self.fields
|
|
596
|
-
if not ff.model_field.many_to_many
|
|
550
|
+
if not ff.model_field.many_to_many
|
|
551
|
+
and not ff.model_field.one_to_many
|
|
552
|
+
and not (ff.model_attribute and "." in ff.model_attribute)
|
|
597
553
|
]
|
|
598
554
|
|
|
599
555
|
@cached_property
|
|
@@ -609,9 +565,9 @@ class FeatureType:
|
|
|
609
565
|
if isinstance(f, gis_models.GeometryField)
|
|
610
566
|
)
|
|
611
567
|
|
|
612
|
-
return [FeatureField(self._geometry_field_name, model=self.model)]
|
|
568
|
+
return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
|
|
613
569
|
else:
|
|
614
|
-
return _get_model_fields(self.model, self._fields)
|
|
570
|
+
return _get_model_fields(self.model, self._fields, feature_type=self)
|
|
615
571
|
|
|
616
572
|
@cached_property
|
|
617
573
|
def display_field(self) -> models.Field | None:
|
|
@@ -652,6 +608,13 @@ class FeatureType:
|
|
|
652
608
|
# Default: take the first geometry
|
|
653
609
|
return self.geometry_fields[0]
|
|
654
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
|
+
|
|
655
618
|
@cached_property
|
|
656
619
|
def geometry_field_name(self) -> str:
|
|
657
620
|
"""Tell which field is the geometry field."""
|
|
@@ -771,6 +734,7 @@ class FeatureType:
|
|
|
771
734
|
type_name=self.name,
|
|
772
735
|
source=pk_field,
|
|
773
736
|
model_attribute=pk_field.name,
|
|
737
|
+
feature_type=self,
|
|
774
738
|
)
|
|
775
739
|
],
|
|
776
740
|
base=XsdTypes.gmlAbstractGMLType,
|
gisserver/geometries.py
CHANGED
|
@@ -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}")
|