c2cgeoportal-admin 2.6.0__py3-none-any.whl → 2.8.1.180__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cgeoportal_admin/__init__.py +32 -10
- c2cgeoportal_admin/lib/lingua_extractor.py +77 -0
- c2cgeoportal_admin/lib/ogcserver_synchronizer.py +168 -56
- c2cgeoportal_admin/py.typed +0 -0
- c2cgeoportal_admin/routes.py +7 -4
- c2cgeoportal_admin/schemas/dimensions.py +12 -10
- c2cgeoportal_admin/schemas/functionalities.py +62 -21
- c2cgeoportal_admin/schemas/interfaces.py +22 -18
- c2cgeoportal_admin/schemas/metadata.py +100 -47
- c2cgeoportal_admin/schemas/restriction_areas.py +21 -19
- c2cgeoportal_admin/schemas/roles.py +7 -5
- c2cgeoportal_admin/schemas/treegroup.py +38 -17
- c2cgeoportal_admin/schemas/treeitem.py +2 -3
- c2cgeoportal_admin/static/layertree.css +3 -4
- c2cgeoportal_admin/static/navbar.css +36 -35
- c2cgeoportal_admin/static/theme.css +16 -9
- c2cgeoportal_admin/subscribers.py +3 -3
- c2cgeoportal_admin/templates/404.jinja2 +18 -2
- c2cgeoportal_admin/templates/layertree.jinja2 +31 -9
- c2cgeoportal_admin/templates/navigation_navbar.jinja2 +33 -0
- c2cgeoportal_admin/templates/ogcserver_synchronize.jinja2 +12 -0
- c2cgeoportal_admin/templates/widgets/functionality_fields.pt +51 -0
- c2cgeoportal_admin/templates/widgets/metadata.pt +7 -1
- c2cgeoportal_admin/views/__init__.py +29 -0
- c2cgeoportal_admin/views/dimension_layers.py +7 -6
- c2cgeoportal_admin/views/functionalities.py +33 -6
- c2cgeoportal_admin/views/home.py +5 -5
- c2cgeoportal_admin/views/interfaces.py +7 -8
- c2cgeoportal_admin/views/layer_groups.py +9 -11
- c2cgeoportal_admin/views/layers.py +8 -7
- c2cgeoportal_admin/views/layers_vectortiles.py +30 -10
- c2cgeoportal_admin/views/layers_wms.py +41 -35
- c2cgeoportal_admin/views/layers_wmts.py +41 -35
- c2cgeoportal_admin/views/layertree.py +36 -28
- c2cgeoportal_admin/views/logged_views.py +80 -0
- c2cgeoportal_admin/views/logs.py +90 -0
- c2cgeoportal_admin/views/oauth2_clients.py +30 -22
- c2cgeoportal_admin/views/ogc_servers.py +119 -37
- c2cgeoportal_admin/views/restriction_areas.py +13 -11
- c2cgeoportal_admin/views/roles.py +16 -12
- c2cgeoportal_admin/views/themes.py +15 -14
- c2cgeoportal_admin/views/themes_ordering.py +13 -8
- c2cgeoportal_admin/views/treeitems.py +14 -12
- c2cgeoportal_admin/views/users.py +12 -7
- c2cgeoportal_admin/widgets.py +17 -14
- {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.8.1.180.dist-info}/METADATA +17 -8
- c2cgeoportal_admin-2.8.1.180.dist-info/RECORD +95 -0
- {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.8.1.180.dist-info}/WHEEL +1 -1
- c2cgeoportal_admin-2.8.1.180.dist-info/entry_points.txt +6 -0
- tests/__init__.py +11 -12
- tests/conftest.py +2 -1
- tests/test_edit_url.py +11 -14
- tests/test_functionalities.py +52 -14
- tests/test_home.py +0 -1
- tests/test_interface.py +34 -11
- tests/test_layer_groups.py +57 -27
- tests/test_layers_vectortiles.py +43 -20
- tests/test_layers_wms.py +67 -45
- tests/test_layers_wmts.py +47 -26
- tests/test_layertree.py +99 -16
- tests/test_left_menu.py +0 -1
- tests/test_lingua_extractor_config.py +64 -0
- tests/test_logs.py +103 -0
- tests/test_main.py +3 -1
- tests/test_metadatas.py +34 -21
- tests/test_oauth2_clients.py +37 -9
- tests/test_ogc_servers.py +84 -35
- tests/test_restriction_areas.py +38 -15
- tests/test_role.py +68 -41
- tests/test_themes.py +71 -37
- tests/test_themes_ordering.py +1 -2
- tests/test_treegroup.py +2 -2
- tests/test_user.py +48 -17
- tests/themes_ordering.py +1 -2
- c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -33
- c2cgeoportal_admin-2.6.0.dist-info/RECORD +0 -89
- c2cgeoportal_admin-2.6.0.dist-info/entry_points.txt +0 -3
- {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.8.1.180.dist-info}/top_level.txt +0 -0
c2cgeoportal_admin/__init__.py
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2017-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2017-2023, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -28,8 +26,11 @@
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
30
28
|
|
29
|
+
from typing import Any
|
30
|
+
|
31
31
|
import c2cgeoform
|
32
32
|
import c2cwsgiutils.pretty_json
|
33
|
+
import sqlalchemy
|
33
34
|
import zope.sqlalchemy
|
34
35
|
from c2c.template.config import config as configuration
|
35
36
|
from pkg_resources import resource_filename
|
@@ -37,9 +38,11 @@ from pyramid.config import Configurator
|
|
37
38
|
from pyramid.events import BeforeRender, NewRequest
|
38
39
|
from sqlalchemy import engine_from_config
|
39
40
|
from sqlalchemy.orm import configure_mappers, sessionmaker
|
41
|
+
from transaction import TransactionManager
|
40
42
|
from translationstring import TranslationStringFactory
|
41
43
|
|
42
44
|
from c2cgeoportal_admin.subscribers import add_localizer, add_renderer_globals
|
45
|
+
from c2cgeoportal_admin.views import IsAdminPredicate
|
43
46
|
|
44
47
|
search_paths = (resource_filename(__name__, "templates/widgets"),) + c2cgeoform.default_search_paths
|
45
48
|
c2cgeoform.default_search_paths = search_paths
|
@@ -48,9 +51,7 @@ _ = TranslationStringFactory("c2cgeoportal_admin")
|
|
48
51
|
|
49
52
|
|
50
53
|
def main(_, **settings):
|
51
|
-
"""
|
52
|
-
This function returns a Pyramid WSGI application.
|
53
|
-
"""
|
54
|
+
"""Return a Pyramid WSGI application."""
|
54
55
|
configuration.init(settings.get("app.cfg"))
|
55
56
|
settings.update(configuration.get_config())
|
56
57
|
|
@@ -68,7 +69,9 @@ def main(_, **settings):
|
|
68
69
|
session_factory = sessionmaker()
|
69
70
|
session_factory.configure(bind=engine)
|
70
71
|
|
71
|
-
def get_tm_session(
|
72
|
+
def get_tm_session(
|
73
|
+
session_factory: sessionmaker, transaction_manager: TransactionManager
|
74
|
+
) -> sqlalchemy.orm.session.Session:
|
72
75
|
dbsession = session_factory()
|
73
76
|
zope.sqlalchemy.register(dbsession, transaction_manager=transaction_manager)
|
74
77
|
return dbsession
|
@@ -81,6 +84,19 @@ def main(_, **settings):
|
|
81
84
|
reify=True,
|
82
85
|
)
|
83
86
|
|
87
|
+
# Add fake user as we do not have authentication from geoportal
|
88
|
+
from c2cgeoportal_commons.models.static import User # pylint: disable=import-outside-toplevel
|
89
|
+
|
90
|
+
config.add_request_method(
|
91
|
+
lambda request: User(
|
92
|
+
username="test_user",
|
93
|
+
),
|
94
|
+
name="user",
|
95
|
+
property=True,
|
96
|
+
)
|
97
|
+
|
98
|
+
config.add_route("ogc_server_clear_cache", "/ogc_server_clear_cache/{id}")
|
99
|
+
|
84
100
|
config.add_subscriber(add_renderer_globals, BeforeRender)
|
85
101
|
config.add_subscriber(add_localizer, NewRequest)
|
86
102
|
|
@@ -88,7 +104,9 @@ def main(_, **settings):
|
|
88
104
|
|
89
105
|
|
90
106
|
class PermissionSetter:
|
91
|
-
|
107
|
+
"""Set the permission to the admin user."""
|
108
|
+
|
109
|
+
def __init__(self, config: Configurator):
|
92
110
|
self.default_permission_to_revert = None
|
93
111
|
self.config = config
|
94
112
|
|
@@ -100,12 +118,13 @@ class PermissionSetter:
|
|
100
118
|
]["introspectable"]["value"]
|
101
119
|
self.config.set_default_permission("admin")
|
102
120
|
|
103
|
-
def __exit__(self, _type, value, traceback):
|
121
|
+
def __exit__(self, _type: Any, value: Any, traceback: Any) -> None:
|
104
122
|
self.config.commit() # avoid .ConfigurationConflictError
|
105
123
|
self.config.set_default_permission(self.default_permission_to_revert)
|
106
124
|
|
107
125
|
|
108
|
-
def includeme(config: Configurator):
|
126
|
+
def includeme(config: Configurator) -> None:
|
127
|
+
"""Initialize the Pyramid application."""
|
109
128
|
config.include("pyramid_jinja2")
|
110
129
|
config.include("c2cgeoform")
|
111
130
|
config.include("c2cgeoportal_commons")
|
@@ -113,6 +132,9 @@ def includeme(config: Configurator):
|
|
113
132
|
# Use pyramid_tm to hook the transaction lifecycle to the request
|
114
133
|
config.include("pyramid_tm")
|
115
134
|
config.add_translation_dirs("c2cgeoportal_admin:locale")
|
135
|
+
config.add_view_predicate("is_admin", IsAdminPredicate)
|
136
|
+
|
137
|
+
configure_mappers()
|
116
138
|
|
117
139
|
with PermissionSetter(config):
|
118
140
|
config.scan()
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# Copyright (c) 2011-2022, Camptocamp SA
|
2
|
+
# All rights reserved.
|
3
|
+
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
# list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
# this list of conditions and the following disclaimer in the documentation
|
11
|
+
# and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
14
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
15
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
17
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
18
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
19
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
20
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
21
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
22
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
23
|
+
|
24
|
+
# The views and conclusions contained in the software and documentation are those
|
25
|
+
# of the authors and should not be interpreted as representing official policies,
|
26
|
+
# either expressed or implied, of the FreeBSD Project.
|
27
|
+
|
28
|
+
|
29
|
+
import os
|
30
|
+
from typing import Any, Dict, List, Optional
|
31
|
+
|
32
|
+
import yaml
|
33
|
+
from lingua.extractors import Extractor, Message
|
34
|
+
|
35
|
+
|
36
|
+
class GeomapfishConfigExtractor(Extractor): # type: ignore
|
37
|
+
"""GeoMapFish config extractor (raster layers, and print templates)."""
|
38
|
+
|
39
|
+
extensions = [".yaml"]
|
40
|
+
|
41
|
+
def __call__(
|
42
|
+
self,
|
43
|
+
filename: str,
|
44
|
+
options: Dict[str, Any],
|
45
|
+
fileobj: Optional[Dict[str, Any]] = None,
|
46
|
+
lineno: int = 0,
|
47
|
+
) -> List[Message]:
|
48
|
+
del options, fileobj, lineno
|
49
|
+
|
50
|
+
print(f"Running {self.__class__.__name__} on {filename}")
|
51
|
+
|
52
|
+
with open(filename, encoding="utf8") as config_file:
|
53
|
+
data = config_file.read()
|
54
|
+
settings = yaml.load(
|
55
|
+
data.replace("{{cookiecutter.geomapfish_main_version}}", os.environ["MAJOR_VERSION"]),
|
56
|
+
Loader=yaml.SafeLoader,
|
57
|
+
)
|
58
|
+
|
59
|
+
admin_interface = settings.get("vars", {}).get("admin_interface", {})
|
60
|
+
|
61
|
+
available_metadata = []
|
62
|
+
for elem in admin_interface.get("available_metadata", []):
|
63
|
+
if "description" in elem:
|
64
|
+
location = f"admin_interface/available_metadata/{elem.get('name', '')}"
|
65
|
+
available_metadata.append(
|
66
|
+
Message(None, elem["description"].strip(), None, [], "", "", (filename, location))
|
67
|
+
)
|
68
|
+
|
69
|
+
available_functionalities = []
|
70
|
+
for elem in admin_interface.get("available_functionalities", []):
|
71
|
+
if "description" in elem:
|
72
|
+
location = f"admin_interface/available_functionalities/{elem.get('name', '')}"
|
73
|
+
available_functionalities.append(
|
74
|
+
Message(None, elem["description"].strip(), None, [], "", "", (filename, location))
|
75
|
+
)
|
76
|
+
|
77
|
+
return available_metadata + available_functionalities
|
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2020-2023, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -28,35 +26,71 @@
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
30
28
|
|
29
|
+
import functools
|
31
30
|
import logging
|
32
31
|
from io import StringIO
|
33
|
-
from typing import
|
32
|
+
from typing import Any, Optional, Set, cast
|
33
|
+
from xml.etree.ElementTree import Element # nosec
|
34
34
|
|
35
|
+
import pyramid.request
|
35
36
|
import requests
|
36
37
|
from defusedxml import ElementTree
|
38
|
+
from sqlalchemy.orm.session import Session
|
37
39
|
|
38
|
-
from c2cgeoportal_commons.lib.url import
|
40
|
+
from c2cgeoportal_commons.lib.url import get_url2
|
39
41
|
from c2cgeoportal_commons.models import main
|
40
42
|
|
41
43
|
|
42
|
-
class dry_run_transaction: # noqa N801: class names should use CapWords convention
|
43
|
-
def __init__(self, dbsession, dry_run):
|
44
|
+
class dry_run_transaction: # noqa ignore=N801: class names should use CapWords convention
|
45
|
+
def __init__(self, dbsession: Session, dry_run: bool):
|
44
46
|
self.dbsession = dbsession
|
45
47
|
self.dry_run = dry_run
|
46
48
|
|
47
|
-
def __enter__(self):
|
49
|
+
def __enter__(self) -> None:
|
48
50
|
if self.dry_run:
|
49
51
|
self.dbsession.begin_nested()
|
50
52
|
|
51
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
53
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
52
54
|
if self.dry_run:
|
53
55
|
self.dbsession.rollback()
|
54
56
|
|
55
57
|
|
56
58
|
class OGCServerSynchronizer:
|
57
|
-
|
59
|
+
"""A processor which imports WMS Capabilities in layer tree."""
|
60
|
+
|
61
|
+
def __init__(
|
62
|
+
self,
|
63
|
+
request: pyramid.request.Request,
|
64
|
+
ogc_server: main.OGCServer,
|
65
|
+
force_parents: bool = False,
|
66
|
+
force_ordering: bool = False,
|
67
|
+
clean: bool = False,
|
68
|
+
) -> None:
|
69
|
+
"""
|
70
|
+
Initialize the Synchronizer.
|
71
|
+
|
72
|
+
request
|
73
|
+
The current pyramid request object. Used to retrieve the SQLAlchemy Session object,
|
74
|
+
and to construct the capabilities URL.
|
75
|
+
|
76
|
+
ogc_server
|
77
|
+
The considered OGCServer from witch to import the capabilities.
|
78
|
+
|
79
|
+
force_parents
|
80
|
+
When set to True, overwrite parents of each node with those from the capabilities.
|
81
|
+
|
82
|
+
force_ordering
|
83
|
+
When set to True, sort children of each node in order from the capabilities.
|
84
|
+
|
85
|
+
clean
|
86
|
+
When set to True, remove layers which do not exist in capabilities and remove all empty groups.
|
87
|
+
"""
|
58
88
|
self._request = request
|
59
89
|
self._ogc_server = ogc_server
|
90
|
+
self._force_parents = force_parents
|
91
|
+
self._force_ordering = force_ordering
|
92
|
+
self._clean = clean
|
93
|
+
|
60
94
|
self._default_wms = main.LayerWMS()
|
61
95
|
self._interfaces = None
|
62
96
|
|
@@ -68,18 +102,20 @@ class OGCServerSynchronizer:
|
|
68
102
|
self._items_found = 0
|
69
103
|
self._themes_added = 0
|
70
104
|
self._groups_added = 0
|
105
|
+
self._groups_removed = 0
|
71
106
|
self._layers_added = 0
|
107
|
+
self._layers_removed = 0
|
72
108
|
|
73
|
-
def __str__(self):
|
74
|
-
return "OGCServerSynchronizer({
|
109
|
+
def __str__(self) -> str:
|
110
|
+
return f"OGCServerSynchronizer({self._ogc_server.name})"
|
75
111
|
|
76
|
-
def logger(self):
|
112
|
+
def logger(self) -> logging.Logger:
|
77
113
|
return self._logger
|
78
114
|
|
79
|
-
def report(self):
|
115
|
+
def report(self) -> str:
|
80
116
|
return self._log.getvalue()
|
81
117
|
|
82
|
-
def check_layers(self):
|
118
|
+
def check_layers(self) -> None:
|
83
119
|
capabilities = ElementTree.fromstring(self.wms_capabilities())
|
84
120
|
layers = self._request.dbsession.query(main.LayerWMS).filter(
|
85
121
|
main.LayerWMS.ogc_server == self._ogc_server
|
@@ -90,15 +126,24 @@ class OGCServerSynchronizer:
|
|
90
126
|
valid = True
|
91
127
|
reason = None
|
92
128
|
for name in layer.layer.split(","):
|
93
|
-
|
129
|
+
if "'" in name:
|
130
|
+
valid = False
|
131
|
+
reason = "Layer name contains quote"
|
132
|
+
self._logger.info(reason)
|
133
|
+
break
|
134
|
+
el = capabilities.find(f".//Layer[Name='{name}']")
|
94
135
|
if el is None:
|
95
136
|
valid = False
|
96
|
-
reason = "Layer {} does not exists on OGC server"
|
137
|
+
reason = f"Layer {name} does not exists on OGC server"
|
97
138
|
self._logger.info(reason)
|
139
|
+
if self._clean:
|
140
|
+
self._request.dbsession.delete(layer)
|
141
|
+
self._logger.info("Removed layer %s", name)
|
142
|
+
self._layers_removed += 1
|
98
143
|
break
|
99
|
-
if layer.style and el.find("./Style/Name[.='{}']"
|
144
|
+
if layer.style and el.find(f"./Style/Name[.='{layer.style}']") is None:
|
100
145
|
valid = False
|
101
|
-
reason = "Style {} does not exists in Layer {}"
|
146
|
+
reason = f"Style {layer.style} does not exists in Layer {name}"
|
102
147
|
self._logger.info(reason)
|
103
148
|
break
|
104
149
|
layer.valid = valid
|
@@ -106,19 +151,36 @@ class OGCServerSynchronizer:
|
|
106
151
|
invalids += 1
|
107
152
|
layer.invalid_reason = reason
|
108
153
|
items += 1
|
154
|
+
|
155
|
+
if self._clean:
|
156
|
+
groups = self._request.dbsession.query(main.LayerGroup)
|
157
|
+
for group in groups:
|
158
|
+
if len(group.children_relation) == 0:
|
159
|
+
self._request.dbsession.delete(group)
|
160
|
+
self._logger.info("Removed empty group %s", group.name)
|
161
|
+
self._groups_removed += 1
|
162
|
+
|
109
163
|
self._logger.info("Checked %s layers, %s are invalid", items, invalids)
|
110
164
|
|
111
|
-
def synchronize(self, dry_run=False):
|
165
|
+
def synchronize(self, dry_run: bool = False) -> None:
|
166
|
+
"""
|
167
|
+
Run the import of capabilities in layer tree.
|
168
|
+
|
169
|
+
dry_run
|
170
|
+
When set to True, do not commit but roll back transaction at end of synchronization.
|
171
|
+
"""
|
112
172
|
with dry_run_transaction(self._request.dbsession, dry_run):
|
113
173
|
self.do_synchronize()
|
114
174
|
if dry_run:
|
115
175
|
self._logger.info("Rolling back transaction due to dry run")
|
116
176
|
|
117
|
-
def do_synchronize(self):
|
177
|
+
def do_synchronize(self) -> None:
|
118
178
|
self._items_found = 0
|
119
179
|
self._themes_added = 0
|
120
180
|
self._groups_added = 0
|
181
|
+
self._groups_removed = 0
|
121
182
|
self._layers_added = 0
|
183
|
+
self._layers_removed = 0
|
122
184
|
|
123
185
|
self._default_wms = cast(
|
124
186
|
main.LayerWMS, main.LayerWMS.get_default(self._request.dbsession) or main.LayerWMS()
|
@@ -130,12 +192,17 @@ class OGCServerSynchronizer:
|
|
130
192
|
for theme_layer in theme_layers:
|
131
193
|
self.synchronize_layer(theme_layer)
|
132
194
|
|
133
|
-
|
134
|
-
|
135
|
-
self._logger.info("%s groups were added", self._groups_added)
|
136
|
-
self._logger.info("%s layers were added", self._layers_added)
|
195
|
+
if self._clean:
|
196
|
+
self.check_layers()
|
137
197
|
|
138
|
-
|
198
|
+
self._logger.info("%s items found", self._items_found)
|
199
|
+
self._logger.info("%s themes added", self._themes_added)
|
200
|
+
self._logger.info("%s groups added", self._groups_added)
|
201
|
+
self._logger.info("%s groups removed", self._groups_removed)
|
202
|
+
self._logger.info("%s layers added", self._layers_added)
|
203
|
+
self._logger.info("%s layers removed", self._layers_removed)
|
204
|
+
|
205
|
+
def synchronize_layer(self, el: Element, parent: Optional[main.TreeGroup] = None) -> main.TreeItem:
|
139
206
|
if el.find("Layer") is None:
|
140
207
|
tree_item = self.get_layer_wms(el, parent)
|
141
208
|
elif parent is None:
|
@@ -143,13 +210,37 @@ class OGCServerSynchronizer:
|
|
143
210
|
else:
|
144
211
|
tree_item = self.get_layer_group(el, parent)
|
145
212
|
|
213
|
+
server_children = []
|
146
214
|
for child in el.findall("Layer"):
|
147
|
-
self.synchronize_layer(child, tree_item)
|
215
|
+
child_item = self.synchronize_layer(child, tree_item)
|
216
|
+
|
217
|
+
if isinstance(tree_item, main.Theme) and isinstance(child_item, main.LayerWMS):
|
218
|
+
# We cannot add layers in themes
|
219
|
+
continue
|
148
220
|
|
149
|
-
|
150
|
-
name = el.find("Name").text
|
221
|
+
server_children.append(child_item)
|
151
222
|
|
152
|
-
|
223
|
+
if self._force_ordering and isinstance(tree_item, main.TreeGroup):
|
224
|
+
# Force children ordering, server_children first, external_children last
|
225
|
+
external_children = [item for item in tree_item.children if item not in server_children]
|
226
|
+
children = server_children + external_children
|
227
|
+
if tree_item.children != children:
|
228
|
+
tree_item._set_children( # pylint: disable=protected-access
|
229
|
+
server_children + external_children, order=True
|
230
|
+
)
|
231
|
+
self._logger.info("Children of %s have been sorted", tree_item.name)
|
232
|
+
|
233
|
+
return tree_item
|
234
|
+
|
235
|
+
def get_theme(self, el: ElementTree) -> main.Theme:
|
236
|
+
name_el = el.find("Name")
|
237
|
+
assert name_el is not None
|
238
|
+
name = name_el.text
|
239
|
+
|
240
|
+
theme = cast(
|
241
|
+
Optional[main.Theme],
|
242
|
+
self._request.dbsession.query(main.Theme).filter(main.Theme.name == name).one_or_none(),
|
243
|
+
)
|
153
244
|
|
154
245
|
if theme is None:
|
155
246
|
theme = main.Theme()
|
@@ -165,16 +256,24 @@ class OGCServerSynchronizer:
|
|
165
256
|
|
166
257
|
return theme
|
167
258
|
|
168
|
-
def get_layer_group(self, el, parent):
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
259
|
+
def get_layer_group(self, el: Element, parent: main.TreeGroup) -> main.LayerGroup:
|
260
|
+
name_el = el.find("Name")
|
261
|
+
assert name_el is not None
|
262
|
+
name = name_el.text
|
263
|
+
assert name is not None
|
264
|
+
|
265
|
+
group = cast(
|
266
|
+
Optional[main.LayerGroup],
|
267
|
+
(
|
268
|
+
self._request.dbsession.query(main.LayerGroup)
|
269
|
+
.filter(main.LayerGroup.name == name)
|
270
|
+
.one_or_none()
|
271
|
+
),
|
173
272
|
)
|
174
273
|
|
175
274
|
if group is None:
|
176
|
-
group = main.LayerGroup(name=
|
177
|
-
group.parents_relation.append(main.LayergroupTreeitem(group=parent))
|
275
|
+
group = main.LayerGroup(name=name)
|
276
|
+
group.parents_relation.append(main.LayergroupTreeitem(group=parent))
|
178
277
|
|
179
278
|
self._request.dbsession.add(group)
|
180
279
|
self._logger.info("Layer %s added as new group in theme %s", name, parent.name)
|
@@ -182,18 +281,27 @@ class OGCServerSynchronizer:
|
|
182
281
|
else:
|
183
282
|
self._items_found += 1
|
184
283
|
|
284
|
+
if self._force_parents and group.parents != [parent]:
|
285
|
+
group.parents_relation = [main.LayergroupTreeitem(group=parent)]
|
286
|
+
self._logger.info("Group %s moved to %s", name, parent.name)
|
287
|
+
|
185
288
|
return group
|
186
289
|
|
187
|
-
def get_layer_wms(self, el, parent):
|
188
|
-
|
290
|
+
def get_layer_wms(self, el: Element, parent: Optional[main.TreeGroup] = None) -> main.LayerWMS:
|
291
|
+
name_el = el.find("Name")
|
292
|
+
assert name_el is not None
|
293
|
+
name = name_el.text
|
189
294
|
|
190
|
-
layer =
|
295
|
+
layer = cast(
|
296
|
+
Optional[main.LayerWMS],
|
297
|
+
self._request.dbsession.query(main.LayerWMS).filter(main.LayerWMS.name == name).one_or_none(),
|
298
|
+
)
|
191
299
|
|
192
300
|
if layer is None:
|
193
301
|
layer = main.LayerWMS()
|
194
302
|
|
195
303
|
# TreeItem
|
196
|
-
layer.name =
|
304
|
+
layer.name = name
|
197
305
|
layer.description = self._default_wms.description
|
198
306
|
layer.metadatas = [main.Metadata(name=m.name, value=m.value) for m in self._default_wms.metadatas]
|
199
307
|
|
@@ -216,17 +324,17 @@ class OGCServerSynchronizer:
|
|
216
324
|
|
217
325
|
# LayerWMS
|
218
326
|
layer.ogc_server = self._ogc_server
|
219
|
-
layer.layer =
|
327
|
+
layer.layer = name
|
220
328
|
layer.style = (
|
221
329
|
self._default_wms.style
|
222
|
-
if el.find("./Style/Name[.='{
|
330
|
+
if el.find(f"./Style/Name[.='{self._default_wms.style}']") is not None
|
223
331
|
else None
|
224
332
|
)
|
225
333
|
# layer.time_mode =
|
226
334
|
# layer.time_widget =
|
227
335
|
|
228
336
|
self._request.dbsession.add(layer)
|
229
|
-
if
|
337
|
+
if not isinstance(parent, main.LayerGroup):
|
230
338
|
self._logger.info("Layer %s added as new layer with no parent", name)
|
231
339
|
else:
|
232
340
|
layer.parents_relation.append(main.LayergroupTreeitem(group=parent))
|
@@ -242,30 +350,35 @@ class OGCServerSynchronizer:
|
|
242
350
|
self._ogc_server.name,
|
243
351
|
)
|
244
352
|
|
353
|
+
parents = [parent] if isinstance(parent, main.LayerGroup) else []
|
354
|
+
if self._force_parents and layer.parents != parents:
|
355
|
+
layer.parents_relation = [main.LayergroupTreeitem(group=parent) for parent in parents]
|
356
|
+
self._logger.info("Layer %s moved to %s", name, parent.name if parent else "root")
|
357
|
+
|
245
358
|
return layer
|
246
359
|
|
247
|
-
|
360
|
+
@functools.lru_cache(maxsize=10)
|
361
|
+
def wms_capabilities(self) -> bytes:
|
248
362
|
errors: Set[str] = set()
|
249
363
|
url = get_url2(
|
250
|
-
"The OGC server '{
|
364
|
+
f"The OGC server '{self._ogc_server.name}'",
|
251
365
|
self._ogc_server.url,
|
252
366
|
self._request,
|
253
367
|
errors,
|
254
368
|
)
|
255
369
|
if url is None:
|
256
|
-
raise Exception("\n".join(errors))
|
370
|
+
raise Exception("\n".join(errors)) # pylint: disable=broad-exception-raised
|
257
371
|
|
258
372
|
# Add functionality params
|
259
373
|
# sparams = get_mapserver_substitution_params(self.request)
|
260
|
-
# url
|
374
|
+
# url.add_query(url, sparams)
|
261
375
|
|
262
|
-
url
|
263
|
-
url,
|
376
|
+
url.add_query(
|
264
377
|
{
|
265
378
|
"SERVICE": "WMS",
|
266
379
|
"VERSION": "1.1.1",
|
267
380
|
"REQUEST": "GetCapabilities",
|
268
|
-
"
|
381
|
+
"ROLE_IDS": "0",
|
269
382
|
"USER_ID": "0",
|
270
383
|
},
|
271
384
|
)
|
@@ -279,7 +392,7 @@ class OGCServerSynchronizer:
|
|
279
392
|
headers["sec-username"] = "root"
|
280
393
|
headers["sec-roles"] = "root"
|
281
394
|
|
282
|
-
response = requests.get(url, headers=headers, timeout=300)
|
395
|
+
response = requests.get(url.url(), headers=headers, timeout=300)
|
283
396
|
self._logger.info("Got response %s in %.1fs.", response.status_code, response.elapsed.total_seconds())
|
284
397
|
response.raise_for_status()
|
285
398
|
|
@@ -288,10 +401,9 @@ class OGCServerSynchronizer:
|
|
288
401
|
"application/vnd.ogc.wms_xml",
|
289
402
|
"text/xml",
|
290
403
|
]:
|
291
|
-
raise Exception(
|
292
|
-
"GetCapabilities from URL {} returns a wrong Content-Type:
|
293
|
-
|
294
|
-
)
|
404
|
+
raise Exception( # pylint: disable=broad-exception-raised
|
405
|
+
f"GetCapabilities from URL '{url}' returns a wrong Content-Type: "
|
406
|
+
f"{response.headers.get('Content-Type', '')}\n{response.text}"
|
295
407
|
)
|
296
408
|
|
297
409
|
return response.content
|
File without changes
|
c2cgeoportal_admin/routes.py
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2017-2021, Camptocamp SA
|
1
|
+
# Copyright (c) 2017-2023, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -32,12 +30,13 @@ from c2cgeoform.routes import register_route, register_routes
|
|
32
30
|
|
33
31
|
|
34
32
|
def includeme(config):
|
33
|
+
"""Initialize the Pyramid routes."""
|
35
34
|
config.add_static_view("c2cgeoportal_admin_node_modules", "c2cgeoportal_admin:node_modules")
|
36
35
|
config.override_asset(
|
37
36
|
to_override="c2cgeoportal_admin:node_modules/", override_with="/opt/c2cgeoportal/admin/node_modules"
|
38
37
|
)
|
39
38
|
# Because c2cgeoform widgets target {root_package}:node_modules/...
|
40
|
-
asset_spec = "{}:node_modules/"
|
39
|
+
asset_spec = f"{config.root_package.__name__}:node_modules/"
|
41
40
|
config.add_static_view("root_package_node_modules", asset_spec)
|
42
41
|
config.override_asset(to_override=asset_spec, override_with="/opt/c2cgeoportal/admin/node_modules")
|
43
42
|
|
@@ -59,8 +58,10 @@ def includeme(config):
|
|
59
58
|
Functionality,
|
60
59
|
Interface,
|
61
60
|
LayerGroup,
|
61
|
+
LayerVectorTiles,
|
62
62
|
LayerWMS,
|
63
63
|
LayerWMTS,
|
64
|
+
Log,
|
64
65
|
OGCServer,
|
65
66
|
RestrictionArea,
|
66
67
|
Role,
|
@@ -76,6 +77,7 @@ def includeme(config):
|
|
76
77
|
("layer_groups", LayerGroup),
|
77
78
|
("layers_wms", LayerWMS),
|
78
79
|
("layers_wmts", LayerWMTS),
|
80
|
+
("layers_vectortiles", LayerVectorTiles),
|
79
81
|
("ogc_servers", OGCServer),
|
80
82
|
("restriction_areas", RestrictionArea),
|
81
83
|
("users", User),
|
@@ -83,6 +85,7 @@ def includeme(config):
|
|
83
85
|
("functionalities", Functionality),
|
84
86
|
("interfaces", Interface),
|
85
87
|
("oauth2_clients", OAuth2Client),
|
88
|
+
("logs", Log),
|
86
89
|
]
|
87
90
|
|
88
91
|
admin_interface_config = config.registry.settings["admin_interface"]
|
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2018-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2018-2021, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -31,13 +29,17 @@
|
|
31
29
|
import colander
|
32
30
|
from c2cgeoform.schema import GeoFormSchemaNode
|
33
31
|
from deform.widget import MappingWidget, SequenceWidget
|
32
|
+
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
34
33
|
|
35
|
-
from c2cgeoportal_admin import _
|
36
34
|
from c2cgeoportal_commons.models.main import Dimension
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
36
|
+
|
37
|
+
def dimensions_schema_node(prop: InstrumentedAttribute) -> colander.SequenceSchema:
|
38
|
+
"""Get the scheme of the dimensions."""
|
39
|
+
return colander.SequenceSchema(
|
40
|
+
GeoFormSchemaNode(Dimension, name="dimension", widget=MappingWidget(template="dimension")),
|
41
|
+
name=prop.key,
|
42
|
+
title=prop.info["colanderalchemy"]["title"],
|
43
|
+
description=prop.info["colanderalchemy"]["description"],
|
44
|
+
widget=SequenceWidget(category="structural", template="dimensions"),
|
45
|
+
)
|