c2cgeoportal-commons 2.6.0__py3-none-any.whl → 2.9rc45__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.

Potentially problematic release.


This version of c2cgeoportal-commons might be problematic. Click here for more details.

Files changed (88) hide show
  1. c2cgeoportal_commons/__init__.py +2 -5
  2. c2cgeoportal_commons/alembic/env.py +49 -33
  3. c2cgeoportal_commons/alembic/main/028477929d13_add_technical_roles.py +10 -7
  4. c2cgeoportal_commons/alembic/main/04f05bfbb05e_remove_the_old_is_expanded_column.py +62 -0
  5. c2cgeoportal_commons/alembic/main/116b9b79fc4d_internal_and_external_layer_tables_.py +42 -43
  6. c2cgeoportal_commons/alembic/main/1418cb05921b_merge_1_6_and_master_branches.py +9 -8
  7. c2cgeoportal_commons/alembic/main/164ac0819a61_add_image_format_to_wmts_layer.py +9 -6
  8. c2cgeoportal_commons/alembic/main/166ff2dcc48d_create_database.py +21 -24
  9. c2cgeoportal_commons/alembic/main/16e43f8c0330_remove_old_metadata_column.py +9 -6
  10. c2cgeoportal_commons/alembic/main/1d5d4abfebd1_add_restricted_theme.py +14 -8
  11. c2cgeoportal_commons/alembic/main/1de20166b274_remove_v1_artifacts.py +9 -6
  12. c2cgeoportal_commons/alembic/main/20137477bd02_update_icons_url.py +9 -6
  13. c2cgeoportal_commons/alembic/main/21f11066f8ec_trigger_on_role_updates_user_in_static.py +21 -20
  14. c2cgeoportal_commons/alembic/main/22e6dfb556de_add_description_tree_.py +9 -6
  15. c2cgeoportal_commons/alembic/main/29f2a32859ec_merge_1_6_and_master_branches.py +9 -8
  16. c2cgeoportal_commons/alembic/main/2b8ed8c1df94_set_layergroup_treeitem_is_as_a_primary_.py +9 -6
  17. c2cgeoportal_commons/alembic/main/2e57710fecfe_update_the_ogc_server_for_ogc_api.py +71 -0
  18. c2cgeoportal_commons/alembic/main/32527659d57b_move_exclude_properties_from_layerv1_to_.py +15 -12
  19. c2cgeoportal_commons/alembic/main/32b21aa1d0ed_merge_e004f76e951a_and_e004f76e951a_.py +9 -8
  20. c2cgeoportal_commons/alembic/main/338b57593823_remove_trigger_on_role_name_change.py +25 -24
  21. c2cgeoportal_commons/alembic/main/415746eb9f6_changes_for_v2.py +45 -43
  22. c2cgeoportal_commons/alembic/main/44c91d82d419_add_table_log.py +72 -0
  23. c2cgeoportal_commons/alembic/main/5109242131ce_add_column_time_widget.py +11 -9
  24. c2cgeoportal_commons/alembic/main/52916d8fde8b_add_sql_fields_to_vector_tiles.py +60 -0
  25. c2cgeoportal_commons/alembic/main/53ba1a68d5fe_add_theme_to_fulltextsearch.py +9 -6
  26. c2cgeoportal_commons/alembic/main/54645a535ad6_add_ordering_in_relation.py +17 -14
  27. c2cgeoportal_commons/alembic/main/56dc90838d90_fix_removing_layerv1.py +10 -8
  28. c2cgeoportal_commons/alembic/main/596ba21e3833_separate_local_internal.py +13 -14
  29. c2cgeoportal_commons/alembic/main/678f88c7ad5e_add_vector_tiles_layers_table.py +9 -6
  30. c2cgeoportal_commons/alembic/main/6a412d9437b1_rename_serverogc_to_ogcserver.py +9 -6
  31. c2cgeoportal_commons/alembic/main/6d87fdad275a_convert_metadata_to_the_right_case.py +13 -20
  32. c2cgeoportal_commons/alembic/main/7530011a66a7_trigger_on_role_updates_user_in_static.py +21 -20
  33. c2cgeoportal_commons/alembic/main/78fd093c8393_add_api_s_intrfaces.py +36 -52
  34. c2cgeoportal_commons/alembic/main/7d271f4527cd_add_layer_column_in_layerv1_table.py +9 -6
  35. c2cgeoportal_commons/alembic/main/809650bd04c3_add_dimension_field.py +9 -6
  36. c2cgeoportal_commons/alembic/main/8117bb9bba16_use_dimension_on_all_the_layers.py +9 -6
  37. c2cgeoportal_commons/alembic/main/87f8330ed64e_add_missing_delete_cascades.py +9 -6
  38. c2cgeoportal_commons/alembic/main/9268a1dffac0_add_trigger_to_be_able_to_correctly_.py +11 -8
  39. c2cgeoportal_commons/alembic/main/94db7e7e5b21_merge_2_2_and_master_branches.py +9 -8
  40. c2cgeoportal_commons/alembic/main/951ff84bd8ec_be_able_to_delete_a_wms_layer_in_sql.py +9 -6
  41. c2cgeoportal_commons/alembic/main/a00109812f89_add_field_layer_wms_valid.py +11 -8
  42. c2cgeoportal_commons/alembic/main/a4558f032d7d_add_support_of_cog_layers.py +74 -0
  43. c2cgeoportal_commons/alembic/main/a4f1aac9bda_merge_1_6_and_master_branches.py +9 -8
  44. c2cgeoportal_commons/alembic/main/b60f2a505f42_remame_uimetadata_to_metadata.py +9 -6
  45. c2cgeoportal_commons/alembic/main/b6b09f414fe8_sync_model_database.py +165 -0
  46. c2cgeoportal_commons/alembic/main/c75124553bf3_remove_deprecated_columns.py +9 -6
  47. c2cgeoportal_commons/alembic/main/d48a63b348f1_change_mapserver_url_for_docker.py +13 -14
  48. c2cgeoportal_commons/alembic/main/d8ef99bc227e_be_able_to_delete_a_linked_functionality.py +15 -12
  49. c2cgeoportal_commons/alembic/main/daf738d5bae4_merge_2_0_and_master_branches.py +9 -8
  50. c2cgeoportal_commons/alembic/main/dba87f2647f9_merge_2_2_on_2_3.py +9 -8
  51. c2cgeoportal_commons/alembic/main/e004f76e951a_add_missing_not_null.py +15 -32
  52. c2cgeoportal_commons/alembic/main/e7e03dedade3_put_the_default_wms_server_in_the_.py +13 -14
  53. c2cgeoportal_commons/alembic/main/e85afd327ab3_cascade_deletes_to_tsearch.py +9 -6
  54. c2cgeoportal_commons/alembic/main/ec82a8906649_add_missing_on_delete_cascade_on_layer_.py +13 -10
  55. c2cgeoportal_commons/alembic/main/ee25d267bf46_main_interface_desktop.py +11 -14
  56. c2cgeoportal_commons/alembic/main/eeb345672454_merge_2_4_and_master_branches.py +9 -8
  57. c2cgeoportal_commons/alembic/static/0c640a58a09a_add_opt_key_column.py +9 -6
  58. c2cgeoportal_commons/alembic/static/107b81f5b9fe_add_missing_delete_cascades.py +9 -6
  59. c2cgeoportal_commons/alembic/static/1857owc78a07_add_last_login_and_expiration_date.py +7 -6
  60. c2cgeoportal_commons/alembic/static/1da396a88908_move_user_table_to_static_schema.py +19 -20
  61. c2cgeoportal_commons/alembic/static/267b4c1bde2e_add_display_name_in_the_user_for_better_.py +62 -0
  62. c2cgeoportal_commons/alembic/static/3f89a7d71a5e_alter_column_url_to_remove_limitation.py +8 -7
  63. c2cgeoportal_commons/alembic/static/44c91d82d419_add_table_log.py +72 -0
  64. c2cgeoportal_commons/alembic/static/53d671b17b20_add_timezone_on_datetime_fields.py +9 -6
  65. c2cgeoportal_commons/alembic/static/5472fbc19f39_add_temp_password_column.py +9 -6
  66. c2cgeoportal_commons/alembic/static/76d72fb3fcb9_add_oauth2_pkce.py +70 -0
  67. c2cgeoportal_commons/alembic/static/7ef947f30f20_add_oauth2_tables.py +9 -6
  68. c2cgeoportal_commons/alembic/static/910b4ca53b68_sync_model_database.py +186 -0
  69. c2cgeoportal_commons/alembic/static/aa41e9613256_wip_add_openid_connect_support.py +64 -0
  70. c2cgeoportal_commons/alembic/static/ae5e88f35669_add_table_user_role.py +15 -18
  71. c2cgeoportal_commons/alembic/static/bd029dbfc11a_fill_tech_data_column.py +10 -8
  72. c2cgeoportal_commons/lib/email_.py +14 -16
  73. c2cgeoportal_commons/lib/literal.py +44 -0
  74. c2cgeoportal_commons/lib/url.py +164 -58
  75. c2cgeoportal_commons/lib/validators.py +2 -4
  76. c2cgeoportal_commons/models/__init__.py +19 -15
  77. c2cgeoportal_commons/models/main.py +1191 -263
  78. c2cgeoportal_commons/models/sqlalchemy.py +14 -18
  79. c2cgeoportal_commons/models/static.py +305 -78
  80. c2cgeoportal_commons/py.typed +0 -0
  81. c2cgeoportal_commons/testing/__init__.py +17 -12
  82. c2cgeoportal_commons/testing/initializedb.py +15 -14
  83. {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/METADATA +15 -16
  84. c2cgeoportal_commons-2.9rc45.dist-info/RECORD +89 -0
  85. {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/WHEEL +1 -1
  86. tests/conftest.py +1 -3
  87. c2cgeoportal_commons-2.6.0.dist-info/RECORD +0 -76
  88. {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2011-2021, Camptocamp SA
1
+ # Copyright (c) 2011-2024, Camptocamp SA
4
2
  # All rights reserved.
5
3
 
6
4
  # Redistribution and use in source and binary forms, with or without
@@ -27,95 +25,171 @@
27
25
  # of the authors and should not be interpreted as representing official policies,
28
26
  # either expressed or implied, of the FreeBSD Project.
29
27
 
30
- # pylint: disable=no-member
31
-
32
28
 
29
+ import enum
33
30
  import logging
31
+ import os
34
32
  import re
35
- from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast # noqa, pylint: disable=unused-import
33
+ from datetime import datetime
34
+ from typing import Any, Literal, Optional, cast, get_args
36
35
 
36
+ import pyramid.request
37
+ import sqlalchemy.orm.base
37
38
  from c2c.template.config import config
38
39
  from geoalchemy2 import Geometry
39
40
  from geoalchemy2.shape import to_shape
40
41
  from papyrus.geo_interface import GeoInterface
41
- from pyramid.request import Request
42
42
  from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, event
43
- from sqlalchemy.orm import Session, backref, relationship
43
+ from sqlalchemy.ext.declarative import AbstractConcreteBase
44
+ from sqlalchemy.orm import Mapped, Session, backref, mapped_column, relationship
44
45
  from sqlalchemy.schema import Index
45
- from sqlalchemy.types import Boolean, Enum, Integer, String, Unicode
46
+ from sqlalchemy.types import Boolean, DateTime, Enum, Integer, String, Unicode
46
47
 
48
+ import c2cgeoportal_commons.lib.literal
47
49
  from c2cgeoportal_commons.lib.url import get_url2
48
50
  from c2cgeoportal_commons.models import Base, _, cache_invalidate_cb
49
51
  from c2cgeoportal_commons.models.sqlalchemy import JSONEncodedDict, TsVector
50
52
 
51
53
  try:
54
+ import colander
52
55
  from c2cgeoform import default_map_settings
53
56
  from c2cgeoform.ext.colander_ext import Geometry as ColanderGeometry
54
57
  from c2cgeoform.ext.deform_ext import MapWidget, RelationSelect2Widget
55
58
  from colander import drop
56
59
  from deform.widget import CheckboxWidget, HiddenWidget, SelectWidget, TextAreaWidget, TextInputWidget
60
+
61
+ colander_null = colander.null
57
62
  except ModuleNotFoundError:
58
- drop = None
63
+ drop = None # pylint: disable=invalid-name
59
64
  default_map_settings = {"srid": 3857, "view": {"projection": "EPSG:3857"}}
65
+ colander_null = None # pylint: disable=invalid-name
60
66
 
61
67
  class GenericClass:
68
+ """Fallback class implementation."""
69
+
62
70
  def __init__(self, *args: Any, **kwargs: Any):
63
71
  pass
64
72
 
65
73
  CheckboxWidget = GenericClass
66
74
  HiddenWidget = GenericClass
67
- MapWidget = GenericClass
75
+ MapWidget = GenericClass # type: ignore[misc,assignment]
68
76
  SelectWidget = GenericClass
69
77
  TextAreaWidget = GenericClass
70
- ColanderGeometry = GenericClass
71
- RelationSelect2Widget = GenericClass
78
+ ColanderGeometry = GenericClass # type: ignore[misc,assignment]
79
+ RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
72
80
  TextInputWidget = GenericClass
73
81
 
74
82
 
75
- LOG = logging.getLogger(__name__)
83
+ if os.environ.get("DEVELOPMENT", "0") == "1":
84
+
85
+ def state_str(state: Any) -> str:
86
+ """Return a string describing an instance via its InstanceState."""
87
+
88
+ return "None" if state is None else f"<{state.class_.__name__} {state.obj()}>"
89
+
90
+ # In the original function sqlalchemy use the id of the object that don't allow us to give some useful
91
+ # information
92
+ sqlalchemy.orm.base.state_str = state_str
93
+
94
+ _LOG = logging.getLogger(__name__)
76
95
 
77
96
  _schema: str = config["schema"] or "main"
78
97
  _srid: int = cast(int, config["srid"]) or 3857
79
98
 
80
99
  # Set some default values for the admin interface
81
- _admin_config: Dict = config.get_config().get("admin_interface", {})
82
- _map_config: Dict = {**default_map_settings, **_admin_config.get("map", {})}
100
+ conf = config.get_config()
101
+ assert conf is not None
102
+ _admin_config: dict[str, Any] = conf.get("admin_interface", {})
103
+ _map_config: dict[str, Any] = {**default_map_settings, **_admin_config.get("map", {})}
83
104
  view_srid_match = re.match(r"EPSG:(\d+)", _map_config["view"]["projection"])
84
105
  if "map_srid" not in _admin_config and view_srid_match is not None:
85
106
  _admin_config["map_srid"] = view_srid_match.group(1)
86
107
 
87
108
 
88
- class FullTextSearch(GeoInterface, Base):
109
+ class FullTextSearch(GeoInterface, Base): # type: ignore
110
+ """The tsearch table representation."""
111
+
89
112
  __tablename__ = "tsearch"
90
- __table_args__ = (Index("tsearch_ts_idx", "ts", postgresql_using="gin"), {"schema": _schema})
113
+ __table_args__ = (
114
+ Index("tsearch_search_index", "ts", "public", "role_id", "interface_id", "lang"),
115
+ Index("tsearch_ts_idx", "ts", postgresql_using="gin"),
116
+ {"schema": _schema},
117
+ )
91
118
 
92
- id = Column(Integer, primary_key=True)
93
- label = Column(Unicode)
94
- layer_name = Column(Unicode)
95
- role_id = Column(Integer, ForeignKey(_schema + ".role.id", ondelete="CASCADE"), nullable=True)
119
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
120
+ label: Mapped[str] = mapped_column(Unicode)
121
+ layer_name: Mapped[str] = mapped_column(Unicode, nullable=True)
122
+ role_id: Mapped[int] = mapped_column(
123
+ Integer, ForeignKey(_schema + ".role.id", ondelete="CASCADE"), nullable=True
124
+ )
96
125
  role = relationship("Role")
97
- interface_id = Column(Integer, ForeignKey(_schema + ".interface.id", ondelete="CASCADE"), nullable=True)
126
+ interface_id: Mapped[int] = mapped_column(
127
+ Integer, ForeignKey(_schema + ".interface.id", ondelete="CASCADE"), nullable=True
128
+ )
98
129
  interface = relationship("Interface")
99
- lang = Column(String(2), nullable=True)
100
- public = Column(Boolean, server_default="true")
101
- ts = Column(TsVector)
102
- the_geom = Column(Geometry("GEOMETRY", srid=_srid))
103
- params = Column(JSONEncodedDict, nullable=True)
104
- actions = Column(JSONEncodedDict, nullable=True)
105
- from_theme = Column(Boolean, server_default="false")
130
+ lang: Mapped[str] = mapped_column(String(2), nullable=True)
131
+ public: Mapped[bool] = mapped_column(Boolean, server_default="true")
132
+ ts = mapped_column(TsVector)
133
+ the_geom = mapped_column(Geometry("GEOMETRY", srid=_srid, spatial_index=False))
134
+ params = mapped_column(JSONEncodedDict, nullable=True)
135
+ actions = mapped_column(JSONEncodedDict, nullable=True)
136
+ from_theme: Mapped[bool] = mapped_column(Boolean, server_default="false")
137
+
138
+ def __str__(self) -> str:
139
+ return f"{self.label}[{self.id}]"
106
140
 
107
141
 
108
- class Functionality(Base):
142
+ class Functionality(Base): # type: ignore
143
+ """The functionality table representation."""
144
+
109
145
  __tablename__ = "functionality"
110
146
  __table_args__ = {"schema": _schema}
111
147
  __colanderalchemy_config__ = {"title": _("Functionality"), "plural": _("Functionalities")}
112
148
 
113
149
  __c2cgeoform_config__ = {"duplicate": True}
114
150
 
115
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
116
- name = Column(Unicode, nullable=False, info={"colanderalchemy": {"title": _("Name")}})
117
- description = Column(Unicode, info={"colanderalchemy": {"title": _("Description")}})
118
- value = Column(Unicode, nullable=False, info={"colanderalchemy": {"title": _("Value")}})
151
+ id: Mapped[int] = mapped_column(
152
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
153
+ )
154
+ name: Mapped[str] = mapped_column(
155
+ Unicode,
156
+ nullable=False,
157
+ info={
158
+ "colanderalchemy": {
159
+ "title": _("Name"),
160
+ "description": _("Name of the functionality."),
161
+ "widget": SelectWidget(
162
+ values=[("", _("- Select -"))]
163
+ + [
164
+ (f["name"], f["name"])
165
+ for f in sorted(
166
+ _admin_config.get("available_functionalities", []),
167
+ key=lambda f: cast(str, f["name"]),
168
+ )
169
+ ],
170
+ ),
171
+ }
172
+ },
173
+ )
174
+ description: Mapped[str | None] = mapped_column(
175
+ Unicode,
176
+ info={
177
+ "colanderalchemy": {
178
+ "title": _("Description"),
179
+ "description": _("An optional description."),
180
+ }
181
+ },
182
+ )
183
+ value: Mapped[str] = mapped_column(
184
+ Unicode,
185
+ nullable=False,
186
+ info={
187
+ "colanderalchemy": {
188
+ "title": _("Value"),
189
+ "description": _("A value for the functionality."),
190
+ }
191
+ },
192
+ )
119
193
 
120
194
  def __init__(self, name: str = "", value: str = "", description: str = "") -> None:
121
195
  self.name = name
@@ -123,7 +197,7 @@ class Functionality(Base):
123
197
  self.description = description
124
198
 
125
199
  def __str__(self) -> str:
126
- return "{} - {}".format(self.name or "", self.value or "") # pragma: no cover
200
+ return f"{self.name}={self.value}[{self.id}]"
127
201
 
128
202
 
129
203
  event.listen(Functionality, "after_update", cache_invalidate_cb)
@@ -159,19 +233,43 @@ theme_functionality = Table(
159
233
  )
160
234
 
161
235
 
162
- class Role(Base):
236
+ class Role(Base): # type: ignore
237
+ """The role table representation."""
238
+
163
239
  __tablename__ = "role"
164
240
  __table_args__ = {"schema": _schema}
165
241
  __colanderalchemy_config__ = {"title": _("Role"), "plural": _("Roles")}
166
242
  __c2cgeoform_config__ = {"duplicate": True}
167
243
 
168
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
169
- name = Column(Unicode, unique=True, nullable=False, info={"colanderalchemy": {"title": _("Name")}})
170
- description = Column(Unicode, info={"colanderalchemy": {"title": _("Description")}})
171
- extent = Column(
172
- Geometry("POLYGON", srid=_srid),
244
+ id: Mapped[int] = mapped_column(
245
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
246
+ )
247
+ name: Mapped[str] = mapped_column(
248
+ Unicode,
249
+ unique=True,
250
+ nullable=False,
251
+ info={
252
+ "colanderalchemy": {
253
+ "title": _("Name"),
254
+ "description": _("A name for this role."),
255
+ }
256
+ },
257
+ )
258
+ description: Mapped[str | None] = mapped_column(
259
+ Unicode,
173
260
  info={
174
261
  "colanderalchemy": {
262
+ "title": _("Description"),
263
+ "description": _("An optional description."),
264
+ }
265
+ },
266
+ )
267
+ extent = mapped_column(
268
+ Geometry("POLYGON", srid=_srid, spatial_index=False),
269
+ info={
270
+ "colanderalchemy": {
271
+ "title": _("Extent"),
272
+ "description": _("Initial extent for this role."),
175
273
  "typ": ColanderGeometry("POLYGON", srid=_srid, map_srid=_admin_config["map_srid"]),
176
274
  "widget": MapWidget(map_options=_map_config),
177
275
  }
@@ -183,15 +281,21 @@ class Role(Base):
183
281
  "Functionality",
184
282
  secondary=role_functionality,
185
283
  cascade="save-update,merge,refresh-expire",
186
- info={"colanderalchemy": {"exclude": True, "title": _("Functionalities")}},
284
+ info={
285
+ "colanderalchemy": {
286
+ "title": _("Functionalities"),
287
+ "description": _("Functionality values for this role."),
288
+ "exclude": True,
289
+ }
290
+ },
187
291
  )
188
292
 
189
293
  def __init__(
190
294
  self,
191
295
  name: str = "",
192
296
  description: str = "",
193
- functionalities: List[Functionality] = None,
194
- extent: Geometry = None,
297
+ functionalities: list[Functionality] | None = None,
298
+ extent: Geometry | None = None,
195
299
  ) -> None:
196
300
  if functionalities is None:
197
301
  functionalities = []
@@ -201,13 +305,13 @@ class Role(Base):
201
305
  self.description = description
202
306
 
203
307
  def __str__(self) -> str:
204
- return self.name or "" # pragma: no cover
308
+ return f"{self.name}[{self.id}]>"
205
309
 
206
310
  @property
207
- def bounds(self) -> None:
311
+ def bounds(self) -> tuple[float, float, float, float] | None: # TODO
208
312
  if self.extent is None:
209
313
  return None
210
- return to_shape(self.extent).bounds
314
+ return cast(tuple[float, float, float, float], to_shape(self.extent).bounds)
211
315
 
212
316
 
213
317
  event.listen(Role.functionalities, "set", cache_invalidate_cb)
@@ -215,26 +319,52 @@ event.listen(Role.functionalities, "append", cache_invalidate_cb)
215
319
  event.listen(Role.functionalities, "remove", cache_invalidate_cb)
216
320
 
217
321
 
218
- class TreeItem(Base):
322
+ class TreeItem(Base): # type: ignore
323
+ """The treeitem table representation."""
324
+
219
325
  __tablename__ = "treeitem"
220
- __table_args__: Union[Tuple, Dict[str, Any]] = (
326
+ __table_args__: tuple[Any, ...] | dict[str, Any] = (
221
327
  UniqueConstraint("type", "name"),
222
328
  {"schema": _schema},
223
329
  )
224
- item_type = Column("type", String(10), nullable=False, info={"colanderalchemy": {"exclude": True}})
330
+ item_type: Mapped[str] = mapped_column(
331
+ "type", String(10), nullable=False, info={"colanderalchemy": {"exclude": True}}
332
+ )
225
333
  __mapper_args__ = {"polymorphic_on": item_type}
226
334
 
227
- id = Column(Integer, primary_key=True)
228
- name = Column(Unicode, nullable=False, info={"colanderalchemy": {"title": _("Name")}})
229
- description = Column(Unicode, info={"colanderalchemy": {"title": _("Description")}})
335
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
336
+ name: Mapped[str] = mapped_column(
337
+ Unicode,
338
+ nullable=False,
339
+ info={
340
+ "colanderalchemy": {
341
+ "title": _("Name"),
342
+ "description": _(
343
+ """
344
+ The name of the node, it is used through the i18n tools to display the name on the layers
345
+ tree.
346
+ """
347
+ ),
348
+ }
349
+ },
350
+ )
351
+ description: Mapped[str | None] = mapped_column(
352
+ Unicode,
353
+ info={
354
+ "colanderalchemy": {
355
+ "title": _("Description"),
356
+ "description": _("An optional description."),
357
+ }
358
+ },
359
+ )
230
360
 
231
361
  @property
232
- # Better: def parents(self) -> List[TreeGroup]: # pragma: no cover
233
- def parents(self) -> List["TreeItem"]: # pragma: no cover
362
+ # Better: def parents(self) -> List[TreeGroup]:
363
+ def parents(self) -> list["TreeItem"]:
234
364
  return [c.treegroup for c in self.parents_relation]
235
365
 
236
366
  def is_in_interface(self, name: str) -> bool:
237
- if not hasattr(self, "interfaces"): # pragma: no cover
367
+ if not hasattr(self, "interfaces"):
238
368
  return False
239
369
 
240
370
  for interface in self.interfaces:
@@ -243,12 +373,15 @@ class TreeItem(Base):
243
373
 
244
374
  return False
245
375
 
246
- def get_metadatas(self, name: str) -> List["Metadata"]: # pragma: no cover
376
+ def get_metadata(self, name: str) -> list["Metadata"]:
247
377
  return [metadata for metadata in self.metadatas if metadata.name == name]
248
378
 
249
379
  def __init__(self, name: str = "") -> None:
250
380
  self.name = name
251
381
 
382
+ def __str__(self) -> str:
383
+ return f"{self.name}[{self.id}]>"
384
+
252
385
 
253
386
  event.listen(TreeItem, "after_insert", cache_invalidate_cb, propagate=True)
254
387
  event.listen(TreeItem, "after_update", cache_invalidate_cb, propagate=True)
@@ -256,16 +389,20 @@ event.listen(TreeItem, "after_delete", cache_invalidate_cb, propagate=True)
256
389
 
257
390
 
258
391
  # association table TreeGroup <> TreeItem
259
- class LayergroupTreeitem(Base):
392
+ class LayergroupTreeitem(Base): # type: ignore
393
+ """The layergroup_treeitem table representation."""
394
+
260
395
  __tablename__ = "layergroup_treeitem"
261
396
  __table_args__ = {"schema": _schema}
262
397
 
263
398
  # required by formalchemy
264
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
265
- description = Column(Unicode, info={"colanderalchemy": {"exclude": True}})
266
- treegroup_id = Column(
399
+ id: Mapped[int] = mapped_column(
400
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
401
+ )
402
+ description: Mapped[str | None] = mapped_column(Unicode, info={"colanderalchemy": {"exclude": True}})
403
+ treegroup_id: Mapped[int] = mapped_column(
267
404
  Integer,
268
- ForeignKey(_schema + ".treegroup.id"),
405
+ ForeignKey(_schema + ".treegroup.id", name="treegroup_id_fkey"),
269
406
  nullable=False,
270
407
  info={"colanderalchemy": {"exclude": True}},
271
408
  )
@@ -280,9 +417,9 @@ class LayergroupTreeitem(Base):
280
417
  primaryjoin="LayergroupTreeitem.treegroup_id==TreeGroup.id",
281
418
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
282
419
  )
283
- treeitem_id = Column(
420
+ treeitem_id: Mapped[int] = mapped_column(
284
421
  Integer,
285
- ForeignKey(_schema + ".treeitem.id"),
422
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
286
423
  nullable=False,
287
424
  info={"colanderalchemy": {"widget": HiddenWidget()}},
288
425
  )
@@ -299,13 +436,18 @@ class LayergroupTreeitem(Base):
299
436
  primaryjoin="LayergroupTreeitem.treeitem_id==TreeItem.id",
300
437
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
301
438
  )
302
- ordering = Column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
439
+ ordering: Mapped[int] = mapped_column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
303
440
 
304
- def __init__(self, group: "TreeGroup" = None, item: TreeItem = None, ordering: int = 0) -> None:
441
+ def __init__(
442
+ self, group: Optional["TreeGroup"] = None, item: TreeItem | None = None, ordering: int = 0
443
+ ) -> None:
305
444
  self.treegroup = group
306
445
  self.treeitem = item
307
446
  self.ordering = ordering
308
447
 
448
+ def __str__(self) -> str:
449
+ return f"{self.id}"
450
+
309
451
 
310
452
  event.listen(LayergroupTreeitem, "after_insert", cache_invalidate_cb, propagate=True)
311
453
  event.listen(LayergroupTreeitem, "after_update", cache_invalidate_cb, propagate=True)
@@ -313,16 +455,33 @@ event.listen(LayergroupTreeitem, "after_delete", cache_invalidate_cb, propagate=
313
455
 
314
456
 
315
457
  class TreeGroup(TreeItem):
458
+ """The treegroup table representation."""
459
+
316
460
  __tablename__ = "treegroup"
317
461
  __table_args__ = {"schema": _schema}
318
- __mapper_args__ = {"polymorphic_identity": "treegroup"} # needed for _identity_class
462
+ __mapper_args__ = {"polymorphic_identity": "treegroup"} # type: ignore[dict-item] # needed for _identity_class
319
463
 
320
- id = Column(Integer, ForeignKey(_schema + ".treeitem.id"), primary_key=True)
464
+ id: Mapped[int] = mapped_column(
465
+ Integer,
466
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE", name="treegroup_id_fkey"),
467
+ primary_key=True,
468
+ )
321
469
 
322
- def _get_children(self) -> List[TreeItem]:
470
+ def _get_children(self) -> list[TreeItem]:
323
471
  return [c.treeitem for c in self.children_relation]
324
472
 
325
- def _set_children(self, children: List[TreeItem], order: bool = False) -> None:
473
+ def _set_children(self, children: list[TreeItem], order: bool = False) -> None:
474
+ """
475
+ Set the current TreeGroup children TreeItem instances.
476
+
477
+ By managing related LayergroupTreeitem instances.
478
+
479
+ If order is False:
480
+ Append new children at end of current ones.
481
+
482
+ If order is True:
483
+ Force order of children.
484
+ """
326
485
  for child in self.children_relation:
327
486
  if child.treeitem not in children:
328
487
  child.treeitem = None
@@ -336,9 +495,9 @@ class TreeGroup(TreeItem):
336
495
  LayergroupTreeitem(self, child, index * 10)
337
496
  self.children_relation.sort(key=lambda child: child.ordering)
338
497
  else:
339
- current_item = [child.treeitem for child in self.children_relation]
498
+ current_children = [child.treeitem for child in self.children_relation]
340
499
  for index, item in enumerate(children):
341
- if item not in current_item:
500
+ if item not in current_children:
342
501
  LayergroupTreeitem(self, item, 1000000 + index)
343
502
  for index, child in enumerate(self.children_relation):
344
503
  child.ordering = index * 10
@@ -346,29 +505,42 @@ class TreeGroup(TreeItem):
346
505
  children = property(_get_children, _set_children)
347
506
 
348
507
  def __init__(self, name: str = "") -> None:
349
- TreeItem.__init__(self, name=name)
508
+ super().__init__(name=name)
350
509
 
351
510
 
352
511
  class LayerGroup(TreeGroup):
512
+ """The layergroup table representation."""
513
+
353
514
  __tablename__ = "layergroup"
354
515
  __table_args__ = {"schema": _schema}
355
- __colanderalchemy_config__ = {"title": _("Layers group"), "plural": _("Layers groups")}
356
- __mapper_args__ = {"polymorphic_identity": "group"}
516
+ __colanderalchemy_config__ = {
517
+ "title": _("Layers group"),
518
+ "plural": _("Layers groups"),
519
+ "description": c2cgeoportal_commons.lib.literal.Literal(
520
+ _(
521
+ """
522
+ <div class="help-block">
523
+ <h4>Background layers</h4>
524
+ <p>The background layers are configured in the database, with the layer group named
525
+ <b>background</b> (by default).</p>
526
+ <hr>
527
+ </div>
528
+ """
529
+ )
530
+ ),
531
+ }
532
+ __mapper_args__ = {"polymorphic_identity": "group"} # type: ignore[dict-item]
357
533
  __c2cgeoform_config__ = {"duplicate": True}
358
534
 
359
- id = Column(
535
+ id: Mapped[int] = mapped_column(
360
536
  Integer,
361
- ForeignKey(_schema + ".treegroup.id"),
537
+ ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
362
538
  primary_key=True,
363
539
  info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
364
540
  )
365
- is_expanded = Column(
366
- Boolean, info={"colanderalchemy": {"title": _("Expanded"), "column": 2}}
367
- ) # shouldn't be used in V3
368
541
 
369
- def __init__(self, name: str = "", is_expanded: bool = False) -> None:
370
- TreeGroup.__init__(self, name=name)
371
- self.is_expanded = is_expanded
542
+ def __init__(self, name: str = "") -> None:
543
+ super().__init__(name=name)
372
544
 
373
545
 
374
546
  # role theme link for restricted theme
@@ -382,30 +554,58 @@ restricted_role_theme = Table(
382
554
 
383
555
 
384
556
  class Theme(TreeGroup):
557
+ """The theme table representation."""
558
+
385
559
  __tablename__ = "theme"
386
560
  __table_args__ = {"schema": _schema}
387
561
  __colanderalchemy_config__ = {"title": _("Theme"), "plural": _("Themes")}
388
- __mapper_args__ = {"polymorphic_identity": "theme"}
562
+ __mapper_args__ = {"polymorphic_identity": "theme"} # type: ignore[dict-item]
389
563
  __c2cgeoform_config__ = {"duplicate": True}
390
564
 
391
- id = Column(
565
+ id: Mapped[int] = mapped_column(
392
566
  Integer,
393
- ForeignKey(_schema + ".treegroup.id"),
567
+ ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
394
568
  primary_key=True,
395
569
  info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
396
570
  )
397
- ordering = Column(
571
+ ordering: Mapped[int] = mapped_column(
398
572
  Integer, nullable=False, info={"colanderalchemy": {"title": _("Order"), "widget": HiddenWidget()}}
399
573
  )
400
- public = Column(Boolean, default=True, nullable=False, info={"colanderalchemy": {"title": _("Public")}})
401
- icon = Column(Unicode, info={"colanderalchemy": {"title": _("Icon")}})
574
+ public: Mapped[bool] = mapped_column(
575
+ Boolean,
576
+ default=True,
577
+ nullable=False,
578
+ info={
579
+ "colanderalchemy": {
580
+ "title": _("Public"),
581
+ "description": _("Makes the theme public."),
582
+ }
583
+ },
584
+ )
585
+ icon: Mapped[str] = mapped_column(
586
+ Unicode,
587
+ nullable=True,
588
+ info={
589
+ "colanderalchemy": {
590
+ "title": _("Icon"),
591
+ "description": _("The icon URL."),
592
+ "missing": "",
593
+ }
594
+ },
595
+ )
402
596
 
403
597
  # functionality
404
598
  functionalities = relationship(
405
599
  "Functionality",
406
600
  secondary=theme_functionality,
407
601
  cascade="save-update,merge,refresh-expire",
408
- info={"colanderalchemy": {"exclude": True, "title": _("Functionalities")}},
602
+ info={
603
+ "colanderalchemy": {
604
+ "title": _("Functionalities"),
605
+ "description": _("The linked functionalities."),
606
+ "exclude": True,
607
+ }
608
+ },
409
609
  )
410
610
 
411
611
  # restricted to role
@@ -413,11 +613,17 @@ class Theme(TreeGroup):
413
613
  "Role",
414
614
  secondary=restricted_role_theme,
415
615
  cascade="save-update,merge,refresh-expire",
416
- info={"colanderalchemy": {"exclude": True, "title": _("Roles")}},
616
+ info={
617
+ "colanderalchemy": {
618
+ "title": _("Roles"),
619
+ "description": _("Users with checked roles will get access to this theme."),
620
+ "exclude": True,
621
+ }
622
+ },
417
623
  )
418
624
 
419
625
  def __init__(self, name: str = "", ordering: int = 100, icon: str = "") -> None:
420
- TreeGroup.__init__(self, name=name)
626
+ super().__init__(name=name)
421
627
  self.ordering = ordering
422
628
  self.icon = icon
423
629
 
@@ -428,123 +634,220 @@ event.listen(Theme.functionalities, "remove", cache_invalidate_cb)
428
634
 
429
635
 
430
636
  class Layer(TreeItem):
637
+ """The layer table representation."""
638
+
431
639
  __tablename__ = "layer"
432
640
  __table_args__ = {"schema": _schema}
433
- __mapper_args__ = {"polymorphic_identity": "layer"} # needed for _identity_class
641
+ __mapper_args__ = {"polymorphic_identity": "layer"} # type: ignore[dict-item] # needed for _identity_class
434
642
 
435
- id = Column(
643
+ id: Mapped[int] = mapped_column(
436
644
  Integer,
437
- ForeignKey(_schema + ".treeitem.id"),
645
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
438
646
  primary_key=True,
439
647
  info={"colanderalchemy": {"widget": HiddenWidget()}},
440
648
  )
441
- public = Column(Boolean, default=True, info={"colanderalchemy": {"title": _("Public")}})
442
- geo_table = Column(Unicode, info={"colanderalchemy": {"title": _("Geo table")}})
443
- exclude_properties = Column(Unicode, info={"colanderalchemy": {"title": _("Exclude properties")}})
649
+ public: Mapped[bool] = mapped_column(
650
+ Boolean,
651
+ default=True,
652
+ info={
653
+ "colanderalchemy": {
654
+ "title": _("Public"),
655
+ "description": _("Makes the layer public."),
656
+ }
657
+ },
658
+ )
659
+ geo_table: Mapped[str | None] = mapped_column(
660
+ Unicode,
661
+ info={
662
+ "colanderalchemy": {
663
+ "title": _("Geo table"),
664
+ "description": _("The related database table, used by the editing module."),
665
+ }
666
+ },
667
+ )
668
+ exclude_properties: Mapped[str] = mapped_column(
669
+ Unicode,
670
+ nullable=True,
671
+ info={
672
+ "colanderalchemy": {
673
+ "title": _("Exclude properties"),
674
+ "description": _(
675
+ """
676
+ The list of attributes (database columns) that should not appear in
677
+ the editing form so that they cannot be modified by the end user.
678
+ For enumerable attributes (foreign key), the column name should end with '_id'.
679
+ """
680
+ ),
681
+ "missing": "",
682
+ }
683
+ },
684
+ )
444
685
 
445
686
  def __init__(self, name: str = "", public: bool = True) -> None:
446
- TreeItem.__init__(self, name=name)
687
+ super().__init__(name=name)
447
688
  self.public = public
448
689
 
449
690
 
450
691
  class DimensionLayer(Layer):
451
- __mapper_args__ = {"polymorphic_identity": "dimensionlayer"} # needed for _identity_class
692
+ """The intermediate class for the leyser with dimension."""
693
+
694
+ __mapper_args__ = {"polymorphic_identity": "dimensionlayer"} # type: ignore[dict-item] # needed for _identity_class
695
+
452
696
 
697
+ OGCServerType = Literal["mapserver", "qgisserver", "geoserver", "arcgis", "other"]
698
+ OGCSERVER_TYPE_MAPSERVER: OGCServerType = "mapserver"
699
+ OGCSERVER_TYPE_QGISSERVER: OGCServerType = "qgisserver"
700
+ OGCSERVER_TYPE_GEOSERVER: OGCServerType = "geoserver"
701
+ OGCSERVER_TYPE_ARCGIS: OGCServerType = "arcgis"
702
+ OGCSERVER_TYPE_OTHER: OGCServerType = "other"
453
703
 
454
- OGCSERVER_TYPE_MAPSERVER = "mapserver"
455
- OGCSERVER_TYPE_QGISSERVER = "qgisserver"
456
- OGCSERVER_TYPE_GEOSERVER = "geoserver"
457
- OGCSERVER_TYPE_ARCGIS = "arcgis"
458
- OGCSERVER_TYPE_OTHER = "other"
459
704
 
460
- OGCSERVER_AUTH_NOAUTH = "No auth"
461
- OGCSERVER_AUTH_STANDARD = "Standard auth"
462
- OGCSERVER_AUTH_GEOSERVER = "Geoserver auth"
463
- OGCSERVER_AUTH_PROXY = "Proxy"
705
+ OGCServerAuth = Literal["No auth", "Standard auth", "Geoserver auth", "Proxy"]
706
+ OGCSERVER_AUTH_NOAUTH: OGCServerAuth = "No auth"
707
+ OGCSERVER_AUTH_STANDARD: OGCServerAuth = "Standard auth"
708
+ OGCSERVER_AUTH_GEOSERVER: OGCServerAuth = "Geoserver auth"
709
+ OGCSERVER_AUTH_PROXY: OGCServerAuth = "Proxy"
464
710
 
465
711
 
466
- class OGCServer(Base):
712
+ ImageType = Literal["image/jpeg", "image/png"]
713
+ TimeMode = Literal["disabled", "value", "range"]
714
+ TimeWidget = Literal["slider", "datepicker"]
715
+
716
+
717
+ class OGCServer(Base): # type: ignore
718
+ """The ogc_server table representation."""
719
+
467
720
  __tablename__ = "ogc_server"
468
721
  __table_args__ = {"schema": _schema}
469
- __colanderalchemy_config__ = {"title": _("OGC Server"), "plural": _("OGC Servers")}
470
- __c2cgeoform_config__ = {"duplicate": True}
471
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
472
- name = Column(Unicode, nullable=False, unique=True, info={"colanderalchemy": {"title": _("Name")}})
473
- description = Column(Unicode, info={"colanderalchemy": {"title": _("Description")}})
474
- url = Column(Unicode, nullable=False, info={"colanderalchemy": {"title": _("Basic URL")}})
475
- url_wfs = Column(Unicode, info={"colanderalchemy": {"title": _("WFS URL")}})
476
- type = Column(
477
- Enum(
478
- OGCSERVER_TYPE_MAPSERVER,
479
- OGCSERVER_TYPE_QGISSERVER,
480
- OGCSERVER_TYPE_GEOSERVER,
481
- OGCSERVER_TYPE_ARCGIS,
482
- OGCSERVER_TYPE_OTHER,
483
- native_enum=False,
722
+ __colanderalchemy_config__ = {
723
+ "title": _("OGC Server"),
724
+ "plural": _("OGC Servers"),
725
+ "description": c2cgeoportal_commons.lib.literal.Literal(
726
+ _(
727
+ """
728
+ <div class="help-block">
729
+ <p>This is the description of an OGC server (WMS/WFS).\n
730
+ For one server we try to create only one request when it is possible.</p>
731
+ <p>If you want to query the same server to get PNG and JPEG images,\n
732
+ you should define two <code>OGC servers</code>.</p>
733
+ <hr>
734
+ </div>
735
+ """
736
+ )
484
737
  ),
738
+ }
739
+ __c2cgeoform_config__ = {"duplicate": True}
740
+ id: Mapped[int] = mapped_column(
741
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
742
+ )
743
+ name: Mapped[str] = mapped_column(
744
+ Unicode,
745
+ nullable=False,
746
+ unique=True,
747
+ info={
748
+ "colanderalchemy": {
749
+ "title": _("Name"),
750
+ "description": _(
751
+ "The name of the OGC Server, should contains only no unaccentuated letters, numbers and _"
752
+ ),
753
+ }
754
+ },
755
+ )
756
+ description: Mapped[str | None] = mapped_column(
757
+ Unicode,
758
+ info={
759
+ "colanderalchemy": {
760
+ "title": _("Description"),
761
+ "description": _("A description"),
762
+ }
763
+ },
764
+ )
765
+ url: Mapped[str] = mapped_column(
766
+ Unicode,
767
+ nullable=False,
768
+ info={
769
+ "colanderalchemy": {
770
+ "title": _("Basic URL"),
771
+ "description": _("The server URL"),
772
+ }
773
+ },
774
+ )
775
+ url_wfs: Mapped[str | None] = mapped_column(
776
+ Unicode,
777
+ info={
778
+ "colanderalchemy": {
779
+ "title": _("WFS URL"),
780
+ "description": _("The WFS server URL. If empty, the ``Basic URL`` is used."),
781
+ }
782
+ },
783
+ )
784
+ type: Mapped[OGCServerType] = mapped_column(
785
+ Enum(*get_args(OGCServerType), native_enum=False),
485
786
  nullable=False,
486
787
  info={
487
788
  "colanderalchemy": {
488
789
  "title": _("Server type"),
489
- "widget": SelectWidget(
490
- values=(
491
- (OGCSERVER_TYPE_MAPSERVER, OGCSERVER_TYPE_MAPSERVER),
492
- (OGCSERVER_TYPE_QGISSERVER, OGCSERVER_TYPE_QGISSERVER),
493
- (OGCSERVER_TYPE_GEOSERVER, OGCSERVER_TYPE_GEOSERVER),
494
- (OGCSERVER_TYPE_ARCGIS, OGCSERVER_TYPE_ARCGIS),
495
- (OGCSERVER_TYPE_OTHER, OGCSERVER_TYPE_OTHER),
496
- )
790
+ "description": _(
791
+ "The server type which is used to know which custom attribute will be used."
497
792
  ),
793
+ "widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerType))),
498
794
  }
499
795
  },
500
796
  )
501
- image_type = Column(
502
- Enum("image/jpeg", "image/png", native_enum=False),
797
+ image_type: Mapped[ImageType] = mapped_column(
798
+ Enum(*get_args(ImageType), native_enum=False),
503
799
  nullable=False,
504
800
  info={
505
801
  "colanderalchemy": {
506
802
  "title": _("Image type"),
507
- "widget": SelectWidget(values=(("image/jpeg", "image/jpeg"), ("image/png", "image/png"))),
803
+ "description": _("The MIME type of the images (e.g.: ``image/png``)."),
804
+ "widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
508
805
  "column": 2,
509
806
  }
510
807
  },
511
808
  )
512
- auth = Column(
513
- Enum(
514
- OGCSERVER_AUTH_NOAUTH,
515
- OGCSERVER_AUTH_STANDARD,
516
- OGCSERVER_AUTH_GEOSERVER,
517
- OGCSERVER_AUTH_PROXY,
518
- native_enum=False,
519
- ),
809
+ auth: Mapped[OGCServerAuth] = mapped_column(
810
+ Enum(*get_args(OGCServerAuth), native_enum=False),
520
811
  nullable=False,
521
812
  info={
522
813
  "colanderalchemy": {
523
814
  "title": _("Authentication type"),
524
- "widget": SelectWidget(
525
- values=(
526
- (OGCSERVER_AUTH_NOAUTH, OGCSERVER_AUTH_NOAUTH),
527
- (OGCSERVER_AUTH_STANDARD, OGCSERVER_AUTH_STANDARD),
528
- (OGCSERVER_AUTH_GEOSERVER, OGCSERVER_AUTH_GEOSERVER),
529
- (OGCSERVER_AUTH_PROXY, OGCSERVER_AUTH_PROXY),
530
- )
531
- ),
815
+ "description": "The kind of authentication to use.",
816
+ "widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerAuth))),
817
+ "column": 2,
818
+ }
819
+ },
820
+ )
821
+ wfs_support: Mapped[bool] = mapped_column(
822
+ Boolean,
823
+ info={
824
+ "colanderalchemy": {
825
+ "title": _("WFS support"),
826
+ "description": _("Whether WFS is supported by the server."),
827
+ "column": 2,
828
+ }
829
+ },
830
+ )
831
+ is_single_tile: Mapped[bool] = mapped_column(
832
+ Boolean,
833
+ info={
834
+ "colanderalchemy": {
835
+ "title": _("Single tile"),
836
+ "description": _("Whether to use the single tile mode (For future use)."),
532
837
  "column": 2,
533
838
  }
534
839
  },
535
840
  )
536
- wfs_support = Column(Boolean, info={"colanderalchemy": {"title": _("WFS support"), "column": 2}})
537
- is_single_tile = Column(Boolean, info={"colanderalchemy": {"title": _("Single tile"), "column": 2}})
538
841
 
539
842
  def __init__(
540
843
  self,
541
844
  name: str = "",
542
- description: Optional[str] = None,
845
+ description: str | None = None,
543
846
  url: str = "https://wms.example.com",
544
- url_wfs: str = None,
545
- type_: str = "mapserver",
546
- image_type: str = "image/png",
547
- auth: str = "Standard auth",
847
+ url_wfs: str | None = None,
848
+ type_: OGCServerType = OGCSERVER_TYPE_MAPSERVER,
849
+ image_type: ImageType = "image/png",
850
+ auth: OGCServerAuth = OGCSERVER_AUTH_STANDARD,
548
851
  wfs_support: bool = True,
549
852
  is_single_tile: bool = False,
550
853
  ) -> None:
@@ -559,42 +862,54 @@ class OGCServer(Base):
559
862
  self.is_single_tile = is_single_tile
560
863
 
561
864
  def __str__(self) -> str:
562
- return self.name or "" # pragma: no cover
865
+ return self.name or ""
563
866
 
564
- def url_description(self, request: Request) -> str:
565
- errors: Set[str] = set()
867
+ def url_description(self, request: pyramid.request.Request) -> str:
868
+ errors: set[str] = set()
566
869
  url = get_url2(self.name, self.url, request, errors)
567
- return url or "\n".join(errors)
870
+ return url.url() if url else "\n".join(errors)
568
871
 
569
- def url_wfs_description(self, request: Request) -> Optional[str]:
872
+ def url_wfs_description(self, request: pyramid.request.Request) -> str | None:
570
873
  if not self.url_wfs:
571
874
  return self.url_description(request)
572
- errors: Set[str] = set()
875
+ errors: set[str] = set()
573
876
  url = get_url2(self.name, self.url_wfs, request, errors)
574
- return url or "\n".join(errors)
575
-
576
-
577
- event.listen(OGCServer, "after_insert", cache_invalidate_cb, propagate=True)
578
- event.listen(OGCServer, "after_update", cache_invalidate_cb, propagate=True)
579
- event.listen(OGCServer, "after_delete", cache_invalidate_cb, propagate=True)
877
+ return url.url() if url else "\n".join(errors)
580
878
 
581
879
 
582
880
  class LayerWMS(DimensionLayer):
881
+ """The layer_wms table representation."""
882
+
583
883
  __tablename__ = "layer_wms"
584
884
  __table_args__ = {"schema": _schema}
585
- __colanderalchemy_config__ = {"title": _("WMS Layer"), "plural": _("WMS Layers")}
885
+ __colanderalchemy_config__ = {
886
+ "title": _("WMS Layer"),
887
+ "plural": _("WMS Layers"),
888
+ "description": c2cgeoportal_commons.lib.literal.Literal(
889
+ _(
890
+ """
891
+ <div class="help-block">
892
+ <p>Definition of a <code>WMS Layer</code>.</p>
893
+ <p>Note: The layers named <code>wms-defaults</code> contains the values
894
+ used when we create a new <code>WMS layer</code>.</p>
895
+ <hr>
896
+ </div>
897
+ """
898
+ )
899
+ ),
900
+ }
586
901
 
587
902
  __c2cgeoform_config__ = {"duplicate": True}
588
903
 
589
- __mapper_args__ = {"polymorphic_identity": "l_wms"}
904
+ __mapper_args__ = {"polymorphic_identity": "l_wms"} # type: ignore[dict-item]
590
905
 
591
- id = Column(
906
+ id: Mapped[int] = mapped_column(
592
907
  Integer,
593
908
  ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
594
909
  primary_key=True,
595
910
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
596
911
  )
597
- ogc_server_id = Column(
912
+ ogc_server_id: Mapped[int] = mapped_column(
598
913
  Integer,
599
914
  ForeignKey(_schema + ".ogc_server.id"),
600
915
  nullable=False,
@@ -608,31 +923,68 @@ class LayerWMS(DimensionLayer):
608
923
  }
609
924
  },
610
925
  )
611
- layer = Column(
612
- Unicode, nullable=False, info={"colanderalchemy": {"title": _("WMS layer name"), "column": 2}}
926
+ layer: Mapped[str] = mapped_column(
927
+ Unicode,
928
+ nullable=False,
929
+ info={
930
+ "colanderalchemy": {
931
+ "title": _("WMS layer name"),
932
+ "description": _(
933
+ """
934
+ The WMS layers. Can be one layer, one group, or a comma separated list of layers.
935
+ In the case of a comma separated list of layers, you will get the legend rule for the
936
+ layer icon on the first layer, and to support the legend you should define a legend
937
+ metadata.
938
+ """
939
+ ),
940
+ "column": 2,
941
+ }
942
+ },
943
+ )
944
+ style: Mapped[str | None] = mapped_column(
945
+ Unicode,
946
+ info={
947
+ "colanderalchemy": {
948
+ "title": _("Style"),
949
+ "description": _("The style to use for this layer, can be empty."),
950
+ "column": 2,
951
+ }
952
+ },
613
953
  )
614
- style = Column(Unicode, info={"colanderalchemy": {"title": _("Style"), "column": 2}})
615
- valid = Column(
954
+ valid: Mapped[bool] = mapped_column(
616
955
  Boolean,
617
- info={"colanderalchemy": {"title": _("Valid"), "column": 2, "widget": CheckboxWidget(readonly=True)}},
956
+ nullable=True,
957
+ info={
958
+ "colanderalchemy": {
959
+ "title": _("Valid"),
960
+ "description": _("The status reported by latest synchronization (readonly)."),
961
+ "column": 2,
962
+ "widget": CheckboxWidget(readonly=True),
963
+ "missing": colander_null,
964
+ }
965
+ },
618
966
  )
619
- invalid_reason = Column(
967
+ invalid_reason: Mapped[str] = mapped_column(
620
968
  Unicode,
969
+ nullable=True,
621
970
  info={
622
971
  "colanderalchemy": {
623
972
  "title": _("Reason why I am not valid"),
973
+ "description": _("The reason for status reported by latest synchronization (readonly)."),
624
974
  "column": 2,
625
975
  "widget": TextInputWidget(readonly=True),
976
+ "missing": "",
626
977
  }
627
978
  },
628
979
  )
629
- time_mode = Column(
630
- Enum("disabled", "value", "range", native_enum=False),
980
+ time_mode: Mapped[TimeMode] = mapped_column(
981
+ Enum(*get_args(TimeMode), native_enum=False),
631
982
  default="disabled",
632
983
  nullable=False,
633
984
  info={
634
985
  "colanderalchemy": {
635
986
  "title": _("Time mode"),
987
+ "description": _("Used for the WMS time component."),
636
988
  "column": 2,
637
989
  "widget": SelectWidget(
638
990
  values=(("disabled", _("Disabled")), ("value", _("Value")), ("range", _("Range")))
@@ -640,13 +992,14 @@ class LayerWMS(DimensionLayer):
640
992
  }
641
993
  },
642
994
  )
643
- time_widget = Column(
644
- Enum("slider", "datepicker", native_enum=False),
995
+ time_widget: Mapped[TimeWidget] = mapped_column(
996
+ Enum(*get_args(TimeWidget), native_enum=False),
645
997
  default="slider",
646
998
  nullable=False,
647
999
  info={
648
1000
  "colanderalchemy": {
649
1001
  "title": _("Time widget"),
1002
+ "description": _("The component type used for the WMS time."),
650
1003
  "column": 2,
651
1004
  "widget": SelectWidget(values=(("slider", _("Slider")), ("datepicker", _("Datepicker")))),
652
1005
  }
@@ -657,7 +1010,13 @@ class LayerWMS(DimensionLayer):
657
1010
  ogc_server = relationship(
658
1011
  "OGCServer",
659
1012
  backref=backref("layers", info={"colanderalchemy": {"exclude": True, "title": _("WMS Layers")}}),
660
- info={"colanderalchemy": {"title": _("OGC server"), "exclude": True}},
1013
+ info={
1014
+ "colanderalchemy": {
1015
+ "title": _("OGC server"),
1016
+ "description": _("The OGC server to use for this layer."),
1017
+ "exclude": True,
1018
+ }
1019
+ },
661
1020
  )
662
1021
 
663
1022
  def __init__(
@@ -665,59 +1024,164 @@ class LayerWMS(DimensionLayer):
665
1024
  name: str = "",
666
1025
  layer: str = "",
667
1026
  public: bool = True,
668
- time_mode: str = "disabled",
669
- time_widget: str = "slider",
1027
+ time_mode: TimeMode = "disabled",
1028
+ time_widget: TimeWidget = "slider",
670
1029
  ) -> None:
671
- DimensionLayer.__init__(self, name=name, public=public)
1030
+ super().__init__(name=name, public=public)
672
1031
  self.layer = layer
673
1032
  self.time_mode = time_mode
674
1033
  self.time_widget = time_widget
675
1034
 
676
1035
  @staticmethod
677
- def get_default(dbsession: Session) -> DimensionLayer:
678
- return dbsession.query(LayerWMS).filter(LayerWMS.name == "wms-defaults").one_or_none()
1036
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1037
+ return cast(
1038
+ Optional[DimensionLayer],
1039
+ dbsession.query(LayerWMS).filter(LayerWMS.name == "wms-defaults").one_or_none(),
1040
+ )
679
1041
 
680
1042
 
681
1043
  class LayerWMTS(DimensionLayer):
1044
+ """The layer_wmts table representation."""
1045
+
682
1046
  __tablename__ = "layer_wmts"
683
1047
  __table_args__ = {"schema": _schema}
684
- __colanderalchemy_config__ = {"title": _("WMTS Layer"), "plural": _("WMTS Layers")}
1048
+ __colanderalchemy_config__ = {
1049
+ "title": _("WMTS Layer"),
1050
+ "plural": _("WMTS Layers"),
1051
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1052
+ _(
1053
+ """
1054
+ <div class="help-block">
1055
+ <p>Definition of a <code>WMTS Layer</code>.</p>
1056
+ <p>Note: The layers named <code>wmts-defaults</code> contains the values used when
1057
+ we create a new <code>WMTS layer</code>.</p>
1058
+
1059
+ <h4>Self generated WMTS tiles</h4>
1060
+ <p>When using self generated WMTS tiles, you should use URL
1061
+ <code>config://local/tiles/1.0.0/WMTSCapabilities.xml</code> where:<p>
1062
+ <ul>
1063
+ <li><code>config://local</code> is a dynamic path based on the project
1064
+ configuration.</li>
1065
+ <li><code>/tiles</code> is a proxy in the tilecloudchain container.</li>
1066
+ </ul>
1067
+
1068
+ <h4>Queryable WMTS</h4>
1069
+ <p>To make the WMTS queryable, you should add the following <code>Metadata</code>:
1070
+ <ul>
1071
+ <li><code>ogcServer</code> with the name of the used <code>OGC server</code>,
1072
+ <li><code>wmsLayers</code> or <code>queryLayers</code> with the layers to query
1073
+ (comma separated list. Groups are not supported).
1074
+ </ul>
1075
+ <p>By default the scale limits for the queryable layers are the
1076
+ <code>minResolution</code> and/or the <code>maxResolution</code>a metadata
1077
+ value(s) of the WMTS layer. These values correspond to the WMTS layer
1078
+ resolution(s) which should be the zoom limit.
1079
+ You can also set a <code>minQueryResolution</code> and/or a
1080
+ <code>maxQueryResolution</code> to set a query zoom limits independent of the
1081
+ WMTS layer.</p>
1082
+
1083
+ <h4>Print WMTS in high quality</h4>
1084
+ <p>To print the layers in high quality, you can define that the image shall be
1085
+ retrieved with a <code>GetMap</code> on the original WMS server.
1086
+ <p>To activate this, you should add the following <code>Metadata</code>:</p>
1087
+ <ul>
1088
+ <li><code>ogcServer</code> with the name of the used <code>OGC server</code>,</li>
1089
+ <li><code>wmsLayers</code> or <code>printLayers</code> with the layers to print
1090
+ (comma separated list).</li>
1091
+ </ul>
1092
+ <hr>
1093
+ </div>
1094
+ """
1095
+ )
1096
+ ),
1097
+ }
685
1098
  __c2cgeoform_config__ = {"duplicate": True}
686
- __mapper_args__ = {"polymorphic_identity": "l_wmts"}
1099
+ __mapper_args__ = {"polymorphic_identity": "l_wmts"} # type: ignore[dict-item]
687
1100
 
688
- id = Column(
1101
+ id: Mapped[int] = mapped_column(
689
1102
  Integer,
690
- ForeignKey(_schema + ".layer.id"),
1103
+ ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
691
1104
  primary_key=True,
692
1105
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
693
1106
  )
694
- url = Column(
695
- Unicode, nullable=False, info={"colanderalchemy": {"title": _("GetCapabilities URL"), "column": 2}}
1107
+ url: Mapped[str] = mapped_column(
1108
+ Unicode,
1109
+ nullable=False,
1110
+ info={
1111
+ "colanderalchemy": {
1112
+ "title": _("GetCapabilities URL"),
1113
+ "description": _("The URL to the WMTS capabilities."),
1114
+ "column": 2,
1115
+ }
1116
+ },
696
1117
  )
697
- layer = Column(
698
- Unicode, nullable=False, info={"colanderalchemy": {"title": _("WMTS layer name"), "column": 2}}
1118
+ layer: Mapped[str] = mapped_column(
1119
+ Unicode,
1120
+ nullable=False,
1121
+ info={
1122
+ "colanderalchemy": {
1123
+ "title": _("WMTS layer name"),
1124
+ "description": _("The name of the WMTS layer to use"),
1125
+ "column": 2,
1126
+ }
1127
+ },
1128
+ )
1129
+ style: Mapped[str] = mapped_column(
1130
+ Unicode,
1131
+ nullable=True,
1132
+ info={
1133
+ "colanderalchemy": {
1134
+ "title": _("Style"),
1135
+ "description": _("The style to use; if not present, the default style is used."),
1136
+ "column": 2,
1137
+ "missing": "",
1138
+ }
1139
+ },
1140
+ )
1141
+ matrix_set: Mapped[str] = mapped_column(
1142
+ Unicode,
1143
+ nullable=True,
1144
+ info={
1145
+ "colanderalchemy": {
1146
+ "title": _("Matrix set"),
1147
+ "description": _(
1148
+ "The matrix set to use; if there is only one matrix set in the capabilities, it can be"
1149
+ "left empty."
1150
+ ),
1151
+ "column": 2,
1152
+ "missing": "",
1153
+ }
1154
+ },
699
1155
  )
700
- style = Column(Unicode, info={"colanderalchemy": {"title": _("Style"), "column": 2}})
701
- matrix_set = Column(Unicode, info={"colanderalchemy": {"title": _("Matrix set"), "column": 2}})
702
- image_type = Column(
703
- Enum("image/jpeg", "image/png", native_enum=False),
1156
+ image_type: Mapped[ImageType] = mapped_column(
1157
+ Enum(*get_args(ImageType), native_enum=False),
704
1158
  nullable=False,
705
1159
  info={
706
1160
  "colanderalchemy": {
707
1161
  "title": _("Image type"),
1162
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1163
+ _(
1164
+ """
1165
+ The MIME type of the images (e.g.: <code>image/png</code>).
1166
+ """
1167
+ )
1168
+ ),
708
1169
  "column": 2,
709
- "widget": SelectWidget(values=(("image/jpeg", "image/jpeg"), ("image/png", "image/png"))),
1170
+ "widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
710
1171
  }
711
1172
  },
712
1173
  )
713
1174
 
714
- def __init__(self, name: str = "", public: bool = True, image_type: str = "image/png") -> None:
715
- DimensionLayer.__init__(self, name=name, public=public)
1175
+ def __init__(self, name: str = "", public: bool = True, image_type: ImageType = "image/png") -> None:
1176
+ super().__init__(name=name, public=public)
716
1177
  self.image_type = image_type
717
1178
 
718
1179
  @staticmethod
719
- def get_default(dbsession: Session) -> DimensionLayer:
720
- return dbsession.query(LayerWMTS).filter(LayerWMTS.name == "wmts-defaults").one_or_none()
1180
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1181
+ return cast(
1182
+ Optional[DimensionLayer],
1183
+ dbsession.query(LayerWMTS).filter(LayerWMTS.name == "wmts-defaults").one_or_none(),
1184
+ )
721
1185
 
722
1186
 
723
1187
  # association table role <> restriction area
@@ -749,61 +1213,224 @@ layer_ra = Table(
749
1213
  )
750
1214
 
751
1215
 
1216
+ class LayerCOG(Layer):
1217
+ """The Cloud Optimized GeoTIFF layer table representation."""
1218
+
1219
+ __tablename__ = "layer_cog"
1220
+ __table_args__ = {"schema": _schema}
1221
+ __colanderalchemy_config__ = {
1222
+ "title": _("COG Layer"),
1223
+ "plural": _("COG Layers"),
1224
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1225
+ _(
1226
+ """
1227
+ <div class="help-block">
1228
+ <p>Definition of a <code>COG Layer</code> (COG for
1229
+ <a href="https://www.cogeo.org/">Cloud Optimized GeoTIFF</a>).</p>
1230
+ <p>Note: The layers named <code>cog-defaults</code> contains the values
1231
+ used when we create a new <code>COG layer</code>.</p>
1232
+ </div>
1233
+ """
1234
+ )
1235
+ ),
1236
+ }
1237
+ __c2cgeoform_config__ = {"duplicate": True}
1238
+ __mapper_args__ = {"polymorphic_identity": "l_cog"} # type: ignore[dict-item]
1239
+
1240
+ id: Mapped[int] = mapped_column(
1241
+ Integer,
1242
+ ForeignKey(_schema + ".layer.id"),
1243
+ primary_key=True,
1244
+ info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1245
+ )
1246
+ url: Mapped[str] = mapped_column(
1247
+ Unicode,
1248
+ nullable=False,
1249
+ info={
1250
+ "colanderalchemy": {
1251
+ "title": _("URL"),
1252
+ "description": _("URL of the COG file."),
1253
+ "column": 2,
1254
+ }
1255
+ },
1256
+ )
1257
+
1258
+ @staticmethod
1259
+ def get_default(dbsession: Session) -> Layer | None:
1260
+ return dbsession.query(LayerCOG).filter(LayerCOG.name == "cog-defaults").one_or_none()
1261
+
1262
+ def url_description(self, request: pyramid.request.Request) -> str:
1263
+ errors: set[str] = set()
1264
+ url = get_url2(self.name, self.url, request, errors)
1265
+ return url.url() if url else "\n".join(errors)
1266
+
1267
+
752
1268
  class LayerVectorTiles(DimensionLayer):
1269
+ """The layer_vectortiles table representation."""
1270
+
753
1271
  __tablename__ = "layer_vectortiles"
754
1272
  __table_args__ = {"schema": _schema}
755
- __colanderalchemy_config__ = {"title": _("Vector Tiles Layer"), "plural": _("Vector Tiles Layers")}
1273
+ __colanderalchemy_config__ = {
1274
+ "title": _("Vector Tiles Layer"),
1275
+ "plural": _("Vector Tiles Layers"),
1276
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1277
+ _(
1278
+ """
1279
+ <div class="help-block">
1280
+ <p>Definition of a <code>Vector Tiles Layer</code>.</p>
1281
+ <p>Note: The layers named <code>vector-tiles-defaults</code> contains the values used when
1282
+ we create a new <code>Vector Tiles layer</code>.</p>
1283
+
1284
+ <h4>Queryable Vector Tiles</h4>
1285
+ <p>To make the Vector Tiles queryable, you should add the following <code>Metadata</code>:
1286
+ <ul>
1287
+ <li><code>ogcServer</code> with the name of the used <code>OGC server</code>,
1288
+ <li><code>wmsLayers</code> or <code>queryLayers</code> with the layers to query
1289
+ (comma separated list. Groups are not supported).
1290
+ </ul>
1291
+
1292
+ <h4>Print Vector Tiles in high quality</h4>
1293
+ <p>To print the layers in high quality, you can define that the image shall be
1294
+ retrieved with a <code>GetMap</code> on the original WMS server.
1295
+ <p>To activate this, you should add the following <code>Metadata</code>:</p>
1296
+ <ul>
1297
+ <li><code>ogcServer</code> with the name of the used <code>OGC server</code>,</li>
1298
+ <li><code>wmsLayers</code> or <code>printLayers</code> with the layers to print
1299
+ (comma separated list).</li>
1300
+ </ul>
1301
+ <hr>
1302
+ </div>
1303
+ """
1304
+ )
1305
+ ),
1306
+ }
756
1307
 
757
1308
  __c2cgeoform_config__ = {"duplicate": True}
758
1309
 
759
- __mapper_args__ = {"polymorphic_identity": "l_mvt"}
1310
+ __mapper_args__ = {"polymorphic_identity": "l_mvt"} # type: ignore[dict-item]
760
1311
 
761
- id = Column(
1312
+ id: Mapped[int] = mapped_column(
762
1313
  Integer,
763
1314
  ForeignKey(_schema + ".layer.id"),
764
1315
  primary_key=True,
765
1316
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
766
1317
  )
767
1318
 
768
- style = Column(
1319
+ style: Mapped[str] = mapped_column(
769
1320
  Unicode,
770
1321
  nullable=False,
771
1322
  info={
772
1323
  "colanderalchemy": {
773
1324
  "title": _("Style"),
774
- "description": "The path to json style. Example: https://url/style.json",
1325
+ "description": _(
1326
+ """
1327
+ The path to a Mapbox Style file (version 8 or higher). Example: https://url/style.json
1328
+ """
1329
+ ),
1330
+ "column": 2,
1331
+ }
1332
+ },
1333
+ )
1334
+
1335
+ sql: Mapped[str] = mapped_column(
1336
+ Unicode,
1337
+ nullable=True,
1338
+ info={
1339
+ "colanderalchemy": {
1340
+ "title": _("SQL query"),
1341
+ "description": _(
1342
+ """
1343
+ A SQL query to get the vector tiles data.
1344
+ """
1345
+ ),
775
1346
  "column": 2,
1347
+ "widget": TextAreaWidget(rows=15),
776
1348
  }
777
1349
  },
778
1350
  )
779
1351
 
780
- xyz = Column(
1352
+ xyz: Mapped[str] = mapped_column(
781
1353
  Unicode,
782
1354
  nullable=True,
783
1355
  info={
784
1356
  "colanderalchemy": {
785
1357
  "title": _("Raster URL"),
786
- "description": "The raster url. Example: https://url/{z}/{x}/{y}.png",
1358
+ "description": _(
1359
+ """
1360
+ The raster url. Example: https://url/{z}/{x}/{y}.png. Alternative to print the
1361
+ layer with a service which rasterises the vector tiles.
1362
+ """
1363
+ ),
787
1364
  "column": 2,
788
1365
  }
789
1366
  },
790
1367
  )
791
1368
 
1369
+ def __init__(self, name: str = "", public: bool = True, style: str = "", sql: str = "") -> None:
1370
+ super().__init__(name=name, public=public)
1371
+ self.style = style
1372
+ self.sql = sql
1373
+
1374
+ @staticmethod
1375
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1376
+ return cast(
1377
+ Optional[DimensionLayer],
1378
+ dbsession.query(LayerVectorTiles)
1379
+ .filter(LayerVectorTiles.name == "vector-tiles-defaults")
1380
+ .one_or_none(),
1381
+ )
1382
+
1383
+ def style_description(self, request: pyramid.request.Request) -> str:
1384
+ errors: set[str] = set()
1385
+ url = get_url2(self.name, self.style, request, errors)
1386
+ return url.url() if url else "\n".join(errors)
1387
+
1388
+
1389
+ class RestrictionArea(Base): # type: ignore
1390
+ """The restrictionarea table representation."""
792
1391
 
793
- class RestrictionArea(Base):
794
1392
  __tablename__ = "restrictionarea"
795
1393
  __table_args__ = {"schema": _schema}
796
1394
  __colanderalchemy_config__ = {"title": _("Restriction area"), "plural": _("Restriction areas")}
797
1395
  __c2cgeoform_config__ = {"duplicate": True}
798
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1396
+ id: Mapped[int] = mapped_column(
1397
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1398
+ )
799
1399
 
800
- name = Column(Unicode, info={"colanderalchemy": {"title": _("Name")}})
801
- description = Column(Unicode, info={"colanderalchemy": {"title": _("Description")}})
802
- readwrite = Column(Boolean, default=False, info={"colanderalchemy": {"title": _("Read/write")}})
803
- area = Column(
1400
+ name: Mapped[str] = mapped_column(
1401
+ Unicode,
1402
+ info={
1403
+ "colanderalchemy": {
1404
+ "title": _("Name"),
1405
+ "description": _("A name."),
1406
+ }
1407
+ },
1408
+ )
1409
+ description: Mapped[str | None] = mapped_column(
1410
+ Unicode,
1411
+ info={
1412
+ "colanderalchemy": {
1413
+ "title": _("Description"),
1414
+ "description": _("An optional description"),
1415
+ }
1416
+ },
1417
+ )
1418
+ readwrite: Mapped[bool] = mapped_column(
1419
+ Boolean,
1420
+ default=False,
1421
+ info={
1422
+ "colanderalchemy": {
1423
+ "title": _("Read/write"),
1424
+ "description": _("Allows the linked users to change the objects."),
1425
+ }
1426
+ },
1427
+ )
1428
+ area = mapped_column(
804
1429
  Geometry("POLYGON", srid=_srid),
805
1430
  info={
806
1431
  "colanderalchemy": {
1432
+ "title": _("Area"),
1433
+ "description": _("Active in the following area, if not defined, it is active everywhere."),
807
1434
  "typ": ColanderGeometry("POLYGON", srid=_srid, map_srid=_map_config["srid"]),
808
1435
  "widget": MapWidget(map_options=_map_config),
809
1436
  }
@@ -814,20 +1441,57 @@ class RestrictionArea(Base):
814
1441
  roles = relationship(
815
1442
  "Role",
816
1443
  secondary=role_ra,
817
- info={"colanderalchemy": {"title": _("Roles"), "exclude": True}},
1444
+ info={
1445
+ "colanderalchemy": {
1446
+ "title": _("Roles"),
1447
+ "description": _("Checked roles will grant access to this restriction area."),
1448
+ "exclude": True,
1449
+ }
1450
+ },
818
1451
  cascade="save-update,merge,refresh-expire",
819
1452
  backref=backref(
820
- "restrictionareas", info={"colanderalchemy": {"exclude": True, "title": _("Restriction areas")}}
1453
+ "restrictionareas",
1454
+ info={
1455
+ "colanderalchemy": {
1456
+ "title": _("Restriction areas"),
1457
+ "description": _(
1458
+ "Users with this role will be granted with access to those restriction areas."
1459
+ ),
1460
+ "exclude": True,
1461
+ }
1462
+ },
821
1463
  ),
822
1464
  )
823
1465
  layers = relationship(
824
1466
  "Layer",
825
1467
  secondary=layer_ra,
826
1468
  order_by=Layer.name,
827
- info={"colanderalchemy": {"title": _("Layers"), "exclude": True}},
1469
+ info={
1470
+ "colanderalchemy": {
1471
+ "title": _("Layers"),
1472
+ "exclude": True,
1473
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1474
+ _(
1475
+ """
1476
+ <div class="help-block">
1477
+ <p>This restriction area will grant access to the checked layers.</p>
1478
+ <hr>
1479
+ </div>
1480
+ """
1481
+ )
1482
+ ),
1483
+ }
1484
+ },
828
1485
  cascade="save-update,merge,refresh-expire",
829
1486
  backref=backref(
830
- "restrictionareas", info={"colanderalchemy": {"title": _("Restriction areas"), "exclude": True}}
1487
+ "restrictionareas",
1488
+ info={
1489
+ "colanderalchemy": {
1490
+ "title": _("Restriction areas"),
1491
+ "exclude": True,
1492
+ "description": _("The areas through which the user can see the layer."),
1493
+ }
1494
+ },
831
1495
  ),
832
1496
  )
833
1497
 
@@ -835,9 +1499,9 @@ class RestrictionArea(Base):
835
1499
  self,
836
1500
  name: str = "",
837
1501
  description: str = "",
838
- layers: List[Layer] = None,
839
- roles: List[Role] = None,
840
- area: Geometry = None,
1502
+ layers: list[Layer] | None = None,
1503
+ roles: list[Role] | None = None,
1504
+ area: Geometry | None = None,
841
1505
  readwrite: bool = False,
842
1506
  ) -> None:
843
1507
  if layers is None:
@@ -851,8 +1515,8 @@ class RestrictionArea(Base):
851
1515
  self.area = area
852
1516
  self.readwrite = readwrite
853
1517
 
854
- def __str__(self) -> str: # pragma: no cover
855
- return self.name or ""
1518
+ def __str__(self) -> str:
1519
+ return f"{self.name}[{self.id}]"
856
1520
 
857
1521
 
858
1522
  event.listen(RestrictionArea, "after_insert", cache_invalidate_cb)
@@ -883,15 +1547,35 @@ interface_theme = Table(
883
1547
  )
884
1548
 
885
1549
 
886
- class Interface(Base):
1550
+ class Interface(Base): # type: ignore
1551
+ """The interface table representation."""
1552
+
887
1553
  __tablename__ = "interface"
888
1554
  __table_args__ = {"schema": _schema}
889
1555
  __c2cgeoform_config__ = {"duplicate": True}
890
1556
  __colanderalchemy_config__ = {"title": _("Interface"), "plural": _("Interfaces")}
891
1557
 
892
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
893
- name = Column(Unicode, info={"colanderalchemy": {"title": _("Name")}})
894
- description = Column(Unicode, info={"colanderalchemy": {"title": _("Description")}})
1558
+ id: Mapped[int] = mapped_column(
1559
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1560
+ )
1561
+ name: Mapped[str] = mapped_column(
1562
+ Unicode,
1563
+ info={
1564
+ "colanderalchemy": {
1565
+ "title": _("Name"),
1566
+ "description": _("The name of the interface, as used in request URL."),
1567
+ }
1568
+ },
1569
+ )
1570
+ description: Mapped[str | None] = mapped_column(
1571
+ Unicode,
1572
+ info={
1573
+ "colanderalchemy": {
1574
+ "title": _("Description"),
1575
+ "description": _("An optional description."),
1576
+ }
1577
+ },
1578
+ )
895
1579
 
896
1580
  # relationship with Layer and Theme
897
1581
  layers = relationship(
@@ -899,39 +1583,92 @@ class Interface(Base):
899
1583
  secondary=interface_layer,
900
1584
  cascade="save-update,merge,refresh-expire",
901
1585
  info={"colanderalchemy": {"title": _("Layers"), "exclude": True}, "c2cgeoform": {"duplicate": False}},
902
- backref=backref("interfaces", info={"colanderalchemy": {"title": _("Interfaces"), "exclude": True}}),
1586
+ backref=backref(
1587
+ "interfaces",
1588
+ info={
1589
+ "colanderalchemy": {
1590
+ "title": _("Interfaces"),
1591
+ "exclude": True,
1592
+ "description": _("Make it visible in the checked interfaces."),
1593
+ }
1594
+ },
1595
+ ),
903
1596
  )
904
1597
  theme = relationship(
905
1598
  "Theme",
906
1599
  secondary=interface_theme,
907
1600
  cascade="save-update,merge,refresh-expire",
908
1601
  info={"colanderalchemy": {"title": _("Themes"), "exclude": True}, "c2cgeoform": {"duplicate": False}},
909
- backref=backref("interfaces", info={"colanderalchemy": {"title": _("Interfaces"), "exclude": True}}),
1602
+ backref=backref(
1603
+ "interfaces",
1604
+ info={
1605
+ "colanderalchemy": {
1606
+ "title": _("Interfaces"),
1607
+ "description": _("Make it visible in the checked interfaces."),
1608
+ "exclude": True,
1609
+ }
1610
+ },
1611
+ ),
910
1612
  )
911
1613
 
912
1614
  def __init__(self, name: str = "", description: str = "") -> None:
913
1615
  self.name = name
914
1616
  self.description = description
915
1617
 
916
- def __str__(self) -> str: # pragma: no cover
917
- return self.name or ""
1618
+ def __str__(self) -> str:
1619
+ return f"{self.name}[{self.id}]"
1620
+
918
1621
 
1622
+ class Metadata(Base): # type: ignore
1623
+ """The metadata table representation."""
919
1624
 
920
- class Metadata(Base):
921
1625
  __tablename__ = "metadata"
922
1626
  __table_args__ = {"schema": _schema}
1627
+ __colanderalchemy_config__ = {
1628
+ "title": _("Metadata"),
1629
+ "plural": _("Metadatas"),
1630
+ }
923
1631
 
924
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
925
- name = Column(Unicode, info={"colanderalchemy": {"title": _("Name")}})
926
- value = Column(Unicode, info={"colanderalchemy": {"exclude": True}})
927
- description = Column(
928
- Unicode, info={"colanderalchemy": {"title": _("Description"), "widget": TextAreaWidget()}}
1632
+ id: Mapped[int] = mapped_column(
1633
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1634
+ )
1635
+ name: Mapped[str] = mapped_column(
1636
+ Unicode,
1637
+ info={
1638
+ "colanderalchemy": {
1639
+ "title": _("Name"),
1640
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1641
+ _("The type of <code>Metadata</code> we want to set.")
1642
+ ),
1643
+ }
1644
+ },
1645
+ )
1646
+ value: Mapped[str] = mapped_column(
1647
+ Unicode,
1648
+ nullable=True,
1649
+ info={
1650
+ "colanderalchemy": {
1651
+ "title": _("Value"),
1652
+ "exclude": True,
1653
+ "description": _("The value of the metadata entry."),
1654
+ }
1655
+ },
1656
+ )
1657
+ description: Mapped[str | None] = mapped_column(
1658
+ Unicode,
1659
+ info={
1660
+ "colanderalchemy": {
1661
+ "title": _("Description"),
1662
+ "widget": TextAreaWidget(),
1663
+ "description": _("An optional description."),
1664
+ }
1665
+ },
929
1666
  )
930
1667
 
931
- item_id = Column(
1668
+ item_id: Mapped[int] = mapped_column(
932
1669
  "item_id",
933
1670
  Integer,
934
- ForeignKey(_schema + ".treeitem.id"),
1671
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
935
1672
  nullable=False,
936
1673
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
937
1674
  )
@@ -942,17 +1679,49 @@ class Metadata(Base):
942
1679
  "metadatas",
943
1680
  cascade="save-update,merge,delete,delete-orphan,expunge",
944
1681
  order_by="Metadata.name",
945
- info={"colanderalchemy": {"title": _("Metadatas"), "exclude": True}},
1682
+ info={
1683
+ "colanderalchemy": {
1684
+ "title": _("Metadatas"),
1685
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1686
+ _(
1687
+ """
1688
+ <div class="help-block">
1689
+ <p>You can associate metadata to all theme elements (tree items).
1690
+ The purpose of this metadata is to trigger specific features, mainly UI features.
1691
+ Each metadata entry has the following attributes:</p>
1692
+ <p>The available names are configured in the <code>vars.yaml</code>
1693
+ files in <code>admin_interface/available_metadata</code>.</p>
1694
+ <p>To set a metadata entry, create or edit an entry in the Metadata view of the
1695
+ administration UI.
1696
+ Regarding effect on the referenced tree item on the client side,
1697
+ you will find an official description for each sort of metadata in the
1698
+ <code>GmfMetaData</code> definition in <code>themes.js</code>
1699
+ <a target="_blank" href="${url}">see ngeo documentation</a>.</p>
1700
+ <hr>
1701
+ </div>
1702
+ """,
1703
+ mapping={
1704
+ "url": (
1705
+ "https://camptocamp.github.io/ngeo/"
1706
+ f"{os.environ.get('MAJOR_VERSION', 'master')}"
1707
+ "/apidoc/interfaces/contribs_gmf_src_themes.GmfMetaData.html"
1708
+ )
1709
+ },
1710
+ )
1711
+ ),
1712
+ "exclude": True,
1713
+ }
1714
+ },
946
1715
  ),
947
1716
  )
948
1717
 
949
- def __init__(self, name: str = "", value: str = "", description: str = None) -> None:
1718
+ def __init__(self, name: str = "", value: str = "", description: str | None = None) -> None:
950
1719
  self.name = name
951
1720
  self.value = value
952
1721
  self.description = description
953
1722
 
954
- def __str__(self) -> str: # pragma: no cover
955
- return "{}: {}".format(self.name or "", self.value or "")
1723
+ def __str__(self) -> str:
1724
+ return f"{self.name}={self.value}[{self.id}]"
956
1725
 
957
1726
 
958
1727
  event.listen(Metadata, "after_insert", cache_invalidate_cb, propagate=True)
@@ -960,19 +1729,61 @@ event.listen(Metadata, "after_update", cache_invalidate_cb, propagate=True)
960
1729
  event.listen(Metadata, "after_delete", cache_invalidate_cb, propagate=True)
961
1730
 
962
1731
 
963
- class Dimension(Base):
1732
+ class Dimension(Base): # type: ignore
1733
+ """The dimension table representation."""
1734
+
964
1735
  __tablename__ = "dimension"
965
1736
  __table_args__ = {"schema": _schema}
1737
+ __colanderalchemy_config__ = {
1738
+ "title": _("Dimension"),
1739
+ "plural": _("Dimensions"),
1740
+ }
966
1741
 
967
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
968
- name = Column(Unicode, info={"colanderalchemy": {"title": _("Name")}})
969
- value = Column(Unicode, info={"colanderalchemy": {"title": _("Value")}})
970
- field = Column(Unicode, info={"colanderalchemy": {"title": _("Field")}})
971
- description = Column(
972
- Unicode, info={"colanderalchemy": {"title": _("Description"), "widget": TextAreaWidget()}}
1742
+ id: Mapped[int] = mapped_column(
1743
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1744
+ )
1745
+ name: Mapped[str] = mapped_column(
1746
+ Unicode,
1747
+ info={
1748
+ "colanderalchemy": {
1749
+ "title": _("Name"),
1750
+ "description": _("The name of the dimension as it will be sent in requests."),
1751
+ }
1752
+ },
1753
+ )
1754
+ value: Mapped[str] = mapped_column(
1755
+ Unicode,
1756
+ nullable=True,
1757
+ info={
1758
+ "colanderalchemy": {
1759
+ "title": _("Value"),
1760
+ "description": _("The default value for this dimension."),
1761
+ }
1762
+ },
1763
+ )
1764
+ field: Mapped[str | None] = mapped_column(
1765
+ Unicode,
1766
+ info={
1767
+ "colanderalchemy": {
1768
+ "title": _("Field"),
1769
+ "description": _(
1770
+ "The name of the field to use for filtering (leave empty when not using OGC filters)."
1771
+ ),
1772
+ }
1773
+ },
1774
+ )
1775
+ description: Mapped[str | None] = mapped_column(
1776
+ Unicode,
1777
+ info={
1778
+ "colanderalchemy": {
1779
+ "title": _("Description"),
1780
+ "description": _("An optional description."),
1781
+ "widget": TextAreaWidget(),
1782
+ }
1783
+ },
973
1784
  )
974
1785
 
975
- layer_id = Column(
1786
+ layer_id: Mapped[int] = mapped_column(
976
1787
  "layer_id",
977
1788
  Integer,
978
1789
  ForeignKey(_schema + ".layer.id"),
@@ -985,12 +1796,32 @@ class Dimension(Base):
985
1796
  backref=backref(
986
1797
  "dimensions",
987
1798
  cascade="save-update,merge,delete,delete-orphan,expunge",
988
- info={"colanderalchemy": {"title": _("Dimensions"), "exclude": True}},
1799
+ info={
1800
+ "colanderalchemy": {
1801
+ "title": _("Dimensions"),
1802
+ "exclude": True,
1803
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1804
+ _(
1805
+ """
1806
+ <div class="help-block">
1807
+ <p>The dimensions, if not provided default values are used.</p>
1808
+ <hr>
1809
+ </div>
1810
+ """
1811
+ )
1812
+ ),
1813
+ }
1814
+ },
989
1815
  ),
990
1816
  )
991
1817
 
992
1818
  def __init__(
993
- self, name: str = "", value: str = "", layer: str = None, field: str = None, description: str = None
1819
+ self,
1820
+ name: str = "",
1821
+ value: str = "",
1822
+ layer: str | None = None,
1823
+ field: str | None = None,
1824
+ description: str | None = None,
994
1825
  ) -> None:
995
1826
  self.name = name
996
1827
  self.value = value
@@ -999,5 +1830,102 @@ class Dimension(Base):
999
1830
  self.layer = layer
1000
1831
  self.description = description
1001
1832
 
1002
- def __str__(self) -> str: # pragma: no cover
1003
- return self.name or ""
1833
+ def __str__(self) -> str:
1834
+ return f"{self.name}={self.value}[{self.id}]"
1835
+
1836
+
1837
+ class LogAction(enum.Enum):
1838
+ """The log action enumeration."""
1839
+
1840
+ INSERT = enum.auto()
1841
+ UPDATE = enum.auto()
1842
+ DELETE = enum.auto()
1843
+ SYNCHRONIZE = enum.auto()
1844
+ CONVERT_TO_WMTS = enum.auto()
1845
+ CONVERT_TO_WMS = enum.auto()
1846
+
1847
+
1848
+ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1849
+ """The abstract log table representation."""
1850
+
1851
+ strict_attrs = True
1852
+ __colanderalchemy_config__ = {
1853
+ "title": _("Log"),
1854
+ "plural": _("Logs"),
1855
+ }
1856
+
1857
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, info={"colanderalchemy": {}})
1858
+ date: Mapped[datetime] = mapped_column(
1859
+ DateTime(timezone=True),
1860
+ nullable=False,
1861
+ info={
1862
+ "colanderalchemy": {
1863
+ "title": _("Date"),
1864
+ }
1865
+ },
1866
+ )
1867
+ action: Mapped[LogAction] = mapped_column(
1868
+ Enum(LogAction, native_enum=False),
1869
+ nullable=False,
1870
+ info={
1871
+ "colanderalchemy": {
1872
+ "title": _("Action"),
1873
+ }
1874
+ },
1875
+ )
1876
+ element_type: Mapped[str] = mapped_column(
1877
+ String(50),
1878
+ nullable=False,
1879
+ info={
1880
+ "colanderalchemy": {
1881
+ "title": _("Element type"),
1882
+ }
1883
+ },
1884
+ )
1885
+ element_id: Mapped[int] = mapped_column(
1886
+ Integer,
1887
+ nullable=False,
1888
+ info={
1889
+ "colanderalchemy": {
1890
+ "title": _("Element identifier"),
1891
+ }
1892
+ },
1893
+ )
1894
+ element_name: Mapped[str] = mapped_column(
1895
+ Unicode,
1896
+ nullable=False,
1897
+ info={
1898
+ "colanderalchemy": {
1899
+ "title": _("Element name"),
1900
+ }
1901
+ },
1902
+ )
1903
+ element_url_table: Mapped[str] = mapped_column(
1904
+ Unicode,
1905
+ nullable=False,
1906
+ info={
1907
+ "colanderalchemy": {
1908
+ "title": _("Table segment of the element URL"),
1909
+ }
1910
+ },
1911
+ )
1912
+ username: Mapped[str] = mapped_column(
1913
+ Unicode,
1914
+ nullable=False,
1915
+ info={
1916
+ "colanderalchemy": {
1917
+ "title": _("Username"),
1918
+ }
1919
+ },
1920
+ )
1921
+
1922
+
1923
+ class Log(AbstractLog):
1924
+ """The main log table representation."""
1925
+
1926
+ __tablename__ = "log"
1927
+ __table_args__ = {"schema": _schema}
1928
+ __mapper_args__ = {
1929
+ "polymorphic_identity": "main",
1930
+ "concrete": True,
1931
+ }