django-gisserver 1.2.7__tar.gz → 1.4.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.2.7 → django-gisserver-1.4.0}/CHANGES.md +21 -1
- {django-gisserver-1.2.7/django_gisserver.egg-info → django-gisserver-1.4.0}/PKG-INFO +2 -2
- {django-gisserver-1.2.7 → django-gisserver-1.4.0/django_gisserver.egg-info}/PKG-INFO +2 -2
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/django_gisserver.egg-info/SOURCES.txt +2 -2
- django-gisserver-1.4.0/gisserver/__init__.py +1 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/conf.py +9 -12
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/db.py +6 -10
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/exceptions.py +1 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/features.py +18 -29
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/geometries.py +11 -25
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/operations/base.py +19 -40
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/operations/wfs20.py +8 -20
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/__init__.py +7 -2
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/base.py +4 -13
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/csv.py +15 -19
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/geojson.py +16 -20
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/gml32.py +310 -283
- django-gisserver-1.4.0/gisserver/output/gml32_lxml.py +612 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/results.py +107 -22
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/utils.py +15 -5
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/output/xmlschema.py +7 -8
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/base.py +2 -4
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/expressions.py +6 -13
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/filters.py +6 -5
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/functions.py +4 -4
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/identifiers.py +1 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/operators.py +16 -43
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/query.py +1 -3
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/sorting.py +1 -3
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/gml/__init__.py +1 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/gml/base.py +1 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/values.py +1 -3
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/queries/__init__.py +1 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/queries/adhoc.py +3 -6
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/queries/base.py +2 -6
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/queries/stored.py +3 -6
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/types.py +59 -46
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/views.py +7 -10
- django-gisserver-1.4.0/pyproject.toml +96 -0
- django-gisserver-1.4.0/setup.cfg +4 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/setup.py +1 -1
- django-gisserver-1.2.7/gisserver/__init__.py +0 -1
- django-gisserver-1.2.7/gisserver/output/buffer.py +0 -64
- django-gisserver-1.2.7/setup.cfg +0 -39
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/LICENSE +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/MANIFEST.in +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/README.md +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/django_gisserver.egg-info/dependency_links.txt +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/django_gisserver.egg-info/not-zip-safe +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/django_gisserver.egg-info/requires.txt +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/django_gisserver.egg-info/top_level.txt +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/operations/__init__.py +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/__init__.py +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/fes20/__init__.py +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/gml/geometries.py +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/parsers/tags.py +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/static/gisserver/index.css +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/index.html +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/service_description.html +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templates/gisserver/wfs/feature_type.html +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templatetags/__init__.py +0 -0
- {django-gisserver-1.2.7 → django-gisserver-1.4.0}/gisserver/templatetags/gisserver_tags.py +0 -0
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# 2024-07-01 (1.4.0)
|
|
2
|
+
|
|
3
|
+
* Added `GISSERVER_COUNT_NUMBER_MATCHED` setting to allow disabling "numberReturned" counting.
|
|
4
|
+
This will avoid an expensive PostgreSQL COUNT query, speeding up returning large sets.
|
|
5
|
+
* Added basic support for array fields in `GetPropertyValue`.
|
|
6
|
+
* Optimized GML rendering performance (around 15-20% faster on large datasets).
|
|
7
|
+
* Optimized overall rendering performance, which improved GeoJSON/CSV output too.
|
|
8
|
+
* Developers: updated pre-commit hooks
|
|
9
|
+
* Cleaned up leftover Python 3.7 compat code.
|
|
10
|
+
|
|
11
|
+
# 2023-06-08 (1.3.0)
|
|
12
|
+
|
|
13
|
+
* Django 5 support added.
|
|
14
|
+
|
|
15
|
+
## Contributors
|
|
16
|
+
We would like to thank the following contributors
|
|
17
|
+
for their work on this release.
|
|
18
|
+
|
|
19
|
+
- [tomdtp](https://github.com/tomdtp)
|
|
20
|
+
|
|
1
21
|
# 2023-06-08 (1.2.7)
|
|
2
22
|
|
|
3
23
|
* WFS endpoints now accept a GML version number in their OUTPUTFORMAT.
|
|
@@ -31,7 +51,7 @@
|
|
|
31
51
|
# 2022-04-13 (1.2.1)
|
|
32
52
|
|
|
33
53
|
* Fixed regression for auto-correcting xmlns for `<Filter>` tags that have leading whitespace.
|
|
34
|
-
* Fixed weird crashes when geometry field is not provided.
|
|
54
|
+
* Fixed weird crashes when geometry field is not provided.
|
|
35
55
|
* Simplify `FeatureType.geometry_field` logic.
|
|
36
56
|
|
|
37
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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,10 +12,10 @@ 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.7
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Framework :: Django
|
|
20
20
|
Classifier: Framework :: Django :: 3.2
|
|
21
21
|
Classifier: Framework :: Django :: 4.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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,10 +12,10 @@ 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.7
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Framework :: Django
|
|
20
20
|
Classifier: Framework :: Django :: 3.2
|
|
21
21
|
Classifier: Framework :: Django :: 4.0
|
|
@@ -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,10 +23,10 @@ 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
|
|
29
|
+
gisserver/output/gml32_lxml.py
|
|
30
30
|
gisserver/output/results.py
|
|
31
31
|
gisserver/output/utils.py
|
|
32
32
|
gisserver/output/xmlschema.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.0" # 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
|
|
@@ -291,7 +289,7 @@ class FeatureField:
|
|
|
291
289
|
|
|
292
290
|
|
|
293
291
|
_FieldDefinition = Union[str, FeatureField]
|
|
294
|
-
_FieldDefinitions = Union[_all_,
|
|
292
|
+
_FieldDefinitions = Union[_all_, list[_FieldDefinition]]
|
|
295
293
|
|
|
296
294
|
|
|
297
295
|
class ComplexFeatureField(FeatureField):
|
|
@@ -455,15 +453,15 @@ class FeatureType:
|
|
|
455
453
|
queryset: models.QuerySet,
|
|
456
454
|
*,
|
|
457
455
|
fields: _FieldDefinitions | None = None,
|
|
458
|
-
display_field_name: str = None,
|
|
459
|
-
geometry_field_name: str = None,
|
|
460
|
-
name: str = None,
|
|
456
|
+
display_field_name: str | None = None,
|
|
457
|
+
geometry_field_name: str | None = None,
|
|
458
|
+
name: str | None = None,
|
|
461
459
|
# 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,
|
|
460
|
+
title: str | None = None,
|
|
461
|
+
abstract: str | None = None,
|
|
462
|
+
keywords: list[str] | None = None,
|
|
463
|
+
crs: CRS | None = None,
|
|
464
|
+
other_crs: list[CRS] | None = None,
|
|
467
465
|
metadata_url: str | None = None,
|
|
468
466
|
# Settings
|
|
469
467
|
show_name_field: bool = True,
|
|
@@ -521,7 +519,6 @@ class FeatureType:
|
|
|
521
519
|
"""Hook that allows subclasses to reject access for datasets.
|
|
522
520
|
It may raise a Django PermissionDenied error.
|
|
523
521
|
"""
|
|
524
|
-
pass
|
|
525
522
|
|
|
526
523
|
@cached_property
|
|
527
524
|
def xml_name(self):
|
|
@@ -551,7 +548,7 @@ class FeatureType:
|
|
|
551
548
|
This gives an object layout based on the XSD elements,
|
|
552
549
|
that can be used for prefetching data.
|
|
553
550
|
"""
|
|
554
|
-
models =
|
|
551
|
+
models = {}
|
|
555
552
|
fields = defaultdict(set)
|
|
556
553
|
elements = defaultdict(list)
|
|
557
554
|
|
|
@@ -677,9 +674,7 @@ class FeatureType:
|
|
|
677
674
|
# That that without .only(), use at least `self.queryset.all()` so a clone is returned.
|
|
678
675
|
return self.queryset.only(*self._local_model_field_names)
|
|
679
676
|
|
|
680
|
-
def get_related_queryset(
|
|
681
|
-
self, feature_relation: FeatureRelation
|
|
682
|
-
) -> models.QuerySet:
|
|
677
|
+
def get_related_queryset(self, feature_relation: FeatureRelation) -> models.QuerySet:
|
|
683
678
|
"""Return the queryset that is used for prefetching related data."""
|
|
684
679
|
if feature_relation.related_model is None:
|
|
685
680
|
raise RuntimeError(
|
|
@@ -688,9 +683,7 @@ class FeatureType:
|
|
|
688
683
|
)
|
|
689
684
|
# Return a queryset that only retrieves the fields that are displayed.
|
|
690
685
|
return self.filter_related_queryset(
|
|
691
|
-
feature_relation.related_model.objects.only(
|
|
692
|
-
*feature_relation._local_model_field_names
|
|
693
|
-
)
|
|
686
|
+
feature_relation.related_model.objects.only(*feature_relation._local_model_field_names)
|
|
694
687
|
)
|
|
695
688
|
|
|
696
689
|
def filter_related_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
|
|
@@ -730,9 +723,7 @@ class FeatureType:
|
|
|
730
723
|
return None
|
|
731
724
|
|
|
732
725
|
# Perform the combining of geometries inside libgeos
|
|
733
|
-
geometry = (
|
|
734
|
-
geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
|
|
735
|
-
)
|
|
726
|
+
geometry = geometries[0] if len(geometries) == 1 else reduce(operator.or_, geometries)
|
|
736
727
|
if crs is not None and geometry.srid != crs.srid:
|
|
737
728
|
crs.apply_to(geometry) # avoid clone
|
|
738
729
|
return BoundingBox.from_geometry(geometry, crs=crs)
|
|
@@ -824,9 +815,7 @@ class FeatureType:
|
|
|
824
815
|
f"XPath selectors with expanded syntax are not supported: {xpath}"
|
|
825
816
|
)
|
|
826
817
|
elif "(" in xpath:
|
|
827
|
-
raise NotImplementedError(
|
|
828
|
-
f"XPath selectors with functions are not supported: {xpath}"
|
|
829
|
-
)
|
|
818
|
+
raise NotImplementedError(f"XPath selectors with functions are not supported: {xpath}")
|
|
830
819
|
|
|
831
820
|
# Allow /app:ElementName/.. as "absolute" path.
|
|
832
821
|
# 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]
|
|
@@ -7,6 +7,7 @@ Useful docs:
|
|
|
7
7
|
* https://mapserver.org/development/rfc/ms-rfc-105.html
|
|
8
8
|
* https://enonline.supermap.com/iExpress9D/API/WFS/WFS200/WFS_2.0.0_introduction.htm
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
import logging
|
|
11
12
|
import math
|
|
12
13
|
import re
|
|
@@ -46,12 +47,7 @@ class GetCapabilities(WFSMethod):
|
|
|
46
47
|
""" "This operation returns map features, and available operations this WFS server supports."""
|
|
47
48
|
|
|
48
49
|
output_formats = [
|
|
49
|
-
OutputFormat(
|
|
50
|
-
"application/gml+xml",
|
|
51
|
-
version="3.2",
|
|
52
|
-
renderer_class=output.gml32_value_renderer,
|
|
53
|
-
title="GML",
|
|
54
|
-
),
|
|
50
|
+
OutputFormat("application/gml+xml", version="3.2", title="GML"), # for FME
|
|
55
51
|
OutputFormat("text/xml"),
|
|
56
52
|
]
|
|
57
53
|
xml_template_name = "get_capabilities.xml"
|
|
@@ -94,9 +90,7 @@ class GetCapabilities(WFSMethod):
|
|
|
94
90
|
"AcceptVersions", "Can't provide both ACCEPTVERSIONS and VERSION"
|
|
95
91
|
)
|
|
96
92
|
|
|
97
|
-
matched_versions = set(accept_versions.split(",")).intersection(
|
|
98
|
-
self.view.accept_versions
|
|
99
|
-
)
|
|
93
|
+
matched_versions = set(accept_versions.split(",")).intersection(self.view.accept_versions)
|
|
100
94
|
if not matched_versions:
|
|
101
95
|
allowed = ", ".join(self.view.accept_versions)
|
|
102
96
|
raise VersionNegotiationFailed(
|
|
@@ -156,7 +150,7 @@ class DescribeFeatureType(WFSTypeNamesMethod):
|
|
|
156
150
|
"application/gml+xml",
|
|
157
151
|
version="3.2",
|
|
158
152
|
renderer_class=output.XMLSchemaRenderer,
|
|
159
|
-
)
|
|
153
|
+
),
|
|
160
154
|
# OutputFormat("text/xml", subtype="gml/3.1.1"),
|
|
161
155
|
]
|
|
162
156
|
|
|
@@ -279,9 +273,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
279
273
|
raise
|
|
280
274
|
logger.exception("WFS request failed: %s\nParams: %r", str(e), params)
|
|
281
275
|
msg = str(e)
|
|
282
|
-
locator = (
|
|
283
|
-
"srsName" if "Cannot find SRID" in msg else self._get_locator(**params)
|
|
284
|
-
)
|
|
276
|
+
locator = "srsName" if "Cannot find SRID" in msg else self._get_locator(**params)
|
|
285
277
|
raise InvalidParameterValue(locator, f"Invalid request: {msg}") from e
|
|
286
278
|
except (TypeError, ValueError) as e:
|
|
287
279
|
# TypeError/ValueError could reference a datatype mismatch in an
|
|
@@ -352,9 +344,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
352
344
|
if not output_crs and collection.results:
|
|
353
345
|
output_crs = collection.results[0].feature_type.crs
|
|
354
346
|
|
|
355
|
-
outputFormat.renderer_class.decorate_collection(
|
|
356
|
-
collection, output_crs, **params
|
|
357
|
-
)
|
|
347
|
+
outputFormat.renderer_class.decorate_collection(collection, output_crs, **params)
|
|
358
348
|
|
|
359
349
|
if stop != math.inf:
|
|
360
350
|
if start > 0:
|
|
@@ -362,7 +352,7 @@ class BaseWFSGetDataMethod(WFSTypeNamesMethod):
|
|
|
362
352
|
STARTINDEX=max(0, start - page_size),
|
|
363
353
|
COUNT=page_size,
|
|
364
354
|
)
|
|
365
|
-
if
|
|
355
|
+
if collection.has_next:
|
|
366
356
|
# TODO: fix this when returning multiple typeNames:
|
|
367
357
|
collection.next = self._replace_url_params(
|
|
368
358
|
STARTINDEX=start + page_size,
|
|
@@ -479,9 +469,7 @@ class GetPropertyValue(BaseWFSGetDataMethod):
|
|
|
479
469
|
renderer_class=output.gml32_value_renderer,
|
|
480
470
|
title="GML",
|
|
481
471
|
),
|
|
482
|
-
OutputFormat(
|
|
483
|
-
"text/xml", subtype="gml/3.2", renderer_class=output.gml32_value_renderer
|
|
484
|
-
),
|
|
472
|
+
OutputFormat("text/xml", subtype="gml/3.2", renderer_class=output.gml32_value_renderer),
|
|
485
473
|
]
|
|
486
474
|
|
|
487
475
|
parameters = BaseWFSGetDataMethod.parameters + [
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"""All supported output formats"""
|
|
2
|
+
|
|
3
|
+
from django.utils.functional import classproperty
|
|
4
|
+
|
|
2
5
|
from gisserver import conf
|
|
3
6
|
|
|
4
7
|
from .base import OutputRenderer
|
|
@@ -13,8 +16,6 @@ from .gml32 import (
|
|
|
13
16
|
from .results import FeatureCollection, SimpleFeatureCollection
|
|
14
17
|
from .xmlschema import XMLSchemaRenderer
|
|
15
18
|
|
|
16
|
-
from django.utils.functional import classproperty
|
|
17
|
-
|
|
18
19
|
__all__ = [
|
|
19
20
|
"OutputRenderer",
|
|
20
21
|
"FeatureCollection",
|
|
@@ -40,6 +41,10 @@ def select_renderer(native_renderer_class, db_renderer_class):
|
|
|
40
41
|
"""
|
|
41
42
|
|
|
42
43
|
class SelectRenderer:
|
|
44
|
+
"""A proxy to the actual rendering class.
|
|
45
|
+
Its structure still allows accessing class-properties of the actual class.
|
|
46
|
+
"""
|
|
47
|
+
|
|
43
48
|
@classproperty
|
|
44
49
|
def real_class(self):
|
|
45
50
|
if conf.GISSERVER_USE_DB_RENDERING:
|