django-gisserver 1.4.1__py3-none-any.whl → 2.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.
Files changed (73) hide show
  1. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/db.py +63 -60
  8. gisserver/exceptions.py +47 -9
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +267 -240
  13. gisserver/geometries.py +34 -39
  14. gisserver/management/__init__.py +0 -0
  15. gisserver/management/commands/__init__.py +0 -0
  16. gisserver/management/commands/loadgeojson.py +291 -0
  17. gisserver/operations/base.py +129 -305
  18. gisserver/operations/wfs20.py +428 -336
  19. gisserver/output/__init__.py +10 -48
  20. gisserver/output/base.py +198 -143
  21. gisserver/output/csv.py +81 -85
  22. gisserver/output/geojson.py +63 -72
  23. gisserver/output/gml32.py +310 -281
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +71 -30
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -154
  28. gisserver/output/xmlschema.py +86 -47
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +15 -11
  32. gisserver/parsers/fes20/expressions.py +89 -50
  33. gisserver/parsers/fes20/filters.py +111 -43
  34. gisserver/parsers/fes20/identifiers.py +44 -26
  35. gisserver/parsers/fes20/lookups.py +144 -0
  36. gisserver/parsers/fes20/operators.py +336 -128
  37. gisserver/parsers/fes20/sorting.py +107 -34
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +6 -3
  40. gisserver/parsers/gml/geometries.py +69 -35
  41. gisserver/parsers/ows/__init__.py +25 -0
  42. gisserver/parsers/ows/kvp.py +190 -0
  43. gisserver/parsers/ows/requests.py +158 -0
  44. gisserver/parsers/query.py +175 -0
  45. gisserver/parsers/values.py +26 -0
  46. gisserver/parsers/wfs20/__init__.py +37 -0
  47. gisserver/parsers/wfs20/adhoc.py +245 -0
  48. gisserver/parsers/wfs20/base.py +143 -0
  49. gisserver/parsers/wfs20/projection.py +103 -0
  50. gisserver/parsers/wfs20/requests.py +482 -0
  51. gisserver/parsers/wfs20/stored.py +192 -0
  52. gisserver/parsers/xml.py +249 -0
  53. gisserver/projection.py +357 -0
  54. gisserver/static/gisserver/index.css +12 -1
  55. gisserver/templates/gisserver/index.html +1 -1
  56. gisserver/templates/gisserver/service_description.html +2 -2
  57. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +11 -11
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +375 -258
  61. gisserver/views.py +206 -75
  62. django_gisserver-1.4.1.dist-info/RECORD +0 -53
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -275
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -34
  67. gisserver/queries/adhoc.py +0 -181
  68. gisserver/queries/base.py +0 -146
  69. gisserver/queries/stored.py +0 -205
  70. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  71. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  72. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  73. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.4.1
3
+ Version: 2.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
7
7
  Author-email: opensource@edoburu.nl
8
8
  License: Mozilla Public License 2.0
9
- Classifier: Development Status :: 4 - Beta
9
+ Platform: UNKNOWN
10
+ Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: Environment :: Web Environment
11
12
  Classifier: Intended Audience :: Developers
12
13
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
@@ -16,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9
16
17
  Classifier: Programming Language :: Python :: 3.10
17
18
  Classifier: Programming Language :: Python :: 3.11
18
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
19
21
  Classifier: Framework :: Django
20
22
  Classifier: Framework :: Django :: 3.2
21
23
  Classifier: Framework :: Django :: 4.0
@@ -27,20 +29,20 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
27
29
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
28
30
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
31
  Requires: Django (>=3.2)
30
- Requires-Python: >=3.8
32
+ Requires-Python: >=3.9
31
33
  Description-Content-Type: text/markdown
32
34
  License-File: LICENSE
33
- Requires-Dist: Django >=3.2
34
- Requires-Dist: defusedxml >=0.6.0
35
- Requires-Dist: lru-dict >=1.1.7
36
- Requires-Dist: orjson >=2.4.0
35
+ Requires-Dist: Django>=3.2
36
+ Requires-Dist: defusedxml>=0.6.0
37
+ Requires-Dist: lru-dict>=1.1.7
38
+ Requires-Dist: orjson>=3.9.15
37
39
  Provides-Extra: tests
38
- Requires-Dist: django-environ >=0.4.5 ; extra == 'tests'
39
- Requires-Dist: psycopg2-binary >=2.8.4 ; extra == 'tests'
40
- Requires-Dist: lxml >=4.5.0 ; 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'
40
+ Requires-Dist: django-environ>=0.4.5; extra == "tests"
41
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
42
+ Requires-Dist: lxml>=4.9.1; extra == "tests"
43
+ Requires-Dist: pytest>=6.2.3; extra == "tests"
44
+ Requires-Dist: pytest-django>=4.1.0; extra == "tests"
45
+ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
44
46
 
45
47
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
46
48
  [![Actions](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml)
@@ -66,6 +68,12 @@ Django speaking WFS 2.0 to expose geo data.
66
68
 
67
69
  For more details, see: <https://django-gisserver.readthedocs.io/>
68
70
 
71
+ ## Test drive
72
+
73
+ * Run `docker compose up`.
74
+ * Open http://localhost:8000/ to see this works.
75
+ * Connect to http://localhost:8000/wfs/ in your GIS client (e.g. QGis) and zoom to Amsterdam.
76
+ * Add more data on the map using the Django admin, username and password are both "admin".
69
77
 
70
78
  ## Quickstart
71
79
 
@@ -168,3 +176,5 @@ software developed with public money is also publicly available.
168
176
 
169
177
  This package is initially developed by the City of Amsterdam, but the tools
170
178
  and concepts created in this project can be used in any city.
179
+
180
+
@@ -0,0 +1,66 @@
1
+ gisserver/__init__.py,sha256=e6waXF42UPN4o15WEYAQ7DskYjyZB_gs3IANkuyGZUk,38
2
+ gisserver/compat.py,sha256=qmLeT7wGRIyA2ujUtVEuUX7qgr98snak9b5OTvnkhas,451
3
+ gisserver/conf.py,sha256=ppoyVS95mMoQXVbPxPar-tkq1t7fE0J3F6IyKitMWuA,2563
4
+ gisserver/db.py,sha256=G4xF3xd8BQjGbKdf6QpOwzUfnm-phTarS71IRCV0Rys,5385
5
+ gisserver/exceptions.py,sha256=q0EZJrlZ7JPf8tWB9de6fPWX74JBrlsoHYN4pBdCGck,6132
6
+ gisserver/features.py,sha256=b7XrDC8XMn6SK3YdOuC5DvXlral5losrFrCeweecJUM,35744
7
+ gisserver/geometries.py,sha256=Ok9e6fbAhW32iglg6gc955z0ZTN2p2lQd3WlN-FPJho,12338
8
+ gisserver/projection.py,sha256=TStCF9xF4V_aozKqTD8uW9mPofZogobVDpdltXwsyZo,14859
9
+ gisserver/types.py,sha256=5J18OUeL8o0VOdxlMGn7fPja7z4s-9Dgy6G-cjXReZM,41453
10
+ gisserver/views.py,sha256=e7Bqr05FaflXOjOXMfQSix59bHqRmb_ASCRqqJlWqEY,18998
11
+ gisserver/extensions/__init__.py,sha256=MsYUsNsoxxjeDHhx4DW9eGXvmOnI5PkoUXvKxRZaRRw,136
12
+ gisserver/extensions/functions.py,sha256=Gv_IoWgJbCONbGg2P4GXdZDI18GvNtHDWLWQjOQMJLY,9085
13
+ gisserver/extensions/queries.py,sha256=_UNWjHMgGgWtBzOl9Ez587e6WeE1_9uN8Qyjzfi4PMo,9435
14
+ gisserver/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ gisserver/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ gisserver/management/commands/loadgeojson.py,sha256=bdCdt37cKPIYV9cQtLFJ0BzFcVgQQ9y59kTtPX7bYkA,11493
17
+ gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ gisserver/operations/base.py,sha256=m56SRTnC6GVWfwugtXgdlIVNh6Vr1h8r_OdfBjWQQ5M,9839
19
+ gisserver/operations/wfs20.py,sha256=WIvGXg1-M01s52VB8nSJGM0tKoviMhnNghQIrfqzudM,24226
20
+ gisserver/output/__init__.py,sha256=5DTFnVEvs-lno_6lOOZheXhF4F96VkwAk71Q5juYe1M,977
21
+ gisserver/output/base.py,sha256=EF3PFUrj2pGzbh2Yt4bpjlwkoGDoI63rHn6wkstXA3g,10303
22
+ gisserver/output/csv.py,sha256=wKtcgwY37NjeLhBxAm2qgKk8UDX-4uUZEzK9LmmVKrQ,6624
23
+ gisserver/output/geojson.py,sha256=fTEB-MRaYLYbJzQVvmZ8ev-Cwrl3lgsR8Jif2zWYSKg,11525
24
+ gisserver/output/gml32.py,sha256=OtBXGup5m1GA9dM_p33E9IXtD-9KcZ58MgMyg-lkGd4,32567
25
+ gisserver/output/iters.py,sha256=YbvtIIy977Snm5m7cYyQext0GCpo8S_u4azTCBNqqgw,8355
26
+ gisserver/output/results.py,sha256=vvwJK1JEgRbs9S4sg5fmsd_avw5ztiC107OizygHk5M,15149
27
+ gisserver/output/stored.py,sha256=m3JcR4MUilyuM_5J6XJDOIyzrhRJbcpg-6FMKy69csc,5763
28
+ gisserver/output/utils.py,sha256=H4lD5Dn_xo8lAHcFv5LjKyj-SNMzzZsZrtDny9HISXk,2671
29
+ gisserver/output/xmlschema.py,sha256=fHj8h9-vG508uRmM-kD5r2omde2wa_myJWWwcWe2lus,6445
30
+ gisserver/parsers/__init__.py,sha256=eGORBeT0F8fR3AadV1uq8dbCeZtIeK0-__CcXwSvXgs,402
31
+ gisserver/parsers/ast.py,sha256=DVUF3977RTjIpsu5yxbKS23Yxepn6JMj1NIUdRmQq2Q,11806
32
+ gisserver/parsers/query.py,sha256=Ndipmq94reShe-DNKwJ96Ig0GdmshRhtVIF6OEGMGeg,6386
33
+ gisserver/parsers/values.py,sha256=7AuOX8Zgb6Cnwb1FuY8Jl0r6TbLrGlNmhICw48sNiCE,2320
34
+ gisserver/parsers/xml.py,sha256=jMsK1enKN0i_rdtjzGqcKdxnr9-n2-9jPY6hX56sU4g,9309
35
+ gisserver/parsers/fes20/__init__.py,sha256=ji0bIRap4fvNW_WWhjnbkBm7ozuEblccIHNzL403Y3E,883
36
+ gisserver/parsers/fes20/expressions.py,sha256=sjBO-S0aQDIorAPg6z7SSUwOreveJEA0w7qcj26UXoA,11275
37
+ gisserver/parsers/fes20/filters.py,sha256=nMAQwgSXl2c7SvVZY_PJSO9XSf55ltvDPnCmIQ5SM9s,6518
38
+ gisserver/parsers/fes20/identifiers.py,sha256=E-6ijz050fjoG-wyzzaIYwxVPplE9RVtoLN_iFNIYw8,3513
39
+ gisserver/parsers/fes20/lookups.py,sha256=nVdqNP0uZntgT1czTxuuUMBVhGKbRsKm8RU-YwGt6EE,5362
40
+ gisserver/parsers/fes20/operators.py,sha256=E5sdepKSxKmklDfHZoNwBwPCqihx5RQJfiA91bv_Hxc,31196
41
+ gisserver/parsers/fes20/sorting.py,sha256=MZXT2RCRKWO9368lkbCbd17IIlTHKuRroafAQVNQuXo,4769
42
+ gisserver/parsers/gml/__init__.py,sha256=DS6CRpFayQp3i5UdwdnAenBb6V6B8vSkn4zQmpo-pBM,1573
43
+ gisserver/parsers/gml/base.py,sha256=hyYAVg-pagH43MFvVgLBzPbIY-uAR1ytDPCtNaXLeww,1165
44
+ gisserver/parsers/gml/geometries.py,sha256=DSBwzjsT16iwSH-bPixLmB5LWpNNUVBL0byl3iMN_Eg,4654
45
+ gisserver/parsers/ows/__init__.py,sha256=0SeM6vfwjWI-WeRD7eS_iN4Jsl8uzXr2yMFpZVHokb4,624
46
+ gisserver/parsers/ows/kvp.py,sha256=et-vlI3EjcK_z1ArGt26RxDyipuwI85NNDe0FVcg7eI,7093
47
+ gisserver/parsers/ows/requests.py,sha256=TZRPX_4RGeNX0qN-OJiRZMGYIWfbn-4vF7zVL4Z1nSk,5496
48
+ gisserver/parsers/wfs20/__init__.py,sha256=6_0TZcLVu48J7Pz802CvkrzEoDDIlI7Bz1t7_VPaOjs,881
49
+ gisserver/parsers/wfs20/adhoc.py,sha256=yM88MrGudYz692QZTL5H6KO1KKZHg0DL44NKWezbYxU,9066
50
+ gisserver/parsers/wfs20/base.py,sha256=_yEIJLrkzPxxj8sjePeSn8PA6L4PTnon5UruDWXejBo,5601
51
+ gisserver/parsers/wfs20/projection.py,sha256=iGpg75VnHj3GrFnSJodEuXbhCF04Phh1IfcAFpurkfw,3930
52
+ gisserver/parsers/wfs20/requests.py,sha256=fl7CUHrqhnEm__M67CHG3hVB9c2r7jFTHJIqlVbW8fI,16756
53
+ gisserver/parsers/wfs20/stored.py,sha256=4YtlildS_lUnY2iZyGQiVQM9TkSjgaaD7iDWtumg82c,7090
54
+ gisserver/static/gisserver/index.css,sha256=h24POXHqJOWtcBRNBFMxZiiknbATRTRWCL6nPyTgPr0,440
55
+ gisserver/templates/gisserver/index.html,sha256=Gq6nasptfquoyd7ihKLtj02tyadijK_Qh8ies6pZxy8,920
56
+ gisserver/templates/gisserver/service_description.html,sha256=y61iVhrcjg44qp-EymaFkUwdsB0LeYTGTbFO0JYOCEs,932
57
+ gisserver/templates/gisserver/wfs/feature_field.html,sha256=Kzbs96GcallCdGLeTPYsFnT6wJ5ar-feyqAmHzJT0u8,587
58
+ gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
59
+ gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=SGZSn7cDU_Oai6d45xrkqhax5CPyI1oIc8BZLOEw1X4,8695
60
+ gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
+ gisserver/templatetags/gisserver_tags.py,sha256=L2YG-PxiKR0QWBMRLLrL_VR94p6wCb5HzgnrPMg4hug,995
62
+ django_gisserver-2.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
63
+ django_gisserver-2.0.dist-info/METADATA,sha256=c9OYER5wjNqHxdxWRA3E3F-v5QMDqPvZ-yCM0NRlFMk,6217
64
+ django_gisserver-2.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
65
+ django_gisserver-2.0.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
66
+ django_gisserver-2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
gisserver/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.4.1" # follows PEP440
1
+ __version__ = "2.0" # follows PEP440
gisserver/compat.py ADDED
@@ -0,0 +1,23 @@
1
+ """Compatability imports"""
2
+
3
+ import sys
4
+
5
+ import django
6
+ from django.conf import settings
7
+ from django.db import models
8
+
9
+ if (
10
+ "django.contrib.postgres" in settings.INSTALLED_APPS
11
+ or "django.contrib.postgres" in sys.modules
12
+ ):
13
+ from django.contrib.postgres.fields import ArrayField
14
+ else:
15
+ ArrayField = None
16
+
17
+ GeneratedField = models.GeneratedField if django.VERSION >= (5, 0) else None
18
+
19
+
20
+ __all__ = (
21
+ "ArrayField",
22
+ "GeneratedField",
23
+ )
gisserver/conf.py CHANGED
@@ -28,6 +28,13 @@ GISSERVER_SUPPORTED_CRS_ONLY = getattr(settings, "GISSERVER_SUPPORTED_CRS_ONLY",
28
28
  # 0 = No counting, 1 = all pages, 2 = only for the first page.
29
29
  GISSERVER_COUNT_NUMBER_MATCHED = getattr(settings, "GISSERVER_COUNT_NUMBER_MATCHED", 1)
30
30
 
31
+ # -- output rendering
32
+
33
+ GISSERVER_EXTRA_OUTPUT_FORMATS = getattr(settings, "GISSERVER_EXTRA_OUTPUT_FORMATS", {})
34
+ GISSERVER_GET_FEATURE_OUTPUT_FORMATS = getattr(
35
+ settings, "GISSERVER_GET_FEATURE_OUTPUT_FORMATS", {}
36
+ )
37
+
31
38
  # -- max page size
32
39
 
33
40
  # Allow tuning the page size without having to override code.
gisserver/db.py CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from functools import lru_cache, reduce
6
- from typing import cast
7
7
 
8
- from django.contrib.gis.db.models import GeometryField, functions
8
+ from django.contrib.gis.db.models import functions
9
9
  from django.db import connection, connections, models
10
10
 
11
11
  from gisserver import conf
12
12
  from gisserver.geometries import CRS
13
- from gisserver.types import XPathMatch, XsdElement
13
+ from gisserver.types import GeometryXsdElement
14
+
15
+ logger = logging.getLogger(__name__)
14
16
 
15
17
 
16
18
  class AsEWKT(functions.GeoFunc):
@@ -54,9 +56,9 @@ class ST_Union(functions.Union):
54
56
  arity = None
55
57
 
56
58
  def as_postgresql(self, compiler, connection, **extra_context):
57
- # PostgreSQL can handle ST_Union(ARRAY(field names)), other databases don't.
59
+ # PostgreSQL can handle ST_Union(ARRAY[field names]), other databases don't.
58
60
  if len(self.source_expressions) > 2:
59
- extra_context["template"] = "%(function)s(ARRAY(%(expressions)s))"
61
+ extra_context["template"] = "%(function)s(ARRAY[%(expressions)s])"
60
62
  return self.as_sql(compiler, connection, **extra_context)
61
63
 
62
64
 
@@ -65,7 +67,7 @@ def get_geometries_union(
65
67
  ) -> str | functions.Union:
66
68
  """Generate a union of multiple geometry fields."""
67
69
  if not expressions:
68
- raise ValueError("Missing geometery fields for get_geometries_union()")
70
+ raise ValueError("Missing geometry fields for get_geometries_union()")
69
71
 
70
72
  if len(expressions) == 1:
71
73
  return next(iter(expressions)) # fastest in set data type
@@ -73,74 +75,75 @@ def get_geometries_union(
73
75
  return functions.Union(*expressions)
74
76
  elif connections[using].vendor == "postgresql":
75
77
  # postgres can handle multiple field names
76
- return ST_Union(expressions)
78
+ return ST_Union(*expressions)
77
79
  else:
78
80
  # other databases do Union(Union(1, 2), 3)
79
81
  return reduce(functions.Union, expressions)
80
82
 
81
83
 
82
- def conditional_transform(
83
- expression: str | functions.GeoFunc, expression_srid: int, output_srid: int
84
- ) -> str | functions.Transform:
85
- """Apply a CRS Transform to the queried field if that is needed."""
86
- if expression_srid != output_srid:
87
- expression = functions.Transform(expression, srid=output_srid)
88
- return expression
89
-
90
-
91
- def build_db_annotations(
92
- selects: dict[str, str | functions.Func],
93
- name_template: str,
94
- wrapper_func: type[functions.Func],
95
- ) -> dict:
96
- """Utility to build annotations for all geometry fields for an XSD type.
97
- This is used by various DB-optimized rendering methods.
98
- """
99
- return {
100
- escape_xml_name(name, name_template): wrapper_func(target)
101
- for name, target in selects.items()
102
- }
103
-
84
+ def replace_queryset_geometries(
85
+ queryset: models.QuerySet,
86
+ geo_elements: list[GeometryXsdElement],
87
+ output_crs: CRS,
88
+ wrapper_func: type[functions.GeoFunc],
89
+ ) -> models.QuerySet:
90
+ """Replace the queryset geometry retrieval with a database-rendered version.
104
91
 
105
- def get_db_annotation(instance: models.Model, name: str, name_template: str):
106
- """Retrieve the value that an annotation has added to the model."""
107
- # The "name" allows any XML-tag elements, escape the most obvious
108
- escaped_name = escape_xml_name(name, name_template)
92
+ This uses absolute paths in the queryset, but can use relative paths for related querysets.
93
+ """
94
+ defer_names = []
95
+ as_geo_map = {}
96
+ for geo_element in geo_elements:
97
+ if geo_element.source is not None: # excludes GmlBoundedByElement
98
+ defer_names.append(geo_element.local_orm_path)
99
+ annotation_name = _as_annotation_name(geo_element.local_orm_path, wrapper_func)
100
+ as_geo_map[annotation_name] = wrapper_func(
101
+ get_db_geometry_target(geo_element, output_crs, use_relative_path=True)
102
+ )
103
+
104
+ if not defer_names:
105
+ return queryset
106
+
107
+ logger.debug(
108
+ "DB rendering: QuerySet for %s replacing %r with %r",
109
+ queryset.model._meta.label,
110
+ defer_names,
111
+ list(as_geo_map.keys()),
112
+ )
113
+ return queryset.defer(*defer_names).annotate(**as_geo_map)
114
+
115
+
116
+ def get_db_rendered_geometry(
117
+ instance: models.Model, geo_element: GeometryXsdElement, replacement: type[functions.GeoFunc]
118
+ ) -> str:
119
+ """Retrieve the database-rendered geometry.
120
+ This includes formatted EWKT or GML output, rendered by the database.
121
+ """
122
+ annotation_name = _as_annotation_name(geo_element.local_orm_path, replacement)
109
123
  try:
110
- return getattr(instance, escaped_name)
124
+ return getattr(instance, annotation_name)
111
125
  except AttributeError as e:
126
+ prefix = _as_annotation_name("", replacement)
127
+ available = ", ".join(key for key in instance.__dict__ if key.startswith(prefix)) or "none"
112
128
  raise AttributeError(
113
- f" DB annotation {instance._meta.model_name}.{escaped_name}"
114
- f" not found (using {name_template})"
129
+ f" DB annotation {instance._meta.label}.{annotation_name} not found. Found {available}."
115
130
  ) from e
116
131
 
117
132
 
118
133
  @lru_cache
119
- def escape_xml_name(name: str, template="{name}") -> str:
134
+ def _as_annotation_name(name: str, func: type[functions.GeoFunc]) -> str:
120
135
  """Escape an XML name to be used as annotation name."""
121
- return template.format(name=name.replace(".", "_"))
136
+ return f"_{func.__name__}_{name.replace('.', '_')}"
122
137
 
123
138
 
124
- def get_db_geometry_selects(
125
- gml_elements: list[XsdElement], output_crs: CRS
126
- ) -> dict[str, str | functions.Transform]:
127
- """Utility to generate select clauses for the geometry fields of a type.
128
- Key is the xsd element name, value is the database select expression.
139
+ def get_db_geometry_target(
140
+ geo_element: GeometryXsdElement, output_crs: CRS, use_relative_path: bool = False
141
+ ) -> str | functions.Transform:
142
+ """Translate a GML geometry field into the proper expression for retrieving it from the database.
143
+ The path will be wrapped into a CRS Transform function if needed.
129
144
  """
130
- return {
131
- xsd_element.name: _get_db_geometry_target(xsd_element, output_crs)
132
- for xsd_element in gml_elements
133
- if xsd_element.source is not None
134
- }
135
-
136
-
137
- def _get_db_geometry_target(xsd_element: XsdElement, output_crs: CRS) -> str | functions.Transform:
138
- """Wrap the selection of a geometry field in a CRS Transform if needed."""
139
- field = cast(GeometryField, xsd_element.source)
140
- return conditional_transform(xsd_element.orm_path, field.srid, output_srid=output_crs.srid)
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)
145
+ orm_path = geo_element.local_orm_path if use_relative_path else geo_element.orm_path
146
+ if geo_element.source_srid != output_crs.srid:
147
+ return functions.Transform(orm_path, srid=output_crs.srid)
148
+ else:
149
+ return orm_path
gisserver/exceptions.py CHANGED
@@ -8,14 +8,37 @@ https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#35
8
8
  https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#411
9
9
  """
10
10
 
11
+ import logging
12
+ from contextlib import contextmanager
13
+
11
14
  from django.conf import settings
15
+ from django.http import HttpResponse
12
16
  from django.utils.html import format_html
13
17
 
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @contextmanager
22
+ def wrap_parser_errors(name: str, locator: str):
23
+ """Convert the value into a Python format.
24
+ This catches any typical exceptions and transforms them into an OWSException.
25
+ """
26
+ try:
27
+ yield
28
+ except ExternalParsingError as e:
29
+ raise OperationParsingFailed(
30
+ f"Unable to parse {name} argument: {e}", locator=locator
31
+ ) from None
32
+ except (TypeError, ValueError, NotImplementedError) as e:
33
+ # TypeError/ValueError are raised by most handlers for unexpected data
34
+ # The NotImplementedError can be raised by fes parsing.
35
+ raise InvalidParameterValue(f"Invalid {name} argument: {e}", locator=locator) from None
36
+
14
37
 
15
38
  class ExternalValueError(ValueError):
16
39
  """Raise a ValueError for external input.
17
40
  This helps to distinguish between internal bugs
18
- (e.g. unpacking values) and misformed external input.
41
+ (e.g. unpacking values) and malformed external input.
19
42
  """
20
43
 
21
44
 
@@ -32,18 +55,31 @@ class OWSException(Exception):
32
55
  version = "2.0.0"
33
56
  code = None
34
57
  text_template = None
58
+ debug_hint = True
35
59
 
36
- def __init__(self, locator, text=None, code=None, status_code=None):
60
+ def __init__(self, text=None, code=None, locator=None, status_code=None):
37
61
  text = text or self.text_template.format(code=self.code, locator=locator)
62
+ if (code and len(text) < len(code)) or (locator and len(text) < len(locator)):
63
+ raise ValueError(f"text/locator arguments are switched: {text!r}, locator={locator!r}")
64
+
38
65
  super().__init__(text)
39
66
  self.locator = locator
40
67
  self.text = text
41
- self.code = code or self.code
68
+ self.code = code or self.code or self.__class__.__name__
42
69
  self.status_code = status_code or self.status_code
43
70
 
71
+ def as_response(self):
72
+ logger.debug("Returning HTTP %d for %s: %s", self.status_code, self.code, self.text)
73
+ xml_body = self.as_xml()
74
+ return HttpResponse(
75
+ b'<?xml version="1.0" encoding="UTF-8"?>\n%b' % xml_body.encode("utf-8"),
76
+ content_type="text/xml; charset=utf-8",
77
+ status=self.status_code,
78
+ reason=self.reason,
79
+ )
80
+
44
81
  def as_xml(self):
45
82
  return format_html(
46
- '<?xml version="1.0" encoding="UTF-8"?>\n'
47
83
  "<ows:ExceptionReport"
48
84
  ' xmlns:ows="http://www.opengis.net/ows/1.1"'
49
85
  ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
@@ -51,16 +87,18 @@ class OWSException(Exception):
51
87
  ' http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd"'
52
88
  ' xml:lang="en-US"'
53
89
  ' version="2.0.0">\n'
54
- ' <ows:Exception exceptionCode="{code}" locator="{locator}">\n'
55
- " <ows:ExceptionText>{text}{debug}</ows:ExceptionText>\n"
90
+ ' <ows:Exception exceptionCode="{code}"{locator_attr}>\n\n'
91
+ " <ows:ExceptionText>{text}{debug}</ows:ExceptionText>\n\n"
56
92
  " </ows:Exception>\n"
57
- "</ows:ExceptionReport>",
93
+ "</ows:ExceptionReport>\n",
58
94
  code=self.code,
59
- locator=self.locator,
95
+ locator_attr=(
96
+ format_html(' locator="{locator}"', locator=self.locator) if self.locator else ""
97
+ ),
60
98
  text=self.text,
61
99
  debug=(
62
100
  ".\n\n(set GISSERVER_WRAP_FILTER_DB_ERRORS=False to see the Django error page)"
63
- if settings.DEBUG and self.status_code >= 500
101
+ if settings.DEBUG and self.status_code >= 500 and self.debug_hint
64
102
  else ""
65
103
  ),
66
104
  )
@@ -0,0 +1,4 @@
1
+ """Package that allows third party code to extend the server.
2
+
3
+ This allows registering extra stored procedures or filter functions.
4
+ """
@@ -22,8 +22,14 @@ FesFunctionBody = Union[models.Func, Callable[..., models.Func]]
22
22
 
23
23
  @dataclass(order=True)
24
24
  class FesFunction:
25
- """Wrapper that defines a fes function, with type descriptions.
26
- This is also used to provide metadata to the GetCapabilities call.
25
+ """A registered database function that can be used by ``<fes:Function name="...">`.
26
+
27
+ The :class:`~gisserver.parsers.fes20.expressions.Function` class will resolve
28
+ these registered functions by name, and call :meth:`build_query` to include them
29
+ in the database query. This will actually insert a Django ORM function in the query!
30
+
31
+ This wrapper class also provides the metadata and type descriptions of the function,
32
+ which is exposed in the ``GetCapabilities`` call.
27
33
  """
28
34
 
29
35
  #: Name of the function
@@ -50,7 +56,7 @@ class FesFunction:
50
56
 
51
57
 
52
58
  class FesFunctionRegistry:
53
- """Registry of functions to be callable by <fes:Function>.
59
+ """Registry of functions to be callable by ``<fes:Function>``.
54
60
 
55
61
  The registered functions should be capable of running an SQL function.
56
62
  """
@@ -97,12 +103,12 @@ class FesFunctionRegistry:
97
103
  return _wrapper
98
104
 
99
105
  def resolve_function(self, function_name) -> FesFunction:
100
- """Resole the function using it's name."""
106
+ """Resole the function using its name."""
101
107
  try:
102
108
  return self.functions[function_name]
103
109
  except KeyError:
104
110
  raise InvalidParameterValue(
105
- "filter", f"Unsupported function: {function_name}"
111
+ f"Unsupported function: {function_name}", locator="filter"
106
112
  ) from None
107
113
 
108
114