django-gisserver 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/METADATA +15 -13
  2. django_gisserver-1.5.0.dist-info/RECORD +54 -0
  3. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/db.py +14 -20
  6. gisserver/exceptions.py +23 -9
  7. gisserver/features.py +64 -100
  8. gisserver/geometries.py +2 -2
  9. gisserver/operations/base.py +31 -21
  10. gisserver/operations/wfs20.py +44 -38
  11. gisserver/output/__init__.py +2 -1
  12. gisserver/output/base.py +43 -27
  13. gisserver/output/csv.py +38 -33
  14. gisserver/output/geojson.py +43 -51
  15. gisserver/output/gml32.py +88 -67
  16. gisserver/output/results.py +23 -8
  17. gisserver/output/utils.py +18 -2
  18. gisserver/output/xmlschema.py +1 -1
  19. gisserver/parsers/base.py +2 -2
  20. gisserver/parsers/fes20/__init__.py +18 -0
  21. gisserver/parsers/fes20/expressions.py +7 -12
  22. gisserver/parsers/fes20/functions.py +1 -1
  23. gisserver/parsers/fes20/operators.py +7 -3
  24. gisserver/parsers/fes20/query.py +11 -1
  25. gisserver/parsers/fes20/sorting.py +3 -1
  26. gisserver/parsers/gml/base.py +1 -1
  27. gisserver/queries/__init__.py +3 -0
  28. gisserver/queries/adhoc.py +16 -12
  29. gisserver/queries/base.py +76 -36
  30. gisserver/queries/projection.py +240 -0
  31. gisserver/queries/stored.py +7 -6
  32. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
  33. gisserver/types.py +78 -24
  34. gisserver/views.py +9 -20
  35. django_gisserver-1.4.0.dist-info/RECORD +0 -54
  36. gisserver/output/gml32_lxml.py +0 -612
  37. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/LICENSE +0 -0
  38. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
@@ -12,33 +12,35 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Framework :: Django
20
20
  Classifier: Framework :: Django :: 3.2
21
21
  Classifier: Framework :: Django :: 4.0
22
22
  Classifier: Framework :: Django :: 4.2
23
+ Classifier: Framework :: Django :: 5.0
24
+ Classifier: Framework :: Django :: 5.1
23
25
  Classifier: Topic :: Internet :: WWW/HTTP
24
26
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
25
27
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
26
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
29
  Requires: Django (>=3.2)
28
- Requires-Python: >=3.6
30
+ Requires-Python: >=3.8
29
31
  Description-Content-Type: text/markdown
30
32
  License-File: LICENSE
31
- Requires-Dist: Django >=3.2
32
- Requires-Dist: defusedxml >=0.6.0
33
- Requires-Dist: lru-dict >=1.1.7
34
- Requires-Dist: orjson >=2.4.0
33
+ Requires-Dist: Django>=3.2
34
+ Requires-Dist: defusedxml>=0.6.0
35
+ Requires-Dist: lru_dict>=1.1.7
36
+ Requires-Dist: orjson>=3.9.15
35
37
  Provides-Extra: tests
36
- Requires-Dist: django-environ >=0.4.5 ; extra == 'tests'
37
- Requires-Dist: psycopg2-binary >=2.8.4 ; extra == 'tests'
38
- Requires-Dist: lxml >=4.5.0 ; extra == 'tests'
39
- Requires-Dist: pytest >=6.2.3 ; extra == 'tests'
40
- Requires-Dist: pytest-django >=4.1.0 ; extra == 'tests'
41
- Requires-Dist: pytest-cov >=2.11.1 ; extra == 'tests'
38
+ Requires-Dist: django-environ>=0.4.5; extra == "tests"
39
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
40
+ Requires-Dist: lxml>=4.9.1; extra == "tests"
41
+ Requires-Dist: pytest>=6.2.3; extra == "tests"
42
+ Requires-Dist: pytest-django>=4.1.0; extra == "tests"
43
+ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
42
44
 
43
45
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
44
46
  [![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)
@@ -0,0 +1,54 @@
1
+ gisserver/__init__.py,sha256=XtaIlx7Gik8RqIFDc1G0JAUfcmeNyTWjev1E1Oym_ms,40
2
+ gisserver/conf.py,sha256=3LSfeDRTdJCLuopAOD14Y5m2iPhovreIaNI9zbaukpo,2343
3
+ gisserver/db.py,sha256=-d6VQRkRWMSWKkA_6cVWhLzSI1cKmRoVktqA6hVwvuM,4928
4
+ gisserver/exceptions.py,sha256=wRsiKWhZtA8NmfMNncRtMeFgWDMdOWRp9Ey96iFAwMI,5194
5
+ gisserver/features.py,sha256=b3hEDs90xwEsANcRgipekZZrp4ucxRWwGeUutk2gRTw,31821
6
+ gisserver/geometries.py,sha256=HLZ3d8E5M2sfs2Kzz_GE3pZdynn-rYXPpuU9GVh0vsg,12398
7
+ gisserver/types.py,sha256=bsl3PezhGcHpUAtuV4AhdBmFLmtZZpDCT_HqpdVyLtM,36936
8
+ gisserver/views.py,sha256=xMHKezn4F1xZ2r78l9IzUlL5M3qZ78fjRJJtSzA8-Ww,12809
9
+ gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ gisserver/operations/base.py,sha256=Ls5i6ulMJqyhqu6o969rJ-mezW-T6OW5mr8oZ3BTBH8,17222
11
+ gisserver/operations/wfs20.py,sha256=bQeMJ4T6AisSBMABYa3li1u53w0zUxs02qBwjS4QbOw,18742
12
+ gisserver/output/__init__.py,sha256=C6hXIVIxBd2o_1Y7VhIU9vCg7pswI2tXsd74wgMdU4o,2134
13
+ gisserver/output/base.py,sha256=GygubFyhPO33fQw3jIuOerEH8yjbk0DJ6NhQdFkT7-g,8645
14
+ gisserver/output/csv.py,sha256=Mm7zYzUs9ffw9fqcKY9NT7qbMX-MzSFnqBg5I1wdRU0,6459
15
+ gisserver/output/geojson.py,sha256=bhRtNdMi1tMcSFBxDTNL8oysAMVhoolctKjZhxIncN8,11297
16
+ gisserver/output/gml32.py,sha256=KbhcBBVp4sEhvKs5e6fP4_GKKL6Kr2yyYaDLWePBQxs,31293
17
+ gisserver/output/results.py,sha256=VD-fJgWrMTwGaEGG_BSVhDv_m60dldBY6yAcTpRICT4,13949
18
+ gisserver/output/utils.py,sha256=FIlkHymA43RLZAfB6-trM3_IQKbIVb_4OUp5fxRbVGY,7379
19
+ gisserver/output/xmlschema.py,sha256=TdTMyP2_r98wkeffWAwrfm3l9XAmivu1KcYWNpgQKR4,4578
20
+ gisserver/parsers/__init__.py,sha256=g72D8EtR-QkhcemzY5-Qty-nhHPKza83Sz3KZm8bfq0,290
21
+ gisserver/parsers/base.py,sha256=9DE73wMphp7At3mGW0xwGU_TWPm55x8JEh4odpdRhlo,4833
22
+ gisserver/parsers/tags.py,sha256=NyJhbt-U2zTewfhghH4LRslTaDK2UOq9Vgk83pWfJh0,3553
23
+ gisserver/parsers/values.py,sha256=ypNPl4HduiVzexsL59ukZntlwXk5gHuSvQO6yhLeXD4,1016
24
+ gisserver/parsers/fes20/__init__.py,sha256=72RuFxAQp9hW0pgFUDyfQ0WsStNNk6J_y3NH-zYaPOA,1140
25
+ gisserver/parsers/fes20/expressions.py,sha256=A1-8JsGahXJPWLuTSVNAf0oSuyLfDMnFKM7viIt35Y8,9573
26
+ gisserver/parsers/fes20/filters.py,sha256=HdWguZvAT2L4Xm-bNa4XPh7MEymqTB6fo8r47Z-Bjuw,3511
27
+ gisserver/parsers/fes20/functions.py,sha256=mutPlbjOlg6R-9nNe8goaNdsAlZPkuckYlhMsQyp6NE,8723
28
+ gisserver/parsers/fes20/identifiers.py,sha256=vcPrMYcsemSlB9ziSUdVulhoI5PGFAh8vqeADBCWd60,2864
29
+ gisserver/parsers/fes20/operators.py,sha256=oQezU553Mts1DoExlH7_U29XXXdk6V84ihlBOozRzLg,23360
30
+ gisserver/parsers/fes20/query.py,sha256=_OxElAgeo34-00pNY5Apc6TSUKokSG9VWSMmIplN7AA,10638
31
+ gisserver/parsers/fes20/sorting.py,sha256=Uj8gsbBXumihWkrcj_pRPscwZAScUJSngAyjKri-Z8Y,2184
32
+ gisserver/parsers/gml/__init__.py,sha256=lNK0SXWkmTKdeJD9xdK6B-N-Y7wwGL4NRBI3dzoJDNI,1463
33
+ gisserver/parsers/gml/base.py,sha256=xWC5FrJyMjq7h5zmKwEvtRF0-CGVbCtvnO8Isvnf4mE,1065
34
+ gisserver/parsers/gml/geometries.py,sha256=cgV3WogA7LyC_H_NF7dGoGLvLdQRACG6AlThRkX_jhU,3171
35
+ gisserver/queries/__init__.py,sha256=yntEYRfaZ1Yjnr8lMvJt9Erh31WktikIf1af4TTuX74,1000
36
+ gisserver/queries/adhoc.py,sha256=Hhcxwt5C2OHqtLc-3QUqx8MZEnhoqkKWjBp56e6aljc,7428
37
+ gisserver/queries/base.py,sha256=EVJiwcLBjxGXJ8k012gGEiZ1q_gQZokSwdMl4H1SPus,7857
38
+ gisserver/queries/projection.py,sha256=KEz9UcZGVkYq7XTm_5nx8w5TnbSqht7rOGEuTGeJwm8,9922
39
+ gisserver/queries/stored.py,sha256=G258R9HVX3OooM5mhQF_N_JrFHLjM2y5iusrzPcN9TU,7125
40
+ gisserver/static/gisserver/index.css,sha256=fiQuCMuiWUhhrNQD6bbUJaX5vH5OpG9qgPdELPB8cpI,163
41
+ gisserver/templates/gisserver/index.html,sha256=yliku8jDqAOcenkbe1YnvJBnuS13MvzzsoDV8sQZ3GM,866
42
+ gisserver/templates/gisserver/service_description.html,sha256=3EHLnQx27_nTDZFD2RH6E10EWnTN90s1DGuEj9PeBrg,993
43
+ gisserver/templates/gisserver/wfs/feature_field.html,sha256=H5LsTAxGVGQqEpOZdEUTszQmFAZSpB9ggRJUsTdjYnw,538
44
+ gisserver/templates/gisserver/wfs/feature_type.html,sha256=pIfa1cMsTlNIrzNiEOaq9LjzzF7_vZ9C7eN6KDBLOHE,788
45
+ gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml,sha256=4Hxw1kCnxfWXTVPbHf8IW9_qiSeIEtnxHK6z6TS3-zA,1041
46
+ gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=Ej6ZXf-nGiPjiKdhNK5vAsX2iiiS12knjD0VaSzSltM,8717
47
+ gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml,sha256=CIBVQrKXmGMQ6BKbX4_0ru4lXtWnW5EIgHUbIi1V0Vc,636
48
+ gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ gisserver/templatetags/gisserver_tags.py,sha256=RFGqEj-EQi9FrJiFJ7HHAQqlU4fDKVKTtZrPNwURXgA,204
50
+ django_gisserver-1.5.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
51
+ django_gisserver-1.5.0.dist-info/METADATA,sha256=IWlSvP8d_4wsCVq4NWAE0T7Q8QZGqFVMvWUyLss85aY,5859
52
+ django_gisserver-1.5.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
53
+ django_gisserver-1.5.0.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
54
+ django_gisserver-1.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
gisserver/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.4.0" # follows PEP440
1
+ __version__ = "1.5.0" # follows PEP440
gisserver/db.py CHANGED
@@ -3,14 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from functools import lru_cache, reduce
6
- from typing import cast
7
6
 
8
- from django.contrib.gis.db.models import GeometryField, functions
7
+ from django.contrib.gis.db.models import functions
9
8
  from django.db import connection, connections, models
10
9
 
11
10
  from gisserver import conf
12
11
  from gisserver.geometries import CRS
13
- from gisserver.types import XPathMatch, XsdElement
12
+ from gisserver.types import GmlElement
14
13
 
15
14
 
16
15
  class AsEWKT(functions.GeoFunc):
@@ -54,9 +53,9 @@ class ST_Union(functions.Union):
54
53
  arity = None
55
54
 
56
55
  def as_postgresql(self, compiler, connection, **extra_context):
57
- # PostgreSQL can handle ST_Union(ARRAY(field names)), other databases don't.
56
+ # PostgreSQL can handle ST_Union(ARRAY[field names]), other databases don't.
58
57
  if len(self.source_expressions) > 2:
59
- extra_context["template"] = "%(function)s(ARRAY(%(expressions)s))"
58
+ extra_context["template"] = "%(function)s(ARRAY[%(expressions)s])"
60
59
  return self.as_sql(compiler, connection, **extra_context)
61
60
 
62
61
 
@@ -65,7 +64,7 @@ def get_geometries_union(
65
64
  ) -> str | functions.Union:
66
65
  """Generate a union of multiple geometry fields."""
67
66
  if not expressions:
68
- raise ValueError("Missing geometery fields for get_geometries_union()")
67
+ raise ValueError("Missing geometry fields for get_geometries_union()")
69
68
 
70
69
  if len(expressions) == 1:
71
70
  return next(iter(expressions)) # fastest in set data type
@@ -73,7 +72,7 @@ def get_geometries_union(
73
72
  return functions.Union(*expressions)
74
73
  elif connections[using].vendor == "postgresql":
75
74
  # postgres can handle multiple field names
76
- return ST_Union(expressions)
75
+ return ST_Union(*expressions)
77
76
  else:
78
77
  # other databases do Union(Union(1, 2), 3)
79
78
  return reduce(functions.Union, expressions)
@@ -122,25 +121,20 @@ def escape_xml_name(name: str, template="{name}") -> str:
122
121
 
123
122
 
124
123
  def get_db_geometry_selects(
125
- gml_elements: list[XsdElement], output_crs: CRS
124
+ gml_elements: list[GmlElement], output_crs: CRS
126
125
  ) -> dict[str, str | functions.Transform]:
127
126
  """Utility to generate select clauses for the geometry fields of a type.
128
127
  Key is the xsd element name, value is the database select expression.
129
128
  """
130
129
  return {
131
- 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
130
+ gml_element.name: get_db_geometry_target(gml_element, output_crs)
131
+ for gml_element in gml_elements
132
+ if gml_element.source is not None
134
133
  }
135
134
 
136
135
 
137
- def _get_db_geometry_target(xsd_element: XsdElement, output_crs: CRS) -> str | functions.Transform:
136
+ def get_db_geometry_target(gml_element: GmlElement, output_crs: CRS) -> str | functions.Transform:
138
137
  """Wrap the selection of a geometry field in a CRS Transform if needed."""
139
- 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)
138
+ return conditional_transform(
139
+ gml_element.orm_path, gml_element.source.srid, output_srid=output_crs.srid
140
+ )
gisserver/exceptions.py CHANGED
@@ -9,13 +9,14 @@ https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#411
9
9
  """
10
10
 
11
11
  from django.conf import settings
12
+ from django.http import HttpResponse
12
13
  from django.utils.html import format_html
13
14
 
14
15
 
15
16
  class ExternalValueError(ValueError):
16
17
  """Raise a ValueError for external input.
17
18
  This helps to distinguish between internal bugs
18
- (e.g. unpacking values) and misformed external input.
19
+ (e.g. unpacking values) and malformed external input.
19
20
  """
20
21
 
21
22
 
@@ -32,18 +33,29 @@ class OWSException(Exception):
32
33
  version = "2.0.0"
33
34
  code = None
34
35
  text_template = None
36
+ debug_hint = True
35
37
 
36
- def __init__(self, locator, text=None, code=None, status_code=None):
38
+ def __init__(self, text=None, code=None, locator=None, status_code=None):
37
39
  text = text or self.text_template.format(code=self.code, locator=locator)
40
+ if (code and len(text) < len(code)) or (locator and len(text) < len(locator)):
41
+ raise ValueError("text/locator arguments are switched")
42
+
38
43
  super().__init__(text)
39
44
  self.locator = locator
40
45
  self.text = text
41
- self.code = code or self.code
46
+ self.code = code or self.code or self.__class__.__name__
42
47
  self.status_code = status_code or self.status_code
43
48
 
49
+ def as_response(self):
50
+ return HttpResponse(
51
+ b'<?xml version="1.0" encoding="UTF-8"?>\n%b' % self.as_xml().encode("utf-8"),
52
+ content_type="text/xml; charset=utf-8",
53
+ status=self.status_code,
54
+ reason=self.reason,
55
+ )
56
+
44
57
  def as_xml(self):
45
58
  return format_html(
46
- '<?xml version="1.0" encoding="UTF-8"?>\n'
47
59
  "<ows:ExceptionReport"
48
60
  ' xmlns:ows="http://www.opengis.net/ows/1.1"'
49
61
  ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
@@ -51,16 +63,18 @@ class OWSException(Exception):
51
63
  ' http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd"'
52
64
  ' xml:lang="en-US"'
53
65
  ' version="2.0.0">\n'
54
- ' <ows:Exception exceptionCode="{code}" locator="{locator}">\n'
55
- " <ows:ExceptionText>{text}{debug}</ows:ExceptionText>\n"
66
+ ' <ows:Exception exceptionCode="{code}"{locator_attr}>\n\n'
67
+ " <ows:ExceptionText>{text}{debug}</ows:ExceptionText>\n\n"
56
68
  " </ows:Exception>\n"
57
- "</ows:ExceptionReport>",
69
+ "</ows:ExceptionReport>\n",
58
70
  code=self.code,
59
- locator=self.locator,
71
+ locator_attr=(
72
+ format_html(' locator="{locator}"', locator=self.locator) if self.locator else ""
73
+ ),
60
74
  text=self.text,
61
75
  debug=(
62
76
  ".\n\n(set GISSERVER_WRAP_FILTER_DB_ERRORS=False to see the Django error page)"
63
- if settings.DEBUG and self.status_code >= 500
77
+ if settings.DEBUG and self.status_code >= 500 and self.debug_hint
64
78
  else ""
65
79
  ),
66
80
  )
gisserver/features.py CHANGED
@@ -18,10 +18,9 @@ from __future__ import annotations
18
18
 
19
19
  import html
20
20
  import operator
21
- from collections import defaultdict
22
21
  from dataclasses import dataclass
23
22
  from functools import cached_property, lru_cache, reduce
24
- from typing import Union
23
+ from typing import TYPE_CHECKING, Literal, Union
25
24
 
26
25
  from django.conf import settings
27
26
  from django.contrib.gis.db import models as gis_models
@@ -35,6 +34,7 @@ from gisserver.exceptions import ExternalValueError
35
34
  from gisserver.geometries import CRS, WGS84, BoundingBox
36
35
  from gisserver.types import (
37
36
  GmlBoundedByElement,
37
+ GmlElement,
38
38
  GmlIdAttribute,
39
39
  GmlNameElement,
40
40
  XPathMatch,
@@ -49,13 +49,10 @@ if "django.contrib.postgres" in settings.INSTALLED_APPS:
49
49
  else:
50
50
  ArrayField = None
51
51
 
52
- try:
53
- from typing import Literal # Python 3.8
54
-
55
- _all_ = Literal["__all__"]
56
- except ImportError:
57
- _all_ = str
52
+ if TYPE_CHECKING:
53
+ from gisserver.queries import FeatureRelation
58
54
 
55
+ _all_ = Literal["__all__"]
59
56
 
60
57
  __all__ = [
61
58
  "FeatureType",
@@ -125,7 +122,12 @@ def get_basic_field_type(
125
122
  return DEFAULT_XSD_TYPE
126
123
 
127
124
 
128
- def _get_model_fields(model, fields, parent=None):
125
+ def _get_model_fields(
126
+ model: type[models.Model],
127
+ fields: _all_ | list[str],
128
+ parent: ComplexFeatureField | None = None,
129
+ feature_type: FeatureType | None = None,
130
+ ):
129
131
  if fields == "__all__":
130
132
  # All regular fields
131
133
  # Relationships will not be expanded since it can expose so many other fields.
@@ -136,6 +138,7 @@ def _get_model_fields(model, fields, parent=None):
136
138
  # .bind() is called directly by providing these arguments via init:
137
139
  model=model,
138
140
  parent=parent,
141
+ feature_type=feature_type,
139
142
  )
140
143
  for f in model._meta.get_fields()
141
144
  if not f.is_relation or f.many_to_one or f.one_to_one # ForeignKey # OneToOneField
@@ -144,7 +147,7 @@ def _get_model_fields(model, fields, parent=None):
144
147
  # Only defined fields
145
148
  fields = [f if isinstance(f, FeatureField) else FeatureField(f) for f in fields]
146
149
  for field in fields:
147
- field.bind(model, parent=parent)
150
+ field.bind(model, parent=parent, feature_type=feature_type)
148
151
  return fields
149
152
 
150
153
 
@@ -180,14 +183,28 @@ class FeatureField:
180
183
  model_attribute=None,
181
184
  model=None,
182
185
  parent: ComplexFeatureField | None = None,
186
+ feature_type: FeatureType | None = None,
183
187
  abstract=None,
184
188
  xsd_class: type[XsdElement] | None = None,
185
189
  ):
190
+ """
191
+ Initialize a single field of the feature.
192
+
193
+ :param name: Name of the model field.
194
+ :param model_attribute: Which model attribute to access. This can be a dotted field path.
195
+ :param model: Which model is accessed. Usually this is passed via :meth:`bind`.
196
+ :param parent: The parent field of this element. Usually this is passed via :meth:`bind`.
197
+ :param feature_type: The feature this field is a part of. Usually this is passed via :meth:`bind`.
198
+ :param abstract: The "help text" or short abstract/description for this field.
199
+ :param xsd_class: Override which class is used to construct the internal XsdElement.
200
+ This controls the schema rendering, value retrieval and rendering of the element.
201
+ """
186
202
  self.name = name
187
203
  self.model_attribute = model_attribute
188
204
  self.model = None
189
205
  self.model_field = None
190
206
  self.parent = parent
207
+ self.feature_type = feature_type
191
208
  self.abstract = abstract
192
209
 
193
210
  # Allow to override the class attribute on 'self',
@@ -197,7 +214,7 @@ class FeatureField:
197
214
 
198
215
  self._nillable_relation = False
199
216
  if model is not None:
200
- self.bind(model)
217
+ self.bind(model, parent=parent, feature_type=feature_type)
201
218
 
202
219
  def __repr__(self):
203
220
  return f"<{self.__class__.__name__}: {self.name}>"
@@ -209,6 +226,7 @@ class FeatureField:
209
226
  self,
210
227
  model: type[models.Model],
211
228
  parent: ComplexFeatureField | None = None,
229
+ feature_type: FeatureType | None = None,
212
230
  ):
213
231
  """Late-binding for the model.
214
232
 
@@ -221,11 +239,13 @@ class FeatureField:
221
239
  :param model: The model is field is linked to.
222
240
  :param parent: When this element is part of a complex feature,
223
241
  this links to the parent field.
242
+ :param feature_type: The original feature type that his element was mentioned in.
224
243
  """
225
244
  if self.model is not None:
226
245
  raise RuntimeError(f"Feature field '{self.name}' cannot be reused")
227
246
  self.model = model
228
247
  self.parent = parent
248
+ self.feature_type = feature_type
229
249
 
230
250
  if self.model_attribute:
231
251
  # Support dot based field traversal.
@@ -285,6 +305,7 @@ class FeatureField:
285
305
  max_occurs=max_occurs,
286
306
  model_attribute=self.model_attribute,
287
307
  source=self.model_field,
308
+ feature_type=self.feature_type,
288
309
  )
289
310
 
290
311
 
@@ -312,9 +333,14 @@ class ComplexFeatureField(FeatureField):
312
333
  ):
313
334
  """
314
335
  :param name: Name of the model field.
315
- :param fields: List of fields to expose for the target model. This can
316
- be a list of :class:`FeatureField` objects, or plain field names.
336
+ :param fields: If the field exposes a foreign key, provide its child element names.
337
+ This can be a list of :func:`field` elements, or plain field names.
317
338
  Using ``__all__`` also works but is not recommended outside testing.
339
+ :param model_attribute: Which model attribute to access. This can be a dotted field path.
340
+ :param abstract: The "help text" or short abstract/description for this field.
341
+ :param xsd_class: Override which class is used to construct the internal XsdElement.
342
+ This controls the schema rendering, value retrieval and rendering of the element.
343
+ :param xsd_base_type: Override which class is the base class for the element.
318
344
  """
319
345
  super().__init__(
320
346
  name,
@@ -344,6 +370,7 @@ class ComplexFeatureField(FeatureField):
344
370
  type_name=self.name,
345
371
  source=pk_field,
346
372
  model_attribute=pk_field.name,
373
+ feature_type=self.feature_type,
347
374
  )
348
375
  ],
349
376
  base=self.xsd_base_type,
@@ -378,9 +405,13 @@ def field(
378
405
  so little knowledge is needed about the internal working.
379
406
 
380
407
  :param name: Name of the model field.
381
- :param fields: If the field exposes a foreign key, provide it's child element names.
408
+ :param model_attribute: Which model attribute to access. This can be a dotted field path.
409
+ :param abstract: The "help text" or short abstract/description for this field.
410
+ :param fields: If the field exposes a foreign key, provide its child element names.
382
411
  This can be a list of :func:`field` elements, or plain field names.
383
412
  Using ``__all__`` also works but is not recommended outside testing.
413
+ :param xsd_class: Override which class is used to construct the internal XsdElement.
414
+ This controls the schema rendering, value retrieval and rendering of the element.
384
415
  """
385
416
  if fields is not None:
386
417
  return ComplexFeatureField(
@@ -399,42 +430,6 @@ def field(
399
430
  )
400
431
 
401
432
 
402
- @dataclass
403
- class FeatureRelation:
404
- """Tell which related fields are queried by the feature.
405
- Each dict holds an ORM-path, with the relevant sub-elements.
406
- """
407
-
408
- #: The ORM path that is queried for this particular relation
409
- orm_path: str
410
- #: The fields that will be retrieved for that path
411
- orm_fields: set[str]
412
- #: The model that is accessed for this relation (if set)
413
- related_model: type[models.Model] | None
414
- #: The source elements that access this relation.
415
- xsd_elements: list[XsdElement]
416
-
417
- @cached_property
418
- def _local_model_field_names(self) -> list[str]:
419
- """Tell which local fields of the model will be accessed by this feature."""
420
-
421
- meta = self.related_model._meta
422
- result = []
423
- for name in self.orm_fields:
424
- model_field = meta.get_field(name)
425
- if not model_field.many_to_many and not model_field.one_to_many:
426
- result.append(name)
427
-
428
- # When this relation is retrieved through a ManyToOneRel (reverse FK),
429
- # the prefetch_related() also needs to have the original foreign key
430
- # in order to link all prefetches to the proper parent instance.
431
- for xsd_element in self.xsd_elements:
432
- if xsd_element.source is not None and xsd_element.source.one_to_many:
433
- result.append(xsd_element.source.field.name)
434
-
435
- return result
436
-
437
-
438
433
  class FeatureType:
439
434
  """Declare a feature that is exposed on the map.
440
435
 
@@ -472,7 +467,7 @@ class FeatureType:
472
467
  :param fields: Define which fields to show in the WFS data.
473
468
  This can be a list of field names, or :class:`FeatureField` objects.
474
469
  :param display_field_name: Name of the field that's used as general string representation.
475
- :param geometry_field: Name of the geometry field to expose (default = auto detect).
470
+ :param geometry_field_name: Name of the geometry field to expose (default = auto-detect).
476
471
  :param name: Name, also used as XML tag name.
477
472
  :param title: Used in WFS metadata.
478
473
  :param abstract: Used in WFS metadata.
@@ -530,6 +525,11 @@ class FeatureType:
530
525
  """Return all spatial reference system ID's that this feature supports."""
531
526
  return [self.crs] + self.other_crs
532
527
 
528
+ @cached_property
529
+ def geometry_elements(self) -> list[GmlElement]:
530
+ """Tell which fields of the XSD have a geometry field."""
531
+ return [ff.xsd_element for ff in self.fields if ff.xsd_element.is_geometry]
532
+
533
533
  @cached_property
534
534
  def geometry_fields(self) -> list[GeometryField]:
535
535
  """Tell which fields of the model have a geometry field.
@@ -541,59 +541,15 @@ class FeatureType:
541
541
  if ff.xsd_element.is_geometry
542
542
  ]
543
543
 
544
- @cached_property
545
- def orm_relations(self) -> list[FeatureRelation]:
546
- """Tell which fields will be retrieved from related fields.
547
-
548
- This gives an object layout based on the XSD elements,
549
- that can be used for prefetching data.
550
- """
551
- models = {}
552
- fields = defaultdict(set)
553
- elements = defaultdict(list)
554
-
555
- # Check all elements that render as "dotted" flattened relation
556
- for xsd_element in self.xsd_type.flattened_elements:
557
- if xsd_element.source is not None:
558
- # Split "relation.field" notation into path, and take the field as child attribute.
559
- obj_path, field = xsd_element.orm_relation
560
- elements[obj_path].append(xsd_element)
561
- fields[obj_path].add(field)
562
- # field is already on relation:
563
- models[obj_path] = xsd_element.source.model
564
-
565
- # Check all elements that render as "nested" complex type:
566
- for xsd_element in self.xsd_type.complex_elements:
567
- # The complex element itself points to the root of the path,
568
- # all sub elements become the child attributes.
569
- obj_path = xsd_element.orm_path
570
- elements[obj_path].append(xsd_element)
571
- fields[obj_path] = {
572
- f.orm_path
573
- for f in xsd_element.type.elements
574
- if not f.is_many or f.is_array # exclude M2M, but include ArrayField
575
- }
576
- if xsd_element.source:
577
- # field references a related object:
578
- models[obj_path] = xsd_element.source.related_model
579
-
580
- return [
581
- FeatureRelation(
582
- orm_path=obj_path,
583
- orm_fields=sub_fields,
584
- related_model=models.get(obj_path),
585
- xsd_elements=elements[obj_path],
586
- )
587
- for obj_path, sub_fields in fields.items()
588
- ]
589
-
590
544
  @cached_property
591
545
  def _local_model_field_names(self) -> list[str]:
592
546
  """Tell which local fields of the model will be accessed by this feature."""
593
547
  return [
594
548
  (ff.model_field.name if ff.model_field else ff.model_attribute)
595
549
  for ff in self.fields
596
- if not ff.model_field.many_to_many and not ff.model_field.one_to_many
550
+ if not ff.model_field.many_to_many
551
+ and not ff.model_field.one_to_many
552
+ and not (ff.model_attribute and "." in ff.model_attribute)
597
553
  ]
598
554
 
599
555
  @cached_property
@@ -609,9 +565,9 @@ class FeatureType:
609
565
  if isinstance(f, gis_models.GeometryField)
610
566
  )
611
567
 
612
- return [FeatureField(self._geometry_field_name, model=self.model)]
568
+ return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
613
569
  else:
614
- return _get_model_fields(self.model, self._fields)
570
+ return _get_model_fields(self.model, self._fields, feature_type=self)
615
571
 
616
572
  @cached_property
617
573
  def display_field(self) -> models.Field | None:
@@ -652,6 +608,13 @@ class FeatureType:
652
608
  # Default: take the first geometry
653
609
  return self.geometry_fields[0]
654
610
 
611
+ @cached_property
612
+ def main_geometry_element(self) -> GmlElement:
613
+ """Give access to the main geometry element."""
614
+ # NOTE: this property should make it easier to move away from having a main geometry field
615
+ # at the model level, and allow a geometry field in any depth of the data model.
616
+ return next((e for e in self.geometry_elements if e.source is self.geometry_field), None)
617
+
655
618
  @cached_property
656
619
  def geometry_field_name(self) -> str:
657
620
  """Tell which field is the geometry field."""
@@ -771,6 +734,7 @@ class FeatureType:
771
734
  type_name=self.name,
772
735
  source=pk_field,
773
736
  model_attribute=pk_field.name,
737
+ feature_type=self,
774
738
  )
775
739
  ],
776
740
  base=XsdTypes.gmlAbstractGMLType,
gisserver/geometries.py CHANGED
@@ -123,7 +123,7 @@ class CRS:
123
123
 
124
124
  @classmethod
125
125
  def from_srid(cls, srid: int, backend=None):
126
- """Instantiate this class using an numeric spatial reference ID
126
+ """Instantiate this class using a numeric spatial reference ID
127
127
 
128
128
  This is logically identical to calling::
129
129
 
@@ -142,7 +142,7 @@ class CRS:
142
142
 
143
143
  @classmethod
144
144
  def _from_urn(cls, urn, backend=None): # noqa: C901
145
- """Instantiate this class using an URN format."""
145
+ """Instantiate this class using a URN format."""
146
146
  urn_match = CRS_URN_REGEX.match(urn)
147
147
  if not urn_match:
148
148
  raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")