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
@@ -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
@@ -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 ListField
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 metadatas_schema_node
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.clone())
53
- base_schema.add(roles_schema_node("restricted_roles"))
54
- base_schema.add(interfaces_schema_node.clone())
55
- base_schema.add(metadatas_schema_node.clone())
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
- "{}={}".format(f.name, f.value)
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, query=None):
99
- return super()._base_query(
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("interfaces")
103
- .outerjoin("restricted_roles")
104
- .outerjoin("functionalities")
105
- .options(subqueryload("functionalities"))
106
- .options(subqueryload("restricted_roles"))
107
- .options(subqueryload("interfaces"))
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
- @view_config(route_name="c2cgeoform_index", renderer="../templates/index.jinja2")
111
- def index(self):
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
- # -*- 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,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.widgets import ChildrenWidget, ThemeOrderWidget
41
- from c2cgeoportal_commons.models.main import Theme
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 treeitems(node, kw): # pylint: disable=unused-argument
53
- return kw["dbsession"].query(Theme, literal_column("0")).order_by(Theme.ordering, Theme.name)
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.id for item, dummy in node.treeitems]:
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_["treeitem_id"], Theme.__tablename__),
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(Theme, includes=["id", "ordering"], name="theme", widget=ThemeOrderWidget()),
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
- treeitems=treeitems,
88
+ candidates=themes,
70
89
  validator=themes_validator,
71
- widget=ChildrenWidget(add_subitem=False, category="structural"),
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, dummy in form.schema["themes"].treeitems
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": (dict_,),
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": tuple(),
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
- # -*- 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
@@ -29,18 +27,26 @@
29
27
 
30
28
 
31
29
  from functools import partial
30
+ from typing import Generic, TypeVar
32
31
 
33
- from c2cgeoform.views.abstract_views import AbstractViews, ListField
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
- class TreeItemViews(AbstractViews):
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 layers_group: ", ".join(
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
- has_to_be_registred_in_parent = (
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 has_to_be_registred_in_parent:
79
- parent = self._request.dbsession.query(TreeGroup).get(has_to_be_registred_in_parent)
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 _base_query(self, query): # pylint: disable=arguments-differ
88
+ def _sub_query(self, query: sqlalchemy.orm.query.Query[TreeItem]) -> sqlalchemy.orm.query.Query[TreeItem]:
85
89
  return (
86
- query.outerjoin("metadatas")
87
- .options(subqueryload("parents_relation").joinedload("treegroup"))
88
- .options(subqueryload("metadatas"))
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
- # -*- 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,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 AbstractViews, ListField
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("roles"))
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(AbstractViews):
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
- _list_field("last_login"),
62
- _list_field("expire_on"),
63
- _list_field("deactivated"),
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("roles")
86
- .options(subqueryload("settings_role"))
87
- .options(subqueryload("roles"))
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()
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
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
- template = "child"
61
+ To be used in conjunction with ChildrenWidget, to manage n-m relationships.
58
62
 
59
- def serialize(self, field, cstruct, **kw):
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
- if cstruct["treeitem_id"] == colander.null:
63
- kw["treeitem"] = TreeItem()
64
- else:
65
- kw["treeitem"] = field.schema.dbsession.query(TreeItem).get(int(cstruct["treeitem_id"]))
66
- return super().serialize(field, cstruct, **kw)
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
- class ThemeOrderWidget(MappingWidget):
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 serialize(self, field, cstruct, **kw):
74
- from c2cgeoportal_commons.models.main import TreeItem # pylint: disable=import-outside-toplevel
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
- if cstruct["id"] == colander.null:
77
- kw["treeitem"] = TreeItem()
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["treeitem"] = field.schema.dbsession.query(TreeItem).get(int(cstruct["id"]))
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
- for i, dict_ in enumerate(pstruct):
94
- dict_["ordering"] = str(i)
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["treeitems"] = [
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)