c2cgeoportal-admin 2.5.0.100__py3-none-any.whl → 2.9rc44__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cgeoportal_admin/__init__.py +44 -14
- c2cgeoportal_admin/lib/__init__.py +0 -0
- c2cgeoportal_admin/lib/lingva_extractor.py +77 -0
- c2cgeoportal_admin/lib/ogcserver_synchronizer.py +410 -0
- c2cgeoportal_admin/py.typed +0 -0
- c2cgeoportal_admin/routes.py +30 -11
- c2cgeoportal_admin/schemas/dimensions.py +17 -11
- c2cgeoportal_admin/schemas/functionalities.py +60 -22
- c2cgeoportal_admin/schemas/interfaces.py +27 -19
- c2cgeoportal_admin/schemas/metadata.py +122 -48
- c2cgeoportal_admin/schemas/restriction_areas.py +26 -20
- c2cgeoportal_admin/schemas/roles.py +13 -7
- c2cgeoportal_admin/schemas/treegroup.py +90 -20
- c2cgeoportal_admin/schemas/treeitem.py +3 -4
- c2cgeoportal_admin/static/layertree.css +26 -4
- c2cgeoportal_admin/static/navbar.css +59 -36
- c2cgeoportal_admin/static/theme.css +51 -11
- c2cgeoportal_admin/subscribers.py +3 -3
- c2cgeoportal_admin/templates/404.jinja2 +41 -2
- c2cgeoportal_admin/templates/edit.jinja2 +23 -0
- c2cgeoportal_admin/templates/home.jinja2 +23 -0
- c2cgeoportal_admin/templates/index.jinja2 +23 -0
- c2cgeoportal_admin/templates/layertree.jinja2 +55 -11
- c2cgeoportal_admin/templates/layout.jinja2 +23 -0
- c2cgeoportal_admin/templates/navigation_navbar.jinja2 +56 -0
- c2cgeoportal_admin/templates/ogcserver_synchronize.jinja2 +90 -0
- c2cgeoportal_admin/templates/widgets/child.pt +35 -3
- c2cgeoportal_admin/templates/widgets/children.pt +121 -92
- c2cgeoportal_admin/templates/widgets/dimension.pt +23 -0
- c2cgeoportal_admin/templates/widgets/dimensions.pt +23 -0
- c2cgeoportal_admin/templates/widgets/functionality_fields.pt +51 -0
- c2cgeoportal_admin/templates/widgets/layer_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/layer_group_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/layer_v1_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/metadata.pt +30 -1
- c2cgeoportal_admin/templates/widgets/metadatas.pt +23 -0
- c2cgeoportal_admin/templates/widgets/ogcserver_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/restriction_area_fields.pt +25 -9
- c2cgeoportal_admin/templates/widgets/role_fields.pt +52 -25
- c2cgeoportal_admin/templates/widgets/theme_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/user_fields.pt +23 -0
- c2cgeoportal_admin/views/__init__.py +29 -0
- c2cgeoportal_admin/views/dimension_layers.py +14 -9
- c2cgeoportal_admin/views/functionalities.py +52 -18
- c2cgeoportal_admin/views/home.py +5 -5
- c2cgeoportal_admin/views/interfaces.py +29 -21
- c2cgeoportal_admin/views/layer_groups.py +36 -25
- c2cgeoportal_admin/views/layers.py +17 -13
- c2cgeoportal_admin/views/layers_cog.py +135 -0
- c2cgeoportal_admin/views/layers_vectortiles.py +62 -27
- c2cgeoportal_admin/views/layers_wms.py +61 -36
- c2cgeoportal_admin/views/layers_wmts.py +54 -32
- c2cgeoportal_admin/views/layertree.py +37 -28
- c2cgeoportal_admin/views/logged_views.py +83 -0
- c2cgeoportal_admin/views/logs.py +91 -0
- c2cgeoportal_admin/views/oauth2_clients.py +96 -0
- c2cgeoportal_admin/views/ogc_servers.py +192 -21
- c2cgeoportal_admin/views/restriction_areas.py +78 -25
- c2cgeoportal_admin/views/roles.py +88 -25
- c2cgeoportal_admin/views/themes.py +47 -35
- c2cgeoportal_admin/views/themes_ordering.py +44 -24
- c2cgeoportal_admin/views/treeitems.py +21 -17
- c2cgeoportal_admin/views/users.py +46 -26
- c2cgeoportal_admin/widgets.py +79 -28
- {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/METADATA +15 -13
- c2cgeoportal_admin-2.9rc44.dist-info/RECORD +97 -0
- {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/WHEEL +1 -1
- c2cgeoportal_admin-2.9rc44.dist-info/entry_points.txt +5 -0
- tests/__init__.py +36 -27
- tests/conftest.py +23 -24
- tests/test_edit_url.py +16 -19
- tests/test_functionalities.py +52 -14
- tests/test_home.py +0 -1
- tests/test_interface.py +35 -12
- tests/test_layer_groups.py +58 -32
- tests/test_layers_cog.py +243 -0
- tests/test_layers_vectortiles.py +46 -30
- tests/test_layers_wms.py +77 -82
- tests/test_layers_wmts.py +51 -30
- tests/test_layertree.py +107 -101
- tests/test_learn.py +1 -1
- tests/test_left_menu.py +0 -1
- tests/test_lingva_extractor_config.py +64 -0
- tests/test_logs.py +102 -0
- tests/test_main.py +4 -2
- tests/test_metadatas.py +79 -71
- tests/test_oauth2_clients.py +186 -0
- tests/test_ogc_servers.py +110 -28
- tests/test_restriction_areas.py +109 -20
- tests/test_role.py +142 -82
- tests/test_themes.py +75 -41
- tests/test_themes_ordering.py +1 -2
- tests/test_treegroup.py +2 -2
- tests/test_user.py +72 -70
- tests/themes_ordering.py +1 -2
- c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -10
- c2cgeoportal_admin-2.5.0.100.dist-info/RECORD +0 -84
- c2cgeoportal_admin-2.5.0.100.dist-info/entry_points.txt +0 -3
- {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.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-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -28,18 +26,23 @@
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
30
28
|
|
31
|
-
from
|
29
|
+
from typing import Any
|
30
|
+
|
32
31
|
import c2cgeoform
|
33
32
|
import c2cwsgiutils.pretty_json
|
33
|
+
import sqlalchemy.orm.session
|
34
|
+
import zope.sqlalchemy
|
35
|
+
from c2c.template.config import config as configuration
|
34
36
|
from pkg_resources import resource_filename
|
35
37
|
from pyramid.config import Configurator
|
36
38
|
from pyramid.events import BeforeRender, NewRequest
|
37
39
|
from sqlalchemy import engine_from_config
|
38
40
|
from sqlalchemy.orm import configure_mappers, sessionmaker
|
41
|
+
from transaction import TransactionManager
|
39
42
|
from translationstring import TranslationStringFactory
|
40
|
-
import zope.sqlalchemy
|
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,11 +51,13 @@ _ = TranslationStringFactory("c2cgeoportal_admin")
|
|
48
51
|
|
49
52
|
|
50
53
|
def main(_, **settings):
|
51
|
-
"""
|
52
|
-
|
53
|
-
|
54
|
-
configuration.init(
|
55
|
-
|
54
|
+
"""Return a Pyramid WSGI application."""
|
55
|
+
app_cfg = settings.get("app.cfg")
|
56
|
+
assert app_cfg is not None
|
57
|
+
configuration.init(app_cfg)
|
58
|
+
conf = configuration.get_config()
|
59
|
+
assert conf is not None
|
60
|
+
settings.update(conf)
|
56
61
|
|
57
62
|
config = Configurator(settings=settings)
|
58
63
|
|
@@ -68,8 +73,14 @@ def main(_, **settings):
|
|
68
73
|
session_factory = sessionmaker()
|
69
74
|
session_factory.configure(bind=engine)
|
70
75
|
|
71
|
-
def get_tm_session(
|
76
|
+
def get_tm_session(
|
77
|
+
session_factory: sessionmaker[ # pylint: disable=unsubscriptable-object
|
78
|
+
sqlalchemy.orm.session.Session
|
79
|
+
],
|
80
|
+
transaction_manager: TransactionManager,
|
81
|
+
) -> sqlalchemy.orm.session.Session:
|
72
82
|
dbsession = session_factory()
|
83
|
+
assert isinstance(dbsession, sqlalchemy.orm.session.Session)
|
73
84
|
zope.sqlalchemy.register(dbsession, transaction_manager=transaction_manager)
|
74
85
|
return dbsession
|
75
86
|
|
@@ -81,6 +92,19 @@ def main(_, **settings):
|
|
81
92
|
reify=True,
|
82
93
|
)
|
83
94
|
|
95
|
+
# Add fake user as we do not have authentication from geoportal
|
96
|
+
from c2cgeoportal_commons.models.static import User # pylint: disable=import-outside-toplevel
|
97
|
+
|
98
|
+
config.add_request_method(
|
99
|
+
lambda request: User(
|
100
|
+
username="test_user",
|
101
|
+
),
|
102
|
+
name="user",
|
103
|
+
property=True,
|
104
|
+
)
|
105
|
+
|
106
|
+
config.add_route("ogc_server_clear_cache", "/admin/ogc_server_clear_cache/{id}")
|
107
|
+
|
84
108
|
config.add_subscriber(add_renderer_globals, BeforeRender)
|
85
109
|
config.add_subscriber(add_localizer, NewRequest)
|
86
110
|
|
@@ -88,7 +112,9 @@ def main(_, **settings):
|
|
88
112
|
|
89
113
|
|
90
114
|
class PermissionSetter:
|
91
|
-
|
115
|
+
"""Set the permission to the admin user."""
|
116
|
+
|
117
|
+
def __init__(self, config: Configurator):
|
92
118
|
self.default_permission_to_revert = None
|
93
119
|
self.config = config
|
94
120
|
|
@@ -100,12 +126,13 @@ class PermissionSetter:
|
|
100
126
|
]["introspectable"]["value"]
|
101
127
|
self.config.set_default_permission("admin")
|
102
128
|
|
103
|
-
def __exit__(self, _type, value, traceback):
|
129
|
+
def __exit__(self, _type: Any, value: Any, traceback: Any) -> None:
|
104
130
|
self.config.commit() # avoid .ConfigurationConflictError
|
105
131
|
self.config.set_default_permission(self.default_permission_to_revert)
|
106
132
|
|
107
133
|
|
108
|
-
def includeme(config: Configurator):
|
134
|
+
def includeme(config: Configurator) -> None:
|
135
|
+
"""Initialize the Pyramid application."""
|
109
136
|
config.include("pyramid_jinja2")
|
110
137
|
config.include("c2cgeoform")
|
111
138
|
config.include("c2cgeoportal_commons")
|
@@ -113,6 +140,9 @@ def includeme(config: Configurator):
|
|
113
140
|
# Use pyramid_tm to hook the transaction lifecycle to the request
|
114
141
|
config.include("pyramid_tm")
|
115
142
|
config.add_translation_dirs("c2cgeoportal_admin:locale")
|
143
|
+
config.add_view_predicate("is_admin", IsAdminPredicate)
|
144
|
+
|
145
|
+
configure_mappers()
|
116
146
|
|
117
147
|
with PermissionSetter(config):
|
118
148
|
config.scan()
|
File without changes
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# Copyright (c) 2011-2024, 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
|
31
|
+
|
32
|
+
import yaml
|
33
|
+
from lingva.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: dict[str, Any] | None = 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
|
@@ -0,0 +1,410 @@
|
|
1
|
+
# Copyright (c) 2020-2024, 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 functools
|
30
|
+
import logging
|
31
|
+
from io import StringIO
|
32
|
+
from typing import Any, Optional, cast
|
33
|
+
from xml.etree.ElementTree import Element # nosec
|
34
|
+
|
35
|
+
import pyramid.request
|
36
|
+
import requests
|
37
|
+
from defusedxml import ElementTree
|
38
|
+
from sqlalchemy.orm.session import Session
|
39
|
+
|
40
|
+
from c2cgeoportal_commons.lib.url import get_url2
|
41
|
+
from c2cgeoportal_commons.models import main
|
42
|
+
|
43
|
+
|
44
|
+
class dry_run_transaction: # noqa ignore=N801: class names should use CapWords convention
|
45
|
+
def __init__(self, dbsession: Session, dry_run: bool):
|
46
|
+
self.dbsession = dbsession
|
47
|
+
self.dry_run = dry_run
|
48
|
+
|
49
|
+
def __enter__(self) -> None:
|
50
|
+
if self.dry_run:
|
51
|
+
self.dbsession.begin_nested()
|
52
|
+
|
53
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
54
|
+
if self.dry_run:
|
55
|
+
self.dbsession.rollback()
|
56
|
+
|
57
|
+
|
58
|
+
class OGCServerSynchronizer:
|
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
|
+
"""
|
88
|
+
self._request = request
|
89
|
+
self._ogc_server = ogc_server
|
90
|
+
self._force_parents = force_parents
|
91
|
+
self._force_ordering = force_ordering
|
92
|
+
self._clean = clean
|
93
|
+
|
94
|
+
self._default_wms = main.LayerWMS()
|
95
|
+
self._interfaces = None
|
96
|
+
|
97
|
+
self._logger = logging.Logger(str(self), logging.INFO)
|
98
|
+
self._log = StringIO()
|
99
|
+
self._log_handler = logging.StreamHandler(self._log)
|
100
|
+
self._logger.addHandler(self._log_handler)
|
101
|
+
|
102
|
+
self._items_found = 0
|
103
|
+
self._themes_added = 0
|
104
|
+
self._groups_added = 0
|
105
|
+
self._groups_removed = 0
|
106
|
+
self._layers_added = 0
|
107
|
+
self._layers_removed = 0
|
108
|
+
|
109
|
+
def __str__(self) -> str:
|
110
|
+
return f"OGCServerSynchronizer({self._ogc_server.name})"
|
111
|
+
|
112
|
+
def logger(self) -> logging.Logger:
|
113
|
+
return self._logger
|
114
|
+
|
115
|
+
def report(self) -> str:
|
116
|
+
return self._log.getvalue()
|
117
|
+
|
118
|
+
def check_layers(self) -> None:
|
119
|
+
capabilities = ElementTree.fromstring(self.wms_capabilities())
|
120
|
+
layers = self._request.dbsession.query(main.LayerWMS).filter(
|
121
|
+
main.LayerWMS.ogc_server == self._ogc_server
|
122
|
+
)
|
123
|
+
items = 0
|
124
|
+
invalids = 0
|
125
|
+
for layer in layers:
|
126
|
+
valid = True
|
127
|
+
reason = None
|
128
|
+
for name in layer.layer.split(","):
|
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}']")
|
135
|
+
if el is None:
|
136
|
+
valid = False
|
137
|
+
reason = f"Layer {name} does not exists on OGC server"
|
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
|
143
|
+
break
|
144
|
+
if layer.style and el.find(f"./Style/Name[.='{layer.style}']") is None:
|
145
|
+
valid = False
|
146
|
+
reason = f"Style {layer.style} does not exists in Layer {name}"
|
147
|
+
self._logger.info(reason)
|
148
|
+
break
|
149
|
+
layer.valid = valid
|
150
|
+
if not valid:
|
151
|
+
invalids += 1
|
152
|
+
layer.invalid_reason = reason
|
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
|
+
|
163
|
+
self._logger.info("Checked %s layers, %s are invalid", items, invalids)
|
164
|
+
|
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
|
+
"""
|
172
|
+
with dry_run_transaction(self._request.dbsession, dry_run):
|
173
|
+
self.do_synchronize()
|
174
|
+
if dry_run:
|
175
|
+
self._logger.info("Rolling back transaction due to dry run")
|
176
|
+
|
177
|
+
def do_synchronize(self) -> None:
|
178
|
+
self._items_found = 0
|
179
|
+
self._themes_added = 0
|
180
|
+
self._groups_added = 0
|
181
|
+
self._groups_removed = 0
|
182
|
+
self._layers_added = 0
|
183
|
+
self._layers_removed = 0
|
184
|
+
|
185
|
+
self._default_wms = cast(
|
186
|
+
main.LayerWMS, main.LayerWMS.get_default(self._request.dbsession) or main.LayerWMS()
|
187
|
+
)
|
188
|
+
self._interfaces = self._request.dbsession.query(main.Interface).all()
|
189
|
+
|
190
|
+
capabilities = ElementTree.fromstring(self.wms_capabilities())
|
191
|
+
theme_layers = capabilities.findall("Capability/Layer/Layer")
|
192
|
+
for theme_layer in theme_layers:
|
193
|
+
self.synchronize_layer(theme_layer)
|
194
|
+
|
195
|
+
if self._clean:
|
196
|
+
self.check_layers()
|
197
|
+
|
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: main.TreeGroup | None = None) -> main.TreeItem:
|
206
|
+
if el.find("Layer") is None:
|
207
|
+
tree_item = self.get_layer_wms(el, parent)
|
208
|
+
elif parent is None:
|
209
|
+
tree_item = self.get_theme(el)
|
210
|
+
else:
|
211
|
+
tree_item = self.get_layer_group(el, parent)
|
212
|
+
|
213
|
+
server_children = []
|
214
|
+
for child in el.findall("Layer"):
|
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
|
220
|
+
|
221
|
+
server_children.append(child_item)
|
222
|
+
|
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
|
+
)
|
244
|
+
|
245
|
+
if theme is None:
|
246
|
+
theme = main.Theme()
|
247
|
+
theme.name = name
|
248
|
+
theme.public = False
|
249
|
+
theme.interfaces = self._interfaces
|
250
|
+
|
251
|
+
self._request.dbsession.add(theme)
|
252
|
+
self._logger.info("Layer %s added as new theme", name)
|
253
|
+
self._themes_added += 1
|
254
|
+
else:
|
255
|
+
self._items_found += 1
|
256
|
+
|
257
|
+
return theme
|
258
|
+
|
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
|
+
),
|
272
|
+
)
|
273
|
+
|
274
|
+
if group is None:
|
275
|
+
group = main.LayerGroup(name=name)
|
276
|
+
group.parents_relation.append(main.LayergroupTreeitem(group=parent))
|
277
|
+
|
278
|
+
self._request.dbsession.add(group)
|
279
|
+
self._logger.info("Layer %s added as new group in theme %s", name, parent.name)
|
280
|
+
self._groups_added += 1
|
281
|
+
else:
|
282
|
+
self._items_found += 1
|
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
|
+
|
288
|
+
return group
|
289
|
+
|
290
|
+
def get_layer_wms(self, el: Element, parent: main.TreeGroup | None = None) -> main.LayerWMS:
|
291
|
+
name_el = el.find("Name")
|
292
|
+
assert name_el is not None
|
293
|
+
name = name_el.text
|
294
|
+
assert name is not None
|
295
|
+
|
296
|
+
layer = cast(
|
297
|
+
Optional[main.LayerWMS],
|
298
|
+
self._request.dbsession.query(main.LayerWMS).filter(main.LayerWMS.name == name).one_or_none(),
|
299
|
+
)
|
300
|
+
|
301
|
+
if layer is None:
|
302
|
+
layer = main.LayerWMS()
|
303
|
+
|
304
|
+
# TreeItem
|
305
|
+
layer.name = name
|
306
|
+
layer.description = self._default_wms.description
|
307
|
+
layer.metadatas = [main.Metadata(name=m.name, value=m.value) for m in self._default_wms.metadatas]
|
308
|
+
|
309
|
+
# Layer
|
310
|
+
layer.public = False
|
311
|
+
layer.geo_table = None
|
312
|
+
layer.exclude_properties = self._default_wms.exclude_properties
|
313
|
+
layer.interfaces = list(self._default_wms.interfaces) or self._interfaces
|
314
|
+
|
315
|
+
# DimensionLayer
|
316
|
+
layer.dimensions = [
|
317
|
+
main.Dimension(
|
318
|
+
name=d.name,
|
319
|
+
value=d.value,
|
320
|
+
field=d.field,
|
321
|
+
description=d.description,
|
322
|
+
)
|
323
|
+
for d in self._default_wms.dimensions
|
324
|
+
]
|
325
|
+
|
326
|
+
# LayerWMS
|
327
|
+
layer.ogc_server = self._ogc_server
|
328
|
+
layer.layer = name
|
329
|
+
layer.style = (
|
330
|
+
self._default_wms.style
|
331
|
+
if el.find(f"./Style/Name[.='{self._default_wms.style}']") is not None
|
332
|
+
else None
|
333
|
+
)
|
334
|
+
# layer.time_mode =
|
335
|
+
# layer.time_widget =
|
336
|
+
|
337
|
+
self._request.dbsession.add(layer)
|
338
|
+
if not isinstance(parent, main.LayerGroup):
|
339
|
+
self._logger.info("Layer %s added as new layer with no parent", name)
|
340
|
+
else:
|
341
|
+
layer.parents_relation.append(main.LayergroupTreeitem(group=parent))
|
342
|
+
self._logger.info("Layer %s added as new layer in group %s", name, parent.name)
|
343
|
+
self._layers_added += 1
|
344
|
+
|
345
|
+
else:
|
346
|
+
self._items_found += 1
|
347
|
+
if layer.ogc_server is not self._ogc_server:
|
348
|
+
self._logger.info(
|
349
|
+
"Layer %s: another layer already exists with the same name in OGC server %s",
|
350
|
+
name,
|
351
|
+
self._ogc_server.name,
|
352
|
+
)
|
353
|
+
|
354
|
+
parents = [parent] if isinstance(parent, main.LayerGroup) else []
|
355
|
+
if self._force_parents and layer.parents != parents:
|
356
|
+
layer.parents_relation = [main.LayergroupTreeitem(group=parent) for parent in parents]
|
357
|
+
self._logger.info("Layer %s moved to %s", name, parent.name if parent else "root")
|
358
|
+
|
359
|
+
return layer
|
360
|
+
|
361
|
+
@functools.lru_cache(maxsize=10)
|
362
|
+
def wms_capabilities(self) -> bytes:
|
363
|
+
errors: set[str] = set()
|
364
|
+
url = get_url2(
|
365
|
+
f"The OGC server '{self._ogc_server.name}'",
|
366
|
+
self._ogc_server.url,
|
367
|
+
self._request,
|
368
|
+
errors,
|
369
|
+
)
|
370
|
+
if url is None:
|
371
|
+
raise Exception("\n".join(errors)) # pylint: disable=broad-exception-raised
|
372
|
+
|
373
|
+
# Add functionality params
|
374
|
+
# sparams = get_mapserver_substitution_params(self.request)
|
375
|
+
# url.add_query(url, sparams)
|
376
|
+
|
377
|
+
url.add_query(
|
378
|
+
{
|
379
|
+
"SERVICE": "WMS",
|
380
|
+
"VERSION": "1.1.1",
|
381
|
+
"REQUEST": "GetCapabilities",
|
382
|
+
"ROLE_IDS": "0",
|
383
|
+
"USER_ID": "0",
|
384
|
+
},
|
385
|
+
)
|
386
|
+
|
387
|
+
self._logger.info("Get WMS GetCapabilities from: %s", url)
|
388
|
+
|
389
|
+
headers = {}
|
390
|
+
|
391
|
+
# Add headers for Geoserver
|
392
|
+
if self._ogc_server.auth == main.OGCSERVER_AUTH_GEOSERVER:
|
393
|
+
headers["sec-username"] = "root"
|
394
|
+
headers["sec-roles"] = "root"
|
395
|
+
|
396
|
+
response = requests.get(url.url(), headers=headers, timeout=300)
|
397
|
+
self._logger.info("Got response %s in %.1fs.", response.status_code, response.elapsed.total_seconds())
|
398
|
+
response.raise_for_status()
|
399
|
+
|
400
|
+
# With WMS 1.3 it returns text/xml also in case of error :-(
|
401
|
+
if response.headers.get("Content-Type", "").split(";")[0].strip() not in [
|
402
|
+
"application/vnd.ogc.wms_xml",
|
403
|
+
"text/xml",
|
404
|
+
]:
|
405
|
+
raise Exception( # pylint: disable=broad-exception-raised
|
406
|
+
f"GetCapabilities from URL '{url}' returns a wrong Content-Type: "
|
407
|
+
f"{response.headers.get('Content-Type', '')}\n{response.text}"
|
408
|
+
)
|
409
|
+
|
410
|
+
return response.content
|
File without changes
|