c2cgeoportal-commons 2.5.0.100__py3-none-any.whl → 2.9rc44__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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