django-gisserver 1.4.1__tar.gz → 1.5.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.
Files changed (64) hide show
  1. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/CHANGES.md +11 -1
  2. {django-gisserver-1.4.1/django_gisserver.egg-info → django_gisserver-1.5.0}/PKG-INFO +12 -2
  3. {django-gisserver-1.4.1 → django_gisserver-1.5.0/django_gisserver.egg-info}/PKG-INFO +12 -2
  4. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/SOURCES.txt +1 -0
  5. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/requires.txt +2 -2
  6. django_gisserver-1.5.0/gisserver/__init__.py +1 -0
  7. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/db.py +14 -20
  8. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/exceptions.py +23 -9
  9. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/features.py +62 -99
  10. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/geometries.py +2 -2
  11. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/operations/base.py +31 -21
  12. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/operations/wfs20.py +44 -38
  13. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/__init__.py +2 -1
  14. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/base.py +43 -27
  15. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/csv.py +38 -33
  16. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/geojson.py +43 -51
  17. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/gml32.py +88 -67
  18. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/results.py +23 -8
  19. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/utils.py +18 -2
  20. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/output/xmlschema.py +1 -1
  21. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/base.py +2 -2
  22. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/__init__.py +18 -0
  23. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/expressions.py +7 -12
  24. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/functions.py +1 -1
  25. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/operators.py +7 -3
  26. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/query.py +11 -1
  27. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/sorting.py +3 -1
  28. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/gml/base.py +1 -1
  29. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/__init__.py +3 -0
  30. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/adhoc.py +16 -12
  31. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/base.py +76 -36
  32. django_gisserver-1.5.0/gisserver/queries/projection.py +240 -0
  33. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/queries/stored.py +7 -6
  34. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
  35. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/types.py +78 -24
  36. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/views.py +9 -20
  37. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/pyproject.toml +1 -1
  38. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/setup.py +2 -2
  39. django-gisserver-1.4.1/gisserver/__init__.py +0 -1
  40. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/LICENSE +0 -0
  41. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/MANIFEST.in +0 -0
  42. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/README.md +0 -0
  43. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/dependency_links.txt +0 -0
  44. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/not-zip-safe +0 -0
  45. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/django_gisserver.egg-info/top_level.txt +0 -0
  46. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/conf.py +0 -0
  47. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/operations/__init__.py +0 -0
  48. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/__init__.py +0 -0
  49. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/filters.py +0 -0
  50. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/fes20/identifiers.py +0 -0
  51. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/gml/__init__.py +0 -0
  52. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/gml/geometries.py +0 -0
  53. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/tags.py +0 -0
  54. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/parsers/values.py +0 -0
  55. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/static/gisserver/index.css +0 -0
  56. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/index.html +0 -0
  57. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/service_description.html +0 -0
  58. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -0
  59. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -0
  60. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
  61. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templates/gisserver/wfs/feature_type.html +0 -0
  62. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templatetags/__init__.py +0 -0
  63. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/gisserver/templatetags/gisserver_tags.py +0 -0
  64. {django-gisserver-1.4.1 → django_gisserver-1.5.0}/setup.cfg +0 -0
@@ -1,3 +1,13 @@
1
+ # 2024-11-25 (1.5.0)
2
+
3
+ * Added `PROPERTYNAME` support
4
+ * Added rendering of `<wfs:truncatedResponse>` when errors happen during output streaming.
5
+ * Make sure Django 4.1+ won't do double prefetches on relations that we prefetch.
6
+ * Hide the erroneous output formats in `GetCapabilities` that are supported for FME.
7
+ * Bump requirements to non-vulnerable versions (of lxml and orjson).
8
+ * Cleaned up code, removing some internal methods.
9
+ * Cleaned up leftover Python 3.7 compat code.
10
+
1
11
  # 2024-08-29 (1.4.1)
2
12
 
3
13
  * Fix 500 error when `model_attribute` points to a dotted-path.
@@ -197,7 +207,7 @@ for their work on this release.
197
207
  # 2020-06-29 (0.8.1)
198
208
 
199
209
  * Added unpaginated GeoJSON support (performance is good enough).
200
- * Added basic support for non-namespaced `FILTER` queries, and `PropertyName` tags (though being WFS2 attributes, clients still use them).
210
+ * Added basic support for non-namespaced `FILTER` queries, and `PropertyName` tags (though being WFS1 attributes, clients still use them).
201
211
  * Added extra strict check that `ValueReference`/`Literal`/`ResourceId` nodes don't have child nodes.
202
212
  * Fixed allowing filtering on unknown or undefined fields (XPaths are now resolved to known elements).
203
213
  * Optimized results streaming by automatically using a queryset-iterator if possible.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.4.1
3
+ Version: 1.5.0
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
@@ -29,8 +29,18 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
29
  Requires: Django (>=3.2)
30
30
  Requires-Python: >=3.8
31
31
  Description-Content-Type: text/markdown
32
- Provides-Extra: tests
33
32
  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>=3.9.15
37
+ 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.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"
34
44
 
35
45
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
36
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-gisserver
3
- Version: 1.4.1
3
+ Version: 1.5.0
4
4
  Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
5
5
  Home-page: https://github.com/amsterdam/django-gisserver
6
6
  Author: Diederik van der Boor
@@ -29,8 +29,18 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
29
  Requires: Django (>=3.2)
30
30
  Requires-Python: >=3.8
31
31
  Description-Content-Type: text/markdown
32
- Provides-Extra: tests
33
32
  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>=3.9.15
37
+ 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.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"
34
44
 
35
45
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
36
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)
@@ -47,6 +47,7 @@ gisserver/parsers/gml/geometries.py
47
47
  gisserver/queries/__init__.py
48
48
  gisserver/queries/adhoc.py
49
49
  gisserver/queries/base.py
50
+ gisserver/queries/projection.py
50
51
  gisserver/queries/stored.py
51
52
  gisserver/static/gisserver/index.css
52
53
  gisserver/templates/gisserver/index.html
@@ -1,12 +1,12 @@
1
1
  Django>=3.2
2
2
  defusedxml>=0.6.0
3
3
  lru_dict>=1.1.7
4
- orjson>=2.4.0
4
+ orjson>=3.9.15
5
5
 
6
6
  [tests]
7
7
  django-environ>=0.4.5
8
8
  psycopg2-binary>=2.8.4
9
- lxml>=4.5.0
9
+ lxml>=4.9.1
10
10
  pytest>=6.2.3
11
11
  pytest-django>=4.1.0
12
12
  pytest-cov>=2.11.1
@@ -0,0 +1 @@
1
+ __version__ = "1.5.0" # follows PEP440
@@ -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
+ )
@@ -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
  )
@@ -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,15 +183,30 @@ 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
209
+
192
210
  # Allow to override the class attribute on 'self',
193
211
  # which avoids having to subclass this field class as well.
194
212
  if xsd_class is not None:
@@ -196,7 +214,7 @@ class FeatureField:
196
214
 
197
215
  self._nillable_relation = False
198
216
  if model is not None:
199
- self.bind(model)
217
+ self.bind(model, parent=parent, feature_type=feature_type)
200
218
 
201
219
  def __repr__(self):
202
220
  return f"<{self.__class__.__name__}: {self.name}>"
@@ -208,6 +226,7 @@ class FeatureField:
208
226
  self,
209
227
  model: type[models.Model],
210
228
  parent: ComplexFeatureField | None = None,
229
+ feature_type: FeatureType | None = None,
211
230
  ):
212
231
  """Late-binding for the model.
213
232
 
@@ -220,11 +239,13 @@ class FeatureField:
220
239
  :param model: The model is field is linked to.
221
240
  :param parent: When this element is part of a complex feature,
222
241
  this links to the parent field.
242
+ :param feature_type: The original feature type that his element was mentioned in.
223
243
  """
224
244
  if self.model is not None:
225
245
  raise RuntimeError(f"Feature field '{self.name}' cannot be reused")
226
246
  self.model = model
227
247
  self.parent = parent
248
+ self.feature_type = feature_type
228
249
 
229
250
  if self.model_attribute:
230
251
  # Support dot based field traversal.
@@ -284,6 +305,7 @@ class FeatureField:
284
305
  max_occurs=max_occurs,
285
306
  model_attribute=self.model_attribute,
286
307
  source=self.model_field,
308
+ feature_type=self.feature_type,
287
309
  )
288
310
 
289
311
 
@@ -311,9 +333,14 @@ class ComplexFeatureField(FeatureField):
311
333
  ):
312
334
  """
313
335
  :param name: Name of the model field.
314
- :param fields: List of fields to expose for the target model. This can
315
- 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.
316
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.
317
344
  """
318
345
  super().__init__(
319
346
  name,
@@ -343,6 +370,7 @@ class ComplexFeatureField(FeatureField):
343
370
  type_name=self.name,
344
371
  source=pk_field,
345
372
  model_attribute=pk_field.name,
373
+ feature_type=self.feature_type,
346
374
  )
347
375
  ],
348
376
  base=self.xsd_base_type,
@@ -377,9 +405,13 @@ def field(
377
405
  so little knowledge is needed about the internal working.
378
406
 
379
407
  :param name: Name of the model field.
380
- :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.
381
411
  This can be a list of :func:`field` elements, or plain field names.
382
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.
383
415
  """
384
416
  if fields is not None:
385
417
  return ComplexFeatureField(
@@ -398,42 +430,6 @@ def field(
398
430
  )
399
431
 
400
432
 
401
- @dataclass
402
- class FeatureRelation:
403
- """Tell which related fields are queried by the feature.
404
- Each dict holds an ORM-path, with the relevant sub-elements.
405
- """
406
-
407
- #: The ORM path that is queried for this particular relation
408
- orm_path: str
409
- #: The fields that will be retrieved for that path
410
- orm_fields: set[str]
411
- #: The model that is accessed for this relation (if set)
412
- related_model: type[models.Model] | None
413
- #: The source elements that access this relation.
414
- xsd_elements: list[XsdElement]
415
-
416
- @cached_property
417
- def _local_model_field_names(self) -> list[str]:
418
- """Tell which local fields of the model will be accessed by this feature."""
419
-
420
- meta = self.related_model._meta
421
- result = []
422
- for name in self.orm_fields:
423
- model_field = meta.get_field(name)
424
- if not model_field.many_to_many and not model_field.one_to_many:
425
- result.append(name)
426
-
427
- # When this relation is retrieved through a ManyToOneRel (reverse FK),
428
- # the prefetch_related() also needs to have the original foreign key
429
- # in order to link all prefetches to the proper parent instance.
430
- for xsd_element in self.xsd_elements:
431
- if xsd_element.source is not None and xsd_element.source.one_to_many:
432
- result.append(xsd_element.source.field.name)
433
-
434
- return result
435
-
436
-
437
433
  class FeatureType:
438
434
  """Declare a feature that is exposed on the map.
439
435
 
@@ -471,7 +467,7 @@ class FeatureType:
471
467
  :param fields: Define which fields to show in the WFS data.
472
468
  This can be a list of field names, or :class:`FeatureField` objects.
473
469
  :param display_field_name: Name of the field that's used as general string representation.
474
- :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).
475
471
  :param name: Name, also used as XML tag name.
476
472
  :param title: Used in WFS metadata.
477
473
  :param abstract: Used in WFS metadata.
@@ -529,6 +525,11 @@ class FeatureType:
529
525
  """Return all spatial reference system ID's that this feature supports."""
530
526
  return [self.crs] + self.other_crs
531
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
+
532
533
  @cached_property
533
534
  def geometry_fields(self) -> list[GeometryField]:
534
535
  """Tell which fields of the model have a geometry field.
@@ -540,52 +541,6 @@ class FeatureType:
540
541
  if ff.xsd_element.is_geometry
541
542
  ]
542
543
 
543
- @cached_property
544
- def orm_relations(self) -> list[FeatureRelation]:
545
- """Tell which fields will be retrieved from related fields.
546
-
547
- This gives an object layout based on the XSD elements,
548
- that can be used for prefetching data.
549
- """
550
- models = {}
551
- fields = defaultdict(set)
552
- elements = defaultdict(list)
553
-
554
- # Check all elements that render as "dotted" flattened relation
555
- for xsd_element in self.xsd_type.flattened_elements:
556
- if xsd_element.source is not None:
557
- # Split "relation.field" notation into path, and take the field as child attribute.
558
- obj_path, field = xsd_element.orm_relation
559
- elements[obj_path].append(xsd_element)
560
- fields[obj_path].add(field)
561
- # field is already on relation:
562
- models[obj_path] = xsd_element.source.model
563
-
564
- # Check all elements that render as "nested" complex type:
565
- for xsd_element in self.xsd_type.complex_elements:
566
- # The complex element itself points to the root of the path,
567
- # all sub elements become the child attributes.
568
- obj_path = xsd_element.orm_path
569
- elements[obj_path].append(xsd_element)
570
- fields[obj_path] = {
571
- f.orm_path
572
- for f in xsd_element.type.elements
573
- if not f.is_many or f.is_array # exclude M2M, but include ArrayField
574
- }
575
- if xsd_element.source:
576
- # field references a related object:
577
- models[obj_path] = xsd_element.source.related_model
578
-
579
- return [
580
- FeatureRelation(
581
- orm_path=obj_path,
582
- orm_fields=sub_fields,
583
- related_model=models.get(obj_path),
584
- xsd_elements=elements[obj_path],
585
- )
586
- for obj_path, sub_fields in fields.items()
587
- ]
588
-
589
544
  @cached_property
590
545
  def _local_model_field_names(self) -> list[str]:
591
546
  """Tell which local fields of the model will be accessed by this feature."""
@@ -610,9 +565,9 @@ class FeatureType:
610
565
  if isinstance(f, gis_models.GeometryField)
611
566
  )
612
567
 
613
- return [FeatureField(self._geometry_field_name, model=self.model)]
568
+ return [FeatureField(self._geometry_field_name, model=self.model, feature_type=self)]
614
569
  else:
615
- return _get_model_fields(self.model, self._fields)
570
+ return _get_model_fields(self.model, self._fields, feature_type=self)
616
571
 
617
572
  @cached_property
618
573
  def display_field(self) -> models.Field | None:
@@ -653,6 +608,13 @@ class FeatureType:
653
608
  # Default: take the first geometry
654
609
  return self.geometry_fields[0]
655
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
+
656
618
  @cached_property
657
619
  def geometry_field_name(self) -> str:
658
620
  """Tell which field is the geometry field."""
@@ -772,6 +734,7 @@ class FeatureType:
772
734
  type_name=self.name,
773
735
  source=pk_field,
774
736
  model_attribute=pk_field.name,
737
+ feature_type=self,
775
738
  )
776
739
  ],
777
740
  base=XsdTypes.gmlAbstractGMLType,
@@ -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}")