django-gisserver 1.3.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.
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/CHANGES.md +18 -3
- {django-gisserver-1.3.0/django_gisserver.egg-info → django-gisserver-1.4.1}/PKG-INFO +6 -7
- {django-gisserver-1.3.0 → django-gisserver-1.4.1/django_gisserver.egg-info}/PKG-INFO +6 -7
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/SOURCES.txt +1 -2
- django-gisserver-1.4.1/gisserver/__init__.py +1 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/conf.py +9 -12
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/db.py +6 -10
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/exceptions.py +1 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/features.py +21 -31
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/geometries.py +11 -25
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/operations/base.py +19 -40
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/operations/wfs20.py +8 -20
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/__init__.py +7 -2
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/base.py +4 -13
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/csv.py +13 -16
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/geojson.py +14 -17
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/gml32.py +309 -281
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/results.py +105 -22
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/utils.py +15 -5
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/output/xmlschema.py +7 -8
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/base.py +2 -4
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/expressions.py +6 -13
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/filters.py +6 -5
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/functions.py +4 -4
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/identifiers.py +1 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/operators.py +16 -43
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/query.py +1 -3
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/sorting.py +1 -3
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/gml/__init__.py +1 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/gml/base.py +1 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/values.py +1 -3
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/queries/__init__.py +1 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/queries/adhoc.py +3 -6
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/queries/base.py +2 -6
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/queries/stored.py +3 -6
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/types.py +59 -46
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/views.py +7 -10
- django-gisserver-1.4.1/pyproject.toml +96 -0
- django-gisserver-1.4.1/setup.cfg +4 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/setup.py +5 -3
- django-gisserver-1.3.0/gisserver/__init__.py +0 -1
- django-gisserver-1.3.0/gisserver/output/buffer.py +0 -64
- django-gisserver-1.3.0/setup.cfg +0 -39
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/LICENSE +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/MANIFEST.in +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/README.md +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/dependency_links.txt +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/not-zip-safe +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/requires.txt +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/django_gisserver.egg-info/top_level.txt +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/operations/__init__.py +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/__init__.py +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/fes20/__init__.py +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/gml/geometries.py +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/parsers/tags.py +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/static/gisserver/index.css +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/index.html +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/service_description.html +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templates/gisserver/wfs/feature_type.html +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templatetags/__init__.py +0 -0
- {django-gisserver-1.3.0 → django-gisserver-1.4.1}/gisserver/templatetags/gisserver_tags.py +0 -0
|
@@ -1,6 +1,21 @@
|
|
|
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
|
+
|
|
7
|
+
# 2024-07-01 (1.4.0)
|
|
8
|
+
|
|
9
|
+
* Added `GISSERVER_COUNT_NUMBER_MATCHED` setting to allow disabling "numberReturned" counting.
|
|
10
|
+
This will avoid an expensive PostgreSQL COUNT query, speeding up returning large sets.
|
|
11
|
+
* Added basic support for array fields in `GetPropertyValue`.
|
|
12
|
+
* Optimized GML rendering performance (around 15-20% faster on large datasets).
|
|
13
|
+
* Optimized overall rendering performance, which improved GeoJSON/CSV output too.
|
|
14
|
+
* Developers: updated pre-commit hooks
|
|
15
|
+
* Cleaned up leftover Python 3.7 compat code.
|
|
16
|
+
|
|
1
17
|
# 2023-06-08 (1.3.0)
|
|
2
18
|
|
|
3
|
-
## Updates
|
|
4
19
|
* Django 5 support added.
|
|
5
20
|
|
|
6
21
|
## Contributors
|
|
@@ -8,7 +23,7 @@ We would like to thank the following contributors
|
|
|
8
23
|
for their work on this release.
|
|
9
24
|
|
|
10
25
|
- [tomdtp](https://github.com/tomdtp)
|
|
11
|
-
|
|
26
|
+
|
|
12
27
|
# 2023-06-08 (1.2.7)
|
|
13
28
|
|
|
14
29
|
* WFS endpoints now accept a GML version number in their OUTPUTFORMAT.
|
|
@@ -42,7 +57,7 @@ for their work on this release.
|
|
|
42
57
|
# 2022-04-13 (1.2.1)
|
|
43
58
|
|
|
44
59
|
* Fixed regression for auto-correcting xmlns for `<Filter>` tags that have leading whitespace.
|
|
45
|
-
* Fixed weird crashes when geometry field is not provided.
|
|
60
|
+
* Fixed weird crashes when geometry field is not provided.
|
|
46
61
|
* Simplify `FeatureType.geometry_field` logic.
|
|
47
62
|
|
|
48
63
|
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.
|
|
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
|
|
7
7
|
Author-email: opensource@edoburu.nl
|
|
8
8
|
License: Mozilla Public License 2.0
|
|
9
|
-
Platform: UNKNOWN
|
|
10
9
|
Classifier: Development Status :: 4 - Beta
|
|
11
10
|
Classifier: Environment :: Web Environment
|
|
12
11
|
Classifier: Intended Audience :: Developers
|
|
13
12
|
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Framework :: Django
|
|
21
20
|
Classifier: Framework :: Django :: 3.2
|
|
22
21
|
Classifier: Framework :: Django :: 4.0
|
|
23
22
|
Classifier: Framework :: Django :: 4.2
|
|
23
|
+
Classifier: Framework :: Django :: 5.0
|
|
24
|
+
Classifier: Framework :: Django :: 5.1
|
|
24
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
26
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
27
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
27
28
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
29
|
Requires: Django (>=3.2)
|
|
29
|
-
Requires-Python: >=3.
|
|
30
|
+
Requires-Python: >=3.8
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
Provides-Extra: tests
|
|
32
33
|
License-File: LICENSE
|
|
@@ -157,5 +158,3 @@ software developed with public money is also publicly available.
|
|
|
157
158
|
|
|
158
159
|
This package is initially developed by the City of Amsterdam, but the tools
|
|
159
160
|
and concepts created in this project can be used in any city.
|
|
160
|
-
|
|
161
|
-
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.
|
|
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
|
|
7
7
|
Author-email: opensource@edoburu.nl
|
|
8
8
|
License: Mozilla Public License 2.0
|
|
9
|
-
Platform: UNKNOWN
|
|
10
9
|
Classifier: Development Status :: 4 - Beta
|
|
11
10
|
Classifier: Environment :: Web Environment
|
|
12
11
|
Classifier: Intended Audience :: Developers
|
|
13
12
|
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Framework :: Django
|
|
21
20
|
Classifier: Framework :: Django :: 3.2
|
|
22
21
|
Classifier: Framework :: Django :: 4.0
|
|
23
22
|
Classifier: Framework :: Django :: 4.2
|
|
23
|
+
Classifier: Framework :: Django :: 5.0
|
|
24
|
+
Classifier: Framework :: Django :: 5.1
|
|
24
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
26
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
27
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
27
28
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
29
|
Requires: Django (>=3.2)
|
|
29
|
-
Requires-Python: >=3.
|
|
30
|
+
Requires-Python: >=3.8
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
Provides-Extra: tests
|
|
32
33
|
License-File: LICENSE
|
|
@@ -157,5 +158,3 @@ software developed with public money is also publicly available.
|
|
|
157
158
|
|
|
158
159
|
This package is initially developed by the City of Amsterdam, but the tools
|
|
159
160
|
and concepts created in this project can be used in any city.
|
|
160
|
-
|
|
161
|
-
|
|
@@ -2,7 +2,7 @@ CHANGES.md
|
|
|
2
2
|
LICENSE
|
|
3
3
|
MANIFEST.in
|
|
4
4
|
README.md
|
|
5
|
-
|
|
5
|
+
pyproject.toml
|
|
6
6
|
setup.py
|
|
7
7
|
django_gisserver.egg-info/PKG-INFO
|
|
8
8
|
django_gisserver.egg-info/SOURCES.txt
|
|
@@ -23,7 +23,6 @@ gisserver/operations/base.py
|
|
|
23
23
|
gisserver/operations/wfs20.py
|
|
24
24
|
gisserver/output/__init__.py
|
|
25
25
|
gisserver/output/base.py
|
|
26
|
-
gisserver/output/buffer.py
|
|
27
26
|
gisserver/output/csv.py
|
|
28
27
|
gisserver/output/geojson.py
|
|
29
28
|
gisserver/output/gml32.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.1" # follows PEP440
|
|
@@ -23,31 +23,28 @@ GISSERVER_DB_PRECISION = getattr(settings, "GISSERVER_DB_PRECISION", 15)
|
|
|
23
23
|
# Otherwise, all database-supported SRID's are allowed.
|
|
24
24
|
GISSERVER_SUPPORTED_CRS_ONLY = getattr(settings, "GISSERVER_SUPPORTED_CRS_ONLY", True)
|
|
25
25
|
|
|
26
|
+
# Whether the total results need to be counted.
|
|
27
|
+
# By disabling this, clients just need to fetch more pages
|
|
28
|
+
# 0 = No counting, 1 = all pages, 2 = only for the first page.
|
|
29
|
+
GISSERVER_COUNT_NUMBER_MATCHED = getattr(settings, "GISSERVER_COUNT_NUMBER_MATCHED", 1)
|
|
30
|
+
|
|
26
31
|
# -- max page size
|
|
27
32
|
|
|
28
33
|
# Allow tuning the page size without having to override code.
|
|
29
34
|
# This corresponds with the "DefaultMaxFeatures" setting.
|
|
30
|
-
GISSERVER_DEFAULT_MAX_PAGE_SIZE = getattr(
|
|
31
|
-
settings, "GISSERVER_DEFAULT_MAX_PAGE_SIZE", 5000
|
|
32
|
-
)
|
|
35
|
+
GISSERVER_DEFAULT_MAX_PAGE_SIZE = getattr(settings, "GISSERVER_DEFAULT_MAX_PAGE_SIZE", 5000)
|
|
33
36
|
|
|
34
37
|
# CSV exports have a higher default page size, as these results can be streamed.
|
|
35
|
-
GISSERVER_GEOJSON_MAX_PAGE_SIZE = getattr(
|
|
36
|
-
settings, "GISSERVER_GEOJSON_MAX_PAGE_SIZE", math.inf
|
|
37
|
-
)
|
|
38
|
+
GISSERVER_GEOJSON_MAX_PAGE_SIZE = getattr(settings, "GISSERVER_GEOJSON_MAX_PAGE_SIZE", math.inf)
|
|
38
39
|
GISSERVER_CSV_MAX_PAGE_SIZE = getattr(settings, "GISSERVER_CSV_MAX_PAGE_SIZE", math.inf)
|
|
39
40
|
|
|
40
41
|
# -- debugging
|
|
41
42
|
|
|
42
43
|
# Whether to follow the WFS standards strictly (breaks CITE conformance testing)
|
|
43
|
-
GISSERVER_WFS_STRICT_STANDARD = getattr(
|
|
44
|
-
settings, "GISSERVER_WFS_STRICT_STANDARD", False
|
|
45
|
-
)
|
|
44
|
+
GISSERVER_WFS_STRICT_STANDARD = getattr(settings, "GISSERVER_WFS_STRICT_STANDARD", False)
|
|
46
45
|
|
|
47
46
|
# Whether to wrap filter errors in a nice response, or raise an exception
|
|
48
|
-
GISSERVER_WRAP_FILTER_DB_ERRORS = getattr(
|
|
49
|
-
settings, "GISSERVER_WRAP_FILTER_DB_ERRORS", True
|
|
50
|
-
)
|
|
47
|
+
GISSERVER_WRAP_FILTER_DB_ERRORS = getattr(settings, "GISSERVER_WRAP_FILTER_DB_ERRORS", True)
|
|
51
48
|
|
|
52
49
|
|
|
53
50
|
@receiver(setting_changed)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Internal module for additional GIS database functions."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
|
-
from functools import reduce
|
|
5
|
+
from functools import lru_cache, reduce
|
|
5
6
|
from typing import cast
|
|
6
7
|
|
|
7
8
|
from django.contrib.gis.db.models import GeometryField, functions
|
|
@@ -114,6 +115,7 @@ def get_db_annotation(instance: models.Model, name: str, name_template: str):
|
|
|
114
115
|
) from e
|
|
115
116
|
|
|
116
117
|
|
|
118
|
+
@lru_cache
|
|
117
119
|
def escape_xml_name(name: str, template="{name}") -> str:
|
|
118
120
|
"""Escape an XML name to be used as annotation name."""
|
|
119
121
|
return template.format(name=name.replace(".", "_"))
|
|
@@ -132,19 +134,13 @@ def get_db_geometry_selects(
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
|
|
135
|
-
def _get_db_geometry_target(
|
|
136
|
-
xsd_element: XsdElement, output_crs: CRS
|
|
137
|
-
) -> str | functions.Transform:
|
|
137
|
+
def _get_db_geometry_target(xsd_element: XsdElement, output_crs: CRS) -> str | functions.Transform:
|
|
138
138
|
"""Wrap the selection of a geometry field in a CRS Transform if needed."""
|
|
139
139
|
field = cast(GeometryField, xsd_element.source)
|
|
140
|
-
return conditional_transform(
|
|
141
|
-
xsd_element.orm_path, field.srid, output_srid=output_crs.srid
|
|
142
|
-
)
|
|
140
|
+
return conditional_transform(xsd_element.orm_path, field.srid, output_srid=output_crs.srid)
|
|
143
141
|
|
|
144
142
|
|
|
145
143
|
def get_db_geometry_target(xpath_match: XPathMatch, output_crs: CRS):
|
|
146
144
|
"""Based on a resolved element, build the proper geometry field select clause."""
|
|
147
145
|
field = cast(GeometryField, xpath_match.child.source)
|
|
148
|
-
return conditional_transform(
|
|
149
|
-
xpath_match.orm_path, field.srid, output_srid=output_crs.srid
|
|
150
|
-
)
|
|
146
|
+
return conditional_transform(xpath_match.orm_path, field.srid, output_srid=output_crs.srid)
|
|
@@ -13,14 +13,15 @@ The feature type classes (and field types) offer a flexible translation
|
|
|
13
13
|
from attribute listings into a schema definition.
|
|
14
14
|
For example, model relationships can be modelled to a different XML layout.
|
|
15
15
|
"""
|
|
16
|
+
|
|
16
17
|
from __future__ import annotations
|
|
17
18
|
|
|
18
19
|
import html
|
|
19
20
|
import operator
|
|
20
21
|
from collections import defaultdict
|
|
21
22
|
from dataclasses import dataclass
|
|
22
|
-
from functools import lru_cache, reduce
|
|
23
|
-
from typing import
|
|
23
|
+
from functools import cached_property, lru_cache, reduce
|
|
24
|
+
from typing import Union
|
|
24
25
|
|
|
25
26
|
from django.conf import settings
|
|
26
27
|
from django.contrib.gis.db import models as gis_models
|
|
@@ -28,7 +29,6 @@ from django.contrib.gis.db.models import Extent, GeometryField
|
|
|
28
29
|
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
|
29
30
|
from django.db import models
|
|
30
31
|
from django.db.models.fields.related import ForeignObjectRel # Django 2.2 import
|
|
31
|
-
from django.utils.functional import cached_property # py3.8: functools
|
|
32
32
|
|
|
33
33
|
from gisserver.db import conditional_transform
|
|
34
34
|
from gisserver.exceptions import ExternalValueError
|
|
@@ -138,9 +138,7 @@ def _get_model_fields(model, fields, parent=None):
|
|
|
138
138
|
parent=parent,
|
|
139
139
|
)
|
|
140
140
|
for f in model._meta.get_fields()
|
|
141
|
-
if not f.is_relation
|
|
142
|
-
or f.many_to_one # ForeignKey
|
|
143
|
-
or f.one_to_one # OneToOneField
|
|
141
|
+
if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey # OneToOneField
|
|
144
142
|
]
|
|
145
143
|
else:
|
|
146
144
|
# Only defined fields
|
|
@@ -191,7 +189,6 @@ class FeatureField:
|
|
|
191
189
|
self.model_field = None
|
|
192
190
|
self.parent = parent
|
|
193
191
|
self.abstract = abstract
|
|
194
|
-
|
|
195
192
|
# Allow to override the class attribute on 'self',
|
|
196
193
|
# which avoids having to subclass this field class as well.
|
|
197
194
|
if xsd_class is not None:
|
|
@@ -291,7 +288,7 @@ class FeatureField:
|
|
|
291
288
|
|
|
292
289
|
|
|
293
290
|
_FieldDefinition = Union[str, FeatureField]
|
|
294
|
-
_FieldDefinitions = Union[_all_,
|
|
291
|
+
_FieldDefinitions = Union[_all_, list[_FieldDefinition]]
|
|
295
292
|
|
|
296
293
|
|
|
297
294
|
class ComplexFeatureField(FeatureField):
|
|
@@ -455,15 +452,15 @@ class FeatureType:
|
|
|
455
452
|
queryset: models.QuerySet,
|
|
456
453
|
*,
|
|
457
454
|
fields: _FieldDefinitions | None = None,
|
|
458
|
-
display_field_name: str = None,
|
|
459
|
-
geometry_field_name: str = None,
|
|
460
|
-
name: str = None,
|
|
455
|
+
display_field_name: str | None = None,
|
|
456
|
+
geometry_field_name: str | None = None,
|
|
457
|
+
name: str | None = None,
|
|
461
458
|
# WFS Metadata:
|
|
462
|
-
title: str = None,
|
|
463
|
-
abstract: str = None,
|
|
464
|
-
keywords: list[str] = None,
|
|
465
|
-
crs: CRS = None,
|
|
466
|
-
other_crs: list[CRS] = None,
|
|
459
|
+
title: str | None = None,
|
|
460
|
+
abstract: str | None = None,
|
|
461
|
+
keywords: list[str] | None = None,
|
|
462
|
+
crs: CRS | None = None,
|
|
463
|
+
other_crs: list[CRS] | None = None,
|
|
467
464
|
metadata_url: str | None = None,
|
|
468
465
|
# Settings
|
|
469
466
|
show_name_field: bool = True,
|
|
@@ -521,7 +518,6 @@ class FeatureType:
|
|
|
521
518
|
"""Hook that allows subclasses to reject access for datasets.
|
|
522
519
|
It may raise a Django PermissionDenied error.
|
|
523
520
|
"""
|
|
524
|
-
pass
|
|
525
521
|
|
|
526
522
|
@cached_property
|
|
527
523
|
def xml_name(self):
|
|
@@ -551,7 +547,7 @@ class FeatureType:
|
|
|
551
547
|
This gives an object layout based on the XSD elements,
|
|
552
548
|
that can be used for prefetching data.
|
|
553
549
|
"""
|
|
554
|
-
models =
|
|
550
|
+
models = {}
|
|
555
551
|
fields = defaultdict(set)
|
|
556
552
|
elements = defaultdict(list)
|
|
557
553
|
|
|
@@ -596,7 +592,9 @@ class FeatureType:
|
|
|
596
592
|
return [
|
|
597
593
|
(ff.model_field.name if ff.model_field else ff.model_attribute)
|
|
598
594
|
for ff in self.fields
|
|
599
|
-
if not ff.model_field.many_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)
|
|
600
598
|
]
|
|
601
599
|
|
|
602
600
|
@cached_property
|
|
@@ -677,9 +675,7 @@ class FeatureType:
|
|
|
677
675
|
# That that without .only(), use at least `self.queryset.all()` so a clone is returned.
|
|
678
676
|
return self.queryset.only(*self._local_model_field_names)
|
|
679
677
|
|
|
680
|
-
def get_related_queryset(
|
|
681
|
-
self, feature_relation: FeatureRelation
|
|
682
|
-
) -> models.QuerySet:
|
|
678
|
+
def get_related_queryset(self, feature_relation: FeatureRelation) -> models.QuerySet:
|
|
683
679
|
"""Return the queryset that is used for prefetching related data."""
|
|
684
680
|
if feature_relation.related_model is None:
|
|
685
681
|
raise RuntimeError(
|
|
@@ -688,9 +684,7 @@ class FeatureType:
|
|
|
688
684
|
)
|
|
689
685
|
# Return a queryset that only retrieves the fields that are displayed.
|
|
690
686
|
return self.filter_related_queryset(
|
|
691
|
-
feature_relation.related_model.objects.only(
|
|
692
|
-
*feature_relation._local_model_field_names
|
|
693
|
-
)
|
|
687
|
+
feature_relation.related_model.objects.only(*feature_relation._local_model_field_names)
|
|
694
688
|
)
|
|
695
689
|
|
|
696
690
|
def filter_related_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
|
|
@@ -730,9 +724,7 @@ class FeatureType:
|
|
|
730
724
|
return None
|
|
731
725
|
|
|
732
726
|
# Perform the combining of geometries inside libgeos
|
|
733
|
-
geometry = (
|
|
734
|
-
geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
|
|
735
|
-
)
|
|
727
|
+
geometry = geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
|
|
736
728
|
if crs is not None and geometry.srid != crs.srid:
|
|
737
729
|
crs.apply_to(geometry) # avoid clone
|
|
738
730
|
return BoundingBox.from_geometry(geometry, crs=crs)
|
|
@@ -824,9 +816,7 @@ class FeatureType:
|
|
|
824
816
|
f"XPath selectors with expanded syntax are not supported: {xpath}"
|
|
825
817
|
)
|
|
826
818
|
elif "(" in xpath:
|
|
827
|
-
raise NotImplementedError(
|
|
828
|
-
f"XPath selectors with functions are not supported: {xpath}"
|
|
829
|
-
)
|
|
819
|
+
raise NotImplementedError(f"XPath selectors with functions are not supported: {xpath}")
|
|
830
820
|
|
|
831
821
|
# Allow /app:ElementName/.. as "absolute" path.
|
|
832
822
|
# Given our internal resolver logic, simple solution is to strip it.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
This includes the CRS parsing, coordinate transforms and bounding box object.
|
|
4
4
|
The bounding box can be calculated within Python, or read from a database result.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import re
|
|
@@ -10,7 +11,7 @@ from dataclasses import dataclass, field
|
|
|
10
11
|
from decimal import Decimal
|
|
11
12
|
from functools import lru_cache
|
|
12
13
|
|
|
13
|
-
from django.contrib.gis.gdal import
|
|
14
|
+
from django.contrib.gis.gdal import CoordTransform, SpatialReference
|
|
14
15
|
from django.contrib.gis.geos import GEOSGeometry, Polygon
|
|
15
16
|
|
|
16
17
|
from gisserver.exceptions import ExternalParsingError, ExternalValueError
|
|
@@ -101,9 +102,7 @@ class CRS:
|
|
|
101
102
|
self.__dict__["has_custom_backend"] = self.backend is not None
|
|
102
103
|
|
|
103
104
|
@classmethod
|
|
104
|
-
def from_string(
|
|
105
|
-
cls, uri: str | int, backend: SpatialReference | None = None
|
|
106
|
-
) -> CRS:
|
|
105
|
+
def from_string(cls, uri: str | int, backend: SpatialReference | None = None) -> CRS:
|
|
107
106
|
"""
|
|
108
107
|
Parse an CRS (Coordinate Reference System) URI, which preferably follows the URN format
|
|
109
108
|
as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_
|
|
@@ -146,17 +145,13 @@ class CRS:
|
|
|
146
145
|
"""Instantiate this class using an URN format."""
|
|
147
146
|
urn_match = CRS_URN_REGEX.match(urn)
|
|
148
147
|
if not urn_match:
|
|
149
|
-
raise ExternalValueError(
|
|
150
|
-
f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}"
|
|
151
|
-
)
|
|
148
|
+
raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
|
|
152
149
|
|
|
153
150
|
domain = urn_match.group("domain")
|
|
154
151
|
authority = urn_match.group("authority").upper()
|
|
155
152
|
|
|
156
153
|
if domain not in ("ogc", "opengis"):
|
|
157
|
-
raise ExternalValueError(
|
|
158
|
-
f"CRS URI [{urn}] contains unknown domain [{domain}]"
|
|
159
|
-
)
|
|
154
|
+
raise ExternalValueError(f"CRS URI [{urn}] contains unknown domain [{domain}]")
|
|
160
155
|
|
|
161
156
|
if authority == "EPSG":
|
|
162
157
|
crsid = urn_match.group("id")
|
|
@@ -169,14 +164,10 @@ class CRS:
|
|
|
169
164
|
elif authority == "OGC":
|
|
170
165
|
crsid = urn_match.group("id").upper()
|
|
171
166
|
if crsid != "CRS84":
|
|
172
|
-
raise ExternalValueError(
|
|
173
|
-
f"OGC CRS URI from [{urn}] contains unknown id [{id}]"
|
|
174
|
-
)
|
|
167
|
+
raise ExternalValueError(f"OGC CRS URI from [{urn}] contains unknown id [{id}]")
|
|
175
168
|
srid = 4326
|
|
176
169
|
else:
|
|
177
|
-
raise ExternalValueError(
|
|
178
|
-
f"CRS URI [{urn}] contains unknown authority [{authority}]"
|
|
179
|
-
)
|
|
170
|
+
raise ExternalValueError(f"CRS URI [{urn}] contains unknown authority [{authority}]")
|
|
180
171
|
|
|
181
172
|
crs = cls(
|
|
182
173
|
domain=domain,
|
|
@@ -253,9 +244,7 @@ class CRS:
|
|
|
253
244
|
if self.origin:
|
|
254
245
|
self.__dict__["backend"] = _get_spatial_reference(self.origin)
|
|
255
246
|
else:
|
|
256
|
-
self.__dict__["backend"] = _get_spatial_reference(
|
|
257
|
-
self.srid, srs_type="epsg"
|
|
258
|
-
)
|
|
247
|
+
self.__dict__["backend"] = _get_spatial_reference(self.srid, srs_type="epsg")
|
|
259
248
|
return self.backend
|
|
260
249
|
|
|
261
250
|
def apply_to(self, geometry: GEOSGeometry, clone=False) -> GEOSGeometry | None:
|
|
@@ -271,7 +260,7 @@ class CRS:
|
|
|
271
260
|
if clone:
|
|
272
261
|
return geometry.clone()
|
|
273
262
|
else:
|
|
274
|
-
return
|
|
263
|
+
return None
|
|
275
264
|
else:
|
|
276
265
|
# Convert using GDAL / proj
|
|
277
266
|
transform = _get_coord_transform(geometry.srid, self._as_gdal())
|
|
@@ -300,8 +289,7 @@ class BoundingBox:
|
|
|
300
289
|
bbox = bbox.split(",")
|
|
301
290
|
if not (4 <= len(bbox) <= 5):
|
|
302
291
|
raise ExternalParsingError(
|
|
303
|
-
f"Input does not contain bounding box, "
|
|
304
|
-
f"expected 4 or 5 values, not {bbox}."
|
|
292
|
+
f"Input does not contain bounding box, expected 4 or 5 values, not {bbox}."
|
|
305
293
|
)
|
|
306
294
|
return cls(
|
|
307
295
|
Decimal(bbox[0]),
|
|
@@ -332,9 +320,7 @@ class BoundingBox:
|
|
|
332
320
|
def __repr__(self):
|
|
333
321
|
return f"BoundingBox({self.south}, {self.west}, {self.north}, {self.east})"
|
|
334
322
|
|
|
335
|
-
def extend_to(
|
|
336
|
-
self, lower_lon: float, lower_lat: float, upper_lon: float, upper_lat: float
|
|
337
|
-
):
|
|
323
|
+
def extend_to(self, lower_lon: float, lower_lat: float, upper_lon: float, upper_lat: float):
|
|
338
324
|
"""Expand the bounding box in-place"""
|
|
339
325
|
self.south = min(self.south, lower_lon)
|
|
340
326
|
self.west = min(self.west, lower_lat)
|
|
@@ -4,12 +4,14 @@ All operations extend from an WFSMethod class.
|
|
|
4
4
|
This defines the parameters and output formats of the method.
|
|
5
5
|
This introspection data is also parsed by the GetCapabilities call.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import math
|
|
10
11
|
import re
|
|
12
|
+
from collections.abc import Callable
|
|
11
13
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
13
15
|
|
|
14
16
|
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
|
15
17
|
from django.http import HttpResponse
|
|
@@ -108,21 +110,15 @@ class Parameter:
|
|
|
108
110
|
This method can be overwritten by a subclass if needed.
|
|
109
111
|
"""
|
|
110
112
|
if self.allowed_values is not None and value not in self.allowed_values:
|
|
111
|
-
msg = self.error_messages.get(
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
raise InvalidParameterValue(
|
|
115
|
-
self.name, msg.format(name=self.name, value=value)
|
|
116
|
-
)
|
|
113
|
+
msg = self.error_messages.get("invalid", "Invalid value for {name}: {value}")
|
|
114
|
+
raise InvalidParameterValue(self.name, msg.format(name=self.name, value=value))
|
|
117
115
|
|
|
118
116
|
|
|
119
117
|
class UnsupportedParameter(Parameter):
|
|
120
118
|
def value_from_query(self, KVP: dict):
|
|
121
119
|
kvp_name = self.name.upper()
|
|
122
120
|
if kvp_name in KVP:
|
|
123
|
-
raise InvalidParameterValue(
|
|
124
|
-
self.name, f"Support for {self.name} is not implemented!"
|
|
125
|
-
)
|
|
121
|
+
raise InvalidParameterValue(self.name, f"Support for {self.name} is not implemented!")
|
|
126
122
|
return None
|
|
127
123
|
|
|
128
124
|
|
|
@@ -262,9 +258,10 @@ class WFSMethod:
|
|
|
262
258
|
|
|
263
259
|
def _parse_output_format(self, value) -> OutputFormat:
|
|
264
260
|
"""Select the proper OutputFormat object based on the input value"""
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
|
|
261
|
+
# When using ?OUTPUTFORMAT=application/gml+xml", it is actually an URL-encoded space
|
|
262
|
+
# character. Hence spaces are replaced back to a '+' character to allow such notation
|
|
263
|
+
# instead of forcing it to to be ?OUTPUTFORMAT=application/gml%2bxml".
|
|
264
|
+
for v in {value, value.replace(" ", "+")}:
|
|
268
265
|
for o in self.output_formats:
|
|
269
266
|
if o.matches(v):
|
|
270
267
|
return o
|
|
@@ -290,9 +287,7 @@ class WFSMethod:
|
|
|
290
287
|
tokens = iter(tokens)
|
|
291
288
|
for prefix in tokens:
|
|
292
289
|
if not prefix.startswith("xmlns("):
|
|
293
|
-
raise InvalidParameterValue(
|
|
294
|
-
"namespaces", f"Expected xmlns(...) format: {value}"
|
|
295
|
-
)
|
|
290
|
+
raise InvalidParameterValue("namespaces", f"Expected xmlns(...) format: {value}")
|
|
296
291
|
if prefix.endswith(")"):
|
|
297
292
|
# xmlns(http://...)
|
|
298
293
|
prefix = ""
|
|
@@ -313,9 +308,7 @@ class WFSMethod:
|
|
|
313
308
|
def parse_request(self, KVP: dict) -> dict[str, Any]:
|
|
314
309
|
"""Parse the parameters of the request"""
|
|
315
310
|
self.namespaces.update(self._parse_namespaces(KVP.get("NAMESPACES")))
|
|
316
|
-
param_values = {
|
|
317
|
-
param.name: param.value_from_query(KVP) for param in self.get_parameters()
|
|
318
|
-
}
|
|
311
|
+
param_values = {param.name: param.value_from_query(KVP) for param in self.get_parameters()}
|
|
319
312
|
param_values["NAMESPACES"] = self.namespaces
|
|
320
313
|
|
|
321
314
|
for param in self.get_parameters():
|
|
@@ -331,7 +324,6 @@ class WFSMethod:
|
|
|
331
324
|
|
|
332
325
|
def validate(self, **params):
|
|
333
326
|
"""Perform final request parameter validation before the method is called"""
|
|
334
|
-
pass
|
|
335
327
|
|
|
336
328
|
def __call__(self, **params):
|
|
337
329
|
"""Default call implementation: render an XML template."""
|
|
@@ -371,18 +363,12 @@ class WFSMethod:
|
|
|
371
363
|
def _get_xml_template_name(self):
|
|
372
364
|
"""Generate the XML template name for this operation, and check it's file pattern"""
|
|
373
365
|
service = self.view.KVP["SERVICE"].lower()
|
|
374
|
-
template_name =
|
|
375
|
-
f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
|
|
376
|
-
)
|
|
366
|
+
template_name = f"gisserver/{service}/{self.view.version}/{self.xml_template_name}"
|
|
377
367
|
|
|
378
368
|
# Since 'service' and 'version' are based on external input,
|
|
379
369
|
# these values are double checked again to avoid remove file inclusion.
|
|
380
|
-
if not RE_SAFE_FILENAME.match(service) or not RE_SAFE_FILENAME.match(
|
|
381
|
-
|
|
382
|
-
):
|
|
383
|
-
raise SuspiciousOperation(
|
|
384
|
-
f"Refusing to render template name {template_name}"
|
|
385
|
-
)
|
|
370
|
+
if not RE_SAFE_FILENAME.match(service) or not RE_SAFE_FILENAME.match(self.view.version):
|
|
371
|
+
raise SuspiciousOperation(f"Refusing to render template name {template_name}")
|
|
386
372
|
|
|
387
373
|
return template_name
|
|
388
374
|
|
|
@@ -397,9 +383,7 @@ class WFSTypeNamesMethod(WFSMethod):
|
|
|
397
383
|
# This is not delayed until _parse_type_names() as that wraps any TypeError
|
|
398
384
|
# from view.get_feature_types() as an InvalidParameterValue exception.
|
|
399
385
|
self.all_feature_types = self.view.get_feature_types()
|
|
400
|
-
self.all_feature_types_by_name = _get_feature_types_by_name(
|
|
401
|
-
self.all_feature_types
|
|
402
|
-
)
|
|
386
|
+
self.all_feature_types_by_name = _get_feature_types_by_name(self.all_feature_types)
|
|
403
387
|
|
|
404
388
|
def get_parameters(self):
|
|
405
389
|
return super().get_parameters() + [
|
|
@@ -423,18 +407,13 @@ class WFSTypeNamesMethod(WFSMethod):
|
|
|
423
407
|
"Parameter lists to perform multiple queries are not supported yet.",
|
|
424
408
|
)
|
|
425
409
|
|
|
426
|
-
return [
|
|
427
|
-
self._parse_type_name(name, locator="typenames")
|
|
428
|
-
for name in type_names.split(",")
|
|
429
|
-
]
|
|
410
|
+
return [self._parse_type_name(name, locator="typenames") for name in type_names.split(",")]
|
|
430
411
|
|
|
431
412
|
def _parse_type_name(self, name, locator="typename") -> FeatureType:
|
|
432
413
|
"""Find the requested feature type for a type name"""
|
|
433
414
|
app_prefix = self.namespaces[self.view.xml_namespace]
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
else:
|
|
437
|
-
local_name = name
|
|
415
|
+
# strip our XML prefix
|
|
416
|
+
local_name = name[len(app_prefix) + 1 :] if name.startswith(f"{app_prefix}:") else name
|
|
438
417
|
|
|
439
418
|
try:
|
|
440
419
|
return self.all_feature_types_by_name[local_name]
|