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