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.
Files changed (78) hide show
  1. c2cgeoportal_admin/__init__.py +32 -10
  2. c2cgeoportal_admin/lib/lingua_extractor.py +77 -0
  3. c2cgeoportal_admin/lib/ogcserver_synchronizer.py +168 -56
  4. c2cgeoportal_admin/py.typed +0 -0
  5. c2cgeoportal_admin/routes.py +7 -4
  6. c2cgeoportal_admin/schemas/dimensions.py +12 -10
  7. c2cgeoportal_admin/schemas/functionalities.py +62 -21
  8. c2cgeoportal_admin/schemas/interfaces.py +22 -18
  9. c2cgeoportal_admin/schemas/metadata.py +100 -47
  10. c2cgeoportal_admin/schemas/restriction_areas.py +21 -19
  11. c2cgeoportal_admin/schemas/roles.py +7 -5
  12. c2cgeoportal_admin/schemas/treegroup.py +38 -17
  13. c2cgeoportal_admin/schemas/treeitem.py +2 -3
  14. c2cgeoportal_admin/static/layertree.css +3 -4
  15. c2cgeoportal_admin/static/navbar.css +36 -35
  16. c2cgeoportal_admin/static/theme.css +16 -9
  17. c2cgeoportal_admin/subscribers.py +3 -3
  18. c2cgeoportal_admin/templates/404.jinja2 +18 -2
  19. c2cgeoportal_admin/templates/layertree.jinja2 +31 -9
  20. c2cgeoportal_admin/templates/navigation_navbar.jinja2 +33 -0
  21. c2cgeoportal_admin/templates/ogcserver_synchronize.jinja2 +12 -0
  22. c2cgeoportal_admin/templates/widgets/functionality_fields.pt +51 -0
  23. c2cgeoportal_admin/templates/widgets/metadata.pt +7 -1
  24. c2cgeoportal_admin/views/__init__.py +29 -0
  25. c2cgeoportal_admin/views/dimension_layers.py +7 -6
  26. c2cgeoportal_admin/views/functionalities.py +33 -6
  27. c2cgeoportal_admin/views/home.py +5 -5
  28. c2cgeoportal_admin/views/interfaces.py +7 -8
  29. c2cgeoportal_admin/views/layer_groups.py +9 -11
  30. c2cgeoportal_admin/views/layers.py +8 -7
  31. c2cgeoportal_admin/views/layers_vectortiles.py +30 -10
  32. c2cgeoportal_admin/views/layers_wms.py +41 -35
  33. c2cgeoportal_admin/views/layers_wmts.py +41 -35
  34. c2cgeoportal_admin/views/layertree.py +36 -28
  35. c2cgeoportal_admin/views/logged_views.py +80 -0
  36. c2cgeoportal_admin/views/logs.py +90 -0
  37. c2cgeoportal_admin/views/oauth2_clients.py +30 -22
  38. c2cgeoportal_admin/views/ogc_servers.py +119 -37
  39. c2cgeoportal_admin/views/restriction_areas.py +13 -11
  40. c2cgeoportal_admin/views/roles.py +16 -12
  41. c2cgeoportal_admin/views/themes.py +15 -14
  42. c2cgeoportal_admin/views/themes_ordering.py +13 -8
  43. c2cgeoportal_admin/views/treeitems.py +14 -12
  44. c2cgeoportal_admin/views/users.py +12 -7
  45. c2cgeoportal_admin/widgets.py +17 -14
  46. {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.8.1.180.dist-info}/METADATA +17 -8
  47. c2cgeoportal_admin-2.8.1.180.dist-info/RECORD +95 -0
  48. {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.8.1.180.dist-info}/WHEEL +1 -1
  49. c2cgeoportal_admin-2.8.1.180.dist-info/entry_points.txt +6 -0
  50. tests/__init__.py +11 -12
  51. tests/conftest.py +2 -1
  52. tests/test_edit_url.py +11 -14
  53. tests/test_functionalities.py +52 -14
  54. tests/test_home.py +0 -1
  55. tests/test_interface.py +34 -11
  56. tests/test_layer_groups.py +57 -27
  57. tests/test_layers_vectortiles.py +43 -20
  58. tests/test_layers_wms.py +67 -45
  59. tests/test_layers_wmts.py +47 -26
  60. tests/test_layertree.py +99 -16
  61. tests/test_left_menu.py +0 -1
  62. tests/test_lingua_extractor_config.py +64 -0
  63. tests/test_logs.py +103 -0
  64. tests/test_main.py +3 -1
  65. tests/test_metadatas.py +34 -21
  66. tests/test_oauth2_clients.py +37 -9
  67. tests/test_ogc_servers.py +84 -35
  68. tests/test_restriction_areas.py +38 -15
  69. tests/test_role.py +68 -41
  70. tests/test_themes.py +71 -37
  71. tests/test_themes_ordering.py +1 -2
  72. tests/test_treegroup.py +2 -2
  73. tests/test_user.py +48 -17
  74. tests/themes_ordering.py +1 -2
  75. c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -33
  76. c2cgeoportal_admin-2.6.0.dist-info/RECORD +0 -89
  77. c2cgeoportal_admin-2.6.0.dist-info/entry_points.txt +0 -3
  78. {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.8.1.180.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
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(session_factory, transaction_manager):
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
- def __init__(self, config):
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
- # -*- coding: utf-8 -*-
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 Set, cast # noqa, pylint: disable=unused-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 add_url_params, get_url2
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
- def __init__(self, request, ogc_server):
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({})".format(self._ogc_server.name)
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
- el = capabilities.find(".//Layer[Name='{}']".format(name))
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".format(name)
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[.='{}']".format(layer.style)) is None:
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 {}".format(layer.style, name)
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
- self._logger.info("%s items were found", self._items_found)
134
- self._logger.info("%s themes were added", self._themes_added)
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
- def synchronize_layer(self, el, parent=None):
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
- def get_theme(self, el):
150
- name = el.find("Name").text
221
+ server_children.append(child_item)
151
222
 
152
- theme = self._request.dbsession.query(main.Theme).filter(main.Theme.name == name).one_or_none()
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
- name = el.find("Name").text
170
-
171
- group = (
172
- self._request.dbsession.query(main.LayerGroup).filter(main.LayerGroup.name == name).one_or_none()
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=el.find("Name").text)
177
- group.parents_relation.append(main.LayergroupTreeitem(group=parent)) # noqa, pylint: no-member
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
- name = el.find("Name").text
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 = self._request.dbsession.query(main.LayerWMS).filter(main.LayerWMS.name == name).one_or_none()
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 = el.find("Name").text
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 = el.find("Name").text
327
+ layer.layer = name
220
328
  layer.style = (
221
329
  self._default_wms.style
222
- if el.find("./Style/Name[.='{}']".format(self._default_wms.style)) is not None
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 parent is None or isinstance(parent, main.Theme):
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
- def wms_capabilities(self):
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 '{}'".format(self._ogc_server.name),
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 = add_url_params(url, sparams)
374
+ # url.add_query(url, sparams)
261
375
 
262
- url = add_url_params(
263
- url,
376
+ url.add_query(
264
377
  {
265
378
  "SERVICE": "WMS",
266
379
  "VERSION": "1.1.1",
267
380
  "REQUEST": "GetCapabilities",
268
- "ROLE_ID": "0",
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: {}\n{}".format(
293
- url, response.headers.get("Content-Type", ""), response.text
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
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
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/".format(config.root_package.__name__)
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
- # -*- coding: utf-8 -*-
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
- dimensions_schema_node = colander.SequenceSchema(
39
- GeoFormSchemaNode(Dimension, name="dimension", widget=MappingWidget(template="dimension")),
40
- name="dimensions",
41
- title=_("Dimensions"),
42
- widget=SequenceWidget(category="structural", template="dimensions"),
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
+ )