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.
Files changed (99) hide show
  1. c2cgeoportal_admin/__init__.py +44 -14
  2. c2cgeoportal_admin/lib/__init__.py +0 -0
  3. c2cgeoportal_admin/lib/lingva_extractor.py +77 -0
  4. c2cgeoportal_admin/lib/ogcserver_synchronizer.py +410 -0
  5. c2cgeoportal_admin/py.typed +0 -0
  6. c2cgeoportal_admin/routes.py +30 -11
  7. c2cgeoportal_admin/schemas/dimensions.py +17 -11
  8. c2cgeoportal_admin/schemas/functionalities.py +60 -22
  9. c2cgeoportal_admin/schemas/interfaces.py +27 -19
  10. c2cgeoportal_admin/schemas/metadata.py +122 -48
  11. c2cgeoportal_admin/schemas/restriction_areas.py +26 -20
  12. c2cgeoportal_admin/schemas/roles.py +13 -7
  13. c2cgeoportal_admin/schemas/treegroup.py +90 -20
  14. c2cgeoportal_admin/schemas/treeitem.py +3 -4
  15. c2cgeoportal_admin/static/layertree.css +26 -4
  16. c2cgeoportal_admin/static/navbar.css +59 -36
  17. c2cgeoportal_admin/static/theme.css +51 -11
  18. c2cgeoportal_admin/subscribers.py +3 -3
  19. c2cgeoportal_admin/templates/404.jinja2 +41 -2
  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/__init__.py +29 -0
  43. c2cgeoportal_admin/views/dimension_layers.py +14 -9
  44. c2cgeoportal_admin/views/functionalities.py +52 -18
  45. c2cgeoportal_admin/views/home.py +5 -5
  46. c2cgeoportal_admin/views/interfaces.py +29 -21
  47. c2cgeoportal_admin/views/layer_groups.py +36 -25
  48. c2cgeoportal_admin/views/layers.py +17 -13
  49. c2cgeoportal_admin/views/layers_cog.py +135 -0
  50. c2cgeoportal_admin/views/layers_vectortiles.py +62 -27
  51. c2cgeoportal_admin/views/layers_wms.py +61 -36
  52. c2cgeoportal_admin/views/layers_wmts.py +54 -32
  53. c2cgeoportal_admin/views/layertree.py +37 -28
  54. c2cgeoportal_admin/views/logged_views.py +83 -0
  55. c2cgeoportal_admin/views/logs.py +91 -0
  56. c2cgeoportal_admin/views/oauth2_clients.py +96 -0
  57. c2cgeoportal_admin/views/ogc_servers.py +192 -21
  58. c2cgeoportal_admin/views/restriction_areas.py +78 -25
  59. c2cgeoportal_admin/views/roles.py +88 -25
  60. c2cgeoportal_admin/views/themes.py +47 -35
  61. c2cgeoportal_admin/views/themes_ordering.py +44 -24
  62. c2cgeoportal_admin/views/treeitems.py +21 -17
  63. c2cgeoportal_admin/views/users.py +46 -26
  64. c2cgeoportal_admin/widgets.py +79 -28
  65. {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/METADATA +15 -13
  66. c2cgeoportal_admin-2.9rc44.dist-info/RECORD +97 -0
  67. {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/WHEEL +1 -1
  68. c2cgeoportal_admin-2.9rc44.dist-info/entry_points.txt +5 -0
  69. tests/__init__.py +36 -27
  70. tests/conftest.py +23 -24
  71. tests/test_edit_url.py +16 -19
  72. tests/test_functionalities.py +52 -14
  73. tests/test_home.py +0 -1
  74. tests/test_interface.py +35 -12
  75. tests/test_layer_groups.py +58 -32
  76. tests/test_layers_cog.py +243 -0
  77. tests/test_layers_vectortiles.py +46 -30
  78. tests/test_layers_wms.py +77 -82
  79. tests/test_layers_wmts.py +51 -30
  80. tests/test_layertree.py +107 -101
  81. tests/test_learn.py +1 -1
  82. tests/test_left_menu.py +0 -1
  83. tests/test_lingva_extractor_config.py +64 -0
  84. tests/test_logs.py +102 -0
  85. tests/test_main.py +4 -2
  86. tests/test_metadatas.py +79 -71
  87. tests/test_oauth2_clients.py +186 -0
  88. tests/test_ogc_servers.py +110 -28
  89. tests/test_restriction_areas.py +109 -20
  90. tests/test_role.py +142 -82
  91. tests/test_themes.py +75 -41
  92. tests/test_themes_ordering.py +1 -2
  93. tests/test_treegroup.py +2 -2
  94. tests/test_user.py +72 -70
  95. tests/themes_ordering.py +1 -2
  96. c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -10
  97. c2cgeoportal_admin-2.5.0.100.dist-info/RECORD +0 -84
  98. c2cgeoportal_admin-2.5.0.100.dist-info/entry_points.txt +0 -3
  99. {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2021-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
+ from functools import partial
30
+
31
+ from c2cgeoform.schema import GeoFormSchemaNode
32
+ from c2cgeoform.views.abstract_views import (
33
+ DeleteResponse,
34
+ GridResponse,
35
+ IndexResponse,
36
+ ListField,
37
+ ObjectResponse,
38
+ SaveResponse,
39
+ )
40
+ from pyramid.view import view_config, view_defaults
41
+
42
+ from c2cgeoportal_admin.views.logged_views import LoggedViews
43
+ from c2cgeoportal_commons.models.static import Log, OAuth2Client
44
+
45
+ _list_field = partial(ListField, OAuth2Client)
46
+
47
+ base_schema = GeoFormSchemaNode(OAuth2Client)
48
+ base_schema.add_unique_validator(OAuth2Client.client_id, OAuth2Client.id)
49
+
50
+
51
+ @view_defaults(match_param="table=oauth2_clients")
52
+ class OAuth2ClientViews(LoggedViews[OAuth2Client]):
53
+ """The oAuth2 client administration view."""
54
+
55
+ _list_fields = [
56
+ _list_field("id"),
57
+ _list_field("client_id"),
58
+ _list_field("secret"),
59
+ _list_field("redirect_uri"),
60
+ ]
61
+ _id_field = "id"
62
+ _model = OAuth2Client
63
+ _base_schema = base_schema
64
+ _log_model = Log
65
+ _name_field = "client_id"
66
+
67
+ def _base_query(self):
68
+ return self._request.dbsession.query(OAuth2Client)
69
+
70
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
71
+ def index(self) -> IndexResponse:
72
+ return super().index()
73
+
74
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
75
+ def grid(self) -> GridResponse:
76
+ return super().grid()
77
+
78
+ @view_config( # type: ignore[misc]
79
+ route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2"
80
+ )
81
+ def view(self) -> ObjectResponse:
82
+ return super().edit()
83
+
84
+ @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
85
+ def save(self) -> SaveResponse:
86
+ return super().save()
87
+
88
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
89
+ def delete(self) -> DeleteResponse:
90
+ return super().delete()
91
+
92
+ @view_config( # type: ignore[misc]
93
+ route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
94
+ )
95
+ def duplicate(self) -> ObjectResponse:
96
+ return super().duplicate()
@@ -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
@@ -28,23 +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, ListField
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
49
+ from pyramid.httpexceptions import HTTPFound
36
50
  from pyramid.view import view_config, view_defaults
51
+ from sqlalchemy import inspect
37
52
 
38
- from c2cgeoportal_commons.models.main import OGCServer
53
+ from c2cgeoportal_admin import _
54
+ from c2cgeoportal_admin.lib.ogcserver_synchronizer import OGCServerSynchronizer
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
39
59
 
40
60
  _list_field = partial(ListField, OGCServer)
41
61
 
42
62
  base_schema = GeoFormSchemaNode(OGCServer, widget=FormWidget(fields_template="ogcserver_fields"))
43
63
  base_schema.add_unique_validator(OGCServer.name, OGCServer.id)
44
64
 
65
+ _LOG = logging.getLogger(__name__)
66
+
45
67
 
46
68
  @view_defaults(match_param="table=ogc_servers")
47
- class OGCServerViews(AbstractViews):
69
+ class OGCServerViews(LoggedViews[OGCServer]):
70
+ """The OGC server administration view."""
71
+
48
72
  _list_fields = [
49
73
  _list_field("id"),
50
74
  _list_field("name"),
@@ -61,28 +85,175 @@ class OGCServerViews(AbstractViews):
61
85
  _model = OGCServer
62
86
  _base_schema = base_schema
63
87
 
64
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
65
- def index(self):
88
+ MSG_COL = {
89
+ **AbstractViews.MSG_COL,
90
+ "cannot_delete": UserMessage(
91
+ _("Impossible to delete this server while it contains WMS layers."),
92
+ "alert-danger",
93
+ ),
94
+ }
95
+
96
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
97
+ def index(self) -> IndexResponse:
66
98
  return super().index()
67
99
 
68
- @view_config(route_name="c2cgeoform_grid", renderer="fast_json")
69
- def grid(self):
100
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
101
+ def grid(self) -> GridResponse:
70
102
  return super().grid()
71
103
 
72
- @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
73
- def view(self):
74
- return super().edit()
104
+ def schema(self) -> GeoFormSchemaNode:
105
+ obj = self._get_object()
106
+
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
+ )
120
+ return schema
121
+
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
+ )
141
+ actions.insert(
142
+ next((i for i, v in enumerate(actions) if v.name() == "delete")),
143
+ ItemAction(
144
+ name="synchronize",
145
+ label=_("Synchronize"),
146
+ icon="glyphicon glyphicon-import",
147
+ url=self._request.route_url("ogcserver_synchronize", id=getattr(item, self._id_field)),
148
+ ),
149
+ )
150
+ return actions
75
151
 
76
- @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
77
- def save(self):
78
- return super().save()
152
+ @view_config( # type: ignore[misc]
153
+ route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2"
154
+ )
155
+ def view(self) -> ObjectResponse:
156
+ return super().edit(self.schema())
157
+
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
79
167
 
80
- @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
81
- def delete(self):
82
- return super().delete()
168
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
169
+ def delete(self) -> DeleteResponse:
170
+ obj = self._get_object()
171
+ if len(obj.layers) > 0:
172
+ return {
173
+ "success": True,
174
+ "redirect": self._request.route_url(
175
+ "c2cgeoform_item",
176
+ action="edit",
177
+ id=obj.id,
178
+ _query=[("msg_col", "cannot_delete")],
179
+ ),
180
+ }
181
+ result = super().delete()
182
+ cache_invalidate_cb()
183
+ return result
83
184
 
84
- @view_config(
185
+ @view_config( # type: ignore[misc]
85
186
  route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
86
187
  )
87
- def duplicate(self):
188
+ def duplicate(self) -> ObjectResponse:
88
189
  return super().duplicate()
190
+
191
+ @view_config( # type: ignore[misc]
192
+ route_name="ogcserver_synchronize", renderer="../templates/ogcserver_synchronize.jinja2"
193
+ )
194
+ def synchronize(self) -> JSONDict:
195
+ obj = self._get_object()
196
+
197
+ if self._request.method == "GET":
198
+ return {
199
+ "ogcserver": obj,
200
+ "success": None,
201
+ "report": None,
202
+ }
203
+
204
+ if self._request.method == "POST":
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
218
+ if "check" in self._request.params:
219
+ synchronizer.check_layers()
220
+
221
+ elif "dry-run" in self._request.params:
222
+ synchronizer.synchronize(dry_run=True)
223
+
224
+ elif "synchronize" in self._request.params:
225
+ synchronizer.synchronize()
226
+
227
+ self._create_log(LogAction.SYNCHRONIZE, obj)
228
+
229
+ return {
230
+ "ogcserver": self._request.dbsession.query(OGCServer).get(ogc_server_id),
231
+ "success": True,
232
+ "report": synchronizer.report(),
233
+ }
234
+
235
+ self._update_cache(obj)
236
+
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
@@ -30,24 +28,77 @@
30
28
 
31
29
  from functools import partial
32
30
 
33
- from c2cgeoform.schema import GeoFormSchemaNode
34
- from c2cgeoform.views.abstract_views import AbstractViews, ListField
31
+ import colander
32
+ import sqlalchemy.orm.query
33
+ from c2cgeoform.schema import GeoFormManyToManySchemaNode, GeoFormSchemaNode
34
+ from c2cgeoform.views.abstract_views import (
35
+ DeleteResponse,
36
+ GridResponse,
37
+ IndexResponse,
38
+ ListField,
39
+ ObjectResponse,
40
+ SaveResponse,
41
+ )
35
42
  from deform.widget import FormWidget
36
43
  from pyramid.view import view_config, view_defaults
37
44
  from sqlalchemy.orm import subqueryload
38
45
 
39
46
  from c2cgeoportal_admin.schemas.roles import roles_schema_node
40
- from c2cgeoportal_commons.models.main import RestrictionArea
47
+ from c2cgeoportal_admin.schemas.treegroup import treeitem_edit_url
48
+ from c2cgeoportal_admin.views.logged_views import LoggedViews
49
+ from c2cgeoportal_admin.widgets import ChildrenWidget, ChildWidget
50
+ from c2cgeoportal_commons.models.main import Layer, RestrictionArea
41
51
 
42
52
  _list_field = partial(ListField, RestrictionArea)
43
53
 
44
54
  base_schema = GeoFormSchemaNode(RestrictionArea, widget=FormWidget(fields_template="restriction_area_fields"))
45
- base_schema.add_before("area", roles_schema_node("roles"))
55
+ base_schema.add_before("area", roles_schema_node(RestrictionArea.roles))
46
56
  base_schema.add_unique_validator(RestrictionArea.name, RestrictionArea.id)
47
57
 
48
58
 
59
+ def layers(node, kw): # pylint: disable=unused-argument
60
+ """Get the layers serializable representation."""
61
+ dbsession = kw["request"].dbsession
62
+ query = dbsession.query(Layer).order_by(Layer.name)
63
+ return [
64
+ {
65
+ "id": layer.id,
66
+ "label": layer.name,
67
+ "icon_class": f"icon-{layer.item_type}",
68
+ "edit_url": treeitem_edit_url(kw["request"], layer),
69
+ "group": "All",
70
+ }
71
+ for layer in query
72
+ ]
73
+
74
+
75
+ base_schema.add(
76
+ colander.SequenceSchema(
77
+ GeoFormManyToManySchemaNode(
78
+ Layer,
79
+ name="layer",
80
+ includes=["id"],
81
+ widget=ChildWidget(
82
+ input_name="id",
83
+ model=Layer,
84
+ label_field="name",
85
+ icon_class=lambda layer: f"icon-{layer.item_type}",
86
+ edit_url=treeitem_edit_url,
87
+ ),
88
+ ),
89
+ name="layers",
90
+ title=RestrictionArea.layers.info["colanderalchemy"]["title"],
91
+ description=RestrictionArea.layers.info["colanderalchemy"]["description"],
92
+ candidates=colander.deferred(layers),
93
+ widget=ChildrenWidget(child_input_name="id", orderable=False),
94
+ )
95
+ )
96
+
97
+
49
98
  @view_defaults(match_param="table=restriction_areas")
50
- class RestrictionAreaViews(AbstractViews):
99
+ class RestrictionAreaViews(LoggedViews[RestrictionArea]):
100
+ """The restriction area administration view."""
101
+
51
102
  _list_fields = [
52
103
  _list_field("id"),
53
104
  _list_field("name"),
@@ -59,7 +110,7 @@ class RestrictionAreaViews(AbstractViews):
59
110
  _list_field(
60
111
  "layers",
61
112
  renderer=lambda restriction_area: ", ".join(
62
- "{}-{}".format(l.item_type, l.name) or "" for l in restriction_area.layers
113
+ f"{layer.item_type}-{layer.name}" or "" for layer in restriction_area.layers
63
114
  ),
64
115
  ),
65
116
  ]
@@ -67,35 +118,37 @@ class RestrictionAreaViews(AbstractViews):
67
118
  _model = RestrictionArea
68
119
  _base_schema = base_schema
69
120
 
70
- 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)
71
124
  return (
72
- self._request.dbsession.query(RestrictionArea)
73
- .options(subqueryload("roles"))
74
- .options(subqueryload("layers"))
125
+ session.query(RestrictionArea)
126
+ .options(subqueryload(RestrictionArea.roles))
127
+ .options(subqueryload(RestrictionArea.layers))
75
128
  )
76
129
 
77
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
78
- def index(self):
130
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
131
+ def index(self) -> IndexResponse:
79
132
  return super().index()
80
133
 
81
- @view_config(route_name="c2cgeoform_grid", renderer="fast_json")
82
- def grid(self):
134
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
135
+ def grid(self) -> GridResponse:
83
136
  return super().grid()
84
137
 
85
- @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
86
- 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:
87
140
  return super().edit()
88
141
 
89
- @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
90
- 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:
91
144
  return super().save()
92
145
 
93
- @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
94
- def delete(self):
146
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
147
+ def delete(self) -> DeleteResponse:
95
148
  return super().delete()
96
149
 
97
- @view_config(
150
+ @view_config( # type: ignore[misc]
98
151
  route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
99
152
  )
100
- def duplicate(self):
153
+ def duplicate(self) -> ObjectResponse:
101
154
  return super().duplicate()
@@ -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
@@ -30,33 +28,96 @@
30
28
 
31
29
  from functools import partial
32
30
 
33
- from c2cgeoform.schema import GeoFormSchemaNode
34
- from c2cgeoform.views.abstract_views import AbstractViews, ListField
31
+ import colander
32
+ import sqlalchemy.orm.query
33
+ from c2cgeoform.schema import GeoFormManyToManySchemaNode, GeoFormSchemaNode
34
+ from c2cgeoform.views.abstract_views import (
35
+ DeleteResponse,
36
+ GridResponse,
37
+ IndexResponse,
38
+ ListField,
39
+ ObjectResponse,
40
+ SaveResponse,
41
+ )
35
42
  from deform.widget import FormWidget
36
43
  from pyramid.view import view_config, view_defaults
37
44
  from sqlalchemy.orm import subqueryload
38
45
 
39
46
  from c2cgeoportal_admin.schemas.functionalities import functionalities_schema_node
40
47
  from c2cgeoportal_admin.schemas.restriction_areas import restrictionareas_schema_node
48
+ from c2cgeoportal_admin.views.logged_views import LoggedViews
49
+ from c2cgeoportal_admin.widgets import ChildrenWidget, ChildWidget
41
50
  from c2cgeoportal_commons.models.main import Role
51
+ from c2cgeoportal_commons.models.static import User
42
52
 
43
53
  _list_field = partial(ListField, Role)
44
54
 
45
55
  base_schema = GeoFormSchemaNode(Role, widget=FormWidget(fields_template="role_fields"))
46
- base_schema.add_before("extent", functionalities_schema_node.clone())
47
- base_schema.add_before("extent", restrictionareas_schema_node.clone())
56
+ base_schema.add_before("extent", functionalities_schema_node(Role.functionalities, Role))
57
+ base_schema.add_before("extent", restrictionareas_schema_node(Role.restrictionareas))
48
58
  base_schema.add_unique_validator(Role.name, Role.id)
49
59
 
50
60
 
61
+ def users(node, kw): # pylint: disable=unused-argument
62
+ """Get the user serializable metadata."""
63
+ dbsession = kw["request"].dbsession
64
+ query = dbsession.query(User).order_by(User.username)
65
+ return [
66
+ {
67
+ "id": user.id,
68
+ "label": user.username,
69
+ "icon_class": "icon-user",
70
+ "edit_url": kw["request"].route_url(
71
+ "c2cgeoform_item",
72
+ table="users",
73
+ id=user.id,
74
+ ),
75
+ "group": "All",
76
+ }
77
+ for user in query
78
+ ]
79
+
80
+
81
+ base_schema.add(
82
+ colander.SequenceSchema(
83
+ GeoFormManyToManySchemaNode(
84
+ User,
85
+ name="user",
86
+ includes=["id"],
87
+ widget=ChildWidget(
88
+ input_name="id",
89
+ model=User,
90
+ label_field="username",
91
+ icon_class=lambda user: "icon-user",
92
+ edit_url=lambda request, user: request.route_url(
93
+ "c2cgeoform_item",
94
+ table="users",
95
+ id=user.id,
96
+ ),
97
+ ),
98
+ ),
99
+ name=Role.users.key,
100
+ title=Role.users.info["colanderalchemy"]["title"],
101
+ description=Role.users.info["colanderalchemy"]["description"],
102
+ candidates=colander.deferred(users),
103
+ widget=ChildrenWidget(child_input_name="id", orderable=False, category="structural"),
104
+ )
105
+ )
106
+ # Not possible to overwrite this in constructor.
107
+ base_schema["users"].children[0].description = ""
108
+
109
+
51
110
  @view_defaults(match_param="table=roles")
52
- class RoleViews(AbstractViews):
111
+ class RoleViews(LoggedViews[Role]):
112
+ """The roles administration view."""
113
+
53
114
  _list_fields = [
54
115
  _list_field("id"),
55
116
  _list_field("name"),
56
117
  _list_field("description"),
57
118
  _list_field(
58
119
  "functionalities",
59
- renderer=lambda role: ", ".join(["{}={}".format(f.name, f.value) for f in role.functionalities]),
120
+ renderer=lambda role: ", ".join([f"{f.name}={f.value}" for f in role.functionalities]),
60
121
  ),
61
122
  _list_field(
62
123
  "restrictionareas", renderer=lambda role: ", ".join([r.name or "" for r in role.restrictionareas])
@@ -66,35 +127,37 @@ class RoleViews(AbstractViews):
66
127
  _model = Role
67
128
  _base_schema = base_schema
68
129
 
69
- def _base_query(self):
130
+ def _base_query(self) -> sqlalchemy.orm.query.Query[Role]:
131
+ session = self._request.dbsession
132
+ assert isinstance(session, sqlalchemy.orm.Session)
70
133
  return (
71
- self._request.dbsession.query(Role)
72
- .options(subqueryload("functionalities"))
73
- .options(subqueryload("restrictionareas"))
134
+ session.query(Role)
135
+ .options(subqueryload(Role.functionalities))
136
+ .options(subqueryload(Role.restrictionareas))
74
137
  )
75
138
 
76
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
77
- def index(self):
139
+ @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
140
+ def index(self) -> IndexResponse:
78
141
  return super().index()
79
142
 
80
- @view_config(route_name="c2cgeoform_grid", renderer="fast_json")
81
- def grid(self):
143
+ @view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
144
+ def grid(self) -> GridResponse:
82
145
  return super().grid()
83
146
 
84
- @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
85
- def view(self):
147
+ @view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2") # type: ignore[misc]
148
+ def view(self) -> ObjectResponse:
86
149
  return super().edit()
87
150
 
88
- @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
89
- def save(self):
151
+ @view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
152
+ def save(self) -> SaveResponse:
90
153
  return super().save()
91
154
 
92
- @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
93
- def delete(self):
155
+ @view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
156
+ def delete(self) -> DeleteResponse:
94
157
  return super().delete()
95
158
 
96
- @view_config(
159
+ @view_config( # type: ignore[misc]
97
160
  route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
98
161
  )
99
- def duplicate(self):
162
+ def duplicate(self) -> ObjectResponse:
100
163
  return super().duplicate()