django-gisserver 2.0__py3-none-any.whl → 2.1__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 (55) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/crs.py +401 -0
  6. gisserver/db.py +71 -5
  7. gisserver/exceptions.py +106 -2
  8. gisserver/extensions/functions.py +122 -28
  9. gisserver/extensions/queries.py +15 -10
  10. gisserver/features.py +44 -36
  11. gisserver/geometries.py +64 -306
  12. gisserver/management/commands/loadgeojson.py +41 -21
  13. gisserver/operations/base.py +11 -7
  14. gisserver/operations/wfs20.py +31 -93
  15. gisserver/output/__init__.py +6 -2
  16. gisserver/output/base.py +28 -13
  17. gisserver/output/csv.py +18 -6
  18. gisserver/output/geojson.py +7 -6
  19. gisserver/output/gml32.py +43 -23
  20. gisserver/output/results.py +25 -39
  21. gisserver/output/utils.py +9 -2
  22. gisserver/parsers/ast.py +171 -65
  23. gisserver/parsers/fes20/__init__.py +76 -4
  24. gisserver/parsers/fes20/expressions.py +97 -27
  25. gisserver/parsers/fes20/filters.py +9 -6
  26. gisserver/parsers/fes20/identifiers.py +27 -7
  27. gisserver/parsers/fes20/lookups.py +8 -6
  28. gisserver/parsers/fes20/operators.py +101 -49
  29. gisserver/parsers/fes20/sorting.py +14 -6
  30. gisserver/parsers/gml/__init__.py +10 -19
  31. gisserver/parsers/gml/base.py +32 -14
  32. gisserver/parsers/gml/geometries.py +48 -21
  33. gisserver/parsers/ows/kvp.py +10 -2
  34. gisserver/parsers/ows/requests.py +6 -4
  35. gisserver/parsers/query.py +6 -2
  36. gisserver/parsers/values.py +61 -4
  37. gisserver/parsers/wfs20/__init__.py +2 -0
  38. gisserver/parsers/wfs20/adhoc.py +25 -17
  39. gisserver/parsers/wfs20/base.py +12 -7
  40. gisserver/parsers/wfs20/projection.py +3 -3
  41. gisserver/parsers/wfs20/requests.py +1 -0
  42. gisserver/parsers/wfs20/stored.py +3 -2
  43. gisserver/parsers/xml.py +12 -0
  44. gisserver/projection.py +17 -7
  45. gisserver/static/gisserver/index.css +8 -3
  46. gisserver/templates/gisserver/base.html +12 -0
  47. gisserver/templates/gisserver/index.html +9 -15
  48. gisserver/templates/gisserver/service_description.html +12 -6
  49. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  50. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  51. gisserver/types.py +150 -81
  52. gisserver/views.py +47 -24
  53. django_gisserver-2.0.dist-info/RECORD +0 -66
  54. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  55. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,41 @@
1
- {% load i18n %}
2
- <h3>{{ feature_type.title }}</h3>
3
- {% if feature_type.abstract %}{{ feature_type.abstract|linebreaks }}{% endif %}
1
+ {% load i18n gisserver_tags %}
4
2
 
5
- {% block formats %}{% if wfs_output_formats %}
6
- <p>
7
- {% trans "Formats" %}:
8
- {% for output_format in wfs_output_formats %}
9
- <a href="?{{ base_query }}SERVICE=WFS&VERSION={{ version }}&REQUEST=GetFeature&TYPENAMES={{ feature_type.name }}&OUTPUTFORMAT={{ output_format.identifier }}">{{ output_format.title|default:output_format }}</a>{% if not forloop.last %},{% endif %}
10
- {% endfor %}
11
- </p>
12
- {% endif %}{% endblock %}
3
+ {% block title %}<h3><a id="feature-{{ feature_type.name }}">{{ feature_type.title }}</a></h3>{% endblock %}
4
+ {% block description %}
5
+ {% if feature_type.abstract %}{{ feature_type.abstract|linebreaks }}{% endif %}
6
+ {% endblock %}
7
+
8
+ {% block metadata %}
9
+ <dl>
10
+ {% block metadata-items %}
11
+ <dt>{% translate "XML Namespace" %}:</dt><dd><code>{{ feature_type.xml_namespace }}</code></dd>
12
+ <dt>{% translate "Typename" %}:</dt><dd><abbr title="{{ feature_type.xml_name }}">{% feature_qname feature_type %}</abbr></dd>
13
+ <dt>{% translate "Supported CRS" %}:</dt>
14
+ <dd>
15
+ {% if GISSERVER_SUPPORTED_CRS_ONLY %}
16
+ {% blocktranslate with default_crs=feature_type.crs %}Any CRS value is supported, source data uses {{ default_crs }}.{% endblocktranslate %}
17
+ {% else %}
18
+ {{ feature_type.supported_crs|join:", " }}
19
+ {% endif %}
20
+ </dd>
21
+ {% if wfs_output_formats %}
22
+ <dt>{% translate "Formats" %}:</dt>
23
+ <dd>{% block metadata-formats %}
24
+ {% for output_format in wfs_output_formats %}
25
+ <a href="?{{ base_query }}SERVICE=WFS&VERSION={{ version }}&REQUEST=GetFeature&TYPENAMES={% feature_qname feature_type %}&OUTPUTFORMAT={{ output_format.identifier }}">{{ output_format.title|default:output_format }}</a>{% block format-sep %},{% endblock %}
26
+ {% endfor %}
27
+ <a href="?{{ base_query }}SERVICE=WFS&VERSION={{ version }}&REQUEST=DescribeFeatureType&TYPENAMES={% feature_qname feature_type %}">XML Schema</a>
28
+ {% endblock %}
29
+ </dd>
30
+ {% endif %}
31
+ {% endblock %}
32
+ </dl>
33
+ {% endblock %}
13
34
 
14
35
  {% block fields %}
15
- <p>{% trans "Fields" %}:</p>
16
- <table>
36
+ <p>{% translate "The following fields are available:" %}</p>
37
+ <table class="table table-striped">
38
+ <thead><tr><th>{% translate "Field Name" %}</th><th>{% translate "Type" %}</th><th>{% translate "Description" %}</th></tr></thead>
17
39
  <tbody>
18
40
  {% for field in feature_type.fields %}
19
41
  {% include "gisserver/wfs/feature_field.html" with level=0 %}
gisserver/types.py CHANGED
@@ -30,9 +30,10 @@ import logging
30
30
  import operator
31
31
  import re
32
32
  from dataclasses import dataclass, field
33
+ from datetime import date, datetime, time, timedelta
33
34
  from decimal import Decimal as D
34
35
  from enum import Enum
35
- from functools import cached_property, reduce
36
+ from functools import cached_property
36
37
  from typing import TYPE_CHECKING, Literal
37
38
 
38
39
  import django
@@ -42,16 +43,15 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
42
43
  from django.db import models
43
44
  from django.db.models import Q
44
45
  from django.db.models.fields.related import RelatedField
45
- from django.utils import dateparse
46
46
 
47
47
  from gisserver.compat import ArrayField, GeneratedField
48
+ from gisserver.crs import CRS
48
49
  from gisserver.exceptions import ExternalParsingError, OperationProcessingFailed
49
- from gisserver.geometries import CRS, BoundingBox
50
+ from gisserver.geometries import BoundingBox
50
51
  from gisserver.parsers import values
51
52
  from gisserver.parsers.xml import parse_qname, split_ns, xmlns
52
53
 
53
54
  logger = logging.getLogger(__name__)
54
- _unbounded = Literal["unbounded"]
55
55
 
56
56
  __all__ = [
57
57
  "GeometryXsdElement",
@@ -98,13 +98,13 @@ class XsdAnyType:
98
98
 
99
99
 
100
100
  class XsdTypes(XsdAnyType, Enum):
101
- """Brief enumeration of basic XMLSchema types.
101
+ """Brief enumeration of common XMLSchema types.
102
102
 
103
103
  The :class:`XsdElement` and :class:`XsdAttribute` can use these enum members
104
104
  to indicate their value is a well-known XML Schema. Some GML types are included as well.
105
105
 
106
- The default namespace is the "xs:" (XMLSchema).
107
- Based on https://www.w3.org/TR/xmlschema-2/#built-in-datatypes
106
+ Each member value is a fully qualified XML name.
107
+ The output rendering will convert these to the chosen prefixes.
108
108
  """
109
109
 
110
110
  anyType = xmlns.xs.qname("anyType") # not "xsd:any", that is an element.
@@ -149,16 +149,25 @@ class XsdTypes(XsdAnyType, Enum):
149
149
  gmlMultiCurvePropertyType = xmlns.gml.qname("MultiCurvePropertyType")
150
150
  gmlMultiGeometryPropertyType = xmlns.gml.qname("MultiGeometryPropertyType")
151
151
 
152
- # Other typical GML values
152
+ # Other typical GML values:
153
+
154
+ #: The type for ``<gml:name>`` elements.
153
155
  gmlCodeType = xmlns.gml.qname("CodeType") # for <gml:name>
154
- gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType") # for <gml:boundedBy>
155
156
 
156
- #: A direct geometry value (used as function argument type)
157
+ #: The type for ``<gml:boundedBy>`` elements.
158
+ gmlBoundingShapeType = xmlns.gml.qname("BoundingShapeType")
159
+
160
+ #: The type for ``<gml:Envelope>`` elements, sometimes used as function argument type.
161
+ gmlEnvelopeType = xmlns.gml.qname("EnvelopeType")
162
+
163
+ #: A direct geometry value, sometimes used as function argument type.
157
164
  gmlAbstractGeometryType = xmlns.gml.qname("AbstractGeometryType")
158
165
 
159
166
  #: A feature that has a gml:name and gml:boundedBy as possible child element.
160
167
  gmlAbstractFeatureType = xmlns.gml.qname("AbstractFeatureType")
161
- gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType") # base of gml:AbstractFeatureType
168
+
169
+ #: The base of gml:AbstractFeatureType
170
+ gmlAbstractGMLType = xmlns.gml.qname("AbstractGMLType")
162
171
 
163
172
  def __str__(self):
164
173
  return self.value
@@ -184,9 +193,12 @@ class XsdTypes(XsdAnyType, Enum):
184
193
  raise NotImplementedError(f'Casting to "{self}" is not implemented.') from None
185
194
 
186
195
  def to_python(self, raw_value):
187
- """Convert a raw string value to this type representation"""
188
- if self.is_geometry:
189
- # Leave complex values as-is.
196
+ """Convert a raw string value to this type representation.
197
+
198
+ :raises ExternalParsingError: When the value can't be converted to the proper type.
199
+ """
200
+ if self.is_geometry or isinstance(raw_value, TYPES_AS_PYTHON[self]):
201
+ # Detect when the value was already parsed, no need to reparse a date for example.
190
202
  return raw_value
191
203
 
192
204
  try:
@@ -195,6 +207,7 @@ class XsdTypes(XsdAnyType, Enum):
195
207
  raise # subclass of ValueError so explicitly caught and reraised
196
208
  except (TypeError, ValueError, ArithmeticError) as e:
197
209
  # ArithmeticError is base of DecimalException
210
+ logger.debug("Parsing error for %r: %s", raw_value, e)
198
211
  name = self.name if self.namespace == xmlns.xsd.value else self.value
199
212
  raise ExternalParsingError(f"Can't cast '{raw_value}' to {name}.") from e
200
213
 
@@ -221,12 +234,12 @@ def _as_is(v):
221
234
  return v
222
235
 
223
236
 
224
- TYPES_TO_PYTHON = {
225
- XsdTypes.date: dateparse.parse_date,
226
- XsdTypes.dateTime: values.parse_iso_datetime,
227
- XsdTypes.time: dateparse.parse_time,
228
- XsdTypes.string: _as_is,
229
- XsdTypes.boolean: values.parse_bool,
237
+ TYPES_AS_PYTHON = {
238
+ XsdTypes.date: date,
239
+ XsdTypes.dateTime: datetime,
240
+ XsdTypes.time: time,
241
+ XsdTypes.string: str,
242
+ XsdTypes.boolean: bool,
230
243
  XsdTypes.integer: int,
231
244
  XsdTypes.int: int,
232
245
  XsdTypes.long: int,
@@ -236,9 +249,28 @@ TYPES_TO_PYTHON = {
236
249
  XsdTypes.unsignedLong: int,
237
250
  XsdTypes.unsignedShort: int,
238
251
  XsdTypes.unsignedByte: int,
239
- XsdTypes.float: D,
252
+ XsdTypes.float: D, # auto_cast() always converts to decimal
240
253
  XsdTypes.double: D,
241
254
  XsdTypes.decimal: D,
255
+ XsdTypes.duration: timedelta,
256
+ XsdTypes.nonNegativeInteger: int,
257
+ XsdTypes.gYear: int,
258
+ XsdTypes.hexBinary: bytes,
259
+ XsdTypes.base64Binary: bytes,
260
+ XsdTypes.token: str,
261
+ XsdTypes.language: str,
262
+ XsdTypes.gmlCodeType: str,
263
+ XsdTypes.anyType: type(Ellipsis),
264
+ }
265
+
266
+ TYPES_TO_PYTHON = {
267
+ **TYPES_AS_PYTHON,
268
+ XsdTypes.date: values.parse_iso_date,
269
+ XsdTypes.dateTime: values.parse_iso_datetime,
270
+ XsdTypes.time: values.parse_iso_time,
271
+ XsdTypes.string: _as_is,
272
+ XsdTypes.boolean: values.parse_bool,
273
+ XsdTypes.duration: values.parse_iso_duration,
242
274
  XsdTypes.gmlCodeType: _as_is,
243
275
  XsdTypes.anyType: values.auto_cast,
244
276
  }
@@ -252,11 +284,16 @@ class XsdNode:
252
284
  parse query input and read model attributes to write as output.
253
285
  """
254
286
 
287
+ #: Whether this node is an :class:`XsdAttribute` (avoids slow ``isinstance()`` checks)
255
288
  is_attribute = False
289
+ #: Whether this node can occur multiple times.
256
290
  is_many = False
257
291
 
292
+ #: The local name of the XML element
258
293
  name: str
259
- type: XsdAnyType # Both XsdComplexType and XsdType are allowed
294
+
295
+ #: The data type of the element/attribute, both :class:`XsdComplexType` and :class:`XsdTypes` are allowed.
296
+ type: XsdAnyType
260
297
 
261
298
  #: XML Namespace of the element
262
299
  namespace: xmlns | str | None
@@ -269,7 +306,7 @@ class XsdNode:
269
306
  #: This supports dot notation to access related attributes.
270
307
  model_attribute: str | None
271
308
 
272
- #: A link back to the parent that described the featuyre this node is a part of.
309
+ #: A link back to the parent that described the feature this node is a part of.
273
310
  #: This helps to perform additional filtering in side meth:get_value: based on user policies.
274
311
  feature_type: FeatureType | None
275
312
 
@@ -291,7 +328,8 @@ class XsdNode:
291
328
  :param source: Original Model field, which can provide more metadata/parsing.
292
329
  :param model_attribute: The Django model path that this element accesses.
293
330
  :param absolute_model_attribute: The full path, including parent elements.
294
- :param feature_type: Typically assigned in :meth:`bind`, needed by some :meth:`get_value` functions.
331
+ :param feature_type: Typically assigned in :meth:`~gisserver.features.FeatureField.bind`,
332
+ needed by some :meth:`get_value` functions.
295
333
  """
296
334
  if ":" in name:
297
335
  raise ValueError(
@@ -466,7 +504,9 @@ class XsdNode:
466
504
  return value
467
505
 
468
506
  def to_python(self, raw_value: str):
469
- """Convert a raw value to the Python data type for this element type."""
507
+ """Convert a raw value to the Python data type for this element type.
508
+ :raises ValidationError: When the value isn't allowed for the field type.
509
+ """
470
510
  try:
471
511
  raw_value = self.type.to_python(raw_value)
472
512
  if self.source is not None:
@@ -493,30 +533,33 @@ class XsdNode:
493
533
  :param tag: The filter operator tag name, e.g. ``PropertyIsEqualTo``.
494
534
  :returns: The parsed Python value.
495
535
  """
496
- if self.source is not None:
497
- # Not calling self.source.validate() as that checks for allowed choices,
498
- # which shouldn't be checked against for a filter query.
499
- raw_value = self.to_python(raw_value)
500
-
501
- # Check whether the Django model field supports the lookup
502
- # This prevents calling LIKE on a datetime or float field.
503
- # For foreign keys, this depends on the target field type.
504
- if self.source.get_lookup(lookup) is None or (
536
+ # Not calling self.source.validate() as that checks for allowed choices,
537
+ # which shouldn't be checked against for a filter query.
538
+ raw_value = self.to_python(raw_value)
539
+
540
+ # Check whether the Django model field supports the lookup
541
+ # This prevents calling LIKE on a datetime or float field.
542
+ # For foreign keys, this depends on the target field type.
543
+ if (
544
+ self.source is not None
545
+ and self.source.get_lookup(lookup) is None
546
+ or (
505
547
  isinstance(self.source, RelatedField)
506
548
  and self.source.target_field.get_lookup(lookup) is None
507
- ):
508
- logger.debug(
509
- "Model field '%s.%s' does not support ORM lookup '%s' used by '%s'.",
510
- self.feature_type.model._meta.model_name,
511
- self.absolute_model_attribute,
512
- lookup,
513
- tag,
514
- )
515
- raise OperationProcessingFailed(
516
- f"Operator '{tag}' is not supported for the '{self.name}' property.",
517
- locator="filter",
518
- status_code=400, # not HTTP 500 here. Spec allows both.
519
- )
549
+ )
550
+ ):
551
+ logger.debug(
552
+ "Model field '%s.%s' does not support ORM lookup '%s' used by '%s'.",
553
+ self.feature_type.model._meta.model_name,
554
+ self.absolute_model_attribute,
555
+ lookup,
556
+ tag,
557
+ )
558
+ raise OperationProcessingFailed(
559
+ f"Operator '{tag}' is not supported for the '{self.name}' property.",
560
+ locator="filter",
561
+ status_code=400, # not HTTP 500 here. Spec allows both.
562
+ )
520
563
 
521
564
  return raw_value
522
565
 
@@ -529,18 +572,21 @@ class XsdElement(XsdNode):
529
572
  This holds the definition for a single property in the WFS server.
530
573
  It's used in ``DescribeFeatureType`` to output the field metadata,
531
574
  and used in ``GetFeature`` to access the actual value from the object.
532
- Overriding :meth:`get_value` allows to override this logic.
575
+ Overriding :meth:`XsdNode.get_value` allows to override this logic.
533
576
 
534
- The :attr:`name` may differ from the underlying :attr:`model_attribute`,
577
+ The :attr:`name` may differ from the underlying :attr:`XsdNode.model_attribute`,
535
578
  so the WFS server can use other field names then the underlying model.
536
579
 
537
- A dotted-path notation can be used for :attr:`model_attribute` to access
580
+ A dotted-path notation can be used for :attr:`XsdNode.model_attribute` to access
538
581
  a related field. For the WFS client, the data appears to be flattened.
539
582
  """
540
583
 
584
+ #: Whether the element can be null
541
585
  nillable: bool | None
586
+ #: The minimal number of times the element occurs in the output.
542
587
  min_occurs: int | None
543
- max_occurs: int | _unbounded | None
588
+ #: The maximum number of times this element occurs in the output.
589
+ max_occurs: int | Literal["unbounded"] | None
544
590
 
545
591
  def __init__(
546
592
  self,
@@ -550,7 +596,7 @@ class XsdElement(XsdNode):
550
596
  *,
551
597
  nillable: bool | None = None,
552
598
  min_occurs: int | None = None,
553
- max_occurs: int | _unbounded | None = None,
599
+ max_occurs: int | Literal["unbounded"] | None = None,
554
600
  source: models.Field | models.ForeignObjectRel | None = None,
555
601
  model_attribute: str | None = None,
556
602
  absolute_model_attribute: str | None = None,
@@ -661,8 +707,8 @@ class GeometryXsdElement(XsdElement):
661
707
 
662
708
 
663
709
  class GmlIdAttribute(XsdAttribute):
664
- """A virtual 'gml:id' attribute that can be queried.
665
- This subclass has overwritten get_value() logic to format the value.
710
+ """A virtual ``gml:id="..."`` attribute that can be queried.
711
+ This subclass has overwritten :meth:`get_value` logic to format the value.
666
712
  """
667
713
 
668
714
  type_name: str
@@ -686,6 +732,7 @@ class GmlIdAttribute(XsdAttribute):
686
732
  object.__setattr__(self, "type_name", type_name)
687
733
 
688
734
  def get_value(self, instance: models.Model):
735
+ """Render the value."""
689
736
  pk = super().get_value(instance) # handle dotted-name notations
690
737
  return f"{self.type_name}.{pk}"
691
738
 
@@ -695,7 +742,7 @@ class GmlIdAttribute(XsdAttribute):
695
742
 
696
743
 
697
744
  class GmlNameElement(XsdElement):
698
- """A subclass to handle the <gml:name> element.
745
+ """A subclass to handle the ``<gml:name>`` element.
699
746
  This displays a human-readable title for the object.
700
747
 
701
748
  Currently, this just reads a single attribute,
@@ -731,7 +778,7 @@ class GmlNameElement(XsdElement):
731
778
 
732
779
 
733
780
  class GmlBoundedByElement(XsdElement):
734
- """A subclass to handle the <gml:boundedBy> element.
781
+ """A subclass to handle the ``<gml:boundedBy>`` element.
735
782
 
736
783
  This override makes sure this non-model element data
737
784
  can be included in the XML tree like every other element.
@@ -780,38 +827,55 @@ class GmlBoundedByElement(XsdElement):
780
827
  if not geometries:
781
828
  return None
782
829
 
783
- # Perform the combining of geometries inside libgeos
784
- if len(geometries) == 1:
785
- geometry = geometries[0]
786
- else:
787
- geometry = reduce(operator.or_, geometries)
788
- if crs is not None and geometry.srid != crs.srid:
789
- crs.apply_to(geometry) # avoid clone
790
-
791
- return BoundingBox.from_geometry(geometry, crs=crs)
830
+ return BoundingBox.from_geometries(geometries, crs)
792
831
 
793
832
 
794
833
  @dataclass(frozen=True)
795
834
  class XsdComplexType(XsdAnyType):
796
- """Define an <xsd:complexType> that represents a whole class definition.
835
+ """Define an ``<xsd:complexType>`` that represents a whole class definition.
797
836
 
798
837
  Typically, this maps into a Django model, with each element pointing to a model field.
838
+ For example:
839
+
840
+ .. code-block:: python
841
+
842
+ XsdComplexType(
843
+ "PersonType",
844
+ elements=[
845
+ XsdElement("name", type=XsdTypes.string),
846
+ XsdElement("age", type=XsdTypes.integer),
847
+ XsdElement("address", type=XsdComplexType(
848
+ "AddressType",
849
+ elements=[
850
+ XsdElement("street", type=XsdTypes.string),
851
+ ...
852
+ ]
853
+ )),
854
+ ],
855
+ attributes=[
856
+ XsdAttribute("id", type=XsdTypes.integer),
857
+ ],
858
+ )
799
859
 
800
860
  A complex type can hold multiple :class:`XsdElement` and :class:`XsdAttribute`
801
- nodes as children, composing an object. The elements themselves can point
802
- to a complex type themselves, to create a nested class structure.
861
+ nodes as children, composing an object. Its :attr:`base` may point to a :class:`XsdComplexType`
862
+ as base class, allowing to define those inherited elements too.
863
+
864
+ Each element can be a complex type themselves, to create a nested class structure.
803
865
  That also allows embedding models with their relations into a single response.
804
866
 
805
- This object definition is the internal "source of truth" regarding
806
- which field names and field elements are used in the WFS server.
807
- The ``DescribeFeatureType`` request uses this definition to render the matching XMLSchema.
808
- Incoming XPath queries are parsed using this object to resolve the XPath to model attributes.
867
+ .. note:: Good to know
868
+ This object definition is the internal "source of truth" regarding
869
+ which field names and field elements are used in the WFS server:
870
+
871
+ * The ``DescribeFeatureType`` request uses this definition to render the matching XMLSchema.
872
+ * Incoming XPath queries are parsed using this object to resolve the XPath to model attributes.
809
873
 
810
- Objects of this type are typically generated by the ``FeatureType`` and
811
- ``ComplexFeatureField`` classes, using the Django model data.
874
+ Objects of this type are typically generated by the :class:`~gisserver.features.FeatureType` and
875
+ :class:`~gisserver.features.ComplexFeatureField` classes, using the Django model data.
812
876
 
813
- By default, The type is declared as subclass of <gml:AbstractFeatureType>,
814
- which allows child elements like <gml:name> and <gml:boundedBy>.
877
+ By default, The :attr:`base` type is detected as ``<gml:AbstractFeatureType>``,
878
+ when there is a geometry element in the definition.
815
879
  """
816
880
 
817
881
  #: Internal class name (without XML namespace/prefix)
@@ -852,7 +916,8 @@ class XsdComplexType(XsdAnyType):
852
916
  return f"{{{self.namespace}}}{self.name}" if self.namespace else self.name
853
917
 
854
918
  @property
855
- def is_complex_type(self):
919
+ def is_complex_type(self) -> bool:
920
+ """Always indicates this is a complex type."""
856
921
  return True # a property to avoid being used as field.
857
922
 
858
923
  @cached_property
@@ -981,7 +1046,7 @@ class ORMPath:
981
1046
 
982
1047
  def build_lhs(self, compiler: CompiledQuery):
983
1048
  """Give the ORM part when this element is used as left-hand-side of a comparison.
984
- For example: "path == value".
1049
+ For example: ``path == value``.
985
1050
  """
986
1051
  if self.is_many:
987
1052
  compiler.add_distinct()
@@ -991,7 +1056,7 @@ class ORMPath:
991
1056
 
992
1057
  def build_rhs(self, compiler: CompiledQuery):
993
1058
  """Give the ORM part when this element would be used as right-hand-side.
994
- For example: "path == path" or "value == path".
1059
+ For example: ``path1 == path2`` or ``value == path``.
995
1060
  """
996
1061
  if self.is_many:
997
1062
  compiler.add_distinct()
@@ -1049,7 +1114,9 @@ class XPathMatch(ORMPath):
1049
1114
  return any(node.is_many for node in self.nodes)
1050
1115
 
1051
1116
  def build_lhs(self, compiler: CompiledQuery):
1052
- """Delegate the LHS construction to the final XsdNode."""
1117
+ """Give the ORM part when this element is used as left-hand-side of a comparison.
1118
+ For example: ``path == value``.
1119
+ """
1053
1120
  if self.is_many:
1054
1121
  compiler.add_distinct()
1055
1122
  if self.orm_filters:
@@ -1057,7 +1124,9 @@ class XPathMatch(ORMPath):
1057
1124
  return self.child.build_lhs_part(compiler, self)
1058
1125
 
1059
1126
  def build_rhs(self, compiler: CompiledQuery):
1060
- """Delegate the RHS construction to the final XsdNode."""
1127
+ """Give the ORM part when this element would be used as right-hand-side.
1128
+ For example: ``path1 == path2`` or ``value == path``.
1129
+ """
1061
1130
  if self.is_many:
1062
1131
  compiler.add_distinct()
1063
1132
  if self.orm_filters: