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.
- c2cgeoportal_commons/__init__.py +2 -5
- c2cgeoportal_commons/alembic/env.py +52 -33
- c2cgeoportal_commons/alembic/main/028477929d13_add_technical_roles.py +10 -7
- c2cgeoportal_commons/alembic/main/04f05bfbb05e_remove_the_old_is_expanded_column.py +62 -0
- c2cgeoportal_commons/alembic/main/116b9b79fc4d_internal_and_external_layer_tables_.py +42 -43
- c2cgeoportal_commons/alembic/main/1418cb05921b_merge_1_6_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/164ac0819a61_add_image_format_to_wmts_layer.py +9 -6
- c2cgeoportal_commons/alembic/main/166ff2dcc48d_create_database.py +22 -25
- c2cgeoportal_commons/alembic/main/16e43f8c0330_remove_old_metadata_column.py +9 -6
- c2cgeoportal_commons/alembic/main/1d5d4abfebd1_add_restricted_theme.py +14 -8
- c2cgeoportal_commons/alembic/main/1de20166b274_remove_v1_artifacts.py +9 -6
- c2cgeoportal_commons/alembic/main/20137477bd02_update_icons_url.py +9 -6
- c2cgeoportal_commons/alembic/main/21f11066f8ec_trigger_on_role_updates_user_in_static.py +21 -20
- c2cgeoportal_commons/alembic/main/22e6dfb556de_add_description_tree_.py +9 -6
- c2cgeoportal_commons/alembic/main/29f2a32859ec_merge_1_6_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/2b8ed8c1df94_set_layergroup_treeitem_is_as_a_primary_.py +9 -6
- c2cgeoportal_commons/alembic/main/2e57710fecfe_update_the_ogc_server_for_ogc_api.py +71 -0
- c2cgeoportal_commons/alembic/main/32527659d57b_move_exclude_properties_from_layerv1_to_.py +15 -12
- c2cgeoportal_commons/alembic/main/32b21aa1d0ed_merge_e004f76e951a_and_e004f76e951a_.py +9 -8
- c2cgeoportal_commons/alembic/main/338b57593823_remove_trigger_on_role_name_change.py +25 -24
- c2cgeoportal_commons/alembic/main/415746eb9f6_changes_for_v2.py +45 -43
- c2cgeoportal_commons/alembic/main/44c91d82d419_add_table_log.py +72 -0
- c2cgeoportal_commons/alembic/main/5109242131ce_add_column_time_widget.py +11 -9
- c2cgeoportal_commons/alembic/main/52916d8fde8b_add_sql_fields_to_vector_tiles.py +60 -0
- c2cgeoportal_commons/alembic/main/53ba1a68d5fe_add_theme_to_fulltextsearch.py +9 -6
- c2cgeoportal_commons/alembic/main/54645a535ad6_add_ordering_in_relation.py +17 -14
- c2cgeoportal_commons/alembic/main/56dc90838d90_fix_removing_layerv1.py +10 -8
- c2cgeoportal_commons/alembic/main/596ba21e3833_separate_local_internal.py +13 -14
- c2cgeoportal_commons/alembic/main/678f88c7ad5e_add_vector_tiles_layers_table.py +9 -6
- c2cgeoportal_commons/alembic/main/6a412d9437b1_rename_serverogc_to_ogcserver.py +9 -6
- c2cgeoportal_commons/alembic/main/6d87fdad275a_convert_metadata_to_the_right_case.py +13 -20
- c2cgeoportal_commons/alembic/main/7530011a66a7_trigger_on_role_updates_user_in_static.py +21 -20
- c2cgeoportal_commons/alembic/main/78fd093c8393_add_api_s_intrfaces.py +36 -52
- c2cgeoportal_commons/alembic/main/7d271f4527cd_add_layer_column_in_layerv1_table.py +9 -6
- c2cgeoportal_commons/alembic/main/809650bd04c3_add_dimension_field.py +9 -6
- c2cgeoportal_commons/alembic/main/8117bb9bba16_use_dimension_on_all_the_layers.py +9 -6
- c2cgeoportal_commons/alembic/main/87f8330ed64e_add_missing_delete_cascades.py +305 -0
- c2cgeoportal_commons/alembic/main/9268a1dffac0_add_trigger_to_be_able_to_correctly_.py +11 -8
- c2cgeoportal_commons/alembic/main/94db7e7e5b21_merge_2_2_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/951ff84bd8ec_be_able_to_delete_a_wms_layer_in_sql.py +9 -6
- c2cgeoportal_commons/alembic/main/a00109812f89_add_field_layer_wms_valid.py +60 -0
- c2cgeoportal_commons/alembic/main/a4558f032d7d_add_support_of_cog_layers.py +74 -0
- c2cgeoportal_commons/alembic/main/a4f1aac9bda_merge_1_6_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/b60f2a505f42_remame_uimetadata_to_metadata.py +9 -6
- c2cgeoportal_commons/alembic/main/b6b09f414fe8_sync_model_database.py +165 -0
- c2cgeoportal_commons/alembic/main/c75124553bf3_remove_deprecated_columns.py +9 -6
- c2cgeoportal_commons/alembic/main/d48a63b348f1_change_mapserver_url_for_docker.py +13 -14
- c2cgeoportal_commons/alembic/main/d8ef99bc227e_be_able_to_delete_a_linked_functionality.py +16 -13
- c2cgeoportal_commons/alembic/main/daf738d5bae4_merge_2_0_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/dba87f2647f9_merge_2_2_on_2_3.py +9 -8
- c2cgeoportal_commons/alembic/main/e004f76e951a_add_missing_not_null.py +15 -32
- c2cgeoportal_commons/alembic/main/e7e03dedade3_put_the_default_wms_server_in_the_.py +13 -14
- c2cgeoportal_commons/alembic/main/e85afd327ab3_cascade_deletes_to_tsearch.py +9 -6
- c2cgeoportal_commons/alembic/main/ec82a8906649_add_missing_on_delete_cascade_on_layer_.py +13 -10
- c2cgeoportal_commons/alembic/main/ee25d267bf46_main_interface_desktop.py +11 -14
- c2cgeoportal_commons/alembic/main/eeb345672454_merge_2_4_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/static/0c640a58a09a_add_opt_key_column.py +9 -6
- c2cgeoportal_commons/alembic/static/107b81f5b9fe_add_missing_delete_cascades.py +78 -0
- c2cgeoportal_commons/alembic/static/1857owc78a07_add_last_login_and_expiration_date.py +7 -6
- c2cgeoportal_commons/alembic/static/1da396a88908_move_user_table_to_static_schema.py +19 -20
- c2cgeoportal_commons/alembic/static/267b4c1bde2e_add_display_name_in_the_user_for_better_.py +62 -0
- c2cgeoportal_commons/alembic/static/3f89a7d71a5e_alter_column_url_to_remove_limitation.py +8 -7
- c2cgeoportal_commons/alembic/static/44c91d82d419_add_table_log.py +72 -0
- c2cgeoportal_commons/alembic/static/53d671b17b20_add_timezone_on_datetime_fields.py +9 -6
- c2cgeoportal_commons/alembic/static/5472fbc19f39_add_temp_password_column.py +9 -6
- c2cgeoportal_commons/alembic/static/76d72fb3fcb9_add_oauth2_pkce.py +70 -0
- c2cgeoportal_commons/alembic/static/7ef947f30f20_add_oauth2_tables.py +115 -0
- c2cgeoportal_commons/alembic/static/910b4ca53b68_sync_model_database.py +186 -0
- c2cgeoportal_commons/alembic/static/aa41e9613256_wip_add_openid_connect_support.py +64 -0
- c2cgeoportal_commons/alembic/static/ae5e88f35669_add_table_user_role.py +15 -18
- c2cgeoportal_commons/alembic/static/bd029dbfc11a_fill_tech_data_column.py +10 -8
- c2cgeoportal_commons/lib/email_.py +16 -18
- c2cgeoportal_commons/lib/literal.py +44 -0
- c2cgeoportal_commons/lib/url.py +221 -0
- c2cgeoportal_commons/lib/validators.py +2 -4
- c2cgeoportal_commons/models/__init__.py +19 -15
- c2cgeoportal_commons/models/main.py +1268 -266
- c2cgeoportal_commons/models/sqlalchemy.py +15 -19
- c2cgeoportal_commons/models/static.py +338 -63
- c2cgeoportal_commons/py.typed +0 -0
- c2cgeoportal_commons/testing/__init__.py +18 -13
- c2cgeoportal_commons/testing/initializedb.py +15 -14
- {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/METADATA +18 -23
- c2cgeoportal_commons-2.9rc44.dist-info/RECORD +89 -0
- {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/WHEEL +1 -1
- tests/conftest.py +4 -6
- c2cgeoportal_commons-2.5.0.100.dist-info/RECORD +0 -75
- tests/test_interface.py +0 -27
- tests/test_roles.py +0 -63
- tests/test_users.py +0 -71
- tests/test_validators.py +0 -20
- {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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__ = (
|
|
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 =
|
|
87
|
-
label =
|
|
88
|
-
layer_name =
|
|
89
|
-
role_id =
|
|
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 =
|
|
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 =
|
|
94
|
-
public =
|
|
95
|
-
ts =
|
|
96
|
-
the_geom =
|
|
97
|
-
params =
|
|
98
|
-
actions =
|
|
99
|
-
from_theme =
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 "{}
|
|
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(
|
|
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(
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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={
|
|
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:
|
|
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
|
|
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__:
|
|
326
|
+
__table_args__: tuple[Any, ...] | dict[str, Any] = (
|
|
205
327
|
UniqueConstraint("type", "name"),
|
|
206
328
|
{"schema": _schema},
|
|
207
329
|
)
|
|
208
|
-
item_type =
|
|
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 =
|
|
212
|
-
name
|
|
213
|
-
|
|
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]:
|
|
217
|
-
def parents(self) ->
|
|
218
|
-
return [c.
|
|
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"):
|
|
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
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 =
|
|
265
|
-
Integer,
|
|
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 =
|
|
439
|
+
ordering: Mapped[int] = mapped_column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
|
|
281
440
|
|
|
282
|
-
def __init__(
|
|
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 =
|
|
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) ->
|
|
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:
|
|
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
|
-
|
|
498
|
+
current_children = [child.treeitem for child in self.children_relation]
|
|
318
499
|
for index, item in enumerate(children):
|
|
319
|
-
if item not in
|
|
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
|
-
|
|
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__ = {
|
|
334
|
-
|
|
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 =
|
|
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 = ""
|
|
348
|
-
|
|
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 =
|
|
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 =
|
|
571
|
+
ordering: Mapped[int] = mapped_column(
|
|
376
572
|
Integer, nullable=False, info={"colanderalchemy": {"title": _("Order"), "widget": HiddenWidget()}}
|
|
377
573
|
)
|
|
378
|
-
public
|
|
379
|
-
|
|
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={
|
|
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={
|
|
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
|
-
|
|
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 =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
687
|
+
super().__init__(name=name)
|
|
420
688
|
self.public = public
|
|
421
689
|
|
|
422
690
|
|
|
423
691
|
class DimensionLayer(Layer):
|
|
424
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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__ = {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
"
|
|
463
|
-
|
|
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 =
|
|
475
|
-
Enum(
|
|
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
|
-
"
|
|
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 =
|
|
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
|
-
"
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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:
|
|
845
|
+
description: str | None = None,
|
|
516
846
|
url: str = "https://wms.example.com",
|
|
517
|
-
url_wfs: str = None,
|
|
518
|
-
type_:
|
|
519
|
-
image_type:
|
|
520
|
-
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 ""
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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__ = {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
573
|
-
Unicode,
|
|
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
|
-
|
|
576
|
-
|
|
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 =
|
|
591
|
-
Enum(
|
|
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",
|
|
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:
|
|
614
|
-
time_widget:
|
|
1027
|
+
time_mode: TimeMode = "disabled",
|
|
1028
|
+
time_widget: TimeWidget = "slider",
|
|
615
1029
|
) -> None:
|
|
616
|
-
|
|
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
|
|
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__ = {
|
|
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 =
|
|
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 =
|
|
640
|
-
Unicode,
|
|
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 =
|
|
643
|
-
Unicode,
|
|
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
|
-
|
|
646
|
-
|
|
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=((
|
|
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:
|
|
660
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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__ = {
|
|
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 =
|
|
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 =
|
|
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":
|
|
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
|
-
|
|
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":
|
|
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
|
|
734
|
-
|
|
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={
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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:
|
|
773
|
-
roles:
|
|
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:
|
|
789
|
-
return self.name
|
|
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(
|
|
802
|
-
|
|
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(
|
|
811
|
-
|
|
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
|
|
823
|
-
|
|
824
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
847
|
-
return self.name
|
|
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
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
Unicode,
|
|
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 =
|
|
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={
|
|
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:
|
|
884
|
-
return "{}
|
|
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
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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 =
|
|
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={
|
|
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__(
|
|
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:
|
|
929
|
-
return self.name
|
|
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
|
+
}
|