c2cgeoportal-admin 2.5.0.100__py3-none-any.whl → 2.9rc44__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cgeoportal_admin/__init__.py +44 -14
- c2cgeoportal_admin/lib/__init__.py +0 -0
- c2cgeoportal_admin/lib/lingva_extractor.py +77 -0
- c2cgeoportal_admin/lib/ogcserver_synchronizer.py +410 -0
- c2cgeoportal_admin/py.typed +0 -0
- c2cgeoportal_admin/routes.py +30 -11
- c2cgeoportal_admin/schemas/dimensions.py +17 -11
- c2cgeoportal_admin/schemas/functionalities.py +60 -22
- c2cgeoportal_admin/schemas/interfaces.py +27 -19
- c2cgeoportal_admin/schemas/metadata.py +122 -48
- c2cgeoportal_admin/schemas/restriction_areas.py +26 -20
- c2cgeoportal_admin/schemas/roles.py +13 -7
- c2cgeoportal_admin/schemas/treegroup.py +90 -20
- c2cgeoportal_admin/schemas/treeitem.py +3 -4
- c2cgeoportal_admin/static/layertree.css +26 -4
- c2cgeoportal_admin/static/navbar.css +59 -36
- c2cgeoportal_admin/static/theme.css +51 -11
- c2cgeoportal_admin/subscribers.py +3 -3
- c2cgeoportal_admin/templates/404.jinja2 +41 -2
- c2cgeoportal_admin/templates/edit.jinja2 +23 -0
- c2cgeoportal_admin/templates/home.jinja2 +23 -0
- c2cgeoportal_admin/templates/index.jinja2 +23 -0
- c2cgeoportal_admin/templates/layertree.jinja2 +55 -11
- c2cgeoportal_admin/templates/layout.jinja2 +23 -0
- c2cgeoportal_admin/templates/navigation_navbar.jinja2 +56 -0
- c2cgeoportal_admin/templates/ogcserver_synchronize.jinja2 +90 -0
- c2cgeoportal_admin/templates/widgets/child.pt +35 -3
- c2cgeoportal_admin/templates/widgets/children.pt +121 -92
- c2cgeoportal_admin/templates/widgets/dimension.pt +23 -0
- c2cgeoportal_admin/templates/widgets/dimensions.pt +23 -0
- c2cgeoportal_admin/templates/widgets/functionality_fields.pt +51 -0
- c2cgeoportal_admin/templates/widgets/layer_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/layer_group_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/layer_v1_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/metadata.pt +30 -1
- c2cgeoportal_admin/templates/widgets/metadatas.pt +23 -0
- c2cgeoportal_admin/templates/widgets/ogcserver_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/restriction_area_fields.pt +25 -9
- c2cgeoportal_admin/templates/widgets/role_fields.pt +52 -25
- c2cgeoportal_admin/templates/widgets/theme_fields.pt +23 -0
- c2cgeoportal_admin/templates/widgets/user_fields.pt +23 -0
- c2cgeoportal_admin/views/__init__.py +29 -0
- c2cgeoportal_admin/views/dimension_layers.py +14 -9
- c2cgeoportal_admin/views/functionalities.py +52 -18
- c2cgeoportal_admin/views/home.py +5 -5
- c2cgeoportal_admin/views/interfaces.py +29 -21
- c2cgeoportal_admin/views/layer_groups.py +36 -25
- c2cgeoportal_admin/views/layers.py +17 -13
- c2cgeoportal_admin/views/layers_cog.py +135 -0
- c2cgeoportal_admin/views/layers_vectortiles.py +62 -27
- c2cgeoportal_admin/views/layers_wms.py +61 -36
- c2cgeoportal_admin/views/layers_wmts.py +54 -32
- c2cgeoportal_admin/views/layertree.py +37 -28
- c2cgeoportal_admin/views/logged_views.py +83 -0
- c2cgeoportal_admin/views/logs.py +91 -0
- c2cgeoportal_admin/views/oauth2_clients.py +96 -0
- c2cgeoportal_admin/views/ogc_servers.py +192 -21
- c2cgeoportal_admin/views/restriction_areas.py +78 -25
- c2cgeoportal_admin/views/roles.py +88 -25
- c2cgeoportal_admin/views/themes.py +47 -35
- c2cgeoportal_admin/views/themes_ordering.py +44 -24
- c2cgeoportal_admin/views/treeitems.py +21 -17
- c2cgeoportal_admin/views/users.py +46 -26
- c2cgeoportal_admin/widgets.py +79 -28
- {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/METADATA +15 -13
- c2cgeoportal_admin-2.9rc44.dist-info/RECORD +97 -0
- {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/WHEEL +1 -1
- c2cgeoportal_admin-2.9rc44.dist-info/entry_points.txt +5 -0
- tests/__init__.py +36 -27
- tests/conftest.py +23 -24
- tests/test_edit_url.py +16 -19
- tests/test_functionalities.py +52 -14
- tests/test_home.py +0 -1
- tests/test_interface.py +35 -12
- tests/test_layer_groups.py +58 -32
- tests/test_layers_cog.py +243 -0
- tests/test_layers_vectortiles.py +46 -30
- tests/test_layers_wms.py +77 -82
- tests/test_layers_wmts.py +51 -30
- tests/test_layertree.py +107 -101
- tests/test_learn.py +1 -1
- tests/test_left_menu.py +0 -1
- tests/test_lingva_extractor_config.py +64 -0
- tests/test_logs.py +102 -0
- tests/test_main.py +4 -2
- tests/test_metadatas.py +79 -71
- tests/test_oauth2_clients.py +186 -0
- tests/test_ogc_servers.py +110 -28
- tests/test_restriction_areas.py +109 -20
- tests/test_role.py +142 -82
- tests/test_themes.py +75 -41
- tests/test_themes_ordering.py +1 -2
- tests/test_treegroup.py +2 -2
- tests/test_user.py +72 -70
- tests/themes_ordering.py +1 -2
- c2cgeoportal_admin/templates/navigation_vertical.jinja2 +0 -10
- c2cgeoportal_admin-2.5.0.100.dist-info/RECORD +0 -84
- c2cgeoportal_admin-2.5.0.100.dist-info/entry_points.txt +0 -3
- {c2cgeoportal_admin-2.5.0.100.dist-info → c2cgeoportal_admin-2.9rc44.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2017-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2017-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -29,9 +27,18 @@
|
|
29
27
|
|
30
28
|
|
31
29
|
from functools import partial
|
30
|
+
from typing import cast
|
32
31
|
|
32
|
+
import sqlalchemy
|
33
33
|
from c2cgeoform.schema import GeoFormSchemaNode
|
34
|
-
from c2cgeoform.views.abstract_views import
|
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
|
@@ -39,7 +46,7 @@ from sqlalchemy.sql.functions import concat
|
|
39
46
|
|
40
47
|
from c2cgeoportal_admin.schemas.functionalities import functionalities_schema_node
|
41
48
|
from c2cgeoportal_admin.schemas.interfaces import interfaces_schema_node
|
42
|
-
from c2cgeoportal_admin.schemas.metadata import
|
49
|
+
from c2cgeoportal_admin.schemas.metadata import metadata_schema_node
|
43
50
|
from c2cgeoportal_admin.schemas.roles import roles_schema_node
|
44
51
|
from c2cgeoportal_admin.schemas.treegroup import children_schema_node
|
45
52
|
from c2cgeoportal_admin.views.treeitems import TreeItemViews
|
@@ -49,18 +56,19 @@ _list_field = partial(ListField, Theme)
|
|
49
56
|
|
50
57
|
base_schema = GeoFormSchemaNode(Theme, widget=FormWidget(fields_template="theme_fields"))
|
51
58
|
base_schema.add(children_schema_node(only_groups=True))
|
52
|
-
base_schema.add(functionalities_schema_node.
|
53
|
-
base_schema.add(roles_schema_node(
|
54
|
-
base_schema.add(interfaces_schema_node.
|
55
|
-
base_schema.add(
|
59
|
+
base_schema.add(functionalities_schema_node(Theme.functionalities, Theme))
|
60
|
+
base_schema.add(roles_schema_node(Theme.restricted_roles))
|
61
|
+
base_schema.add(interfaces_schema_node(Theme.interfaces))
|
62
|
+
base_schema.add(metadata_schema_node(Theme.metadatas, Theme))
|
56
63
|
base_schema.add_unique_validator(Theme.name, Theme.id)
|
57
64
|
|
58
65
|
|
59
66
|
@view_defaults(match_param="table=themes")
|
60
|
-
class ThemeViews(TreeItemViews):
|
67
|
+
class ThemeViews(TreeItemViews[Theme]):
|
68
|
+
"""The theme administration view."""
|
61
69
|
|
62
70
|
_list_fields = (
|
63
|
-
TreeItemViews._list_fields
|
71
|
+
TreeItemViews._list_fields # type: ignore[misc] # pylint: disable=protected-access
|
64
72
|
+ [
|
65
73
|
_list_field("ordering"),
|
66
74
|
_list_field("public"),
|
@@ -69,8 +77,8 @@ class ThemeViews(TreeItemViews):
|
|
69
77
|
"functionalities",
|
70
78
|
renderer=lambda themes: ", ".join(
|
71
79
|
[
|
72
|
-
"{}={
|
73
|
-
for f in sorted(themes.functionalities, key=lambda f: f.name)
|
80
|
+
f"{f.name}={f.value}"
|
81
|
+
for f in sorted(themes.functionalities, key=lambda f: cast(str, f.name))
|
74
82
|
]
|
75
83
|
),
|
76
84
|
filter_column=concat(Functionality.name, "=", Functionality.value),
|
@@ -83,52 +91,56 @@ class ThemeViews(TreeItemViews):
|
|
83
91
|
_list_field(
|
84
92
|
"interfaces",
|
85
93
|
renderer=lambda themes: ", ".join(
|
86
|
-
[i.name or "" for i in sorted(themes.interfaces, key=lambda i: i.name)]
|
94
|
+
[i.name or "" for i in sorted(themes.interfaces, key=lambda i: cast(str, i.name))]
|
87
95
|
),
|
88
96
|
filter_column=Interface.name,
|
89
97
|
),
|
90
98
|
]
|
91
|
-
+ TreeItemViews._extra_list_fields_no_parents
|
99
|
+
+ TreeItemViews._extra_list_fields_no_parents # pylint: disable=protected-access
|
92
100
|
)
|
93
101
|
|
94
102
|
_id_field = "id"
|
95
103
|
_model = Theme
|
96
104
|
_base_schema = base_schema
|
97
105
|
|
98
|
-
def _base_query(self
|
99
|
-
return super().
|
106
|
+
def _base_query(self) -> sqlalchemy.orm.query.Query[Theme]:
|
107
|
+
return super()._sub_query(
|
100
108
|
self._request.dbsession.query(Theme)
|
101
109
|
.distinct()
|
102
|
-
.outerjoin(
|
103
|
-
.outerjoin(
|
104
|
-
.outerjoin(
|
105
|
-
.options(subqueryload(
|
106
|
-
.options(subqueryload(
|
107
|
-
.options(subqueryload(
|
110
|
+
.outerjoin(Theme.interfaces)
|
111
|
+
.outerjoin(Theme.restricted_roles)
|
112
|
+
.outerjoin(Theme.functionalities)
|
113
|
+
.options(subqueryload(Theme.functionalities))
|
114
|
+
.options(subqueryload(Theme.restricted_roles))
|
115
|
+
.options(subqueryload(Theme.interfaces))
|
108
116
|
)
|
109
117
|
|
110
|
-
|
111
|
-
|
118
|
+
def _sub_query(self, query: sqlalchemy.orm.query.Query[Theme]) -> sqlalchemy.orm.query.Query[Theme]:
|
119
|
+
del query
|
120
|
+
return self._base_query()
|
121
|
+
|
122
|
+
@view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
|
123
|
+
def index(self) -> IndexResponse:
|
112
124
|
return super().index()
|
113
125
|
|
114
|
-
@view_config(route_name="c2cgeoform_grid", renderer="fast_json")
|
115
|
-
def grid(self):
|
126
|
+
@view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
|
127
|
+
def grid(self) -> GridResponse:
|
116
128
|
return super().grid()
|
117
129
|
|
118
|
-
@view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
|
119
|
-
def view(self):
|
130
|
+
@view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
131
|
+
def view(self) -> ObjectResponse:
|
120
132
|
return super().edit()
|
121
133
|
|
122
|
-
@view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
|
123
|
-
def save(self):
|
134
|
+
@view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
135
|
+
def save(self) -> SaveResponse:
|
124
136
|
return super().save()
|
125
137
|
|
126
|
-
@view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
|
127
|
-
def delete(self):
|
138
|
+
@view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
|
139
|
+
def delete(self) -> DeleteResponse:
|
128
140
|
return super().delete()
|
129
141
|
|
130
|
-
@view_config(
|
142
|
+
@view_config( # type: ignore[misc]
|
131
143
|
route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
|
132
144
|
)
|
133
|
-
def duplicate(self):
|
145
|
+
def duplicate(self) -> ObjectResponse:
|
134
146
|
return super().duplicate()
|
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2017-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2017-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -28,20 +26,22 @@
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
30
28
|
|
31
|
-
from c2cgeoform.schema import GeoFormSchemaNode
|
32
|
-
from c2cgeoform.views.abstract_views import AbstractViews
|
33
29
|
import colander
|
30
|
+
from c2cgeoform.schema import GeoFormSchemaNode
|
31
|
+
from c2cgeoform.views.abstract_views import AbstractViews, ObjectResponse, SaveResponse
|
34
32
|
from deform import ValidationFailure
|
35
33
|
from pyramid.httpexceptions import HTTPFound
|
36
34
|
from pyramid.view import view_config
|
37
|
-
from sqlalchemy.sql.expression import literal_column
|
38
35
|
|
39
36
|
from c2cgeoportal_admin import _
|
40
|
-
from c2cgeoportal_admin.
|
41
|
-
from
|
37
|
+
from c2cgeoportal_admin.schemas.treegroup import treeitem_edit_url
|
38
|
+
from c2cgeoportal_admin.widgets import ChildrenWidget, ChildWidget
|
39
|
+
from c2cgeoportal_commons.models.main import Theme, TreeItem
|
42
40
|
|
43
41
|
|
44
42
|
class ThemeOrderSchema(GeoFormSchemaNode): # pylint: disable=abstract-method
|
43
|
+
"""The theme order schema."""
|
44
|
+
|
45
45
|
def objectify(self, dict_, context=None):
|
46
46
|
context = self.dbsession.query(Theme).get(dict_["id"])
|
47
47
|
context = super().objectify(dict_, context)
|
@@ -49,52 +49,72 @@ class ThemeOrderSchema(GeoFormSchemaNode): # pylint: disable=abstract-method
|
|
49
49
|
|
50
50
|
|
51
51
|
@colander.deferred
|
52
|
-
def
|
53
|
-
|
52
|
+
def themes(node, kw): # pylint: disable=unused-argument
|
53
|
+
"""Get some theme metadata."""
|
54
|
+
query = kw["dbsession"].query(Theme).order_by(Theme.ordering, Theme.name)
|
55
|
+
return [
|
56
|
+
{"id": item.id, "label": item.name, "icon_class": f"icon-{item.item_type}", "group": "All"}
|
57
|
+
for item in query
|
58
|
+
]
|
54
59
|
|
55
60
|
|
56
61
|
def themes_validator(node, cstruct):
|
62
|
+
"""Validate the theme."""
|
57
63
|
for dict_ in cstruct:
|
58
|
-
if not dict_["id"] in [item
|
64
|
+
if not dict_["id"] in [item["id"] for item in node.candidates]:
|
59
65
|
raise colander.Invalid(
|
60
66
|
node,
|
61
|
-
_("Value {} does not exist in table {}").format(dict_["
|
67
|
+
_("Value {} does not exist in table {}").format(dict_["id"], Theme.__tablename__),
|
62
68
|
)
|
63
69
|
|
64
70
|
|
65
|
-
class ThemesOrderingSchema(colander.MappingSchema):
|
71
|
+
class ThemesOrderingSchema(colander.MappingSchema): # type: ignore[misc]
|
72
|
+
"""The theme ordering schema."""
|
73
|
+
|
66
74
|
themes = colander.SequenceSchema(
|
67
|
-
ThemeOrderSchema(
|
75
|
+
ThemeOrderSchema(
|
76
|
+
Theme,
|
77
|
+
includes=["id", "ordering"],
|
78
|
+
name="theme",
|
79
|
+
widget=ChildWidget(
|
80
|
+
input_name="id",
|
81
|
+
model=TreeItem,
|
82
|
+
label_field="name",
|
83
|
+
icon_class=lambda item: f"icon-{item.item_type}",
|
84
|
+
edit_url=treeitem_edit_url,
|
85
|
+
),
|
86
|
+
),
|
68
87
|
name="themes",
|
69
|
-
|
88
|
+
candidates=themes,
|
70
89
|
validator=themes_validator,
|
71
|
-
widget=ChildrenWidget(add_subitem=False,
|
90
|
+
widget=ChildrenWidget(child_input_name="id", add_subitem=False, orderable=True),
|
72
91
|
)
|
73
92
|
|
74
93
|
|
75
|
-
class ThemesOrdering(AbstractViews):
|
94
|
+
class ThemesOrdering(AbstractViews[ThemesOrderingSchema]):
|
95
|
+
"""The theme ordering admin view."""
|
76
96
|
|
77
97
|
_base_schema = ThemesOrderingSchema()
|
78
98
|
|
79
|
-
@view_config(route_name="layertree_ordering", request_method="GET", renderer="../templates/edit.jinja2")
|
80
|
-
def view(self):
|
99
|
+
@view_config(route_name="layertree_ordering", request_method="GET", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
100
|
+
def view(self) -> ObjectResponse:
|
81
101
|
form = self._form()
|
82
102
|
dict_ = {
|
83
103
|
"themes": [
|
84
104
|
form.schema["themes"].children[0].dictify(theme)
|
85
|
-
for theme
|
105
|
+
for theme in self._request.dbsession.query(Theme).order_by(Theme.ordering)
|
86
106
|
]
|
87
107
|
}
|
88
108
|
return {
|
89
109
|
"title": form.title,
|
90
110
|
"form": form,
|
91
|
-
"form_render_args":
|
111
|
+
"form_render_args": [dict_],
|
92
112
|
"form_render_kwargs": {"request": self._request, "actions": []},
|
93
113
|
"deform_dependencies": form.get_widget_resources(),
|
94
114
|
}
|
95
115
|
|
96
|
-
@view_config(route_name="layertree_ordering", request_method="POST", renderer="../templates/edit.jinja2")
|
97
|
-
def save(self):
|
116
|
+
@view_config(route_name="layertree_ordering", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
117
|
+
def save(self) -> SaveResponse:
|
98
118
|
try:
|
99
119
|
form = self._form()
|
100
120
|
form_data = self._request.POST.items()
|
@@ -111,7 +131,7 @@ class ThemesOrdering(AbstractViews):
|
|
111
131
|
return {
|
112
132
|
"title": form.title,
|
113
133
|
"form": e,
|
114
|
-
"form_render_args":
|
134
|
+
"form_render_args": [],
|
115
135
|
"form_render_kwargs": {"request": self._request, "actions": []},
|
116
136
|
"deform_dependencies": form.get_widget_resources(),
|
117
137
|
}
|
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2017-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2017-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -29,18 +27,26 @@
|
|
29
27
|
|
30
28
|
|
31
29
|
from functools import partial
|
30
|
+
from typing import Generic, TypeVar
|
32
31
|
|
33
|
-
|
32
|
+
import sqlalchemy
|
33
|
+
from c2cgeoform.views.abstract_views import ListField, SaveResponse
|
34
34
|
from pyramid.view import view_config
|
35
35
|
from sqlalchemy.orm import subqueryload
|
36
36
|
from sqlalchemy.sql.functions import concat
|
37
37
|
|
38
|
+
from c2cgeoportal_admin.views.logged_views import LoggedViews
|
38
39
|
from c2cgeoportal_commons.models.main import LayergroupTreeitem, Metadata, TreeGroup, TreeItem
|
39
40
|
|
40
41
|
_list_field = partial(ListField, TreeItem)
|
41
42
|
|
42
43
|
|
43
|
-
|
44
|
+
_T = TypeVar("_T", bound=TreeItem)
|
45
|
+
|
46
|
+
|
47
|
+
class TreeItemViews(LoggedViews[_T], Generic[_T]):
|
48
|
+
"""The admin tree item view."""
|
49
|
+
|
44
50
|
_list_fields = [
|
45
51
|
_list_field("id"),
|
46
52
|
_list_field("name"),
|
@@ -50,9 +56,7 @@ class TreeItemViews(AbstractViews):
|
|
50
56
|
_extra_list_fields_no_parents = [
|
51
57
|
_list_field(
|
52
58
|
"metadatas",
|
53
|
-
renderer=lambda
|
54
|
-
["{}: {}".format(m.name, m.value) or "" for m in layers_group.metadatas]
|
55
|
-
),
|
59
|
+
renderer=lambda treeitem: ", ".join([f"{m.name}: {m.value}" or "" for m in treeitem.metadatas]),
|
56
60
|
filter_column=concat(Metadata.name, ": ", Metadata.value).label("metadata"),
|
57
61
|
)
|
58
62
|
]
|
@@ -68,22 +72,22 @@ class TreeItemViews(AbstractViews):
|
|
68
72
|
)
|
69
73
|
] + _extra_list_fields_no_parents
|
70
74
|
|
71
|
-
@view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
|
72
|
-
def save(self):
|
75
|
+
@view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
76
|
+
def save(self) -> SaveResponse:
|
73
77
|
response = super().save()
|
74
78
|
# correctly handles the validation error as if there is a validation error, cstruct is empty
|
75
|
-
|
79
|
+
has_to_be_registered_in_parent = (
|
76
80
|
hasattr(self, "_appstruct") and self._appstruct is not None and self._appstruct.get("parent_id")
|
77
81
|
)
|
78
|
-
if
|
79
|
-
parent = self._request.dbsession.query(TreeGroup).get(
|
82
|
+
if has_to_be_registered_in_parent:
|
83
|
+
parent = self._request.dbsession.query(TreeGroup).get(has_to_be_registered_in_parent)
|
80
84
|
rel = LayergroupTreeitem(parent, self._obj, 100)
|
81
85
|
self._request.dbsession.add(rel)
|
82
86
|
return response
|
83
87
|
|
84
|
-
def
|
88
|
+
def _sub_query(self, query: sqlalchemy.orm.query.Query[TreeItem]) -> sqlalchemy.orm.query.Query[TreeItem]:
|
85
89
|
return (
|
86
|
-
query.outerjoin(
|
87
|
-
.options(subqueryload(
|
88
|
-
.options(subqueryload(
|
90
|
+
query.outerjoin(TreeItem.metadatas)
|
91
|
+
.options(subqueryload(TreeItem.parents_relation).joinedload(LayergroupTreeitem.treegroup))
|
92
|
+
.options(subqueryload(TreeItem.metadatas))
|
89
93
|
)
|
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2017-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2017-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -28,10 +26,18 @@
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
30
28
|
|
29
|
+
import os
|
31
30
|
from functools import partial
|
32
31
|
|
33
32
|
from c2cgeoform.schema import GeoFormSchemaNode
|
34
|
-
from c2cgeoform.views.abstract_views import
|
33
|
+
from c2cgeoform.views.abstract_views import (
|
34
|
+
DeleteResponse,
|
35
|
+
GridResponse,
|
36
|
+
IndexResponse,
|
37
|
+
ListField,
|
38
|
+
ObjectResponse,
|
39
|
+
SaveResponse,
|
40
|
+
)
|
35
41
|
from deform.widget import FormWidget
|
36
42
|
from passwordgenerator import pwgenerator
|
37
43
|
from pyramid.httpexceptions import HTTPFound
|
@@ -39,28 +45,36 @@ from pyramid.view import view_config, view_defaults
|
|
39
45
|
from sqlalchemy.orm import aliased, subqueryload
|
40
46
|
|
41
47
|
from c2cgeoportal_admin.schemas.roles import roles_schema_node
|
48
|
+
from c2cgeoportal_admin.views.logged_views import LoggedViews
|
42
49
|
from c2cgeoportal_commons.lib.email_ import send_email_config
|
43
50
|
from c2cgeoportal_commons.models.main import Role
|
44
|
-
from c2cgeoportal_commons.models.static import User
|
51
|
+
from c2cgeoportal_commons.models.static import Log, User
|
45
52
|
|
46
53
|
_list_field = partial(ListField, User)
|
47
54
|
|
48
55
|
base_schema = GeoFormSchemaNode(User, widget=FormWidget(fields_template="user_fields"))
|
49
|
-
base_schema.add(roles_schema_node(
|
56
|
+
base_schema.add(roles_schema_node(User.roles))
|
50
57
|
base_schema.add_unique_validator(User.username, User.id)
|
51
58
|
|
52
59
|
settings_role = aliased(Role)
|
53
60
|
|
61
|
+
_OPENID_CONNECT_ENABLED = os.environ.get("OPENID_CONNECT_ENABLED", "false").lower() in ("true", "yes", "1")
|
62
|
+
|
54
63
|
|
55
64
|
@view_defaults(match_param="table=users")
|
56
|
-
class UserViews(
|
65
|
+
class UserViews(LoggedViews[User]):
|
66
|
+
"""The admin user view."""
|
67
|
+
|
57
68
|
_list_fields = [
|
58
69
|
_list_field("id"),
|
59
|
-
_list_field("username"),
|
70
|
+
*([_list_field("username")] if not _OPENID_CONNECT_ENABLED else []),
|
71
|
+
_list_field("display_name"),
|
60
72
|
_list_field("email"),
|
61
|
-
|
62
|
-
|
63
|
-
|
73
|
+
*(
|
74
|
+
[_list_field("last_login"), _list_field("expire_on"), _list_field("deactivated")]
|
75
|
+
if not _OPENID_CONNECT_ENABLED
|
76
|
+
else []
|
77
|
+
),
|
64
78
|
_list_field(
|
65
79
|
"settings_role",
|
66
80
|
renderer=lambda user: user.settings_role.name if user.settings_role else "",
|
@@ -76,31 +90,33 @@ class UserViews(AbstractViews):
|
|
76
90
|
_id_field = "id"
|
77
91
|
_model = User
|
78
92
|
_base_schema = base_schema
|
93
|
+
_log_model = Log
|
94
|
+
_name_field = "username"
|
79
95
|
|
80
96
|
def _base_query(self):
|
81
97
|
return (
|
82
98
|
self._request.dbsession.query(User)
|
83
99
|
.distinct()
|
84
100
|
.outerjoin(settings_role, settings_role.id == User.settings_role_id)
|
85
|
-
.outerjoin(
|
86
|
-
.options(subqueryload(
|
87
|
-
.options(subqueryload(
|
101
|
+
.outerjoin(User.roles)
|
102
|
+
.options(subqueryload(User.settings_role))
|
103
|
+
.options(subqueryload(User.roles))
|
88
104
|
)
|
89
105
|
|
90
|
-
@view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
|
91
|
-
def index(self):
|
106
|
+
@view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2") # type: ignore[misc]
|
107
|
+
def index(self) -> IndexResponse:
|
92
108
|
return super().index()
|
93
109
|
|
94
|
-
@view_config(route_name="c2cgeoform_grid", renderer="fast_json")
|
95
|
-
def grid(self):
|
110
|
+
@view_config(route_name="c2cgeoform_grid", renderer="fast_json") # type: ignore[misc]
|
111
|
+
def grid(self) -> GridResponse:
|
96
112
|
return super().grid()
|
97
113
|
|
98
|
-
@view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2")
|
99
|
-
def view(self):
|
114
|
+
@view_config(route_name="c2cgeoform_item", request_method="GET", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
115
|
+
def view(self) -> ObjectResponse:
|
100
116
|
return super().edit()
|
101
117
|
|
102
|
-
@view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2")
|
103
|
-
def save(self):
|
118
|
+
@view_config(route_name="c2cgeoform_item", request_method="POST", renderer="../templates/edit.jinja2") # type: ignore[misc]
|
119
|
+
def save(self) -> SaveResponse:
|
104
120
|
if self._is_new():
|
105
121
|
response = super().save()
|
106
122
|
|
@@ -108,9 +124,11 @@ class UserViews(AbstractViews):
|
|
108
124
|
password = pwgenerator.generate()
|
109
125
|
|
110
126
|
user = self._obj
|
127
|
+
assert user is not None
|
111
128
|
user.password = password
|
112
129
|
user.is_password_changed = False
|
113
130
|
user = self._request.dbsession.merge(user)
|
131
|
+
assert user is not None
|
114
132
|
self._request.dbsession.flush()
|
115
133
|
|
116
134
|
send_email_config(
|
@@ -119,18 +137,20 @@ class UserViews(AbstractViews):
|
|
119
137
|
email=user.email,
|
120
138
|
user=user.username,
|
121
139
|
password=password,
|
140
|
+
application_url=self._request.route_url("base"),
|
141
|
+
current_url=self._request.current_route_url(),
|
122
142
|
)
|
123
143
|
|
124
144
|
return response
|
125
145
|
|
126
146
|
return super().save()
|
127
147
|
|
128
|
-
@view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json")
|
129
|
-
def delete(self):
|
148
|
+
@view_config(route_name="c2cgeoform_item", request_method="DELETE", renderer="fast_json") # type: ignore[misc]
|
149
|
+
def delete(self) -> DeleteResponse:
|
130
150
|
return super().delete()
|
131
151
|
|
132
|
-
@view_config(
|
152
|
+
@view_config( # type: ignore[misc]
|
133
153
|
route_name="c2cgeoform_item_duplicate", request_method="GET", renderer="../templates/edit.jinja2"
|
134
154
|
)
|
135
|
-
def duplicate(self):
|
155
|
+
def duplicate(self) -> ObjectResponse:
|
136
156
|
return super().duplicate()
|
c2cgeoportal_admin/widgets.py
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2018-2020, Camptocamp SA
|
1
|
+
# Copyright (c) 2018-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -27,12 +25,16 @@
|
|
27
25
|
# of the authors and should not be interpreted as representing official policies,
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
28
|
+
from typing import Any
|
30
29
|
|
31
30
|
import colander
|
31
|
+
import pyramid.request
|
32
32
|
from colander import Mapping, SchemaNode
|
33
33
|
from deform import widget
|
34
34
|
from deform.widget import MappingWidget, SequenceWidget
|
35
35
|
|
36
|
+
from c2cgeoportal_commons.models.main import TreeItem
|
37
|
+
|
36
38
|
registry = widget.default_resource_registry
|
37
39
|
registry.set_js_resources(
|
38
40
|
"magicsuggest", None, "c2cgeoportal_admin:node_modules/magicsuggest-alpine/magicsuggest-min.js"
|
@@ -52,51 +54,100 @@ widget.DateTimeInputWidget._pstruct_schema = SchemaNode( # pylint: disable=prot
|
|
52
54
|
)
|
53
55
|
|
54
56
|
|
55
|
-
class ChildWidget(MappingWidget):
|
57
|
+
class ChildWidget(MappingWidget): # type: ignore
|
58
|
+
"""
|
59
|
+
Extension of the widget ````deform.widget.MappingWidget``.
|
56
60
|
|
57
|
-
|
61
|
+
To be used in conjunction with ChildrenWidget, to manage n-m relationships.
|
58
62
|
|
59
|
-
|
60
|
-
from c2cgeoportal_commons.models.main import TreeItem # pylint: disable=import-outside-toplevel
|
63
|
+
Do not embed complete children forms, but just an hidden input for child primary key.
|
61
64
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
**Attributes/Arguments**
|
66
|
+
|
67
|
+
input_name (required)
|
68
|
+
Form input name namely the name of the schema field identifying the child in the relation.
|
69
|
+
|
70
|
+
model (required)
|
71
|
+
The child model class.
|
72
|
+
|
73
|
+
label_field (required)
|
74
|
+
The name of the field used for display.
|
75
|
+
|
76
|
+
icon_class (optional)
|
77
|
+
A function which takes a child as parameter and returns a CSS class.
|
67
78
|
|
79
|
+
edit_url (optional)
|
80
|
+
A function taking request and child as parameter and returning
|
81
|
+
an URL to the corresponding resource.
|
68
82
|
|
69
|
-
|
83
|
+
For further attributes, please refer to the documentation of
|
84
|
+
``deform.widget.MappingWidget`` in the deform documentation:
|
85
|
+
<https://deform.readthedocs.org/en/latest/api.html>
|
86
|
+
"""
|
70
87
|
|
71
88
|
template = "child"
|
89
|
+
input_name = "treeitem_id"
|
90
|
+
model = TreeItem
|
91
|
+
label_field = "name"
|
72
92
|
|
73
|
-
def
|
74
|
-
|
93
|
+
def icon_class(self, child: Any) -> str | None: # pylint: disable=useless-return
|
94
|
+
del child
|
95
|
+
return None
|
96
|
+
|
97
|
+
def edit_url( # pylint: disable=useless-return
|
98
|
+
self, request: pyramid.request.Request, child: Any
|
99
|
+
) -> str | None:
|
100
|
+
del request
|
101
|
+
del child
|
102
|
+
return None
|
75
103
|
|
76
|
-
|
77
|
-
|
104
|
+
def serialize(self, field, cstruct, **kw):
|
105
|
+
if cstruct[self.input_name] == colander.null:
|
106
|
+
kw["child"] = self.model()
|
78
107
|
else:
|
79
|
-
kw["
|
108
|
+
kw["child"] = field.schema.dbsession.query(self.model).get(int(cstruct[self.input_name]))
|
80
109
|
return super().serialize(field, cstruct, **kw)
|
81
110
|
|
82
111
|
|
83
|
-
class ChildrenWidget(SequenceWidget):
|
112
|
+
class ChildrenWidget(SequenceWidget): # type: ignore
|
113
|
+
"""
|
114
|
+
Extension of the widget ````deform.widget.SequenceWidget``.
|
115
|
+
|
116
|
+
To be used in conjunction with ChildWidget, to manage n-m relationships.
|
117
|
+
|
118
|
+
Use Magicsuggest for searching into parent schema candidates property, which should be a list of
|
119
|
+
dictionaries of the form:
|
120
|
+
{
|
121
|
+
"id": "Value to be set in child identifier input (child_input_name)",
|
122
|
+
"label": "The text to display in MagicSuggest",
|
123
|
+
"icon_class": "An optional icon class for the MagisSuggest entries",
|
124
|
+
"edit_url": "An optional url to edit the child resource",
|
125
|
+
"group": "An optional group name for grouping entries in MagicSuggest",
|
126
|
+
}
|
127
|
+
|
128
|
+
**Attributes/Arguments**
|
129
|
+
|
130
|
+
child_input_name (required)
|
131
|
+
The name of the child input to fill with selected child primary key.
|
132
|
+
|
133
|
+
For further attributes, please refer to the documentation of
|
134
|
+
``deform.widget.SequenceWidget`` in the deform documentation:
|
135
|
+
<https://deform.readthedocs.org/en/latest/api.html>
|
136
|
+
"""
|
84
137
|
|
85
138
|
template = "children"
|
139
|
+
category = "structural"
|
86
140
|
add_subitem = True
|
141
|
+
orderable = True
|
142
|
+
child_input_name = "treeitem_id"
|
87
143
|
requirements = SequenceWidget.requirements + (("magicsuggest", None),)
|
88
144
|
|
89
|
-
def __init__(self, **kw):
|
90
|
-
SequenceWidget.__init__(self, orderable=True, **kw)
|
91
|
-
|
92
145
|
def deserialize(self, field, pstruct):
|
93
|
-
|
94
|
-
dict_
|
146
|
+
if self.orderable and pstruct != colander.null:
|
147
|
+
for i, dict_ in enumerate(pstruct):
|
148
|
+
dict_["ordering"] = str(i)
|
95
149
|
return super().deserialize(field, pstruct)
|
96
150
|
|
97
151
|
def serialize(self, field, cstruct, **kw):
|
98
|
-
kw["
|
99
|
-
{"id": item.id, "name": item.name, "item_type": item.item_type, "group": group}
|
100
|
-
for item, group in field.schema.treeitems
|
101
|
-
]
|
152
|
+
kw["candidates"] = field.schema.candidates
|
102
153
|
return super().serialize(field, cstruct, **kw)
|