c2cgeoportal-admin 2.6.0__py3-none-any.whl → 2.9rc45__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 (80) hide show
  1. c2cgeoportal_admin/__init__.py +42 -12
  2. c2cgeoportal_admin/lib/lingva_extractor.py +77 -0
  3. c2cgeoportal_admin/lib/ogcserver_synchronizer.py +170 -57
  4. c2cgeoportal_admin/py.typed +0 -0
  5. c2cgeoportal_admin/routes.py +18 -6
  6. c2cgeoportal_admin/schemas/dimensions.py +16 -10
  7. c2cgeoportal_admin/schemas/functionalities.py +59 -21
  8. c2cgeoportal_admin/schemas/interfaces.py +26 -18
  9. c2cgeoportal_admin/schemas/metadata.py +101 -48
  10. c2cgeoportal_admin/schemas/restriction_areas.py +25 -19
  11. c2cgeoportal_admin/schemas/roles.py +12 -6
  12. c2cgeoportal_admin/schemas/treegroup.py +46 -21
  13. c2cgeoportal_admin/schemas/treeitem.py +3 -4
  14. c2cgeoportal_admin/static/layertree.css +3 -4
  15. c2cgeoportal_admin/static/navbar.css +36 -35
  16. c2cgeoportal_admin/static/theme.css +19 -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 +14 -9
  26. c2cgeoportal_admin/views/functionalities.py +52 -18
  27. c2cgeoportal_admin/views/home.py +5 -5
  28. c2cgeoportal_admin/views/interfaces.py +26 -20
  29. c2cgeoportal_admin/views/layer_groups.py +36 -25
  30. c2cgeoportal_admin/views/layers.py +17 -13
  31. c2cgeoportal_admin/views/layers_cog.py +135 -0
  32. c2cgeoportal_admin/views/layers_vectortiles.py +62 -27
  33. c2cgeoportal_admin/views/layers_wms.py +55 -34
  34. c2cgeoportal_admin/views/layers_wmts.py +54 -34
  35. c2cgeoportal_admin/views/layertree.py +38 -29
  36. c2cgeoportal_admin/views/logged_views.py +83 -0
  37. c2cgeoportal_admin/views/logs.py +91 -0
  38. c2cgeoportal_admin/views/oauth2_clients.py +30 -18
  39. c2cgeoportal_admin/views/ogc_servers.py +132 -36
  40. c2cgeoportal_admin/views/restriction_areas.py +39 -27
  41. c2cgeoportal_admin/views/roles.py +42 -28
  42. c2cgeoportal_admin/views/themes.py +47 -35
  43. c2cgeoportal_admin/views/themes_ordering.py +19 -14
  44. c2cgeoportal_admin/views/treeitems.py +21 -17
  45. c2cgeoportal_admin/views/users.py +46 -26
  46. c2cgeoportal_admin/widgets.py +17 -14
  47. {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.9rc45.dist-info}/METADATA +12 -12
  48. c2cgeoportal_admin-2.9rc45.dist-info/RECORD +97 -0
  49. {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.9rc45.dist-info}/WHEEL +1 -1
  50. c2cgeoportal_admin-2.9rc45.dist-info/entry_points.txt +5 -0
  51. tests/__init__.py +24 -20
  52. tests/conftest.py +22 -11
  53. tests/test_edit_url.py +11 -14
  54. tests/test_functionalities.py +52 -14
  55. tests/test_home.py +0 -1
  56. tests/test_interface.py +34 -11
  57. tests/test_layer_groups.py +57 -27
  58. tests/test_layers_cog.py +243 -0
  59. tests/test_layers_vectortiles.py +43 -25
  60. tests/test_layers_wms.py +67 -45
  61. tests/test_layers_wmts.py +47 -26
  62. tests/test_layertree.py +99 -16
  63. tests/test_left_menu.py +0 -1
  64. tests/test_lingva_extractor_config.py +64 -0
  65. tests/test_logs.py +102 -0
  66. tests/test_main.py +3 -1
  67. tests/test_metadatas.py +34 -21
  68. tests/test_oauth2_clients.py +40 -11
  69. tests/test_ogc_servers.py +84 -35
  70. tests/test_restriction_areas.py +38 -15
  71. tests/test_role.py +71 -43
  72. tests/test_themes.py +71 -37
  73. tests/test_themes_ordering.py +1 -2
  74. tests/test_treegroup.py +2 -2
  75. tests/test_user.py +56 -19
  76. tests/themes_ordering.py +1 -2
  77. c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -33
  78. c2cgeoportal_admin-2.6.0.dist-info/RECORD +0 -89
  79. c2cgeoportal_admin-2.6.0.dist-info/entry_points.txt +0 -3
  80. {c2cgeoportal_admin-2.6.0.dist-info → c2cgeoportal_admin-2.9rc45.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2023-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
+ import datetime
29
+ from typing import Generic, TypeVar
30
+
31
+ from c2cgeoform.views.abstract_views import AbstractViews, DeleteResponse, SaveResponse
32
+ from pyramid.httpexceptions import HTTPFound
33
+
34
+ from c2cgeoportal_commons.models import Base
35
+ from c2cgeoportal_commons.models.main import Log, LogAction
36
+
37
+ _T = TypeVar("_T", bound=Log)
38
+
39
+
40
+ class LoggedViews(AbstractViews[_T], Generic[_T]):
41
+ """Extension of AbstractViews which log actions in a table."""
42
+
43
+ _log_model = Log # main.Log or static.Log
44
+ _name_field = "name"
45
+
46
+ def save(self) -> SaveResponse:
47
+ response = super().save()
48
+ if isinstance(response, HTTPFound):
49
+ self._create_log(
50
+ action=LogAction.INSERT if self._is_new() else LogAction.UPDATE,
51
+ obj=self._obj,
52
+ )
53
+
54
+ return response
55
+
56
+ def delete(self) -> DeleteResponse:
57
+ obj = self._get_object()
58
+
59
+ response = super().delete()
60
+
61
+ self._create_log(LogAction.DELETE, obj)
62
+
63
+ return response
64
+
65
+ def _create_log(
66
+ self,
67
+ action: LogAction,
68
+ obj: Base, # type: ignore[valid-type]
69
+ element_url_table: str | None = None,
70
+ ) -> None:
71
+ assert self._model is not None
72
+ assert self._name_field is not None
73
+ assert self._id_field is not None
74
+ log = self._log_model(
75
+ date=datetime.datetime.now(datetime.timezone.utc),
76
+ action=action,
77
+ element_type=self._model.__tablename__,
78
+ element_id=getattr(obj, self._id_field),
79
+ element_name=getattr(obj, self._name_field),
80
+ element_url_table=element_url_table or self._request.matchdict.get("table", None),
81
+ username=self._request.user.username,
82
+ )
83
+ self._request.dbsession.add(log)
@@ -0,0 +1,91 @@
1
+ # Copyright (c) 2023-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
+ from functools import partial
29
+
30
+ from c2cgeoform import JSONDict
31
+ from c2cgeoform.views.abstract_views import AbstractViews, GridResponse, IndexResponse, ItemAction, ListField
32
+ from pyramid.view import view_config, view_defaults
33
+
34
+ from c2cgeoportal_commons.models import _
35
+ from c2cgeoportal_commons.models.main import AbstractLog
36
+
37
+ _list_field = partial(ListField, AbstractLog)
38
+
39
+
40
+ @view_defaults(match_param="table=logs")
41
+ class LogViews(AbstractViews[AbstractLog]):
42
+ """The theme administration view."""
43
+
44
+ # We pass labels explicitly because actually we are not able to get info
45
+ # from InstrumentedAttribute created from AbstractConcreteBase.
46
+ _list_fields = [
47
+ _list_field("id"),
48
+ _list_field("date", label=_("Date")),
49
+ _list_field("username", label=_("Username")),
50
+ _list_field("action", label=_("Action"), renderer=lambda log: log.action.name),
51
+ _list_field("element_type", label=_("Element type")),
52
+ _list_field("element_id", label=_("Element identifier")),
53
+ _list_field("element_name", label=_("Element name")),
54
+ ]
55
+ _list_ordered_fields = [AbstractLog.date.desc()]
56
+
57
+ _id_field = "id"
58
+ _model = AbstractLog
59
+
60
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
61
+ def index(self) -> IndexResponse:
62
+ return super().index()
63
+
64
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
65
+ def grid(self) -> GridResponse:
66
+ return super().grid()
67
+
68
+ def _grid_actions(self):
69
+ return []
70
+
71
+ def _grid_item_actions(self, item: AbstractLog) -> JSONDict:
72
+ element_url = self._request.route_url(
73
+ "c2cgeoform_item",
74
+ table=item.element_url_table,
75
+ id=item.element_id,
76
+ )
77
+
78
+ return {
79
+ "dblclick": element_url,
80
+ "dropdown": [
81
+ ItemAction(
82
+ name="edit_element",
83
+ label=_("Edit element"),
84
+ icon="glyphicon glyphicon-pencil",
85
+ url=element_url,
86
+ ).to_dict(self._request)
87
+ ],
88
+ }
89
+
90
+ def _item_actions(self, item, readonly=False):
91
+ return []
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2021, Camptocamp SA
1
+ # Copyright (c) 2021-2024, Camptocamp SA
4
2
  # All rights reserved.
5
3
 
6
4
  # Redistribution and use in source and binary forms, with or without
@@ -31,10 +29,18 @@
31
29
  from functools import partial
32
30
 
33
31
  from c2cgeoform.schema import GeoFormSchemaNode
34
- from c2cgeoform.views.abstract_views import AbstractViews, ListField
32
+ from c2cgeoform.views.abstract_views import (
33
+ DeleteResponse,
34
+ GridResponse,
35
+ IndexResponse,
36
+ ListField,
37
+ ObjectResponse,
38
+ SaveResponse,
39
+ )
35
40
  from pyramid.view import view_config, view_defaults
36
41
 
37
- from c2cgeoportal_commons.models.static import OAuth2Client
42
+ from c2cgeoportal_admin.views.logged_views import LoggedViews
43
+ from c2cgeoportal_commons.models.static import Log, OAuth2Client
38
44
 
39
45
  _list_field = partial(ListField, OAuth2Client)
40
46
 
@@ -43,7 +49,9 @@ base_schema.add_unique_validator(OAuth2Client.client_id, OAuth2Client.id)
43
49
 
44
50
 
45
51
  @view_defaults(match_param="table=oauth2_clients")
46
- class OAuth2ClientViews(AbstractViews):
52
+ class OAuth2ClientViews(LoggedViews[OAuth2Client]):
53
+ """The oAuth2 client administration view."""
54
+
47
55
  _list_fields = [
48
56
  _list_field("id"),
49
57
  _list_field("client_id"),
@@ -53,32 +61,36 @@ class OAuth2ClientViews(AbstractViews):
53
61
  _id_field = "id"
54
62
  _model = OAuth2Client
55
63
  _base_schema = base_schema
64
+ _log_model = Log
65
+ _name_field = "client_id"
56
66
 
57
67
  def _base_query(self):
58
68
  return self._request.dbsession.query(OAuth2Client)
59
69
 
60
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
61
- def index(self):
70
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
71
+ def index(self) -> IndexResponse:
62
72
  return super().index()
63
73
 
64
- @view_config(route_name="c2cgeoform_grid", renderer="fast_json")
65
- def grid(self):
74
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
75
+ def grid(self) -> GridResponse:
66
76
  return super().grid()
67
77
 
68
- @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
69
- def view(self):
78
+ @view_config( # type: ignore[misc]
79
+ route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2"
80
+ )
81
+ def view(self) -> ObjectResponse:
70
82
  return super().edit()
71
83
 
72
- @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
73
- def save(self):
84
+ @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
85
+ def save(self) -> SaveResponse:
74
86
  return super().save()
75
87
 
76
- @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
77
- def delete(self):
88
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
89
+ def delete(self) -> DeleteResponse:
78
90
  return super().delete()
79
91
 
80
- @view_config(
92
+ @view_config( # type: ignore[misc]
81
93
  route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
82
94
  )
83
- def duplicate(self):
95
+ def duplicate(self) -> ObjectResponse:
84
96
  return super().duplicate()
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2017-2021, 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,27 +26,49 @@
28
26
  # either expressed or implied, of the FreeBSD Project.
29
27
 
30
28
 
29
+ import logging
30
+ import threading
31
31
  from functools import partial
32
+ from typing import Any, cast
32
33
 
34
+ import requests
35
+ from c2cgeoform import JSONDict
33
36
  from c2cgeoform.schema import GeoFormSchemaNode
34
- from c2cgeoform.views.abstract_views import AbstractViews, ItemAction, ListField, UserMessage
37
+ from c2cgeoform.views.abstract_views import (
38
+ AbstractViews,
39
+ DeleteResponse,
40
+ GridResponse,
41
+ IndexResponse,
42
+ ItemAction,
43
+ ListField,
44
+ ObjectResponse,
45
+ SaveResponse,
46
+ UserMessage,
47
+ )
35
48
  from deform.widget import FormWidget
36
- from pyramid.httpexceptions import HTTPNotFound
49
+ from pyramid.httpexceptions import HTTPFound
37
50
  from pyramid.view import view_config, view_defaults
38
51
  from sqlalchemy import inspect
39
52
 
40
53
  from c2cgeoportal_admin import _
41
54
  from c2cgeoportal_admin.lib.ogcserver_synchronizer import OGCServerSynchronizer
42
- from c2cgeoportal_commons.models.main import OGCServer
55
+ from c2cgeoportal_admin.views.logged_views import LoggedViews
56
+ from c2cgeoportal_commons.lib.literal import Literal
57
+ from c2cgeoportal_commons.models import cache_invalidate_cb
58
+ from c2cgeoportal_commons.models.main import LogAction, OGCServer
43
59
 
44
60
  _list_field = partial(ListField, OGCServer)
45
61
 
46
62
  base_schema = GeoFormSchemaNode(OGCServer, widget=FormWidget(fields_template="ogcserver_fields"))
47
63
  base_schema.add_unique_validator(OGCServer.name, OGCServer.id)
48
64
 
65
+ _LOG = logging.getLogger(__name__)
66
+
49
67
 
50
68
  @view_defaults(match_param="table=ogc_servers")
51
- class OGCServerViews(AbstractViews):
69
+ class OGCServerViews(LoggedViews[OGCServer]):
70
+ """The OGC server administration view."""
71
+
52
72
  _list_fields = [
53
73
  _list_field("id"),
54
74
  _list_field("name"),
@@ -73,28 +93,51 @@ class OGCServerViews(AbstractViews):
73
93
  ),
74
94
  }
75
95
 
76
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
77
- def index(self):
96
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
97
+ def index(self) -> IndexResponse:
78
98
  return super().index()
79
99
 
80
- @view_config(route_name="c2cgeoform_grid", renderer="fast_json")
81
- def grid(self):
100
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
101
+ def grid(self) -> GridResponse:
82
102
  return super().grid()
83
103
 
84
- def schema(self):
85
- try:
86
- obj = self._get_object()
87
- except HTTPNotFound:
88
- obj = None
104
+ def schema(self) -> GeoFormSchemaNode:
105
+ obj = self._get_object()
89
106
 
90
- schema = self._base_schema.clone()
91
- schema["url"].description = obj.url_description(self._request)
92
- schema["url_wfs"].description = obj.url_wfs_description(self._request)
107
+ schema = cast(GeoFormSchemaNode, self._base_schema.clone())
108
+ schema["url"].description = Literal(
109
+ _("{}<br>Current runtime value is: {}").format(
110
+ schema["url"].description,
111
+ obj.url_description(self._request),
112
+ )
113
+ )
114
+ schema["url_wfs"].description = Literal(
115
+ _("{}<br>Current runtime value is: {}").format(
116
+ schema["url_wfs"].description,
117
+ obj.url_wfs_description(self._request),
118
+ )
119
+ )
93
120
  return schema
94
121
 
95
- def _item_actions(self, item, readonly=False):
96
- actions = super()._item_actions(item, readonly)
97
- if inspect(item).persistent:
122
+ def _item_actions(self, item: OGCServer, readonly: bool = False) -> list[Any]:
123
+ actions = cast(list[Any], super()._item_actions(item, readonly))
124
+ if inspect(item).persistent: # type: ignore[attr-defined]
125
+ actions.insert(
126
+ next((i for i, v in enumerate(actions) if v.name() == "delete")),
127
+ ItemAction(
128
+ name="clear-cache",
129
+ label=_("Clear the cache"),
130
+ icon="glyphicon glyphicon-hdd",
131
+ url=self._request.route_url(
132
+ "ogc_server_clear_cache",
133
+ id=getattr(item, self._id_field),
134
+ _query={
135
+ "came_from": self._request.current_route_url(),
136
+ },
137
+ ),
138
+ confirmation=_("The current changes will be lost."),
139
+ ),
140
+ )
98
141
  actions.insert(
99
142
  next((i for i, v in enumerate(actions) if v.name() == "delete")),
100
143
  ItemAction(
@@ -106,16 +149,24 @@ class OGCServerViews(AbstractViews):
106
149
  )
107
150
  return actions
108
151
 
109
- @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
110
- def view(self):
152
+ @view_config( # type: ignore[misc]
153
+ route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2"
154
+ )
155
+ def view(self) -> ObjectResponse:
111
156
  return super().edit(self.schema())
112
157
 
113
- @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
114
- def save(self):
115
- return super().save()
158
+ @view_config( # type: ignore[misc]
159
+ route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2"
160
+ )
161
+ def save(self) -> SaveResponse:
162
+ result = super().save()
163
+ if isinstance(result, HTTPFound):
164
+ assert self._obj is not None
165
+ self._update_cache(self._obj)
166
+ return result
116
167
 
117
- @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
118
- def delete(self):
168
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
169
+ def delete(self) -> DeleteResponse:
119
170
  obj = self._get_object()
120
171
  if len(obj.layers) > 0:
121
172
  return {
@@ -127,16 +178,20 @@ class OGCServerViews(AbstractViews):
127
178
  _query=[("msg_col", "cannot_delete")],
128
179
  ),
129
180
  }
130
- return super().delete()
181
+ result = super().delete()
182
+ cache_invalidate_cb()
183
+ return result
131
184
 
132
- @view_config(
185
+ @view_config( # type: ignore[misc]
133
186
  route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
134
187
  )
135
- def duplicate(self):
188
+ def duplicate(self) -> ObjectResponse:
136
189
  return super().duplicate()
137
190
 
138
- @view_config(route_name="ogcserver_synchronize", renderer="../templates/ogcserver_synchronize.jinja2")
139
- def synchronize(self):
191
+ @view_config( # type: ignore[misc]
192
+ route_name="ogcserver_synchronize", renderer="../templates/ogcserver_synchronize.jinja2"
193
+ )
194
+ def synchronize(self) -> JSONDict:
140
195
  obj = self._get_object()
141
196
 
142
197
  if self._request.method == "GET":
@@ -147,17 +202,58 @@ class OGCServerViews(AbstractViews):
147
202
  }
148
203
 
149
204
  if self._request.method == "POST":
150
- synchronizer = OGCServerSynchronizer(self._request, obj)
205
+ force_parents = self._request.POST.get("force-parents", "false") == "on"
206
+ force_ordering = self._request.POST.get("force-ordering", "false") == "on"
207
+ clean = self._request.POST.get("clean", "false") == "on"
208
+
209
+ synchronizer = OGCServerSynchronizer(
210
+ self._request,
211
+ obj,
212
+ force_parents=force_parents,
213
+ force_ordering=force_ordering,
214
+ clean=clean,
215
+ )
216
+
217
+ ogc_server_id = obj.id
151
218
  if "check" in self._request.params:
152
219
  synchronizer.check_layers()
220
+
153
221
  elif "dry-run" in self._request.params:
154
222
  synchronizer.synchronize(dry_run=True)
223
+
155
224
  elif "synchronize" in self._request.params:
156
225
  synchronizer.synchronize()
226
+
227
+ self._create_log(LogAction.SYNCHRONIZE, obj)
228
+
157
229
  return {
158
- "ogcserver": obj,
230
+ "ogcserver": self._request.dbsession.query(OGCServer).get(ogc_server_id),
159
231
  "success": True,
160
232
  "report": synchronizer.report(),
161
233
  }
162
234
 
235
+ self._update_cache(obj)
236
+
163
237
  return {}
238
+
239
+ def _update_cache(self, ogc_server: OGCServer) -> None:
240
+ try:
241
+ ogc_server_id = ogc_server.id
242
+
243
+ def update_cache() -> None:
244
+ response = requests.get(
245
+ self._request.route_url(
246
+ "ogc_server_clear_cache",
247
+ id=ogc_server_id,
248
+ _query={
249
+ "came_from": self._request.current_route_url(),
250
+ },
251
+ ),
252
+ timeout=60,
253
+ )
254
+ if not response.ok:
255
+ _LOG.error("Error while cleaning the OGC server cache:\n%s", response.text)
256
+
257
+ threading.Thread(target=update_cache).start()
258
+ except Exception: # pylint: disable=broad-exception-caught
259
+ _LOG.error("Error on cleaning the OGC server cache", exc_info=True)
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
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
@@ -31,33 +29,42 @@
31
29
  from functools import partial
32
30
 
33
31
  import colander
32
+ import sqlalchemy.orm.query
34
33
  from c2cgeoform.schema import GeoFormManyToManySchemaNode, GeoFormSchemaNode
35
- from c2cgeoform.views.abstract_views import AbstractViews, ListField
34
+ from c2cgeoform.views.abstract_views import (
35
+ DeleteResponse,
36
+ GridResponse,
37
+ IndexResponse,
38
+ ListField,
39
+ ObjectResponse,
40
+ SaveResponse,
41
+ )
36
42
  from deform.widget import FormWidget
37
43
  from pyramid.view import view_config, view_defaults
38
44
  from sqlalchemy.orm import subqueryload
39
45
 
40
- from c2cgeoportal_admin import _
41
46
  from c2cgeoportal_admin.schemas.roles import roles_schema_node
42
47
  from c2cgeoportal_admin.schemas.treegroup import treeitem_edit_url
48
+ from c2cgeoportal_admin.views.logged_views import LoggedViews
43
49
  from c2cgeoportal_admin.widgets import ChildrenWidget, ChildWidget
44
50
  from c2cgeoportal_commons.models.main import Layer, RestrictionArea
45
51
 
46
52
  _list_field = partial(ListField, RestrictionArea)
47
53
 
48
54
  base_schema = GeoFormSchemaNode(RestrictionArea, widget=FormWidget(fields_template="restriction_area_fields"))
49
- base_schema.add_before("area", roles_schema_node("roles"))
55
+ base_schema.add_before("area", roles_schema_node(RestrictionArea.roles))
50
56
  base_schema.add_unique_validator(RestrictionArea.name, RestrictionArea.id)
51
57
 
52
58
 
53
59
  def layers(node, kw): # pylint: disable=unused-argument
60
+ """Get the layers serializable representation."""
54
61
  dbsession = kw["request"].dbsession
55
62
  query = dbsession.query(Layer).order_by(Layer.name)
56
63
  return [
57
64
  {
58
65
  "id": layer.id,
59
66
  "label": layer.name,
60
- "icon_class": "icon-{}".format(layer.item_type),
67
+ "icon_class": f"icon-{layer.item_type}",
61
68
  "edit_url": treeitem_edit_url(kw["request"], layer),
62
69
  "group": "All",
63
70
  }
@@ -75,12 +82,13 @@ base_schema.add(
75
82
  input_name="id",
76
83
  model=Layer,
77
84
  label_field="name",
78
- icon_class=lambda layer: "icon-{}".format(layer.item_type),
85
+ icon_class=lambda layer: f"icon-{layer.item_type}",
79
86
  edit_url=treeitem_edit_url,
80
87
  ),
81
88
  ),
82
89
  name="layers",
83
- title=_("Layers"),
90
+ title=RestrictionArea.layers.info["colanderalchemy"]["title"],
91
+ description=RestrictionArea.layers.info["colanderalchemy"]["description"],
84
92
  candidates=colander.deferred(layers),
85
93
  widget=ChildrenWidget(child_input_name="id", orderable=False),
86
94
  )
@@ -88,7 +96,9 @@ base_schema.add(
88
96
 
89
97
 
90
98
  @view_defaults(match_param="table=restriction_areas")
91
- class RestrictionAreaViews(AbstractViews):
99
+ class RestrictionAreaViews(LoggedViews[RestrictionArea]):
100
+ """The restriction area administration view."""
101
+
92
102
  _list_fields = [
93
103
  _list_field("id"),
94
104
  _list_field("name"),
@@ -100,7 +110,7 @@ class RestrictionAreaViews(AbstractViews):
100
110
  _list_field(
101
111
  "layers",
102
112
  renderer=lambda restriction_area: ", ".join(
103
- "{}-{}".format(layer.item_type, layer.name) or "" for layer in restriction_area.layers
113
+ f"{layer.item_type}-{layer.name}" or "" for layer in restriction_area.layers
104
114
  ),
105
115
  ),
106
116
  ]
@@ -108,35 +118,37 @@ class RestrictionAreaViews(AbstractViews):
108
118
  _model = RestrictionArea
109
119
  _base_schema = base_schema
110
120
 
111
- def _base_query(self):
121
+ def _base_query(self) -> sqlalchemy.orm.query.Query[RestrictionArea]:
122
+ session = self._request.dbsession
123
+ assert isinstance(session, sqlalchemy.orm.Session)
112
124
  return (
113
- self._request.dbsession.query(RestrictionArea)
114
- .options(subqueryload("roles"))
115
- .options(subqueryload("layers"))
125
+ session.query(RestrictionArea)
126
+ .options(subqueryload(RestrictionArea.roles))
127
+ .options(subqueryload(RestrictionArea.layers))
116
128
  )
117
129
 
118
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
119
- def index(self):
130
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
131
+ def index(self) -> IndexResponse:
120
132
  return super().index()
121
133
 
122
- @view_config(route_name="c2cgeoform_grid", renderer="fast_json")
123
- def grid(self):
134
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
135
+ def grid(self) -> GridResponse:
124
136
  return super().grid()
125
137
 
126
- @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
127
- def view(self):
138
+ @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2") # type: ignore[misc]
139
+ def view(self) -> ObjectResponse:
128
140
  return super().edit()
129
141
 
130
- @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
131
- def save(self):
142
+ @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
143
+ def save(self) -> SaveResponse:
132
144
  return super().save()
133
145
 
134
- @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
135
- def delete(self):
146
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
147
+ def delete(self) -> DeleteResponse:
136
148
  return super().delete()
137
149
 
138
- @view_config(
150
+ @view_config( # type: ignore[misc]
139
151
  route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
140
152
  )
141
- def duplicate(self):
153
+ def duplicate(self) -> ObjectResponse:
142
154
  return super().duplicate()