c2cgeoportal-commons 2.7.1.145__py3-none-any.whl → 2.9.0.350__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 (87) hide show
  1. c2cgeoportal_commons/__init__.py +1 -1
  2. c2cgeoportal_commons/alembic/env.py +18 -15
  3. c2cgeoportal_commons/alembic/main/028477929d13_add_technical_roles.py +3 -3
  4. c2cgeoportal_commons/alembic/main/04f05bfbb05e_remove_the_old_is_expanded_column.py +3 -1
  5. c2cgeoportal_commons/alembic/main/116b9b79fc4d_internal_and_external_layer_tables_.py +35 -39
  6. c2cgeoportal_commons/alembic/main/1418cb05921b_merge_1_6_and_master_branches.py +2 -4
  7. c2cgeoportal_commons/alembic/main/164ac0819a61_add_image_format_to_wmts_layer.py +2 -2
  8. c2cgeoportal_commons/alembic/main/166ff2dcc48d_create_database.py +5 -5
  9. c2cgeoportal_commons/alembic/main/16e43f8c0330_remove_old_metadata_column.py +2 -2
  10. c2cgeoportal_commons/alembic/main/1d5d4abfebd1_add_restricted_theme.py +3 -3
  11. c2cgeoportal_commons/alembic/main/1de20166b274_remove_v1_artifacts.py +2 -2
  12. c2cgeoportal_commons/alembic/main/20137477bd02_update_icons_url.py +2 -2
  13. c2cgeoportal_commons/alembic/main/21f11066f8ec_trigger_on_role_updates_user_in_static.py +14 -16
  14. c2cgeoportal_commons/alembic/main/22e6dfb556de_add_description_tree_.py +2 -2
  15. c2cgeoportal_commons/alembic/main/29f2a32859ec_merge_1_6_and_master_branches.py +2 -4
  16. c2cgeoportal_commons/alembic/main/2b8ed8c1df94_set_layergroup_treeitem_is_as_a_primary_.py +2 -2
  17. c2cgeoportal_commons/alembic/main/2e57710fecfe_update_the_ogc_server_for_ogc_api.py +77 -0
  18. c2cgeoportal_commons/alembic/main/32527659d57b_move_exclude_properties_from_layerv1_to_.py +8 -8
  19. c2cgeoportal_commons/alembic/main/32b21aa1d0ed_merge_e004f76e951a_and_e004f76e951a_.py +2 -4
  20. c2cgeoportal_commons/alembic/main/338b57593823_remove_trigger_on_role_name_change.py +16 -18
  21. c2cgeoportal_commons/alembic/main/415746eb9f6_changes_for_v2.py +31 -31
  22. c2cgeoportal_commons/alembic/main/44c91d82d419_add_table_log.py +72 -0
  23. c2cgeoportal_commons/alembic/main/5109242131ce_add_column_time_widget.py +4 -3
  24. c2cgeoportal_commons/alembic/main/52916d8fde8b_add_sql_fields_to_vector_tiles.py +5 -3
  25. c2cgeoportal_commons/alembic/main/53ba1a68d5fe_add_theme_to_fulltextsearch.py +2 -2
  26. c2cgeoportal_commons/alembic/main/54645a535ad6_add_ordering_in_relation.py +10 -10
  27. c2cgeoportal_commons/alembic/main/56dc90838d90_fix_removing_layerv1.py +2 -2
  28. c2cgeoportal_commons/alembic/main/596ba21e3833_separate_local_internal.py +6 -10
  29. c2cgeoportal_commons/alembic/main/678f88c7ad5e_add_vector_tiles_layers_table.py +2 -2
  30. c2cgeoportal_commons/alembic/main/6a412d9437b1_rename_serverogc_to_ogcserver.py +4 -4
  31. c2cgeoportal_commons/alembic/main/6d87fdad275a_convert_metadata_to_the_right_case.py +2 -2
  32. c2cgeoportal_commons/alembic/main/7530011a66a7_trigger_on_role_updates_user_in_static.py +14 -16
  33. c2cgeoportal_commons/alembic/main/78fd093c8393_add_api_s_intrfaces.py +30 -49
  34. c2cgeoportal_commons/alembic/main/7d271f4527cd_add_layer_column_in_layerv1_table.py +2 -2
  35. c2cgeoportal_commons/alembic/main/809650bd04c3_add_dimension_field.py +2 -2
  36. c2cgeoportal_commons/alembic/main/8117bb9bba16_use_dimension_on_all_the_layers.py +4 -4
  37. c2cgeoportal_commons/alembic/main/87f8330ed64e_add_missing_delete_cascades.py +2 -2
  38. c2cgeoportal_commons/alembic/main/9268a1dffac0_add_trigger_to_be_able_to_correctly_.py +2 -2
  39. c2cgeoportal_commons/alembic/main/94db7e7e5b21_merge_2_2_and_master_branches.py +2 -4
  40. c2cgeoportal_commons/alembic/main/951ff84bd8ec_be_able_to_delete_a_wms_layer_in_sql.py +2 -2
  41. c2cgeoportal_commons/alembic/main/a00109812f89_add_field_layer_wms_valid.py +4 -4
  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 +2 -4
  44. c2cgeoportal_commons/alembic/main/b60f2a505f42_remame_uimetadata_to_metadata.py +2 -2
  45. c2cgeoportal_commons/alembic/main/b6b09f414fe8_sync_model_database.py +174 -0
  46. c2cgeoportal_commons/alembic/main/c75124553bf3_remove_deprecated_columns.py +2 -2
  47. c2cgeoportal_commons/alembic/main/d48a63b348f1_change_mapserver_url_for_docker.py +6 -10
  48. c2cgeoportal_commons/alembic/main/d8ef99bc227e_be_able_to_delete_a_linked_functionality.py +2 -2
  49. c2cgeoportal_commons/alembic/main/daf738d5bae4_merge_2_0_and_master_branches.py +2 -4
  50. c2cgeoportal_commons/alembic/main/dba87f2647f9_merge_2_2_on_2_3.py +2 -4
  51. c2cgeoportal_commons/alembic/main/e004f76e951a_add_missing_not_null.py +6 -22
  52. c2cgeoportal_commons/alembic/main/e7e03dedade3_put_the_default_wms_server_in_the_.py +6 -10
  53. c2cgeoportal_commons/alembic/main/e85afd327ab3_cascade_deletes_to_tsearch.py +2 -2
  54. c2cgeoportal_commons/alembic/main/ec82a8906649_add_missing_on_delete_cascade_on_layer_.py +2 -2
  55. c2cgeoportal_commons/alembic/main/ee25d267bf46_main_interface_desktop.py +4 -10
  56. c2cgeoportal_commons/alembic/main/eeb345672454_merge_2_4_and_master_branches.py +2 -4
  57. c2cgeoportal_commons/alembic/static/0c640a58a09a_add_opt_key_column.py +2 -2
  58. c2cgeoportal_commons/alembic/static/107b81f5b9fe_add_missing_delete_cascades.py +2 -2
  59. c2cgeoportal_commons/alembic/static/1857owc78a07_add_last_login_and_expiration_date.py +2 -2
  60. c2cgeoportal_commons/alembic/static/1da396a88908_move_user_table_to_static_schema.py +6 -10
  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 +2 -2
  63. c2cgeoportal_commons/alembic/static/44c91d82d419_add_table_log.py +72 -0
  64. c2cgeoportal_commons/alembic/static/53d671b17b20_add_timezone_on_datetime_fields.py +2 -2
  65. c2cgeoportal_commons/alembic/static/5472fbc19f39_add_temp_password_column.py +2 -2
  66. c2cgeoportal_commons/alembic/static/76d72fb3fcb9_add_oauth2_pkce.py +70 -0
  67. c2cgeoportal_commons/alembic/static/7ef947f30f20_add_oauth2_tables.py +3 -1
  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 +8 -14
  71. c2cgeoportal_commons/alembic/static/bd029dbfc11a_fill_tech_data_column.py +2 -2
  72. c2cgeoportal_commons/lib/email_.py +15 -19
  73. c2cgeoportal_commons/lib/literal.py +3 -3
  74. c2cgeoportal_commons/lib/url.py +15 -16
  75. c2cgeoportal_commons/lib/validators.py +1 -3
  76. c2cgeoportal_commons/models/__init__.py +16 -9
  77. c2cgeoportal_commons/models/main.py +415 -226
  78. c2cgeoportal_commons/models/sqlalchemy.py +13 -13
  79. c2cgeoportal_commons/models/static.py +162 -71
  80. c2cgeoportal_commons/testing/__init__.py +12 -7
  81. c2cgeoportal_commons/testing/initializedb.py +7 -6
  82. {c2cgeoportal_commons-2.7.1.145.dist-info → c2cgeoportal_commons-2.9.0.350.dist-info}/METADATA +4 -5
  83. c2cgeoportal_commons-2.9.0.350.dist-info/RECORD +89 -0
  84. {c2cgeoportal_commons-2.7.1.145.dist-info → c2cgeoportal_commons-2.9.0.350.dist-info}/WHEEL +1 -1
  85. tests/conftest.py +1 -1
  86. c2cgeoportal_commons-2.7.1.145.dist-info/RECORD +0 -80
  87. {c2cgeoportal_commons-2.7.1.145.dist-info → c2cgeoportal_commons-2.9.0.350.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2021, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -26,10 +26,12 @@
26
26
  # either expressed or implied, of the FreeBSD Project.
27
27
 
28
28
 
29
+ import enum
29
30
  import logging
30
31
  import os
31
32
  import re
32
- from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
33
+ from datetime import datetime
34
+ from typing import Any, Literal, Optional, cast, get_args
33
35
 
34
36
  import pyramid.request
35
37
  import sqlalchemy.orm.base
@@ -38,24 +40,29 @@ from geoalchemy2 import Geometry
38
40
  from geoalchemy2.shape import to_shape
39
41
  from papyrus.geo_interface import GeoInterface
40
42
  from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, event
41
- 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
42
45
  from sqlalchemy.schema import Index
43
- from sqlalchemy.types import Boolean, Enum, Integer, String, Unicode
46
+ from sqlalchemy.types import Boolean, DateTime, Enum, Integer, String, Unicode
44
47
 
45
- from c2cgeoportal_commons.lib.literal import Literal
48
+ import c2cgeoportal_commons.lib.literal
46
49
  from c2cgeoportal_commons.lib.url import get_url2
47
50
  from c2cgeoportal_commons.models import Base, _, cache_invalidate_cb
48
51
  from c2cgeoportal_commons.models.sqlalchemy import JSONEncodedDict, TsVector
49
52
 
50
53
  try:
54
+ import colander
51
55
  from c2cgeoform import default_map_settings
52
56
  from c2cgeoform.ext.colander_ext import Geometry as ColanderGeometry
53
57
  from c2cgeoform.ext.deform_ext import MapWidget, RelationSelect2Widget
54
58
  from colander import drop
55
59
  from deform.widget import CheckboxWidget, HiddenWidget, SelectWidget, TextAreaWidget, TextInputWidget
60
+
61
+ colander_null = colander.null
56
62
  except ModuleNotFoundError:
57
- drop = None
63
+ drop = None # pylint: disable=invalid-name
58
64
  default_map_settings = {"srid": 3857, "view": {"projection": "EPSG:3857"}}
65
+ colander_null = None # pylint: disable=invalid-name
59
66
 
60
67
  class GenericClass:
61
68
  """Fallback class implementation."""
@@ -65,11 +72,11 @@ except ModuleNotFoundError:
65
72
 
66
73
  CheckboxWidget = GenericClass
67
74
  HiddenWidget = GenericClass
68
- MapWidget = GenericClass
75
+ MapWidget = GenericClass # type: ignore[misc,assignment]
69
76
  SelectWidget = GenericClass
70
77
  TextAreaWidget = GenericClass
71
- ColanderGeometry = GenericClass
72
- RelationSelect2Widget = GenericClass
78
+ ColanderGeometry = GenericClass # type: ignore[misc,assignment]
79
+ RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
73
80
  TextInputWidget = GenericClass
74
81
 
75
82
 
@@ -84,14 +91,16 @@ if os.environ.get("DEVELOPMENT", "0") == "1":
84
91
  # information
85
92
  sqlalchemy.orm.base.state_str = state_str
86
93
 
87
- LOG = logging.getLogger(__name__)
94
+ _LOG = logging.getLogger(__name__)
88
95
 
89
96
  _schema: str = config["schema"] or "main"
90
97
  _srid: int = cast(int, config["srid"]) or 3857
91
98
 
92
99
  # Set some default values for the admin interface
93
- _admin_config: Dict[str, Any] = config.get_config().get("admin_interface", {})
94
- _map_config: Dict[str, Any] = {**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", {})}
95
104
  view_srid_match = re.match(r"EPSG:(\d+)", _map_config["view"]["projection"])
96
105
  if "map_srid" not in _admin_config and view_srid_match is not None:
97
106
  _admin_config["map_srid"] = view_srid_match.group(1)
@@ -101,22 +110,30 @@ class FullTextSearch(GeoInterface, Base): # type: ignore
101
110
  """The tsearch table representation."""
102
111
 
103
112
  __tablename__ = "tsearch"
104
- __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
+ )
105
118
 
106
- id = Column(Integer, primary_key=True)
107
- label = Column(Unicode)
108
- layer_name = Column(Unicode)
109
- 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
+ )
110
125
  role = relationship("Role")
111
- 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
+ )
112
129
  interface = relationship("Interface")
113
- lang = Column(String(2), nullable=True)
114
- public = Column(Boolean, server_default="true")
115
- ts = Column(TsVector)
116
- the_geom = Column(Geometry("GEOMETRY", srid=_srid))
117
- params = Column(JSONEncodedDict, nullable=True)
118
- actions = Column(JSONEncodedDict, nullable=True)
119
- 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")
120
137
 
121
138
  def __str__(self) -> str:
122
139
  return f"{self.label}[{self.id}]"
@@ -131,8 +148,10 @@ class Functionality(Base): # type: ignore
131
148
 
132
149
  __c2cgeoform_config__ = {"duplicate": True}
133
150
 
134
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
135
- name = Column(
151
+ id: Mapped[int] = mapped_column(
152
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
153
+ )
154
+ name: Mapped[str] = mapped_column(
136
155
  Unicode,
137
156
  nullable=False,
138
157
  info={
@@ -152,7 +171,7 @@ class Functionality(Base): # type: ignore
152
171
  }
153
172
  },
154
173
  )
155
- description = Column(
174
+ description: Mapped[str | None] = mapped_column(
156
175
  Unicode,
157
176
  info={
158
177
  "colanderalchemy": {
@@ -161,7 +180,7 @@ class Functionality(Base): # type: ignore
161
180
  }
162
181
  },
163
182
  )
164
- value = Column(
183
+ value: Mapped[str] = mapped_column(
165
184
  Unicode,
166
185
  nullable=False,
167
186
  info={
@@ -222,8 +241,10 @@ class Role(Base): # type: ignore
222
241
  __colanderalchemy_config__ = {"title": _("Role"), "plural": _("Roles")}
223
242
  __c2cgeoform_config__ = {"duplicate": True}
224
243
 
225
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
226
- name = Column(
244
+ id: Mapped[int] = mapped_column(
245
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
246
+ )
247
+ name: Mapped[str] = mapped_column(
227
248
  Unicode,
228
249
  unique=True,
229
250
  nullable=False,
@@ -234,7 +255,7 @@ class Role(Base): # type: ignore
234
255
  }
235
256
  },
236
257
  )
237
- description = Column(
258
+ description: Mapped[str | None] = mapped_column(
238
259
  Unicode,
239
260
  info={
240
261
  "colanderalchemy": {
@@ -243,8 +264,8 @@ class Role(Base): # type: ignore
243
264
  }
244
265
  },
245
266
  )
246
- extent = Column(
247
- Geometry("POLYGON", srid=_srid),
267
+ extent = mapped_column(
268
+ Geometry("POLYGON", srid=_srid, spatial_index=False),
248
269
  info={
249
270
  "colanderalchemy": {
250
271
  "title": _("Extent"),
@@ -273,8 +294,8 @@ class Role(Base): # type: ignore
273
294
  self,
274
295
  name: str = "",
275
296
  description: str = "",
276
- functionalities: Optional[List[Functionality]] = None,
277
- extent: Geometry = None,
297
+ functionalities: list[Functionality] | None = None,
298
+ extent: Geometry | None = None,
278
299
  ) -> None:
279
300
  if functionalities is None:
280
301
  functionalities = []
@@ -287,10 +308,10 @@ class Role(Base): # type: ignore
287
308
  return f"{self.name}[{self.id}]>"
288
309
 
289
310
  @property
290
- def bounds(self) -> Optional[Tuple[float, float, float, float]]: # TODO
311
+ def bounds(self) -> tuple[float, float, float, float] | None: # TODO
291
312
  if self.extent is None:
292
313
  return None
293
- return cast(Tuple[float, float, float, float], to_shape(self.extent).bounds)
314
+ return cast(tuple[float, float, float, float], to_shape(self.extent).bounds)
294
315
 
295
316
 
296
317
  event.listen(Role.functionalities, "set", cache_invalidate_cb)
@@ -302,15 +323,17 @@ class TreeItem(Base): # type: ignore
302
323
  """The treeitem table representation."""
303
324
 
304
325
  __tablename__ = "treeitem"
305
- __table_args__: Union[Tuple[Any, ...], Dict[str, Any]] = (
326
+ __table_args__: tuple[Any, ...] | dict[str, Any] = (
306
327
  UniqueConstraint("type", "name"),
307
328
  {"schema": _schema},
308
329
  )
309
- 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
+ )
310
333
  __mapper_args__ = {"polymorphic_on": item_type}
311
334
 
312
- id = Column(Integer, primary_key=True)
313
- name = Column(
335
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
336
+ name: Mapped[str] = mapped_column(
314
337
  Unicode,
315
338
  nullable=False,
316
339
  info={
@@ -325,7 +348,7 @@ class TreeItem(Base): # type: ignore
325
348
  }
326
349
  },
327
350
  )
328
- description = Column(
351
+ description: Mapped[str | None] = mapped_column(
329
352
  Unicode,
330
353
  info={
331
354
  "colanderalchemy": {
@@ -337,7 +360,7 @@ class TreeItem(Base): # type: ignore
337
360
 
338
361
  @property
339
362
  # Better: def parents(self) -> List[TreeGroup]:
340
- def parents(self) -> List["TreeItem"]:
363
+ def parents(self) -> list["TreeItem"]:
341
364
  return [c.treegroup for c in self.parents_relation]
342
365
 
343
366
  def is_in_interface(self, name: str) -> bool:
@@ -350,7 +373,7 @@ class TreeItem(Base): # type: ignore
350
373
 
351
374
  return False
352
375
 
353
- def get_metadata(self, name: str) -> List["Metadata"]:
376
+ def get_metadata(self, name: str) -> list["Metadata"]:
354
377
  return [metadata for metadata in self.metadatas if metadata.name == name]
355
378
 
356
379
  def __init__(self, name: str = "") -> None:
@@ -373,11 +396,13 @@ class LayergroupTreeitem(Base): # type: ignore
373
396
  __table_args__ = {"schema": _schema}
374
397
 
375
398
  # required by formalchemy
376
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
377
- description = Column(Unicode, info={"colanderalchemy": {"exclude": True}})
378
- 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(
379
404
  Integer,
380
- ForeignKey(_schema + ".treegroup.id"),
405
+ ForeignKey(_schema + ".treegroup.id", name="treegroup_id_fkey"),
381
406
  nullable=False,
382
407
  info={"colanderalchemy": {"exclude": True}},
383
408
  )
@@ -392,9 +417,9 @@ class LayergroupTreeitem(Base): # type: ignore
392
417
  primaryjoin="LayergroupTreeitem.treegroup_id==TreeGroup.id",
393
418
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
394
419
  )
395
- treeitem_id = Column(
420
+ treeitem_id: Mapped[int] = mapped_column(
396
421
  Integer,
397
- ForeignKey(_schema + ".treeitem.id"),
422
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
398
423
  nullable=False,
399
424
  info={"colanderalchemy": {"widget": HiddenWidget()}},
400
425
  )
@@ -411,10 +436,10 @@ class LayergroupTreeitem(Base): # type: ignore
411
436
  primaryjoin="LayergroupTreeitem.treeitem_id==TreeItem.id",
412
437
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
413
438
  )
414
- ordering = Column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
439
+ ordering: Mapped[int] = mapped_column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
415
440
 
416
441
  def __init__(
417
- self, group: Optional["TreeGroup"] = None, item: Optional[TreeItem] = None, ordering: int = 0
442
+ self, group: Optional["TreeGroup"] = None, item: TreeItem | None = None, ordering: int = 0
418
443
  ) -> None:
419
444
  self.treegroup = group
420
445
  self.treeitem = item
@@ -434,14 +459,18 @@ class TreeGroup(TreeItem):
434
459
 
435
460
  __tablename__ = "treegroup"
436
461
  __table_args__ = {"schema": _schema}
437
- __mapper_args__ = {"polymorphic_identity": "treegroup"} # needed for _identity_class
462
+ __mapper_args__ = {"polymorphic_identity": "treegroup"} # type: ignore[dict-item] # needed for _identity_class
438
463
 
439
- 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
+ )
440
469
 
441
- def _get_children(self) -> List[TreeItem]:
470
+ def _get_children(self) -> list[TreeItem]:
442
471
  return [c.treeitem for c in self.children_relation]
443
472
 
444
- def _set_children(self, children: List[TreeItem], order: bool = False) -> None:
473
+ def _set_children(self, children: list[TreeItem], order: bool = False) -> None:
445
474
  """
446
475
  Set the current TreeGroup children TreeItem instances.
447
476
 
@@ -487,7 +516,7 @@ class LayerGroup(TreeGroup):
487
516
  __colanderalchemy_config__ = {
488
517
  "title": _("Layers group"),
489
518
  "plural": _("Layers groups"),
490
- "description": Literal(
519
+ "description": c2cgeoportal_commons.lib.literal.Literal(
491
520
  _(
492
521
  """
493
522
  <div class="help-block">
@@ -500,12 +529,12 @@ class LayerGroup(TreeGroup):
500
529
  )
501
530
  ),
502
531
  }
503
- __mapper_args__ = {"polymorphic_identity": "group"}
532
+ __mapper_args__ = {"polymorphic_identity": "group"} # type: ignore[dict-item]
504
533
  __c2cgeoform_config__ = {"duplicate": True}
505
534
 
506
- id = Column(
535
+ id: Mapped[int] = mapped_column(
507
536
  Integer,
508
- ForeignKey(_schema + ".treegroup.id"),
537
+ ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
509
538
  primary_key=True,
510
539
  info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
511
540
  )
@@ -530,19 +559,19 @@ class Theme(TreeGroup):
530
559
  __tablename__ = "theme"
531
560
  __table_args__ = {"schema": _schema}
532
561
  __colanderalchemy_config__ = {"title": _("Theme"), "plural": _("Themes")}
533
- __mapper_args__ = {"polymorphic_identity": "theme"}
562
+ __mapper_args__ = {"polymorphic_identity": "theme"} # type: ignore[dict-item]
534
563
  __c2cgeoform_config__ = {"duplicate": True}
535
564
 
536
- id = Column(
565
+ id: Mapped[int] = mapped_column(
537
566
  Integer,
538
- ForeignKey(_schema + ".treegroup.id"),
567
+ ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
539
568
  primary_key=True,
540
569
  info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
541
570
  )
542
- ordering = Column(
571
+ ordering: Mapped[int] = mapped_column(
543
572
  Integer, nullable=False, info={"colanderalchemy": {"title": _("Order"), "widget": HiddenWidget()}}
544
573
  )
545
- public = Column(
574
+ public: Mapped[bool] = mapped_column(
546
575
  Boolean,
547
576
  default=True,
548
577
  nullable=False,
@@ -553,12 +582,14 @@ class Theme(TreeGroup):
553
582
  }
554
583
  },
555
584
  )
556
- icon = Column(
585
+ icon: Mapped[str] = mapped_column(
557
586
  Unicode,
587
+ nullable=True,
558
588
  info={
559
589
  "colanderalchemy": {
560
590
  "title": _("Icon"),
561
591
  "description": _("The icon URL."),
592
+ "missing": "",
562
593
  }
563
594
  },
564
595
  )
@@ -607,15 +638,15 @@ class Layer(TreeItem):
607
638
 
608
639
  __tablename__ = "layer"
609
640
  __table_args__ = {"schema": _schema}
610
- __mapper_args__ = {"polymorphic_identity": "layer"} # needed for _identity_class
641
+ __mapper_args__ = {"polymorphic_identity": "layer"} # type: ignore[dict-item] # needed for _identity_class
611
642
 
612
- id = Column(
643
+ id: Mapped[int] = mapped_column(
613
644
  Integer,
614
- ForeignKey(_schema + ".treeitem.id"),
645
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
615
646
  primary_key=True,
616
647
  info={"colanderalchemy": {"widget": HiddenWidget()}},
617
648
  )
618
- public = Column(
649
+ public: Mapped[bool] = mapped_column(
619
650
  Boolean,
620
651
  default=True,
621
652
  info={
@@ -625,7 +656,7 @@ class Layer(TreeItem):
625
656
  }
626
657
  },
627
658
  )
628
- geo_table = Column(
659
+ geo_table: Mapped[str | None] = mapped_column(
629
660
  Unicode,
630
661
  info={
631
662
  "colanderalchemy": {
@@ -634,18 +665,22 @@ class Layer(TreeItem):
634
665
  }
635
666
  },
636
667
  )
637
- exclude_properties = Column(
668
+ exclude_properties: Mapped[str] = mapped_column(
638
669
  Unicode,
670
+ nullable=True,
639
671
  info={
640
672
  "colanderalchemy": {
641
673
  "title": _("Exclude properties"),
642
- "description": _(
643
- """
674
+ "description": c2cgeoportal_commons.lib.literal.Literal(
675
+ _(
676
+ """
644
677
  The list of attributes (database columns) that should not appear in
645
678
  the editing form so that they cannot be modified by the end user.
646
- For enumerable attributes (foreign key), the column name should end with '_id'.
679
+ For enumerable attributes (foreign key), the column name should end with <code>_id</code>.
647
680
  """
681
+ )
648
682
  ),
683
+ "missing": "",
649
684
  }
650
685
  },
651
686
  )
@@ -658,19 +693,27 @@ class Layer(TreeItem):
658
693
  class DimensionLayer(Layer):
659
694
  """The intermediate class for the leyser with dimension."""
660
695
 
661
- __mapper_args__ = {"polymorphic_identity": "dimensionlayer"} # needed for _identity_class
696
+ __mapper_args__ = {"polymorphic_identity": "dimensionlayer"} # type: ignore[dict-item] # needed for _identity_class
662
697
 
663
698
 
664
- OGCSERVER_TYPE_MAPSERVER = "mapserver"
665
- OGCSERVER_TYPE_QGISSERVER = "qgisserver"
666
- OGCSERVER_TYPE_GEOSERVER = "geoserver"
667
- OGCSERVER_TYPE_ARCGIS = "arcgis"
668
- OGCSERVER_TYPE_OTHER = "other"
699
+ OGCServerType = Literal["mapserver", "qgisserver", "geoserver", "arcgis", "other"]
700
+ OGCSERVER_TYPE_MAPSERVER: OGCServerType = "mapserver"
701
+ OGCSERVER_TYPE_QGISSERVER: OGCServerType = "qgisserver"
702
+ OGCSERVER_TYPE_GEOSERVER: OGCServerType = "geoserver"
703
+ OGCSERVER_TYPE_ARCGIS: OGCServerType = "arcgis"
704
+ OGCSERVER_TYPE_OTHER: OGCServerType = "other"
669
705
 
670
- OGCSERVER_AUTH_NOAUTH = "No auth"
671
- OGCSERVER_AUTH_STANDARD = "Standard auth"
672
- OGCSERVER_AUTH_GEOSERVER = "Geoserver auth"
673
- OGCSERVER_AUTH_PROXY = "Proxy"
706
+
707
+ OGCServerAuth = Literal["No auth", "Standard auth", "Geoserver auth", "Proxy"]
708
+ OGCSERVER_AUTH_NOAUTH: OGCServerAuth = "No auth"
709
+ OGCSERVER_AUTH_STANDARD: OGCServerAuth = "Standard auth"
710
+ OGCSERVER_AUTH_GEOSERVER: OGCServerAuth = "Geoserver auth"
711
+ OGCSERVER_AUTH_PROXY: OGCServerAuth = "Proxy"
712
+
713
+
714
+ ImageType = Literal["image/jpeg", "image/png", "image/webp"]
715
+ TimeMode = Literal["disabled", "value", "range"]
716
+ TimeWidget = Literal["slider", "datepicker"]
674
717
 
675
718
 
676
719
  class OGCServer(Base): # type: ignore
@@ -681,7 +724,7 @@ class OGCServer(Base): # type: ignore
681
724
  __colanderalchemy_config__ = {
682
725
  "title": _("OGC Server"),
683
726
  "plural": _("OGC Servers"),
684
- "description": Literal(
727
+ "description": c2cgeoportal_commons.lib.literal.Literal(
685
728
  _(
686
729
  """
687
730
  <div class="help-block">
@@ -696,55 +739,58 @@ class OGCServer(Base): # type: ignore
696
739
  ),
697
740
  }
698
741
  __c2cgeoform_config__ = {"duplicate": True}
699
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
700
- name = Column(
742
+ id: Mapped[int] = mapped_column(
743
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
744
+ )
745
+ name: Mapped[str] = mapped_column(
701
746
  Unicode,
702
747
  nullable=False,
703
748
  unique=True,
704
749
  info={
705
750
  "colanderalchemy": {
706
751
  "title": _("Name"),
707
- "description": _("The name of the OGC Server"),
752
+ "description": c2cgeoportal_commons.lib.literal.Literal(
753
+ _(
754
+ "The name of the OGC Server should contain only unaccentuated letters, numbers and _. "
755
+ "When you rename it, do not forget to update the <code>ogcServer</code> metadata on the "
756
+ "WMTS and COG layers."
757
+ )
758
+ ),
708
759
  }
709
760
  },
710
761
  )
711
- description = Column(
762
+ description: Mapped[str | None] = mapped_column(
712
763
  Unicode,
713
764
  info={
714
765
  "colanderalchemy": {
715
766
  "title": _("Description"),
716
- "description": _("A description"),
767
+ "description": _("An optional description."),
717
768
  }
718
769
  },
719
770
  )
720
- url = Column(
771
+ url: Mapped[str] = mapped_column(
721
772
  Unicode,
722
773
  nullable=False,
723
774
  info={
724
775
  "colanderalchemy": {
725
776
  "title": _("Basic URL"),
726
- "description": _("The server URL"),
777
+ "description": _("The server URL."),
727
778
  }
728
779
  },
729
780
  )
730
- url_wfs = Column(
781
+ url_wfs: Mapped[str | None] = mapped_column(
731
782
  Unicode,
732
783
  info={
733
784
  "colanderalchemy": {
734
785
  "title": _("WFS URL"),
735
- "description": _("The WFS server URL. If empty, the ``Basic URL`` is used."),
786
+ "description": c2cgeoportal_commons.lib.literal.Literal(
787
+ _("The WFS server URL. If empty, the <code>Basic URL</code> is used.")
788
+ ),
736
789
  }
737
790
  },
738
791
  )
739
- type = Column(
740
- Enum(
741
- OGCSERVER_TYPE_MAPSERVER,
742
- OGCSERVER_TYPE_QGISSERVER,
743
- OGCSERVER_TYPE_GEOSERVER,
744
- OGCSERVER_TYPE_ARCGIS,
745
- OGCSERVER_TYPE_OTHER,
746
- native_enum=False,
747
- ),
792
+ type: Mapped[OGCServerType] = mapped_column(
793
+ Enum(*get_args(OGCServerType), native_enum=False),
748
794
  nullable=False,
749
795
  info={
750
796
  "colanderalchemy": {
@@ -752,56 +798,39 @@ class OGCServer(Base): # type: ignore
752
798
  "description": _(
753
799
  "The server type which is used to know which custom attribute will be used."
754
800
  ),
755
- "widget": SelectWidget(
756
- values=(
757
- (OGCSERVER_TYPE_MAPSERVER, OGCSERVER_TYPE_MAPSERVER),
758
- (OGCSERVER_TYPE_QGISSERVER, OGCSERVER_TYPE_QGISSERVER),
759
- (OGCSERVER_TYPE_GEOSERVER, OGCSERVER_TYPE_GEOSERVER),
760
- (OGCSERVER_TYPE_ARCGIS, OGCSERVER_TYPE_ARCGIS),
761
- (OGCSERVER_TYPE_OTHER, OGCSERVER_TYPE_OTHER),
762
- )
763
- ),
801
+ "widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerType))),
764
802
  }
765
803
  },
766
804
  )
767
- image_type = Column(
768
- Enum("image/jpeg", "image/png", native_enum=False),
805
+ image_type: Mapped[ImageType] = mapped_column(
806
+ Enum(*get_args(ImageType), native_enum=False),
769
807
  nullable=False,
770
808
  info={
771
809
  "colanderalchemy": {
772
810
  "title": _("Image type"),
773
- "description": _("The MIME type of the images (e.g.: ``image/png``)."),
774
- "widget": SelectWidget(values=(("image/jpeg", "image/jpeg"), ("image/png", "image/png"))),
811
+ "description": c2cgeoportal_commons.lib.literal.Literal(
812
+ _(
813
+ "The MIME type of the images (e.g.: <code>image/png</code>, <code>image/webp</code> is experimental)."
814
+ )
815
+ ),
816
+ "widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
775
817
  "column": 2,
776
818
  }
777
819
  },
778
820
  )
779
- auth = Column(
780
- Enum(
781
- OGCSERVER_AUTH_NOAUTH,
782
- OGCSERVER_AUTH_STANDARD,
783
- OGCSERVER_AUTH_GEOSERVER,
784
- OGCSERVER_AUTH_PROXY,
785
- native_enum=False,
786
- ),
821
+ auth: Mapped[OGCServerAuth] = mapped_column(
822
+ Enum(*get_args(OGCServerAuth), native_enum=False),
787
823
  nullable=False,
788
824
  info={
789
825
  "colanderalchemy": {
790
826
  "title": _("Authentication type"),
791
827
  "description": "The kind of authentication to use.",
792
- "widget": SelectWidget(
793
- values=(
794
- (OGCSERVER_AUTH_NOAUTH, OGCSERVER_AUTH_NOAUTH),
795
- (OGCSERVER_AUTH_STANDARD, OGCSERVER_AUTH_STANDARD),
796
- (OGCSERVER_AUTH_GEOSERVER, OGCSERVER_AUTH_GEOSERVER),
797
- (OGCSERVER_AUTH_PROXY, OGCSERVER_AUTH_PROXY),
798
- )
799
- ),
828
+ "widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerAuth))),
800
829
  "column": 2,
801
830
  }
802
831
  },
803
832
  )
804
- wfs_support = Column(
833
+ wfs_support: Mapped[bool] = mapped_column(
805
834
  Boolean,
806
835
  info={
807
836
  "colanderalchemy": {
@@ -811,7 +840,7 @@ class OGCServer(Base): # type: ignore
811
840
  }
812
841
  },
813
842
  )
814
- is_single_tile = Column(
843
+ is_single_tile: Mapped[bool] = mapped_column(
815
844
  Boolean,
816
845
  info={
817
846
  "colanderalchemy": {
@@ -825,12 +854,12 @@ class OGCServer(Base): # type: ignore
825
854
  def __init__(
826
855
  self,
827
856
  name: str = "",
828
- description: Optional[str] = None,
857
+ description: str | None = None,
829
858
  url: str = "https://wms.example.com",
830
- url_wfs: Optional[str] = None,
831
- type_: str = "mapserver",
832
- image_type: str = "image/png",
833
- auth: str = "Standard auth",
859
+ url_wfs: str | None = None,
860
+ type_: OGCServerType = OGCSERVER_TYPE_MAPSERVER,
861
+ image_type: ImageType = "image/png",
862
+ auth: OGCServerAuth = OGCSERVER_AUTH_STANDARD,
834
863
  wfs_support: bool = True,
835
864
  is_single_tile: bool = False,
836
865
  ) -> None:
@@ -848,23 +877,18 @@ class OGCServer(Base): # type: ignore
848
877
  return self.name or ""
849
878
 
850
879
  def url_description(self, request: pyramid.request.Request) -> str:
851
- errors: Set[str] = set()
880
+ errors: set[str] = set()
852
881
  url = get_url2(self.name, self.url, request, errors)
853
882
  return url.url() if url else "\n".join(errors)
854
883
 
855
- def url_wfs_description(self, request: pyramid.request.Request) -> Optional[str]:
884
+ def url_wfs_description(self, request: pyramid.request.Request) -> str | None:
856
885
  if not self.url_wfs:
857
886
  return self.url_description(request)
858
- errors: Set[str] = set()
887
+ errors: set[str] = set()
859
888
  url = get_url2(self.name, self.url_wfs, request, errors)
860
889
  return url.url() if url else "\n".join(errors)
861
890
 
862
891
 
863
- event.listen(OGCServer, "after_insert", cache_invalidate_cb, propagate=True)
864
- event.listen(OGCServer, "after_update", cache_invalidate_cb, propagate=True)
865
- event.listen(OGCServer, "after_delete", cache_invalidate_cb, propagate=True)
866
-
867
-
868
892
  class LayerWMS(DimensionLayer):
869
893
  """The layer_wms table representation."""
870
894
 
@@ -873,7 +897,7 @@ class LayerWMS(DimensionLayer):
873
897
  __colanderalchemy_config__ = {
874
898
  "title": _("WMS Layer"),
875
899
  "plural": _("WMS Layers"),
876
- "description": Literal(
900
+ "description": c2cgeoportal_commons.lib.literal.Literal(
877
901
  _(
878
902
  """
879
903
  <div class="help-block">
@@ -889,15 +913,15 @@ class LayerWMS(DimensionLayer):
889
913
 
890
914
  __c2cgeoform_config__ = {"duplicate": True}
891
915
 
892
- __mapper_args__ = {"polymorphic_identity": "l_wms"}
916
+ __mapper_args__ = {"polymorphic_identity": "l_wms"} # type: ignore[dict-item]
893
917
 
894
- id = Column(
918
+ id: Mapped[int] = mapped_column(
895
919
  Integer,
896
920
  ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
897
921
  primary_key=True,
898
922
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
899
923
  )
900
- ogc_server_id = Column(
924
+ ogc_server_id: Mapped[int] = mapped_column(
901
925
  Integer,
902
926
  ForeignKey(_schema + ".ogc_server.id"),
903
927
  nullable=False,
@@ -911,7 +935,7 @@ class LayerWMS(DimensionLayer):
911
935
  }
912
936
  },
913
937
  )
914
- layer = Column(
938
+ layer: Mapped[str] = mapped_column(
915
939
  Unicode,
916
940
  nullable=False,
917
941
  info={
@@ -929,7 +953,7 @@ class LayerWMS(DimensionLayer):
929
953
  }
930
954
  },
931
955
  )
932
- style = Column(
956
+ style: Mapped[str | None] = mapped_column(
933
957
  Unicode,
934
958
  info={
935
959
  "colanderalchemy": {
@@ -939,30 +963,34 @@ class LayerWMS(DimensionLayer):
939
963
  }
940
964
  },
941
965
  )
942
- valid = Column(
966
+ valid: Mapped[bool] = mapped_column(
943
967
  Boolean,
968
+ nullable=True,
944
969
  info={
945
970
  "colanderalchemy": {
946
971
  "title": _("Valid"),
947
972
  "description": _("The status reported by latest synchronization (readonly)."),
948
973
  "column": 2,
949
974
  "widget": CheckboxWidget(readonly=True),
975
+ "missing": colander_null,
950
976
  }
951
977
  },
952
978
  )
953
- invalid_reason = Column(
979
+ invalid_reason: Mapped[str] = mapped_column(
954
980
  Unicode,
981
+ nullable=True,
955
982
  info={
956
983
  "colanderalchemy": {
957
984
  "title": _("Reason why I am not valid"),
958
985
  "description": _("The reason for status reported by latest synchronization (readonly)."),
959
986
  "column": 2,
960
987
  "widget": TextInputWidget(readonly=True),
988
+ "missing": "",
961
989
  }
962
990
  },
963
991
  )
964
- time_mode = Column(
965
- Enum("disabled", "value", "range", native_enum=False),
992
+ time_mode: Mapped[TimeMode] = mapped_column(
993
+ Enum(*get_args(TimeMode), native_enum=False),
966
994
  default="disabled",
967
995
  nullable=False,
968
996
  info={
@@ -976,8 +1004,8 @@ class LayerWMS(DimensionLayer):
976
1004
  }
977
1005
  },
978
1006
  )
979
- time_widget = Column(
980
- Enum("slider", "datepicker", native_enum=False),
1007
+ time_widget: Mapped[TimeWidget] = mapped_column(
1008
+ Enum(*get_args(TimeWidget), native_enum=False),
981
1009
  default="slider",
982
1010
  nullable=False,
983
1011
  info={
@@ -1008,8 +1036,8 @@ class LayerWMS(DimensionLayer):
1008
1036
  name: str = "",
1009
1037
  layer: str = "",
1010
1038
  public: bool = True,
1011
- time_mode: str = "disabled",
1012
- time_widget: str = "slider",
1039
+ time_mode: TimeMode = "disabled",
1040
+ time_widget: TimeWidget = "slider",
1013
1041
  ) -> None:
1014
1042
  super().__init__(name=name, public=public)
1015
1043
  self.layer = layer
@@ -1017,7 +1045,7 @@ class LayerWMS(DimensionLayer):
1017
1045
  self.time_widget = time_widget
1018
1046
 
1019
1047
  @staticmethod
1020
- def get_default(dbsession: Session) -> Optional[DimensionLayer]:
1048
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1021
1049
  return cast(
1022
1050
  Optional[DimensionLayer],
1023
1051
  dbsession.query(LayerWMS).filter(LayerWMS.name == "wms-defaults").one_or_none(),
@@ -1032,7 +1060,7 @@ class LayerWMTS(DimensionLayer):
1032
1060
  __colanderalchemy_config__ = {
1033
1061
  "title": _("WMTS Layer"),
1034
1062
  "plural": _("WMTS Layers"),
1035
- "description": Literal(
1063
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1036
1064
  _(
1037
1065
  """
1038
1066
  <div class="help-block">
@@ -1080,15 +1108,15 @@ class LayerWMTS(DimensionLayer):
1080
1108
  ),
1081
1109
  }
1082
1110
  __c2cgeoform_config__ = {"duplicate": True}
1083
- __mapper_args__ = {"polymorphic_identity": "l_wmts"}
1111
+ __mapper_args__ = {"polymorphic_identity": "l_wmts"} # type: ignore[dict-item]
1084
1112
 
1085
- id = Column(
1113
+ id: Mapped[int] = mapped_column(
1086
1114
  Integer,
1087
- ForeignKey(_schema + ".layer.id"),
1115
+ ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
1088
1116
  primary_key=True,
1089
1117
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1090
1118
  )
1091
- url = Column(
1119
+ url: Mapped[str] = mapped_column(
1092
1120
  Unicode,
1093
1121
  nullable=False,
1094
1122
  info={
@@ -1099,29 +1127,32 @@ class LayerWMTS(DimensionLayer):
1099
1127
  }
1100
1128
  },
1101
1129
  )
1102
- layer = Column(
1130
+ layer: Mapped[str] = mapped_column(
1103
1131
  Unicode,
1104
1132
  nullable=False,
1105
1133
  info={
1106
1134
  "colanderalchemy": {
1107
1135
  "title": _("WMTS layer name"),
1108
- "description": _("The name of the WMTS layer to use"),
1136
+ "description": _("The name of the WMTS layer to use."),
1109
1137
  "column": 2,
1110
1138
  }
1111
1139
  },
1112
1140
  )
1113
- style = Column(
1141
+ style: Mapped[str] = mapped_column(
1114
1142
  Unicode,
1143
+ nullable=True,
1115
1144
  info={
1116
1145
  "colanderalchemy": {
1117
1146
  "title": _("Style"),
1118
1147
  "description": _("The style to use; if not present, the default style is used."),
1119
1148
  "column": 2,
1149
+ "missing": "",
1120
1150
  }
1121
1151
  },
1122
1152
  )
1123
- matrix_set = Column(
1153
+ matrix_set: Mapped[str] = mapped_column(
1124
1154
  Unicode,
1155
+ nullable=True,
1125
1156
  info={
1126
1157
  "colanderalchemy": {
1127
1158
  "title": _("Matrix set"),
@@ -1130,34 +1161,35 @@ class LayerWMTS(DimensionLayer):
1130
1161
  "left empty."
1131
1162
  ),
1132
1163
  "column": 2,
1164
+ "missing": "",
1133
1165
  }
1134
1166
  },
1135
1167
  )
1136
- image_type = Column(
1137
- Enum("image/jpeg", "image/png", native_enum=False),
1168
+ image_type: Mapped[ImageType] = mapped_column(
1169
+ Enum(*get_args(ImageType), native_enum=False),
1138
1170
  nullable=False,
1139
1171
  info={
1140
1172
  "colanderalchemy": {
1141
1173
  "title": _("Image type"),
1142
- "description": Literal(
1174
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1143
1175
  _(
1144
1176
  """
1145
- The MIME type of the images (e.g.: <code>image/png</code>).
1177
+ The MIME type of the images (e.g.: <code>image/png</code>, <code>image/webp</code> is experimental).
1146
1178
  """
1147
1179
  )
1148
1180
  ),
1149
1181
  "column": 2,
1150
- "widget": SelectWidget(values=(("image/jpeg", "image/jpeg"), ("image/png", "image/png"))),
1182
+ "widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
1151
1183
  }
1152
1184
  },
1153
1185
  )
1154
1186
 
1155
- def __init__(self, name: str = "", public: bool = True, image_type: str = "image/png") -> None:
1187
+ def __init__(self, name: str = "", public: bool = True, image_type: ImageType = "image/png") -> None:
1156
1188
  super().__init__(name=name, public=public)
1157
1189
  self.image_type = image_type
1158
1190
 
1159
1191
  @staticmethod
1160
- def get_default(dbsession: Session) -> Optional[DimensionLayer]:
1192
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1161
1193
  return cast(
1162
1194
  Optional[DimensionLayer],
1163
1195
  dbsession.query(LayerWMTS).filter(LayerWMTS.name == "wmts-defaults").one_or_none(),
@@ -1193,6 +1225,58 @@ layer_ra = Table(
1193
1225
  )
1194
1226
 
1195
1227
 
1228
+ class LayerCOG(Layer):
1229
+ """The Cloud Optimized GeoTIFF layer table representation."""
1230
+
1231
+ __tablename__ = "layer_cog"
1232
+ __table_args__ = {"schema": _schema}
1233
+ __colanderalchemy_config__ = {
1234
+ "title": _("COG Layer"),
1235
+ "plural": _("COG Layers"),
1236
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1237
+ _(
1238
+ """
1239
+ <div class="help-block">
1240
+ <p>Definition of a <code>COG Layer</code> (COG for
1241
+ <a href="https://www.cogeo.org/">Cloud Optimized GeoTIFF</a>).</p>
1242
+ <p>Note: The layer named <code>cog-defaults</code> contain the values
1243
+ used when a new <code>COG layer</code> is created.</p>
1244
+ </div>
1245
+ """
1246
+ )
1247
+ ),
1248
+ }
1249
+ __c2cgeoform_config__ = {"duplicate": True}
1250
+ __mapper_args__ = {"polymorphic_identity": "l_cog"} # type: ignore[dict-item]
1251
+
1252
+ id: Mapped[int] = mapped_column(
1253
+ Integer,
1254
+ ForeignKey(_schema + ".layer.id"),
1255
+ primary_key=True,
1256
+ info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1257
+ )
1258
+ url: Mapped[str] = mapped_column(
1259
+ Unicode,
1260
+ nullable=False,
1261
+ info={
1262
+ "colanderalchemy": {
1263
+ "title": _("URL"),
1264
+ "description": _("URL of the COG file."),
1265
+ "column": 2,
1266
+ }
1267
+ },
1268
+ )
1269
+
1270
+ @staticmethod
1271
+ def get_default(dbsession: Session) -> Layer | None:
1272
+ return dbsession.query(LayerCOG).filter(LayerCOG.name == "cog-defaults").one_or_none()
1273
+
1274
+ def url_description(self, request: pyramid.request.Request) -> str:
1275
+ errors: set[str] = set()
1276
+ url = get_url2(self.name, self.url, request, errors)
1277
+ return url.url() if url else "\n".join(errors)
1278
+
1279
+
1196
1280
  class LayerVectorTiles(DimensionLayer):
1197
1281
  """The layer_vectortiles table representation."""
1198
1282
 
@@ -1201,7 +1285,7 @@ class LayerVectorTiles(DimensionLayer):
1201
1285
  __colanderalchemy_config__ = {
1202
1286
  "title": _("Vector Tiles Layer"),
1203
1287
  "plural": _("Vector Tiles Layers"),
1204
- "description": Literal(
1288
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1205
1289
  _(
1206
1290
  """
1207
1291
  <div class="help-block">
@@ -1235,16 +1319,16 @@ class LayerVectorTiles(DimensionLayer):
1235
1319
 
1236
1320
  __c2cgeoform_config__ = {"duplicate": True}
1237
1321
 
1238
- __mapper_args__ = {"polymorphic_identity": "l_mvt"}
1322
+ __mapper_args__ = {"polymorphic_identity": "l_mvt"} # type: ignore[dict-item]
1239
1323
 
1240
- id = Column(
1324
+ id: Mapped[int] = mapped_column(
1241
1325
  Integer,
1242
1326
  ForeignKey(_schema + ".layer.id"),
1243
1327
  primary_key=True,
1244
1328
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1245
1329
  )
1246
1330
 
1247
- style = Column(
1331
+ style: Mapped[str] = mapped_column(
1248
1332
  Unicode,
1249
1333
  nullable=False,
1250
1334
  info={
@@ -1252,7 +1336,7 @@ class LayerVectorTiles(DimensionLayer):
1252
1336
  "title": _("Style"),
1253
1337
  "description": _(
1254
1338
  """
1255
- The path to a Mapbox Style file (version 8 or higher). Example: https://url/style.json
1339
+ The path to a Mapbox Style file (version 8 or higher). Example: https://url/style.json.
1256
1340
  """
1257
1341
  ),
1258
1342
  "column": 2,
@@ -1260,24 +1344,20 @@ class LayerVectorTiles(DimensionLayer):
1260
1344
  },
1261
1345
  )
1262
1346
 
1263
- sql = Column(
1347
+ sql: Mapped[str] = mapped_column(
1264
1348
  Unicode,
1265
1349
  nullable=True,
1266
1350
  info={
1267
1351
  "colanderalchemy": {
1268
1352
  "title": _("SQL query"),
1269
- "description": _(
1270
- """
1271
- A SQL query to get the vector tiles data.
1272
- """
1273
- ),
1353
+ "description": _("An SQL query to retrieve the vector tiles data."),
1274
1354
  "column": 2,
1275
1355
  "widget": TextAreaWidget(rows=15),
1276
1356
  }
1277
1357
  },
1278
1358
  )
1279
1359
 
1280
- xyz = Column(
1360
+ xyz: Mapped[str] = mapped_column(
1281
1361
  Unicode,
1282
1362
  nullable=True,
1283
1363
  info={
@@ -1300,7 +1380,7 @@ class LayerVectorTiles(DimensionLayer):
1300
1380
  self.sql = sql
1301
1381
 
1302
1382
  @staticmethod
1303
- def get_default(dbsession: Session) -> Optional[DimensionLayer]:
1383
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1304
1384
  return cast(
1305
1385
  Optional[DimensionLayer],
1306
1386
  dbsession.query(LayerVectorTiles)
@@ -1309,7 +1389,7 @@ class LayerVectorTiles(DimensionLayer):
1309
1389
  )
1310
1390
 
1311
1391
  def style_description(self, request: pyramid.request.Request) -> str:
1312
- errors: Set[str] = set()
1392
+ errors: set[str] = set()
1313
1393
  url = get_url2(self.name, self.style, request, errors)
1314
1394
  return url.url() if url else "\n".join(errors)
1315
1395
 
@@ -1321,9 +1401,11 @@ class RestrictionArea(Base): # type: ignore
1321
1401
  __table_args__ = {"schema": _schema}
1322
1402
  __colanderalchemy_config__ = {"title": _("Restriction area"), "plural": _("Restriction areas")}
1323
1403
  __c2cgeoform_config__ = {"duplicate": True}
1324
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1404
+ id: Mapped[int] = mapped_column(
1405
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1406
+ )
1325
1407
 
1326
- name = Column(
1408
+ name: Mapped[str] = mapped_column(
1327
1409
  Unicode,
1328
1410
  info={
1329
1411
  "colanderalchemy": {
@@ -1332,16 +1414,16 @@ class RestrictionArea(Base): # type: ignore
1332
1414
  }
1333
1415
  },
1334
1416
  )
1335
- description = Column(
1417
+ description: Mapped[str | None] = mapped_column(
1336
1418
  Unicode,
1337
1419
  info={
1338
1420
  "colanderalchemy": {
1339
1421
  "title": _("Description"),
1340
- "description": _("An optional description"),
1422
+ "description": _("An optional description."),
1341
1423
  }
1342
1424
  },
1343
1425
  )
1344
- readwrite = Column(
1426
+ readwrite: Mapped[bool] = mapped_column(
1345
1427
  Boolean,
1346
1428
  default=False,
1347
1429
  info={
@@ -1351,7 +1433,7 @@ class RestrictionArea(Base): # type: ignore
1351
1433
  }
1352
1434
  },
1353
1435
  )
1354
- area = Column(
1436
+ area = mapped_column(
1355
1437
  Geometry("POLYGON", srid=_srid),
1356
1438
  info={
1357
1439
  "colanderalchemy": {
@@ -1396,7 +1478,7 @@ class RestrictionArea(Base): # type: ignore
1396
1478
  "colanderalchemy": {
1397
1479
  "title": _("Layers"),
1398
1480
  "exclude": True,
1399
- "description": Literal(
1481
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1400
1482
  _(
1401
1483
  """
1402
1484
  <div class="help-block">
@@ -1425,9 +1507,9 @@ class RestrictionArea(Base): # type: ignore
1425
1507
  self,
1426
1508
  name: str = "",
1427
1509
  description: str = "",
1428
- layers: Optional[List[Layer]] = None,
1429
- roles: Optional[List[Role]] = None,
1430
- area: Geometry = None,
1510
+ layers: list[Layer] | None = None,
1511
+ roles: list[Role] | None = None,
1512
+ area: Geometry | None = None,
1431
1513
  readwrite: bool = False,
1432
1514
  ) -> None:
1433
1515
  if layers is None:
@@ -1481,8 +1563,10 @@ class Interface(Base): # type: ignore
1481
1563
  __c2cgeoform_config__ = {"duplicate": True}
1482
1564
  __colanderalchemy_config__ = {"title": _("Interface"), "plural": _("Interfaces")}
1483
1565
 
1484
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1485
- name = Column(
1566
+ id: Mapped[int] = mapped_column(
1567
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1568
+ )
1569
+ name: Mapped[str] = mapped_column(
1486
1570
  Unicode,
1487
1571
  info={
1488
1572
  "colanderalchemy": {
@@ -1491,7 +1575,7 @@ class Interface(Base): # type: ignore
1491
1575
  }
1492
1576
  },
1493
1577
  )
1494
- description = Column(
1578
+ description: Mapped[str | None] = mapped_column(
1495
1579
  Unicode,
1496
1580
  info={
1497
1581
  "colanderalchemy": {
@@ -1553,18 +1637,23 @@ class Metadata(Base): # type: ignore
1553
1637
  "plural": _("Metadatas"),
1554
1638
  }
1555
1639
 
1556
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1557
- name = Column(
1640
+ id: Mapped[int] = mapped_column(
1641
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1642
+ )
1643
+ name: Mapped[str] = mapped_column(
1558
1644
  Unicode,
1559
1645
  info={
1560
1646
  "colanderalchemy": {
1561
1647
  "title": _("Name"),
1562
- "description": Literal(_("The type of <code>Metadata</code> we want to set.")),
1648
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1649
+ _("The type of <code>Metadata</code> we want to set.")
1650
+ ),
1563
1651
  }
1564
1652
  },
1565
1653
  )
1566
- value = Column(
1654
+ value: Mapped[str] = mapped_column(
1567
1655
  Unicode,
1656
+ nullable=True,
1568
1657
  info={
1569
1658
  "colanderalchemy": {
1570
1659
  "title": _("Value"),
@@ -1573,7 +1662,7 @@ class Metadata(Base): # type: ignore
1573
1662
  }
1574
1663
  },
1575
1664
  )
1576
- description = Column(
1665
+ description: Mapped[str | None] = mapped_column(
1577
1666
  Unicode,
1578
1667
  info={
1579
1668
  "colanderalchemy": {
@@ -1584,10 +1673,10 @@ class Metadata(Base): # type: ignore
1584
1673
  },
1585
1674
  )
1586
1675
 
1587
- item_id = Column(
1676
+ item_id: Mapped[int] = mapped_column(
1588
1677
  "item_id",
1589
1678
  Integer,
1590
- ForeignKey(_schema + ".treeitem.id"),
1679
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
1591
1680
  nullable=False,
1592
1681
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
1593
1682
  )
@@ -1601,7 +1690,7 @@ class Metadata(Base): # type: ignore
1601
1690
  info={
1602
1691
  "colanderalchemy": {
1603
1692
  "title": _("Metadatas"),
1604
- "description": Literal(
1693
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1605
1694
  _(
1606
1695
  """
1607
1696
  <div class="help-block">
@@ -1634,7 +1723,7 @@ class Metadata(Base): # type: ignore
1634
1723
  ),
1635
1724
  )
1636
1725
 
1637
- def __init__(self, name: str = "", value: str = "", description: Optional[str] = None) -> None:
1726
+ def __init__(self, name: str = "", value: str = "", description: str | None = None) -> None:
1638
1727
  self.name = name
1639
1728
  self.value = value
1640
1729
  self.description = description
@@ -1658,8 +1747,10 @@ class Dimension(Base): # type: ignore
1658
1747
  "plural": _("Dimensions"),
1659
1748
  }
1660
1749
 
1661
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1662
- name = Column(
1750
+ id: Mapped[int] = mapped_column(
1751
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1752
+ )
1753
+ name: Mapped[str] = mapped_column(
1663
1754
  Unicode,
1664
1755
  info={
1665
1756
  "colanderalchemy": {
@@ -1668,8 +1759,9 @@ class Dimension(Base): # type: ignore
1668
1759
  }
1669
1760
  },
1670
1761
  )
1671
- value = Column(
1762
+ value: Mapped[str] = mapped_column(
1672
1763
  Unicode,
1764
+ nullable=True,
1673
1765
  info={
1674
1766
  "colanderalchemy": {
1675
1767
  "title": _("Value"),
@@ -1677,7 +1769,7 @@ class Dimension(Base): # type: ignore
1677
1769
  }
1678
1770
  },
1679
1771
  )
1680
- field = Column(
1772
+ field: Mapped[str | None] = mapped_column(
1681
1773
  Unicode,
1682
1774
  info={
1683
1775
  "colanderalchemy": {
@@ -1688,7 +1780,7 @@ class Dimension(Base): # type: ignore
1688
1780
  }
1689
1781
  },
1690
1782
  )
1691
- description = Column(
1783
+ description: Mapped[str | None] = mapped_column(
1692
1784
  Unicode,
1693
1785
  info={
1694
1786
  "colanderalchemy": {
@@ -1699,7 +1791,7 @@ class Dimension(Base): # type: ignore
1699
1791
  },
1700
1792
  )
1701
1793
 
1702
- layer_id = Column(
1794
+ layer_id: Mapped[int] = mapped_column(
1703
1795
  "layer_id",
1704
1796
  Integer,
1705
1797
  ForeignKey(_schema + ".layer.id"),
@@ -1716,7 +1808,7 @@ class Dimension(Base): # type: ignore
1716
1808
  "colanderalchemy": {
1717
1809
  "title": _("Dimensions"),
1718
1810
  "exclude": True,
1719
- "description": Literal(
1811
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1720
1812
  _(
1721
1813
  """
1722
1814
  <div class="help-block">
@@ -1735,9 +1827,9 @@ class Dimension(Base): # type: ignore
1735
1827
  self,
1736
1828
  name: str = "",
1737
1829
  value: str = "",
1738
- layer: Optional[str] = None,
1739
- field: Optional[str] = None,
1740
- description: Optional[str] = None,
1830
+ layer: str | None = None,
1831
+ field: str | None = None,
1832
+ description: str | None = None,
1741
1833
  ) -> None:
1742
1834
  self.name = name
1743
1835
  self.value = value
@@ -1748,3 +1840,100 @@ class Dimension(Base): # type: ignore
1748
1840
 
1749
1841
  def __str__(self) -> str:
1750
1842
  return f"{self.name}={self.value}[{self.id}]"
1843
+
1844
+
1845
+ class LogAction(enum.Enum):
1846
+ """The log action enumeration."""
1847
+
1848
+ INSERT = enum.auto()
1849
+ UPDATE = enum.auto()
1850
+ DELETE = enum.auto()
1851
+ SYNCHRONIZE = enum.auto()
1852
+ CONVERT_TO_WMTS = enum.auto()
1853
+ CONVERT_TO_WMS = enum.auto()
1854
+
1855
+
1856
+ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1857
+ """The abstract log table representation."""
1858
+
1859
+ strict_attrs = True
1860
+ __colanderalchemy_config__ = {
1861
+ "title": _("Log"),
1862
+ "plural": _("Logs"),
1863
+ }
1864
+
1865
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, info={"colanderalchemy": {}})
1866
+ date: Mapped[datetime] = mapped_column(
1867
+ DateTime(timezone=True),
1868
+ nullable=False,
1869
+ info={
1870
+ "colanderalchemy": {
1871
+ "title": _("Date"),
1872
+ }
1873
+ },
1874
+ )
1875
+ action: Mapped[LogAction] = mapped_column(
1876
+ Enum(LogAction, native_enum=False),
1877
+ nullable=False,
1878
+ info={
1879
+ "colanderalchemy": {
1880
+ "title": _("Action"),
1881
+ }
1882
+ },
1883
+ )
1884
+ element_type: Mapped[str] = mapped_column(
1885
+ String(50),
1886
+ nullable=False,
1887
+ info={
1888
+ "colanderalchemy": {
1889
+ "title": _("Element type"),
1890
+ }
1891
+ },
1892
+ )
1893
+ element_id: Mapped[int] = mapped_column(
1894
+ Integer,
1895
+ nullable=False,
1896
+ info={
1897
+ "colanderalchemy": {
1898
+ "title": _("Element identifier"),
1899
+ }
1900
+ },
1901
+ )
1902
+ element_name: Mapped[str] = mapped_column(
1903
+ Unicode,
1904
+ nullable=False,
1905
+ info={
1906
+ "colanderalchemy": {
1907
+ "title": _("Element name"),
1908
+ }
1909
+ },
1910
+ )
1911
+ element_url_table: Mapped[str] = mapped_column(
1912
+ Unicode,
1913
+ nullable=False,
1914
+ info={
1915
+ "colanderalchemy": {
1916
+ "title": _("Table segment of the element URL"),
1917
+ }
1918
+ },
1919
+ )
1920
+ username: Mapped[str] = mapped_column(
1921
+ Unicode,
1922
+ nullable=False,
1923
+ info={
1924
+ "colanderalchemy": {
1925
+ "title": _("Username"),
1926
+ }
1927
+ },
1928
+ )
1929
+
1930
+
1931
+ class Log(AbstractLog):
1932
+ """The main log table representation."""
1933
+
1934
+ __tablename__ = "log"
1935
+ __table_args__ = {"schema": _schema}
1936
+ __mapper_args__ = {
1937
+ "polymorphic_identity": "main",
1938
+ "concrete": True,
1939
+ }