c2cgeoportal-commons 2.6.0__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 +49 -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 +21 -24
- 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 +9 -6
- 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 +11 -8
- 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 +15 -12
- 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 +9 -6
- 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 +9 -6
- 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 +14 -16
- c2cgeoportal_commons/lib/literal.py +44 -0
- c2cgeoportal_commons/lib/url.py +164 -58
- c2cgeoportal_commons/lib/validators.py +2 -4
- c2cgeoportal_commons/models/__init__.py +19 -15
- c2cgeoportal_commons/models/main.py +1191 -263
- c2cgeoportal_commons/models/sqlalchemy.py +14 -18
- c2cgeoportal_commons/models/static.py +305 -78
- c2cgeoportal_commons/py.typed +0 -0
- c2cgeoportal_commons/testing/__init__.py +17 -12
- c2cgeoportal_commons/testing/initializedb.py +15 -14
- {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/METADATA +15 -16
- c2cgeoportal_commons-2.9rc44.dist-info/RECORD +89 -0
- {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -3
- c2cgeoportal_commons-2.6.0.dist-info/RECORD +0 -76
- {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2011-2021, 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
|
|
@@ -27,95 +25,171 @@
|
|
|
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
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
from typing import Any, Literal, Optional, cast, get_args
|
|
36
35
|
|
|
36
|
+
import pyramid.request
|
|
37
|
+
import sqlalchemy.orm.base
|
|
37
38
|
from c2c.template.config import config
|
|
38
39
|
from geoalchemy2 import Geometry
|
|
39
40
|
from geoalchemy2.shape import to_shape
|
|
40
41
|
from papyrus.geo_interface import GeoInterface
|
|
41
|
-
from pyramid.request import Request
|
|
42
42
|
from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, event
|
|
43
|
-
from sqlalchemy.
|
|
43
|
+
from sqlalchemy.ext.declarative import AbstractConcreteBase
|
|
44
|
+
from sqlalchemy.orm import Mapped, Session, backref, mapped_column, relationship
|
|
44
45
|
from sqlalchemy.schema import Index
|
|
45
|
-
from sqlalchemy.types import Boolean, Enum, Integer, String, Unicode
|
|
46
|
+
from sqlalchemy.types import Boolean, DateTime, Enum, Integer, String, Unicode
|
|
46
47
|
|
|
48
|
+
import c2cgeoportal_commons.lib.literal
|
|
47
49
|
from c2cgeoportal_commons.lib.url import get_url2
|
|
48
50
|
from c2cgeoportal_commons.models import Base, _, cache_invalidate_cb
|
|
49
51
|
from c2cgeoportal_commons.models.sqlalchemy import JSONEncodedDict, TsVector
|
|
50
52
|
|
|
51
53
|
try:
|
|
54
|
+
import colander
|
|
52
55
|
from c2cgeoform import default_map_settings
|
|
53
56
|
from c2cgeoform.ext.colander_ext import Geometry as ColanderGeometry
|
|
54
57
|
from c2cgeoform.ext.deform_ext import MapWidget, RelationSelect2Widget
|
|
55
58
|
from colander import drop
|
|
56
59
|
from deform.widget import CheckboxWidget, HiddenWidget, SelectWidget, TextAreaWidget, TextInputWidget
|
|
60
|
+
|
|
61
|
+
colander_null = colander.null
|
|
57
62
|
except ModuleNotFoundError:
|
|
58
|
-
drop = None
|
|
63
|
+
drop = None # pylint: disable=invalid-name
|
|
59
64
|
default_map_settings = {"srid": 3857, "view": {"projection": "EPSG:3857"}}
|
|
65
|
+
colander_null = None # pylint: disable=invalid-name
|
|
60
66
|
|
|
61
67
|
class GenericClass:
|
|
68
|
+
"""Fallback class implementation."""
|
|
69
|
+
|
|
62
70
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
63
71
|
pass
|
|
64
72
|
|
|
65
73
|
CheckboxWidget = GenericClass
|
|
66
74
|
HiddenWidget = GenericClass
|
|
67
|
-
MapWidget = GenericClass
|
|
75
|
+
MapWidget = GenericClass # type: ignore[misc,assignment]
|
|
68
76
|
SelectWidget = GenericClass
|
|
69
77
|
TextAreaWidget = GenericClass
|
|
70
|
-
ColanderGeometry = GenericClass
|
|
71
|
-
RelationSelect2Widget = GenericClass
|
|
78
|
+
ColanderGeometry = GenericClass # type: ignore[misc,assignment]
|
|
79
|
+
RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
|
|
72
80
|
TextInputWidget = GenericClass
|
|
73
81
|
|
|
74
82
|
|
|
75
|
-
|
|
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."""
|
|
87
|
+
|
|
88
|
+
return "None" if state is None else f"<{state.class_.__name__} {state.obj()}>"
|
|
89
|
+
|
|
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__)
|
|
76
95
|
|
|
77
96
|
_schema: str = config["schema"] or "main"
|
|
78
97
|
_srid: int = cast(int, config["srid"]) or 3857
|
|
79
98
|
|
|
80
99
|
# Set some default values for the admin interface
|
|
81
|
-
|
|
82
|
-
|
|
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", {})}
|
|
83
104
|
view_srid_match = re.match(r"EPSG:(\d+)", _map_config["view"]["projection"])
|
|
84
105
|
if "map_srid" not in _admin_config and view_srid_match is not None:
|
|
85
106
|
_admin_config["map_srid"] = view_srid_match.group(1)
|
|
86
107
|
|
|
87
108
|
|
|
88
|
-
class FullTextSearch(GeoInterface, Base):
|
|
109
|
+
class FullTextSearch(GeoInterface, Base): # type: ignore
|
|
110
|
+
"""The tsearch table representation."""
|
|
111
|
+
|
|
89
112
|
__tablename__ = "tsearch"
|
|
90
|
-
__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
|
+
)
|
|
91
118
|
|
|
92
|
-
id =
|
|
93
|
-
label =
|
|
94
|
-
layer_name =
|
|
95
|
-
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
|
+
)
|
|
96
125
|
role = relationship("Role")
|
|
97
|
-
interface_id =
|
|
126
|
+
interface_id: Mapped[int] = mapped_column(
|
|
127
|
+
Integer, ForeignKey(_schema + ".interface.id", ondelete="CASCADE"), nullable=True
|
|
128
|
+
)
|
|
98
129
|
interface = relationship("Interface")
|
|
99
|
-
lang =
|
|
100
|
-
public =
|
|
101
|
-
ts =
|
|
102
|
-
the_geom =
|
|
103
|
-
params =
|
|
104
|
-
actions =
|
|
105
|
-
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}]"
|
|
106
140
|
|
|
107
141
|
|
|
108
|
-
class Functionality(Base):
|
|
142
|
+
class Functionality(Base): # type: ignore
|
|
143
|
+
"""The functionality table representation."""
|
|
144
|
+
|
|
109
145
|
__tablename__ = "functionality"
|
|
110
146
|
__table_args__ = {"schema": _schema}
|
|
111
147
|
__colanderalchemy_config__ = {"title": _("Functionality"), "plural": _("Functionalities")}
|
|
112
148
|
|
|
113
149
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
114
150
|
|
|
115
|
-
id
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
)
|
|
119
193
|
|
|
120
194
|
def __init__(self, name: str = "", value: str = "", description: str = "") -> None:
|
|
121
195
|
self.name = name
|
|
@@ -123,7 +197,7 @@ class Functionality(Base):
|
|
|
123
197
|
self.description = description
|
|
124
198
|
|
|
125
199
|
def __str__(self) -> str:
|
|
126
|
-
return "{}
|
|
200
|
+
return f"{self.name}={self.value}[{self.id}]"
|
|
127
201
|
|
|
128
202
|
|
|
129
203
|
event.listen(Functionality, "after_update", cache_invalidate_cb)
|
|
@@ -159,19 +233,43 @@ theme_functionality = Table(
|
|
|
159
233
|
)
|
|
160
234
|
|
|
161
235
|
|
|
162
|
-
class Role(Base):
|
|
236
|
+
class Role(Base): # type: ignore
|
|
237
|
+
"""The role table representation."""
|
|
238
|
+
|
|
163
239
|
__tablename__ = "role"
|
|
164
240
|
__table_args__ = {"schema": _schema}
|
|
165
241
|
__colanderalchemy_config__ = {"title": _("Role"), "plural": _("Roles")}
|
|
166
242
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
167
243
|
|
|
168
|
-
id
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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,
|
|
173
260
|
info={
|
|
174
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."),
|
|
175
273
|
"typ": ColanderGeometry("POLYGON", srid=_srid, map_srid=_admin_config["map_srid"]),
|
|
176
274
|
"widget": MapWidget(map_options=_map_config),
|
|
177
275
|
}
|
|
@@ -183,15 +281,21 @@ class Role(Base):
|
|
|
183
281
|
"Functionality",
|
|
184
282
|
secondary=role_functionality,
|
|
185
283
|
cascade="save-update,merge,refresh-expire",
|
|
186
|
-
info={
|
|
284
|
+
info={
|
|
285
|
+
"colanderalchemy": {
|
|
286
|
+
"title": _("Functionalities"),
|
|
287
|
+
"description": _("Functionality values for this role."),
|
|
288
|
+
"exclude": True,
|
|
289
|
+
}
|
|
290
|
+
},
|
|
187
291
|
)
|
|
188
292
|
|
|
189
293
|
def __init__(
|
|
190
294
|
self,
|
|
191
295
|
name: str = "",
|
|
192
296
|
description: str = "",
|
|
193
|
-
functionalities:
|
|
194
|
-
extent: Geometry = None,
|
|
297
|
+
functionalities: list[Functionality] | None = None,
|
|
298
|
+
extent: Geometry | None = None,
|
|
195
299
|
) -> None:
|
|
196
300
|
if functionalities is None:
|
|
197
301
|
functionalities = []
|
|
@@ -201,13 +305,13 @@ class Role(Base):
|
|
|
201
305
|
self.description = description
|
|
202
306
|
|
|
203
307
|
def __str__(self) -> str:
|
|
204
|
-
return self.name
|
|
308
|
+
return f"{self.name}[{self.id}]>"
|
|
205
309
|
|
|
206
310
|
@property
|
|
207
|
-
def bounds(self) -> None:
|
|
311
|
+
def bounds(self) -> tuple[float, float, float, float] | None: # TODO
|
|
208
312
|
if self.extent is None:
|
|
209
313
|
return None
|
|
210
|
-
return to_shape(self.extent).bounds
|
|
314
|
+
return cast(tuple[float, float, float, float], to_shape(self.extent).bounds)
|
|
211
315
|
|
|
212
316
|
|
|
213
317
|
event.listen(Role.functionalities, "set", cache_invalidate_cb)
|
|
@@ -215,26 +319,52 @@ event.listen(Role.functionalities, "append", cache_invalidate_cb)
|
|
|
215
319
|
event.listen(Role.functionalities, "remove", cache_invalidate_cb)
|
|
216
320
|
|
|
217
321
|
|
|
218
|
-
class TreeItem(Base):
|
|
322
|
+
class TreeItem(Base): # type: ignore
|
|
323
|
+
"""The treeitem table representation."""
|
|
324
|
+
|
|
219
325
|
__tablename__ = "treeitem"
|
|
220
|
-
__table_args__:
|
|
326
|
+
__table_args__: tuple[Any, ...] | dict[str, Any] = (
|
|
221
327
|
UniqueConstraint("type", "name"),
|
|
222
328
|
{"schema": _schema},
|
|
223
329
|
)
|
|
224
|
-
item_type =
|
|
330
|
+
item_type: Mapped[str] = mapped_column(
|
|
331
|
+
"type", String(10), nullable=False, info={"colanderalchemy": {"exclude": True}}
|
|
332
|
+
)
|
|
225
333
|
__mapper_args__ = {"polymorphic_on": item_type}
|
|
226
334
|
|
|
227
|
-
id =
|
|
228
|
-
name
|
|
229
|
-
|
|
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
|
+
)
|
|
230
360
|
|
|
231
361
|
@property
|
|
232
|
-
# Better: def parents(self) -> List[TreeGroup]:
|
|
233
|
-
def parents(self) ->
|
|
362
|
+
# Better: def parents(self) -> List[TreeGroup]:
|
|
363
|
+
def parents(self) -> list["TreeItem"]:
|
|
234
364
|
return [c.treegroup for c in self.parents_relation]
|
|
235
365
|
|
|
236
366
|
def is_in_interface(self, name: str) -> bool:
|
|
237
|
-
if not hasattr(self, "interfaces"):
|
|
367
|
+
if not hasattr(self, "interfaces"):
|
|
238
368
|
return False
|
|
239
369
|
|
|
240
370
|
for interface in self.interfaces:
|
|
@@ -243,12 +373,15 @@ class TreeItem(Base):
|
|
|
243
373
|
|
|
244
374
|
return False
|
|
245
375
|
|
|
246
|
-
def
|
|
376
|
+
def get_metadata(self, name: str) -> list["Metadata"]:
|
|
247
377
|
return [metadata for metadata in self.metadatas if metadata.name == name]
|
|
248
378
|
|
|
249
379
|
def __init__(self, name: str = "") -> None:
|
|
250
380
|
self.name = name
|
|
251
381
|
|
|
382
|
+
def __str__(self) -> str:
|
|
383
|
+
return f"{self.name}[{self.id}]>"
|
|
384
|
+
|
|
252
385
|
|
|
253
386
|
event.listen(TreeItem, "after_insert", cache_invalidate_cb, propagate=True)
|
|
254
387
|
event.listen(TreeItem, "after_update", cache_invalidate_cb, propagate=True)
|
|
@@ -256,16 +389,20 @@ event.listen(TreeItem, "after_delete", cache_invalidate_cb, propagate=True)
|
|
|
256
389
|
|
|
257
390
|
|
|
258
391
|
# association table TreeGroup <> TreeItem
|
|
259
|
-
class LayergroupTreeitem(Base):
|
|
392
|
+
class LayergroupTreeitem(Base): # type: ignore
|
|
393
|
+
"""The layergroup_treeitem table representation."""
|
|
394
|
+
|
|
260
395
|
__tablename__ = "layergroup_treeitem"
|
|
261
396
|
__table_args__ = {"schema": _schema}
|
|
262
397
|
|
|
263
398
|
# required by formalchemy
|
|
264
|
-
id
|
|
265
|
-
|
|
266
|
-
|
|
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(
|
|
267
404
|
Integer,
|
|
268
|
-
ForeignKey(_schema + ".treegroup.id"),
|
|
405
|
+
ForeignKey(_schema + ".treegroup.id", name="treegroup_id_fkey"),
|
|
269
406
|
nullable=False,
|
|
270
407
|
info={"colanderalchemy": {"exclude": True}},
|
|
271
408
|
)
|
|
@@ -280,9 +417,9 @@ class LayergroupTreeitem(Base):
|
|
|
280
417
|
primaryjoin="LayergroupTreeitem.treegroup_id==TreeGroup.id",
|
|
281
418
|
info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
|
|
282
419
|
)
|
|
283
|
-
treeitem_id =
|
|
420
|
+
treeitem_id: Mapped[int] = mapped_column(
|
|
284
421
|
Integer,
|
|
285
|
-
ForeignKey(_schema + ".treeitem.id"),
|
|
422
|
+
ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
|
|
286
423
|
nullable=False,
|
|
287
424
|
info={"colanderalchemy": {"widget": HiddenWidget()}},
|
|
288
425
|
)
|
|
@@ -299,13 +436,18 @@ class LayergroupTreeitem(Base):
|
|
|
299
436
|
primaryjoin="LayergroupTreeitem.treeitem_id==TreeItem.id",
|
|
300
437
|
info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
|
|
301
438
|
)
|
|
302
|
-
ordering =
|
|
439
|
+
ordering: Mapped[int] = mapped_column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
|
|
303
440
|
|
|
304
|
-
def __init__(
|
|
441
|
+
def __init__(
|
|
442
|
+
self, group: Optional["TreeGroup"] = None, item: TreeItem | None = None, ordering: int = 0
|
|
443
|
+
) -> None:
|
|
305
444
|
self.treegroup = group
|
|
306
445
|
self.treeitem = item
|
|
307
446
|
self.ordering = ordering
|
|
308
447
|
|
|
448
|
+
def __str__(self) -> str:
|
|
449
|
+
return f"{self.id}"
|
|
450
|
+
|
|
309
451
|
|
|
310
452
|
event.listen(LayergroupTreeitem, "after_insert", cache_invalidate_cb, propagate=True)
|
|
311
453
|
event.listen(LayergroupTreeitem, "after_update", cache_invalidate_cb, propagate=True)
|
|
@@ -313,16 +455,33 @@ event.listen(LayergroupTreeitem, "after_delete", cache_invalidate_cb, propagate=
|
|
|
313
455
|
|
|
314
456
|
|
|
315
457
|
class TreeGroup(TreeItem):
|
|
458
|
+
"""The treegroup table representation."""
|
|
459
|
+
|
|
316
460
|
__tablename__ = "treegroup"
|
|
317
461
|
__table_args__ = {"schema": _schema}
|
|
318
|
-
__mapper_args__ = {"polymorphic_identity": "treegroup"} # needed for _identity_class
|
|
462
|
+
__mapper_args__ = {"polymorphic_identity": "treegroup"} # type: ignore[dict-item] # needed for _identity_class
|
|
319
463
|
|
|
320
|
-
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
|
+
)
|
|
321
469
|
|
|
322
|
-
def _get_children(self) ->
|
|
470
|
+
def _get_children(self) -> list[TreeItem]:
|
|
323
471
|
return [c.treeitem for c in self.children_relation]
|
|
324
472
|
|
|
325
|
-
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
|
+
"""
|
|
326
485
|
for child in self.children_relation:
|
|
327
486
|
if child.treeitem not in children:
|
|
328
487
|
child.treeitem = None
|
|
@@ -336,9 +495,9 @@ class TreeGroup(TreeItem):
|
|
|
336
495
|
LayergroupTreeitem(self, child, index * 10)
|
|
337
496
|
self.children_relation.sort(key=lambda child: child.ordering)
|
|
338
497
|
else:
|
|
339
|
-
|
|
498
|
+
current_children = [child.treeitem for child in self.children_relation]
|
|
340
499
|
for index, item in enumerate(children):
|
|
341
|
-
if item not in
|
|
500
|
+
if item not in current_children:
|
|
342
501
|
LayergroupTreeitem(self, item, 1000000 + index)
|
|
343
502
|
for index, child in enumerate(self.children_relation):
|
|
344
503
|
child.ordering = index * 10
|
|
@@ -346,29 +505,42 @@ class TreeGroup(TreeItem):
|
|
|
346
505
|
children = property(_get_children, _set_children)
|
|
347
506
|
|
|
348
507
|
def __init__(self, name: str = "") -> None:
|
|
349
|
-
|
|
508
|
+
super().__init__(name=name)
|
|
350
509
|
|
|
351
510
|
|
|
352
511
|
class LayerGroup(TreeGroup):
|
|
512
|
+
"""The layergroup table representation."""
|
|
513
|
+
|
|
353
514
|
__tablename__ = "layergroup"
|
|
354
515
|
__table_args__ = {"schema": _schema}
|
|
355
|
-
__colanderalchemy_config__ = {
|
|
356
|
-
|
|
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]
|
|
357
533
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
358
534
|
|
|
359
|
-
id =
|
|
535
|
+
id: Mapped[int] = mapped_column(
|
|
360
536
|
Integer,
|
|
361
|
-
ForeignKey(_schema + ".treegroup.id"),
|
|
537
|
+
ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
|
|
362
538
|
primary_key=True,
|
|
363
539
|
info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
|
|
364
540
|
)
|
|
365
|
-
is_expanded = Column(
|
|
366
|
-
Boolean, info={"colanderalchemy": {"title": _("Expanded"), "column": 2}}
|
|
367
|
-
) # shouldn't be used in V3
|
|
368
541
|
|
|
369
|
-
def __init__(self, name: str = ""
|
|
370
|
-
|
|
371
|
-
self.is_expanded = is_expanded
|
|
542
|
+
def __init__(self, name: str = "") -> None:
|
|
543
|
+
super().__init__(name=name)
|
|
372
544
|
|
|
373
545
|
|
|
374
546
|
# role theme link for restricted theme
|
|
@@ -382,30 +554,58 @@ restricted_role_theme = Table(
|
|
|
382
554
|
|
|
383
555
|
|
|
384
556
|
class Theme(TreeGroup):
|
|
557
|
+
"""The theme table representation."""
|
|
558
|
+
|
|
385
559
|
__tablename__ = "theme"
|
|
386
560
|
__table_args__ = {"schema": _schema}
|
|
387
561
|
__colanderalchemy_config__ = {"title": _("Theme"), "plural": _("Themes")}
|
|
388
|
-
__mapper_args__ = {"polymorphic_identity": "theme"}
|
|
562
|
+
__mapper_args__ = {"polymorphic_identity": "theme"} # type: ignore[dict-item]
|
|
389
563
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
390
564
|
|
|
391
|
-
id =
|
|
565
|
+
id: Mapped[int] = mapped_column(
|
|
392
566
|
Integer,
|
|
393
|
-
ForeignKey(_schema + ".treegroup.id"),
|
|
567
|
+
ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
|
|
394
568
|
primary_key=True,
|
|
395
569
|
info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
|
|
396
570
|
)
|
|
397
|
-
ordering =
|
|
571
|
+
ordering: Mapped[int] = mapped_column(
|
|
398
572
|
Integer, nullable=False, info={"colanderalchemy": {"title": _("Order"), "widget": HiddenWidget()}}
|
|
399
573
|
)
|
|
400
|
-
public
|
|
401
|
-
|
|
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
|
+
)
|
|
402
596
|
|
|
403
597
|
# functionality
|
|
404
598
|
functionalities = relationship(
|
|
405
599
|
"Functionality",
|
|
406
600
|
secondary=theme_functionality,
|
|
407
601
|
cascade="save-update,merge,refresh-expire",
|
|
408
|
-
info={
|
|
602
|
+
info={
|
|
603
|
+
"colanderalchemy": {
|
|
604
|
+
"title": _("Functionalities"),
|
|
605
|
+
"description": _("The linked functionalities."),
|
|
606
|
+
"exclude": True,
|
|
607
|
+
}
|
|
608
|
+
},
|
|
409
609
|
)
|
|
410
610
|
|
|
411
611
|
# restricted to role
|
|
@@ -413,11 +613,17 @@ class Theme(TreeGroup):
|
|
|
413
613
|
"Role",
|
|
414
614
|
secondary=restricted_role_theme,
|
|
415
615
|
cascade="save-update,merge,refresh-expire",
|
|
416
|
-
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
|
+
},
|
|
417
623
|
)
|
|
418
624
|
|
|
419
625
|
def __init__(self, name: str = "", ordering: int = 100, icon: str = "") -> None:
|
|
420
|
-
|
|
626
|
+
super().__init__(name=name)
|
|
421
627
|
self.ordering = ordering
|
|
422
628
|
self.icon = icon
|
|
423
629
|
|
|
@@ -428,123 +634,220 @@ event.listen(Theme.functionalities, "remove", cache_invalidate_cb)
|
|
|
428
634
|
|
|
429
635
|
|
|
430
636
|
class Layer(TreeItem):
|
|
637
|
+
"""The layer table representation."""
|
|
638
|
+
|
|
431
639
|
__tablename__ = "layer"
|
|
432
640
|
__table_args__ = {"schema": _schema}
|
|
433
|
-
__mapper_args__ = {"polymorphic_identity": "layer"} # needed for _identity_class
|
|
641
|
+
__mapper_args__ = {"polymorphic_identity": "layer"} # type: ignore[dict-item] # needed for _identity_class
|
|
434
642
|
|
|
435
|
-
id =
|
|
643
|
+
id: Mapped[int] = mapped_column(
|
|
436
644
|
Integer,
|
|
437
|
-
ForeignKey(_schema + ".treeitem.id"),
|
|
645
|
+
ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
|
|
438
646
|
primary_key=True,
|
|
439
647
|
info={"colanderalchemy": {"widget": HiddenWidget()}},
|
|
440
648
|
)
|
|
441
|
-
public
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
)
|
|
444
685
|
|
|
445
686
|
def __init__(self, name: str = "", public: bool = True) -> None:
|
|
446
|
-
|
|
687
|
+
super().__init__(name=name)
|
|
447
688
|
self.public = public
|
|
448
689
|
|
|
449
690
|
|
|
450
691
|
class DimensionLayer(Layer):
|
|
451
|
-
|
|
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
|
+
|
|
452
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"
|
|
453
703
|
|
|
454
|
-
OGCSERVER_TYPE_MAPSERVER = "mapserver"
|
|
455
|
-
OGCSERVER_TYPE_QGISSERVER = "qgisserver"
|
|
456
|
-
OGCSERVER_TYPE_GEOSERVER = "geoserver"
|
|
457
|
-
OGCSERVER_TYPE_ARCGIS = "arcgis"
|
|
458
|
-
OGCSERVER_TYPE_OTHER = "other"
|
|
459
704
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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"
|
|
464
710
|
|
|
465
711
|
|
|
466
|
-
|
|
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."""
|
|
719
|
+
|
|
467
720
|
__tablename__ = "ogc_server"
|
|
468
721
|
__table_args__ = {"schema": _schema}
|
|
469
|
-
__colanderalchemy_config__ = {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
)
|
|
484
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),
|
|
485
786
|
nullable=False,
|
|
486
787
|
info={
|
|
487
788
|
"colanderalchemy": {
|
|
488
789
|
"title": _("Server type"),
|
|
489
|
-
"
|
|
490
|
-
|
|
491
|
-
(OGCSERVER_TYPE_MAPSERVER, OGCSERVER_TYPE_MAPSERVER),
|
|
492
|
-
(OGCSERVER_TYPE_QGISSERVER, OGCSERVER_TYPE_QGISSERVER),
|
|
493
|
-
(OGCSERVER_TYPE_GEOSERVER, OGCSERVER_TYPE_GEOSERVER),
|
|
494
|
-
(OGCSERVER_TYPE_ARCGIS, OGCSERVER_TYPE_ARCGIS),
|
|
495
|
-
(OGCSERVER_TYPE_OTHER, OGCSERVER_TYPE_OTHER),
|
|
496
|
-
)
|
|
790
|
+
"description": _(
|
|
791
|
+
"The server type which is used to know which custom attribute will be used."
|
|
497
792
|
),
|
|
793
|
+
"widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerType))),
|
|
498
794
|
}
|
|
499
795
|
},
|
|
500
796
|
)
|
|
501
|
-
image_type =
|
|
502
|
-
Enum(
|
|
797
|
+
image_type: Mapped[ImageType] = mapped_column(
|
|
798
|
+
Enum(*get_args(ImageType), native_enum=False),
|
|
503
799
|
nullable=False,
|
|
504
800
|
info={
|
|
505
801
|
"colanderalchemy": {
|
|
506
802
|
"title": _("Image type"),
|
|
507
|
-
"
|
|
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))),
|
|
508
805
|
"column": 2,
|
|
509
806
|
}
|
|
510
807
|
},
|
|
511
808
|
)
|
|
512
|
-
auth =
|
|
513
|
-
Enum(
|
|
514
|
-
OGCSERVER_AUTH_NOAUTH,
|
|
515
|
-
OGCSERVER_AUTH_STANDARD,
|
|
516
|
-
OGCSERVER_AUTH_GEOSERVER,
|
|
517
|
-
OGCSERVER_AUTH_PROXY,
|
|
518
|
-
native_enum=False,
|
|
519
|
-
),
|
|
809
|
+
auth: Mapped[OGCServerAuth] = mapped_column(
|
|
810
|
+
Enum(*get_args(OGCServerAuth), native_enum=False),
|
|
520
811
|
nullable=False,
|
|
521
812
|
info={
|
|
522
813
|
"colanderalchemy": {
|
|
523
814
|
"title": _("Authentication type"),
|
|
524
|
-
"
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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)."),
|
|
532
837
|
"column": 2,
|
|
533
838
|
}
|
|
534
839
|
},
|
|
535
840
|
)
|
|
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}})
|
|
538
841
|
|
|
539
842
|
def __init__(
|
|
540
843
|
self,
|
|
541
844
|
name: str = "",
|
|
542
|
-
description:
|
|
845
|
+
description: str | None = None,
|
|
543
846
|
url: str = "https://wms.example.com",
|
|
544
|
-
url_wfs: str = None,
|
|
545
|
-
type_:
|
|
546
|
-
image_type:
|
|
547
|
-
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,
|
|
548
851
|
wfs_support: bool = True,
|
|
549
852
|
is_single_tile: bool = False,
|
|
550
853
|
) -> None:
|
|
@@ -559,42 +862,54 @@ class OGCServer(Base):
|
|
|
559
862
|
self.is_single_tile = is_single_tile
|
|
560
863
|
|
|
561
864
|
def __str__(self) -> str:
|
|
562
|
-
return self.name or ""
|
|
865
|
+
return self.name or ""
|
|
563
866
|
|
|
564
|
-
def url_description(self, request: Request) -> str:
|
|
565
|
-
errors:
|
|
867
|
+
def url_description(self, request: pyramid.request.Request) -> str:
|
|
868
|
+
errors: set[str] = set()
|
|
566
869
|
url = get_url2(self.name, self.url, request, errors)
|
|
567
|
-
return url
|
|
870
|
+
return url.url() if url else "\n".join(errors)
|
|
568
871
|
|
|
569
|
-
def url_wfs_description(self, request: Request) ->
|
|
872
|
+
def url_wfs_description(self, request: pyramid.request.Request) -> str | None:
|
|
570
873
|
if not self.url_wfs:
|
|
571
874
|
return self.url_description(request)
|
|
572
|
-
errors:
|
|
875
|
+
errors: set[str] = set()
|
|
573
876
|
url = get_url2(self.name, self.url_wfs, request, errors)
|
|
574
|
-
return url
|
|
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)
|
|
877
|
+
return url.url() if url else "\n".join(errors)
|
|
580
878
|
|
|
581
879
|
|
|
582
880
|
class LayerWMS(DimensionLayer):
|
|
881
|
+
"""The layer_wms table representation."""
|
|
882
|
+
|
|
583
883
|
__tablename__ = "layer_wms"
|
|
584
884
|
__table_args__ = {"schema": _schema}
|
|
585
|
-
__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
|
+
}
|
|
586
901
|
|
|
587
902
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
588
903
|
|
|
589
|
-
__mapper_args__ = {"polymorphic_identity": "l_wms"}
|
|
904
|
+
__mapper_args__ = {"polymorphic_identity": "l_wms"} # type: ignore[dict-item]
|
|
590
905
|
|
|
591
|
-
id =
|
|
906
|
+
id: Mapped[int] = mapped_column(
|
|
592
907
|
Integer,
|
|
593
908
|
ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
|
|
594
909
|
primary_key=True,
|
|
595
910
|
info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
|
|
596
911
|
)
|
|
597
|
-
ogc_server_id =
|
|
912
|
+
ogc_server_id: Mapped[int] = mapped_column(
|
|
598
913
|
Integer,
|
|
599
914
|
ForeignKey(_schema + ".ogc_server.id"),
|
|
600
915
|
nullable=False,
|
|
@@ -608,31 +923,68 @@ class LayerWMS(DimensionLayer):
|
|
|
608
923
|
}
|
|
609
924
|
},
|
|
610
925
|
)
|
|
611
|
-
layer =
|
|
612
|
-
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
|
+
},
|
|
613
953
|
)
|
|
614
|
-
|
|
615
|
-
valid = Column(
|
|
954
|
+
valid: Mapped[bool] = mapped_column(
|
|
616
955
|
Boolean,
|
|
617
|
-
|
|
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
|
+
},
|
|
618
966
|
)
|
|
619
|
-
invalid_reason =
|
|
967
|
+
invalid_reason: Mapped[str] = mapped_column(
|
|
620
968
|
Unicode,
|
|
969
|
+
nullable=True,
|
|
621
970
|
info={
|
|
622
971
|
"colanderalchemy": {
|
|
623
972
|
"title": _("Reason why I am not valid"),
|
|
973
|
+
"description": _("The reason for status reported by latest synchronization (readonly)."),
|
|
624
974
|
"column": 2,
|
|
625
975
|
"widget": TextInputWidget(readonly=True),
|
|
976
|
+
"missing": "",
|
|
626
977
|
}
|
|
627
978
|
},
|
|
628
979
|
)
|
|
629
|
-
time_mode =
|
|
630
|
-
Enum(
|
|
980
|
+
time_mode: Mapped[TimeMode] = mapped_column(
|
|
981
|
+
Enum(*get_args(TimeMode), native_enum=False),
|
|
631
982
|
default="disabled",
|
|
632
983
|
nullable=False,
|
|
633
984
|
info={
|
|
634
985
|
"colanderalchemy": {
|
|
635
986
|
"title": _("Time mode"),
|
|
987
|
+
"description": _("Used for the WMS time component."),
|
|
636
988
|
"column": 2,
|
|
637
989
|
"widget": SelectWidget(
|
|
638
990
|
values=(("disabled", _("Disabled")), ("value", _("Value")), ("range", _("Range")))
|
|
@@ -640,13 +992,14 @@ class LayerWMS(DimensionLayer):
|
|
|
640
992
|
}
|
|
641
993
|
},
|
|
642
994
|
)
|
|
643
|
-
time_widget =
|
|
644
|
-
Enum(
|
|
995
|
+
time_widget: Mapped[TimeWidget] = mapped_column(
|
|
996
|
+
Enum(*get_args(TimeWidget), native_enum=False),
|
|
645
997
|
default="slider",
|
|
646
998
|
nullable=False,
|
|
647
999
|
info={
|
|
648
1000
|
"colanderalchemy": {
|
|
649
1001
|
"title": _("Time widget"),
|
|
1002
|
+
"description": _("The component type used for the WMS time."),
|
|
650
1003
|
"column": 2,
|
|
651
1004
|
"widget": SelectWidget(values=(("slider", _("Slider")), ("datepicker", _("Datepicker")))),
|
|
652
1005
|
}
|
|
@@ -657,7 +1010,13 @@ class LayerWMS(DimensionLayer):
|
|
|
657
1010
|
ogc_server = relationship(
|
|
658
1011
|
"OGCServer",
|
|
659
1012
|
backref=backref("layers", info={"colanderalchemy": {"exclude": True, "title": _("WMS Layers")}}),
|
|
660
|
-
info={
|
|
1013
|
+
info={
|
|
1014
|
+
"colanderalchemy": {
|
|
1015
|
+
"title": _("OGC server"),
|
|
1016
|
+
"description": _("The OGC server to use for this layer."),
|
|
1017
|
+
"exclude": True,
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
661
1020
|
)
|
|
662
1021
|
|
|
663
1022
|
def __init__(
|
|
@@ -665,59 +1024,164 @@ class LayerWMS(DimensionLayer):
|
|
|
665
1024
|
name: str = "",
|
|
666
1025
|
layer: str = "",
|
|
667
1026
|
public: bool = True,
|
|
668
|
-
time_mode:
|
|
669
|
-
time_widget:
|
|
1027
|
+
time_mode: TimeMode = "disabled",
|
|
1028
|
+
time_widget: TimeWidget = "slider",
|
|
670
1029
|
) -> None:
|
|
671
|
-
|
|
1030
|
+
super().__init__(name=name, public=public)
|
|
672
1031
|
self.layer = layer
|
|
673
1032
|
self.time_mode = time_mode
|
|
674
1033
|
self.time_widget = time_widget
|
|
675
1034
|
|
|
676
1035
|
@staticmethod
|
|
677
|
-
def get_default(dbsession: Session) -> DimensionLayer:
|
|
678
|
-
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
|
+
)
|
|
679
1041
|
|
|
680
1042
|
|
|
681
1043
|
class LayerWMTS(DimensionLayer):
|
|
1044
|
+
"""The layer_wmts table representation."""
|
|
1045
|
+
|
|
682
1046
|
__tablename__ = "layer_wmts"
|
|
683
1047
|
__table_args__ = {"schema": _schema}
|
|
684
|
-
__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
|
+
}
|
|
685
1098
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
686
|
-
__mapper_args__ = {"polymorphic_identity": "l_wmts"}
|
|
1099
|
+
__mapper_args__ = {"polymorphic_identity": "l_wmts"} # type: ignore[dict-item]
|
|
687
1100
|
|
|
688
|
-
id =
|
|
1101
|
+
id: Mapped[int] = mapped_column(
|
|
689
1102
|
Integer,
|
|
690
|
-
ForeignKey(_schema + ".layer.id"),
|
|
1103
|
+
ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
|
|
691
1104
|
primary_key=True,
|
|
692
1105
|
info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
|
|
693
1106
|
)
|
|
694
|
-
url =
|
|
695
|
-
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
|
+
},
|
|
696
1117
|
)
|
|
697
|
-
layer =
|
|
698
|
-
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
|
+
},
|
|
699
1155
|
)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
image_type = Column(
|
|
703
|
-
Enum("image/jpeg", "image/png", native_enum=False),
|
|
1156
|
+
image_type: Mapped[ImageType] = mapped_column(
|
|
1157
|
+
Enum(*get_args(ImageType), native_enum=False),
|
|
704
1158
|
nullable=False,
|
|
705
1159
|
info={
|
|
706
1160
|
"colanderalchemy": {
|
|
707
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
|
+
),
|
|
708
1169
|
"column": 2,
|
|
709
|
-
"widget": SelectWidget(values=((
|
|
1170
|
+
"widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
|
|
710
1171
|
}
|
|
711
1172
|
},
|
|
712
1173
|
)
|
|
713
1174
|
|
|
714
|
-
def __init__(self, name: str = "", public: bool = True, image_type:
|
|
715
|
-
|
|
1175
|
+
def __init__(self, name: str = "", public: bool = True, image_type: ImageType = "image/png") -> None:
|
|
1176
|
+
super().__init__(name=name, public=public)
|
|
716
1177
|
self.image_type = image_type
|
|
717
1178
|
|
|
718
1179
|
@staticmethod
|
|
719
|
-
def get_default(dbsession: Session) -> DimensionLayer:
|
|
720
|
-
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
|
+
)
|
|
721
1185
|
|
|
722
1186
|
|
|
723
1187
|
# association table role <> restriction area
|
|
@@ -749,61 +1213,224 @@ layer_ra = Table(
|
|
|
749
1213
|
)
|
|
750
1214
|
|
|
751
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
|
+
|
|
752
1268
|
class LayerVectorTiles(DimensionLayer):
|
|
1269
|
+
"""The layer_vectortiles table representation."""
|
|
1270
|
+
|
|
753
1271
|
__tablename__ = "layer_vectortiles"
|
|
754
1272
|
__table_args__ = {"schema": _schema}
|
|
755
|
-
__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
|
+
}
|
|
756
1307
|
|
|
757
1308
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
758
1309
|
|
|
759
|
-
__mapper_args__ = {"polymorphic_identity": "l_mvt"}
|
|
1310
|
+
__mapper_args__ = {"polymorphic_identity": "l_mvt"} # type: ignore[dict-item]
|
|
760
1311
|
|
|
761
|
-
id =
|
|
1312
|
+
id: Mapped[int] = mapped_column(
|
|
762
1313
|
Integer,
|
|
763
1314
|
ForeignKey(_schema + ".layer.id"),
|
|
764
1315
|
primary_key=True,
|
|
765
1316
|
info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
|
|
766
1317
|
)
|
|
767
1318
|
|
|
768
|
-
style =
|
|
1319
|
+
style: Mapped[str] = mapped_column(
|
|
769
1320
|
Unicode,
|
|
770
1321
|
nullable=False,
|
|
771
1322
|
info={
|
|
772
1323
|
"colanderalchemy": {
|
|
773
1324
|
"title": _("Style"),
|
|
774
|
-
"description":
|
|
1325
|
+
"description": _(
|
|
1326
|
+
"""
|
|
1327
|
+
The path to a Mapbox Style file (version 8 or higher). Example: https://url/style.json
|
|
1328
|
+
"""
|
|
1329
|
+
),
|
|
1330
|
+
"column": 2,
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
)
|
|
1334
|
+
|
|
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
|
+
),
|
|
775
1346
|
"column": 2,
|
|
1347
|
+
"widget": TextAreaWidget(rows=15),
|
|
776
1348
|
}
|
|
777
1349
|
},
|
|
778
1350
|
)
|
|
779
1351
|
|
|
780
|
-
xyz =
|
|
1352
|
+
xyz: Mapped[str] = mapped_column(
|
|
781
1353
|
Unicode,
|
|
782
1354
|
nullable=True,
|
|
783
1355
|
info={
|
|
784
1356
|
"colanderalchemy": {
|
|
785
1357
|
"title": _("Raster URL"),
|
|
786
|
-
"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
|
+
),
|
|
787
1364
|
"column": 2,
|
|
788
1365
|
}
|
|
789
1366
|
},
|
|
790
1367
|
)
|
|
791
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."""
|
|
792
1391
|
|
|
793
|
-
class RestrictionArea(Base):
|
|
794
1392
|
__tablename__ = "restrictionarea"
|
|
795
1393
|
__table_args__ = {"schema": _schema}
|
|
796
1394
|
__colanderalchemy_config__ = {"title": _("Restriction area"), "plural": _("Restriction areas")}
|
|
797
1395
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
798
|
-
id
|
|
1396
|
+
id: Mapped[int] = mapped_column(
|
|
1397
|
+
Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
|
|
1398
|
+
)
|
|
799
1399
|
|
|
800
|
-
name
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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(
|
|
804
1429
|
Geometry("POLYGON", srid=_srid),
|
|
805
1430
|
info={
|
|
806
1431
|
"colanderalchemy": {
|
|
1432
|
+
"title": _("Area"),
|
|
1433
|
+
"description": _("Active in the following area, if not defined, it is active everywhere."),
|
|
807
1434
|
"typ": ColanderGeometry("POLYGON", srid=_srid, map_srid=_map_config["srid"]),
|
|
808
1435
|
"widget": MapWidget(map_options=_map_config),
|
|
809
1436
|
}
|
|
@@ -814,20 +1441,57 @@ class RestrictionArea(Base):
|
|
|
814
1441
|
roles = relationship(
|
|
815
1442
|
"Role",
|
|
816
1443
|
secondary=role_ra,
|
|
817
|
-
info={
|
|
1444
|
+
info={
|
|
1445
|
+
"colanderalchemy": {
|
|
1446
|
+
"title": _("Roles"),
|
|
1447
|
+
"description": _("Checked roles will grant access to this restriction area."),
|
|
1448
|
+
"exclude": True,
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
818
1451
|
cascade="save-update,merge,refresh-expire",
|
|
819
1452
|
backref=backref(
|
|
820
|
-
"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
|
+
},
|
|
821
1463
|
),
|
|
822
1464
|
)
|
|
823
1465
|
layers = relationship(
|
|
824
1466
|
"Layer",
|
|
825
1467
|
secondary=layer_ra,
|
|
826
1468
|
order_by=Layer.name,
|
|
827
|
-
info={
|
|
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
|
+
},
|
|
828
1485
|
cascade="save-update,merge,refresh-expire",
|
|
829
1486
|
backref=backref(
|
|
830
|
-
"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
|
+
},
|
|
831
1495
|
),
|
|
832
1496
|
)
|
|
833
1497
|
|
|
@@ -835,9 +1499,9 @@ class RestrictionArea(Base):
|
|
|
835
1499
|
self,
|
|
836
1500
|
name: str = "",
|
|
837
1501
|
description: str = "",
|
|
838
|
-
layers:
|
|
839
|
-
roles:
|
|
840
|
-
area: Geometry = None,
|
|
1502
|
+
layers: list[Layer] | None = None,
|
|
1503
|
+
roles: list[Role] | None = None,
|
|
1504
|
+
area: Geometry | None = None,
|
|
841
1505
|
readwrite: bool = False,
|
|
842
1506
|
) -> None:
|
|
843
1507
|
if layers is None:
|
|
@@ -851,8 +1515,8 @@ class RestrictionArea(Base):
|
|
|
851
1515
|
self.area = area
|
|
852
1516
|
self.readwrite = readwrite
|
|
853
1517
|
|
|
854
|
-
def __str__(self) -> str:
|
|
855
|
-
return self.name
|
|
1518
|
+
def __str__(self) -> str:
|
|
1519
|
+
return f"{self.name}[{self.id}]"
|
|
856
1520
|
|
|
857
1521
|
|
|
858
1522
|
event.listen(RestrictionArea, "after_insert", cache_invalidate_cb)
|
|
@@ -883,15 +1547,35 @@ interface_theme = Table(
|
|
|
883
1547
|
)
|
|
884
1548
|
|
|
885
1549
|
|
|
886
|
-
class Interface(Base):
|
|
1550
|
+
class Interface(Base): # type: ignore
|
|
1551
|
+
"""The interface table representation."""
|
|
1552
|
+
|
|
887
1553
|
__tablename__ = "interface"
|
|
888
1554
|
__table_args__ = {"schema": _schema}
|
|
889
1555
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
890
1556
|
__colanderalchemy_config__ = {"title": _("Interface"), "plural": _("Interfaces")}
|
|
891
1557
|
|
|
892
|
-
id
|
|
893
|
-
|
|
894
|
-
|
|
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
|
+
)
|
|
895
1579
|
|
|
896
1580
|
# relationship with Layer and Theme
|
|
897
1581
|
layers = relationship(
|
|
@@ -899,39 +1583,92 @@ class Interface(Base):
|
|
|
899
1583
|
secondary=interface_layer,
|
|
900
1584
|
cascade="save-update,merge,refresh-expire",
|
|
901
1585
|
info={"colanderalchemy": {"title": _("Layers"), "exclude": True}, "c2cgeoform": {"duplicate": False}},
|
|
902
|
-
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
|
+
),
|
|
903
1596
|
)
|
|
904
1597
|
theme = relationship(
|
|
905
1598
|
"Theme",
|
|
906
1599
|
secondary=interface_theme,
|
|
907
1600
|
cascade="save-update,merge,refresh-expire",
|
|
908
1601
|
info={"colanderalchemy": {"title": _("Themes"), "exclude": True}, "c2cgeoform": {"duplicate": False}},
|
|
909
|
-
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
|
+
),
|
|
910
1612
|
)
|
|
911
1613
|
|
|
912
1614
|
def __init__(self, name: str = "", description: str = "") -> None:
|
|
913
1615
|
self.name = name
|
|
914
1616
|
self.description = description
|
|
915
1617
|
|
|
916
|
-
def __str__(self) -> str:
|
|
917
|
-
return self.name
|
|
1618
|
+
def __str__(self) -> str:
|
|
1619
|
+
return f"{self.name}[{self.id}]"
|
|
1620
|
+
|
|
918
1621
|
|
|
1622
|
+
class Metadata(Base): # type: ignore
|
|
1623
|
+
"""The metadata table representation."""
|
|
919
1624
|
|
|
920
|
-
class Metadata(Base):
|
|
921
1625
|
__tablename__ = "metadata"
|
|
922
1626
|
__table_args__ = {"schema": _schema}
|
|
1627
|
+
__colanderalchemy_config__ = {
|
|
1628
|
+
"title": _("Metadata"),
|
|
1629
|
+
"plural": _("Metadatas"),
|
|
1630
|
+
}
|
|
923
1631
|
|
|
924
|
-
id
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
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
|
+
},
|
|
929
1666
|
)
|
|
930
1667
|
|
|
931
|
-
item_id =
|
|
1668
|
+
item_id: Mapped[int] = mapped_column(
|
|
932
1669
|
"item_id",
|
|
933
1670
|
Integer,
|
|
934
|
-
ForeignKey(_schema + ".treeitem.id"),
|
|
1671
|
+
ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
|
|
935
1672
|
nullable=False,
|
|
936
1673
|
info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
|
|
937
1674
|
)
|
|
@@ -942,17 +1679,49 @@ class Metadata(Base):
|
|
|
942
1679
|
"metadatas",
|
|
943
1680
|
cascade="save-update,merge,delete,delete-orphan,expunge",
|
|
944
1681
|
order_by="Metadata.name",
|
|
945
|
-
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
|
+
},
|
|
946
1715
|
),
|
|
947
1716
|
)
|
|
948
1717
|
|
|
949
|
-
def __init__(self, name: str = "", value: str = "", description: str = None) -> None:
|
|
1718
|
+
def __init__(self, name: str = "", value: str = "", description: str | None = None) -> None:
|
|
950
1719
|
self.name = name
|
|
951
1720
|
self.value = value
|
|
952
1721
|
self.description = description
|
|
953
1722
|
|
|
954
|
-
def __str__(self) -> str:
|
|
955
|
-
return "{}
|
|
1723
|
+
def __str__(self) -> str:
|
|
1724
|
+
return f"{self.name}={self.value}[{self.id}]"
|
|
956
1725
|
|
|
957
1726
|
|
|
958
1727
|
event.listen(Metadata, "after_insert", cache_invalidate_cb, propagate=True)
|
|
@@ -960,19 +1729,61 @@ event.listen(Metadata, "after_update", cache_invalidate_cb, propagate=True)
|
|
|
960
1729
|
event.listen(Metadata, "after_delete", cache_invalidate_cb, propagate=True)
|
|
961
1730
|
|
|
962
1731
|
|
|
963
|
-
class Dimension(Base):
|
|
1732
|
+
class Dimension(Base): # type: ignore
|
|
1733
|
+
"""The dimension table representation."""
|
|
1734
|
+
|
|
964
1735
|
__tablename__ = "dimension"
|
|
965
1736
|
__table_args__ = {"schema": _schema}
|
|
1737
|
+
__colanderalchemy_config__ = {
|
|
1738
|
+
"title": _("Dimension"),
|
|
1739
|
+
"plural": _("Dimensions"),
|
|
1740
|
+
}
|
|
966
1741
|
|
|
967
|
-
id
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
+
},
|
|
973
1784
|
)
|
|
974
1785
|
|
|
975
|
-
layer_id =
|
|
1786
|
+
layer_id: Mapped[int] = mapped_column(
|
|
976
1787
|
"layer_id",
|
|
977
1788
|
Integer,
|
|
978
1789
|
ForeignKey(_schema + ".layer.id"),
|
|
@@ -985,12 +1796,32 @@ class Dimension(Base):
|
|
|
985
1796
|
backref=backref(
|
|
986
1797
|
"dimensions",
|
|
987
1798
|
cascade="save-update,merge,delete,delete-orphan,expunge",
|
|
988
|
-
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
|
+
},
|
|
989
1815
|
),
|
|
990
1816
|
)
|
|
991
1817
|
|
|
992
1818
|
def __init__(
|
|
993
|
-
self,
|
|
1819
|
+
self,
|
|
1820
|
+
name: str = "",
|
|
1821
|
+
value: str = "",
|
|
1822
|
+
layer: str | None = None,
|
|
1823
|
+
field: str | None = None,
|
|
1824
|
+
description: str | None = None,
|
|
994
1825
|
) -> None:
|
|
995
1826
|
self.name = name
|
|
996
1827
|
self.value = value
|
|
@@ -999,5 +1830,102 @@ class Dimension(Base):
|
|
|
999
1830
|
self.layer = layer
|
|
1000
1831
|
self.description = description
|
|
1001
1832
|
|
|
1002
|
-
def __str__(self) -> str:
|
|
1003
|
-
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
|
+
}
|