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