c2cgeoportal-admin 2.5.0.100__py3-none-any.whl → 2.7.1.156__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 (91) hide show
  1. c2cgeoportal_admin/__init__.py +19 -12
  2. c2cgeoportal_admin/lib/__init__.py +0 -0
  3. c2cgeoportal_admin/lib/lingua_extractor.py +77 -0
  4. c2cgeoportal_admin/lib/ogcserver_synchronizer.py +409 -0
  5. c2cgeoportal_admin/py.typed +0 -0
  6. c2cgeoportal_admin/routes.py +18 -10
  7. c2cgeoportal_admin/schemas/dimensions.py +13 -11
  8. c2cgeoportal_admin/schemas/functionalities.py +63 -22
  9. c2cgeoportal_admin/schemas/interfaces.py +23 -19
  10. c2cgeoportal_admin/schemas/metadata.py +121 -47
  11. c2cgeoportal_admin/schemas/restriction_areas.py +22 -20
  12. c2cgeoportal_admin/schemas/roles.py +8 -6
  13. c2cgeoportal_admin/schemas/treegroup.py +84 -18
  14. c2cgeoportal_admin/schemas/treeitem.py +2 -3
  15. c2cgeoportal_admin/static/layertree.css +26 -4
  16. c2cgeoportal_admin/static/navbar.css +59 -36
  17. c2cgeoportal_admin/static/theme.css +48 -11
  18. c2cgeoportal_admin/subscribers.py +3 -3
  19. c2cgeoportal_admin/templates/404.jinja2 +23 -0
  20. c2cgeoportal_admin/templates/edit.jinja2 +23 -0
  21. c2cgeoportal_admin/templates/home.jinja2 +23 -0
  22. c2cgeoportal_admin/templates/index.jinja2 +23 -0
  23. c2cgeoportal_admin/templates/layertree.jinja2 +55 -11
  24. c2cgeoportal_admin/templates/layout.jinja2 +23 -0
  25. c2cgeoportal_admin/templates/navigation_navbar.jinja2 +56 -0
  26. c2cgeoportal_admin/templates/ogcserver_synchronize.jinja2 +90 -0
  27. c2cgeoportal_admin/templates/widgets/child.pt +35 -3
  28. c2cgeoportal_admin/templates/widgets/children.pt +121 -92
  29. c2cgeoportal_admin/templates/widgets/dimension.pt +23 -0
  30. c2cgeoportal_admin/templates/widgets/dimensions.pt +23 -0
  31. c2cgeoportal_admin/templates/widgets/functionality_fields.pt +51 -0
  32. c2cgeoportal_admin/templates/widgets/layer_fields.pt +23 -0
  33. c2cgeoportal_admin/templates/widgets/layer_group_fields.pt +23 -0
  34. c2cgeoportal_admin/templates/widgets/layer_v1_fields.pt +23 -0
  35. c2cgeoportal_admin/templates/widgets/metadata.pt +30 -1
  36. c2cgeoportal_admin/templates/widgets/metadatas.pt +23 -0
  37. c2cgeoportal_admin/templates/widgets/ogcserver_fields.pt +23 -0
  38. c2cgeoportal_admin/templates/widgets/restriction_area_fields.pt +25 -9
  39. c2cgeoportal_admin/templates/widgets/role_fields.pt +52 -25
  40. c2cgeoportal_admin/templates/widgets/theme_fields.pt +23 -0
  41. c2cgeoportal_admin/templates/widgets/user_fields.pt +23 -0
  42. c2cgeoportal_admin/views/dimension_layers.py +7 -6
  43. c2cgeoportal_admin/views/functionalities.py +31 -5
  44. c2cgeoportal_admin/views/home.py +5 -5
  45. c2cgeoportal_admin/views/interfaces.py +8 -8
  46. c2cgeoportal_admin/views/layer_groups.py +9 -11
  47. c2cgeoportal_admin/views/layers.py +8 -7
  48. c2cgeoportal_admin/views/layers_vectortiles.py +30 -10
  49. c2cgeoportal_admin/views/layers_wms.py +45 -37
  50. c2cgeoportal_admin/views/layers_wmts.py +39 -33
  51. c2cgeoportal_admin/views/layertree.py +34 -26
  52. c2cgeoportal_admin/views/oauth2_clients.py +89 -0
  53. c2cgeoportal_admin/views/ogc_servers.py +130 -27
  54. c2cgeoportal_admin/views/restriction_areas.py +50 -8
  55. c2cgeoportal_admin/views/roles.py +60 -8
  56. c2cgeoportal_admin/views/themes.py +15 -14
  57. c2cgeoportal_admin/views/themes_ordering.py +38 -18
  58. c2cgeoportal_admin/views/treeitems.py +12 -11
  59. c2cgeoportal_admin/views/users.py +7 -5
  60. c2cgeoportal_admin/widgets.py +79 -28
  61. {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.7.1.156.dist-info}/METADATA +16 -11
  62. c2cgeoportal_admin-2.7.1.156.dist-info/RECORD +92 -0
  63. {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.7.1.156.dist-info}/WHEEL +1 -1
  64. c2cgeoportal_admin-2.7.1.156.dist-info/entry_points.txt +5 -0
  65. tests/__init__.py +23 -18
  66. tests/conftest.py +4 -15
  67. tests/test_edit_url.py +16 -18
  68. tests/test_functionalities.py +23 -10
  69. tests/test_interface.py +8 -8
  70. tests/test_layer_groups.py +15 -23
  71. tests/test_layers_vectortiles.py +16 -20
  72. tests/test_layers_wms.py +37 -75
  73. tests/test_layers_wmts.py +20 -24
  74. tests/test_layertree.py +107 -100
  75. tests/test_learn.py +1 -1
  76. tests/test_lingua_extractor_config.py +66 -0
  77. tests/test_main.py +4 -2
  78. tests/test_metadatas.py +79 -70
  79. tests/test_oauth2_clients.py +157 -0
  80. tests/test_ogc_servers.py +51 -7
  81. tests/test_restriction_areas.py +81 -17
  82. tests/test_role.py +110 -76
  83. tests/test_themes.py +44 -37
  84. tests/test_themes_ordering.py +1 -1
  85. tests/test_treegroup.py +2 -2
  86. tests/test_user.py +31 -64
  87. tests/themes_ordering.py +1 -1
  88. c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -10
  89. c2cgeoportal_admin-2.5.0.100.dist-info/RECORD +0 -84
  90. c2cgeoportal_admin-2.5.0.100.dist-info/entry_points.txt +0 -3
  91. {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.7.1.156.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-2021, Camptocamp SA
4
2
  # All rights reserved.
5
3
 
6
4
  # Redistribution and use in source and binary forms, with or without
@@ -28,16 +26,20 @@
28
26
  # either expressed or implied, of the FreeBSD Project.
29
27
 
30
28
 
31
- from c2c.template.config import config as configuration
29
+ from typing import Any
30
+
32
31
  import c2cgeoform
33
32
  import c2cwsgiutils.pretty_json
33
+ import sqlalchemy
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
43
45
 
@@ -48,9 +50,7 @@ _ = TranslationStringFactory("c2cgeoportal_admin")
48
50
 
49
51
 
50
52
  def main(_, **settings):
51
- """
52
- This function returns a Pyramid WSGI application.
53
- """
53
+ """Return a Pyramid WSGI application."""
54
54
  configuration.init(settings.get("app.cfg"))
55
55
  settings.update(configuration.get_config())
56
56
 
@@ -68,7 +68,9 @@ def main(_, **settings):
68
68
  session_factory = sessionmaker()
69
69
  session_factory.configure(bind=engine)
70
70
 
71
- def get_tm_session(session_factory, transaction_manager):
71
+ def get_tm_session(
72
+ session_factory: sessionmaker, transaction_manager: TransactionManager
73
+ ) -> sqlalchemy.orm.session.Session:
72
74
  dbsession = session_factory()
73
75
  zope.sqlalchemy.register(dbsession, transaction_manager=transaction_manager)
74
76
  return dbsession
@@ -88,7 +90,9 @@ def main(_, **settings):
88
90
 
89
91
 
90
92
  class PermissionSetter:
91
- def __init__(self, config):
93
+ """Set the permission to the admin user."""
94
+
95
+ def __init__(self, config: Configurator):
92
96
  self.default_permission_to_revert = None
93
97
  self.config = config
94
98
 
@@ -100,12 +104,13 @@ class PermissionSetter:
100
104
  ]["introspectable"]["value"]
101
105
  self.config.set_default_permission("admin")
102
106
 
103
- def __exit__(self, _type, value, traceback):
107
+ def __exit__(self, _type: Any, value: Any, traceback: Any) -> None:
104
108
  self.config.commit() # avoid .ConfigurationConflictError
105
109
  self.config.set_default_permission(self.default_permission_to_revert)
106
110
 
107
111
 
108
- def includeme(config: Configurator):
112
+ def includeme(config: Configurator) -> None:
113
+ """Initialize the Pyramid application."""
109
114
  config.include("pyramid_jinja2")
110
115
  config.include("c2cgeoform")
111
116
  config.include("c2cgeoportal_commons")
@@ -114,5 +119,7 @@ def includeme(config: Configurator):
114
119
  config.include("pyramid_tm")
115
120
  config.add_translation_dirs("c2cgeoportal_admin:locale")
116
121
 
122
+ configure_mappers()
123
+
117
124
  with PermissionSetter(config):
118
125
  config.scan()
File without changes
@@ -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
@@ -0,0 +1,409 @@
1
+ # Copyright (c) 2020-2021, 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, Set, 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: Optional[main.TreeGroup] = 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: Optional[main.TreeGroup] = None) -> main.LayerWMS:
291
+ name_el = el.find("Name")
292
+ assert name_el is not None
293
+ name = name_el.text
294
+
295
+ layer = cast(
296
+ Optional[main.LayerWMS],
297
+ self._request.dbsession.query(main.LayerWMS).filter(main.LayerWMS.name == name).one_or_none(),
298
+ )
299
+
300
+ if layer is None:
301
+ layer = main.LayerWMS()
302
+
303
+ # TreeItem
304
+ layer.name = name
305
+ layer.description = self._default_wms.description
306
+ layer.metadatas = [main.Metadata(name=m.name, value=m.value) for m in self._default_wms.metadatas]
307
+
308
+ # Layer
309
+ layer.public = False
310
+ layer.geo_table = None
311
+ layer.exclude_properties = self._default_wms.exclude_properties
312
+ layer.interfaces = list(self._default_wms.interfaces) or self._interfaces
313
+
314
+ # DimensionLayer
315
+ layer.dimensions = [
316
+ main.Dimension(
317
+ name=d.name,
318
+ value=d.value,
319
+ field=d.field,
320
+ description=d.description,
321
+ )
322
+ for d in self._default_wms.dimensions
323
+ ]
324
+
325
+ # LayerWMS
326
+ layer.ogc_server = self._ogc_server
327
+ layer.layer = name
328
+ layer.style = (
329
+ self._default_wms.style
330
+ if el.find(f"./Style/Name[.='{self._default_wms.style}']") is not None
331
+ else None
332
+ )
333
+ # layer.time_mode =
334
+ # layer.time_widget =
335
+
336
+ self._request.dbsession.add(layer)
337
+ if not isinstance(parent, main.LayerGroup):
338
+ self._logger.info("Layer %s added as new layer with no parent", name)
339
+ else:
340
+ layer.parents_relation.append(main.LayergroupTreeitem(group=parent))
341
+ self._logger.info("Layer %s added as new layer in group %s", name, parent.name)
342
+ self._layers_added += 1
343
+
344
+ else:
345
+ self._items_found += 1
346
+ if layer.ogc_server is not self._ogc_server:
347
+ self._logger.info(
348
+ "Layer %s: another layer already exists with the same name in OGC server %s",
349
+ name,
350
+ self._ogc_server.name,
351
+ )
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
+
358
+ return layer
359
+
360
+ @functools.lru_cache()
361
+ def wms_capabilities(self) -> bytes:
362
+ errors: Set[str] = set()
363
+ url = get_url2(
364
+ f"The OGC server '{self._ogc_server.name}'",
365
+ self._ogc_server.url,
366
+ self._request,
367
+ errors,
368
+ )
369
+ if url is None:
370
+ raise Exception("\n".join(errors))
371
+
372
+ # Add functionality params
373
+ # sparams = get_mapserver_substitution_params(self.request)
374
+ # url.add_query(url, sparams)
375
+
376
+ url.add_query(
377
+ {
378
+ "SERVICE": "WMS",
379
+ "VERSION": "1.1.1",
380
+ "REQUEST": "GetCapabilities",
381
+ "ROLE_IDS": "0",
382
+ "USER_ID": "0",
383
+ },
384
+ )
385
+
386
+ self._logger.info("Get WMS GetCapabilities from: %s", url)
387
+
388
+ headers = {}
389
+
390
+ # Add headers for Geoserver
391
+ if self._ogc_server.auth == main.OGCSERVER_AUTH_GEOSERVER:
392
+ headers["sec-username"] = "root"
393
+ headers["sec-roles"] = "root"
394
+
395
+ response = requests.get(url.url(), headers=headers, timeout=300)
396
+ self._logger.info("Got response %s in %.1fs.", response.status_code, response.elapsed.total_seconds())
397
+ response.raise_for_status()
398
+
399
+ # With WMS 1.3 it returns text/xml also in case of error :-(
400
+ if response.headers.get("Content-Type", "").split(";")[0].strip() not in [
401
+ "application/vnd.ogc.wms_xml",
402
+ "text/xml",
403
+ ]:
404
+ raise Exception(
405
+ f"GetCapabilities from URL '{url}' returns a wrong Content-Type: "
406
+ f"{response.headers.get('Content-Type', '')}\n{response.text}"
407
+ )
408
+
409
+ return response.content
File without changes
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2017-2020, Camptocamp SA
1
+ # Copyright (c) 2017-2021, 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
 
@@ -51,31 +50,40 @@ def includeme(config):
51
50
  register_route(config, "layertree_delete", "/{application:admin}/layertree/delete/{item_id}")
52
51
  register_route(config, "convert_to_wms", "/{application:admin}/{table:layers_wmts}/{id}/convert_to_wms")
53
52
  register_route(config, "convert_to_wmts", "/{application:admin}/{table:layers_wms}/{id}/convert_to_wmts")
53
+ register_route(
54
+ config, "ogcserver_synchronize", "/{application:admin}/{table:ogc_servers}/{id}/synchronize"
55
+ )
54
56
 
55
57
  from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
56
- Role,
58
+ Functionality,
59
+ Interface,
60
+ LayerGroup,
61
+ LayerVectorTiles,
57
62
  LayerWMS,
58
63
  LayerWMTS,
59
- Theme,
60
- LayerGroup,
61
- Interface,
62
64
  OGCServer,
63
- Functionality,
64
65
  RestrictionArea,
66
+ Role,
67
+ Theme,
68
+ )
69
+ from c2cgeoportal_commons.models.static import ( # pylint: disable=import-outside-toplevel
70
+ OAuth2Client,
71
+ User,
65
72
  )
66
- from c2cgeoportal_commons.models.static import User # pylint: disable=import-outside-toplevel
67
73
 
68
74
  visible_routes = [
69
75
  ("themes", Theme),
70
76
  ("layer_groups", LayerGroup),
71
77
  ("layers_wms", LayerWMS),
72
78
  ("layers_wmts", LayerWMTS),
79
+ ("layers_vectortiles", LayerVectorTiles),
73
80
  ("ogc_servers", OGCServer),
74
81
  ("restriction_areas", RestrictionArea),
75
82
  ("users", User),
76
83
  ("roles", Role),
77
84
  ("functionalities", Functionality),
78
85
  ("interfaces", Interface),
86
+ ("oauth2_clients", OAuth2Client),
79
87
  ]
80
88
 
81
89
  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
@@ -28,16 +26,20 @@
28
26
  # either expressed or implied, of the FreeBSD Project.
29
27
 
30
28
 
31
- from c2cgeoform.schema import GeoFormSchemaNode
32
29
  import colander
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
+ )