WuttaWeb 0.6.0__tar.gz → 0.8.0__tar.gz

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 (153) hide show
  1. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/CHANGELOG.md +29 -0
  2. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/PKG-INFO +2 -2
  3. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/pyproject.toml +2 -2
  4. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/app.py +6 -0
  5. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/auth.py +90 -0
  6. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/forms/base.py +82 -23
  7. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/forms/schema.py +104 -3
  8. wuttaweb-0.8.0/src/wuttaweb/forms/widgets.py +192 -0
  9. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/grids/base.py +115 -4
  10. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/menus.py +2 -3
  11. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/subscribers.py +57 -8
  12. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/base.mako +15 -12
  13. wuttaweb-0.8.0/src/wuttaweb/templates/deform/checkbox_choice.pt +18 -0
  14. wuttaweb-0.8.0/src/wuttaweb/templates/deform/permissions.pt +23 -0
  15. wuttaweb-0.8.0/src/wuttaweb/templates/deform/readonly/notes.pt +7 -0
  16. wuttaweb-0.8.0/src/wuttaweb/templates/deform/readonly/objectref.pt +1 -0
  17. wuttaweb-0.8.0/src/wuttaweb/templates/deform/readonly/permissions.pt +18 -0
  18. wuttaweb-0.8.0/src/wuttaweb/templates/deform/textarea.pt +11 -0
  19. wuttaweb-0.8.0/src/wuttaweb/templates/forbidden.mako +26 -0
  20. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/form.mako +9 -5
  21. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/grids/vue_template.mako +2 -1
  22. wuttaweb-0.8.0/src/wuttaweb/templates/notfound.mako +23 -0
  23. wuttaweb-0.8.0/src/wuttaweb/templates/people/view_profile.mako +12 -0
  24. wuttaweb-0.8.0/src/wuttaweb/templates/setup.mako +20 -0
  25. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/util.py +13 -8
  26. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/auth.py +13 -8
  27. wuttaweb-0.8.0/src/wuttaweb/views/common.py +223 -0
  28. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/master.py +281 -51
  29. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/people.py +44 -0
  30. wuttaweb-0.8.0/src/wuttaweb/views/roles.py +271 -0
  31. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/settings.py +4 -0
  32. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/users.py +86 -2
  33. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/forms/test_base.py +67 -0
  34. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/forms/test_schema.py +58 -2
  35. wuttaweb-0.8.0/tests/forms/test_widgets.py +115 -0
  36. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/grids/test_base.py +57 -2
  37. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_auth.py +24 -0
  38. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_menus.py +24 -9
  39. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_subscribers.py +132 -1
  40. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_util.py +8 -0
  41. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/util.py +11 -5
  42. wuttaweb-0.8.0/tests/views/test___init__.py +10 -0
  43. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/views/test_auth.py +45 -52
  44. wuttaweb-0.8.0/tests/views/test_common.py +95 -0
  45. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/views/test_master.py +288 -35
  46. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/views/test_people.py +34 -0
  47. wuttaweb-0.8.0/tests/views/test_roles.py +242 -0
  48. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/views/test_settings.py +4 -0
  49. wuttaweb-0.8.0/tests/views/test_users.py +188 -0
  50. wuttaweb-0.6.0/src/wuttaweb/forms/widgets.py +0 -82
  51. wuttaweb-0.6.0/src/wuttaweb/views/common.py +0 -74
  52. wuttaweb-0.6.0/src/wuttaweb/views/roles.py +0 -100
  53. wuttaweb-0.6.0/tests/forms/test_widgets.py +0 -32
  54. wuttaweb-0.6.0/tests/views/test___init__.py +0 -14
  55. wuttaweb-0.6.0/tests/views/test_common.py +0 -27
  56. wuttaweb-0.6.0/tests/views/test_roles.py +0 -57
  57. wuttaweb-0.6.0/tests/views/test_users.py +0 -57
  58. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/.gitignore +0 -0
  59. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/COPYING.txt +0 -0
  60. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/README.md +0 -0
  61. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/Makefile +0 -0
  62. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/_static/.keepme +0 -0
  63. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/index.rst +0 -0
  64. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/app.rst +0 -0
  65. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/auth.rst +0 -0
  66. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/db.rst +0 -0
  67. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/forms.base.rst +0 -0
  68. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/forms.rst +0 -0
  69. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/forms.schema.rst +0 -0
  70. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/forms.widgets.rst +0 -0
  71. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/grids.base.rst +0 -0
  72. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/grids.rst +0 -0
  73. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/handler.rst +0 -0
  74. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/helpers.rst +0 -0
  75. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/index.rst +0 -0
  76. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/menus.rst +0 -0
  77. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/static.rst +0 -0
  78. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/subscribers.rst +0 -0
  79. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/util.rst +0 -0
  80. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.auth.rst +0 -0
  81. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.base.rst +0 -0
  82. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.common.rst +0 -0
  83. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.essential.rst +0 -0
  84. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.master.rst +0 -0
  85. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.people.rst +0 -0
  86. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.roles.rst +0 -0
  87. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.rst +0 -0
  88. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.settings.rst +0 -0
  89. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/api/wuttaweb/views.users.rst +0 -0
  90. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/conf.py +0 -0
  91. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/glossary.rst +0 -0
  92. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/index.rst +0 -0
  93. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/make.bat +0 -0
  94. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/docs/narr/index.rst +0 -0
  95. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/__init__.py +0 -0
  96. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/_version.py +0 -0
  97. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/db.py +0 -0
  98. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/forms/__init__.py +0 -0
  99. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/grids/__init__.py +0 -0
  100. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/handler.py +0 -0
  101. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/helpers.py +0 -0
  102. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/static/__init__.py +0 -0
  103. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  104. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/static/img/logo.png +0 -0
  105. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/static/img/testing.png +0 -0
  106. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  107. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  108. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  109. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  110. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  111. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/configure.mako +0 -0
  112. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  113. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  114. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  115. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  116. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  117. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  118. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/home.mako +0 -0
  119. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  120. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/create.mako +0 -0
  121. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  122. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  123. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/form.mako +0 -0
  124. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/index.mako +0 -0
  125. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/master/view.mako +0 -0
  126. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/page.mako +0 -0
  127. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/templates/wutta-components.mako +0 -0
  128. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/__init__.py +0 -0
  129. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/base.py +0 -0
  130. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/src/wuttaweb/views/essential.py +0 -0
  131. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tasks.py +0 -0
  132. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/__init__.py +0 -0
  133. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/forms/__init__.py +0 -0
  134. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/grids/__init__.py +0 -0
  135. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  136. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  137. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_oruga.js +0 -0
  138. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  139. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  140. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_vue.js +0 -0
  141. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  142. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/buefy.css +0 -0
  143. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/buefy.js +0 -0
  144. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/fontawesome.js +0 -0
  145. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/vue.js +0 -0
  146. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/libcache/vue_resource.js +0 -0
  147. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_app.py +0 -0
  148. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_handler.py +0 -0
  149. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_helpers.py +0 -0
  150. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/test_static.py +0 -0
  151. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/views/__init__.py +0 -0
  152. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tests/views/test_base.py +0 -0
  153. {wuttaweb-0.6.0 → wuttaweb-0.8.0}/tox.ini +0 -0
@@ -5,6 +5,35 @@ All notable changes to wuttaweb will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v0.8.0 (2024-08-15)
9
+
10
+ ### Feat
11
+
12
+ - add form/grid label auto-overrides for master view
13
+
14
+ ### Fix
15
+
16
+ - add `person` to template context for `PersonView.view_profile()`
17
+
18
+ ## v0.7.0 (2024-08-15)
19
+
20
+ ### Feat
21
+
22
+ - add sane views for 403 Forbidden and 404 Not Found
23
+ - add permission checks for menus, view routes
24
+ - add first-time setup page to create admin user
25
+ - expose User password for editing in master views
26
+ - expose Role permissions for editing
27
+ - expose User "roles" for editing
28
+ - improve widget, rendering for Role notes
29
+
30
+ ### Fix
31
+
32
+ - add stub for `PersonView.make_user()`
33
+ - allow arbitrary kwargs for `Form.render_vue_field()`
34
+ - make some tweaks for better tailbone compatibility
35
+ - prevent delete for built-in roles
36
+
8
37
  ## v0.6.0 (2024-08-13)
9
38
 
10
39
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: WuttaWeb
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: Web App for Wutta Framework
5
5
  Project-URL: Homepage, https://wuttaproject.org/
6
6
  Project-URL: Repository, https://forgejo.wuttaproject.org/wutta/wuttaweb
@@ -33,7 +33,7 @@ Requires-Dist: pyramid-tm
33
33
  Requires-Dist: pyramid>=2
34
34
  Requires-Dist: waitress
35
35
  Requires-Dist: webhelpers2
36
- Requires-Dist: wuttjamaican[db]>=0.11.0
36
+ Requires-Dist: wuttjamaican[db]>=0.12.0
37
37
  Requires-Dist: zope-sqlalchemy>=1.5
38
38
  Provides-Extra: docs
39
39
  Requires-Dist: furo; extra == 'docs'
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.6.0"
9
+ version = "0.8.0"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@@ -39,7 +39,7 @@ dependencies = [
39
39
  "pyramid_tm",
40
40
  "waitress",
41
41
  "WebHelpers2",
42
- "WuttJamaican[db]>=0.11.0",
42
+ "WuttJamaican[db]>=0.12.0",
43
43
  "zope.sqlalchemy>=1.5",
44
44
  ]
45
45
 
@@ -135,6 +135,12 @@ def make_pyramid_config(settings):
135
135
  pyramid_config.include('pyramid_mako')
136
136
  pyramid_config.include('pyramid_tm')
137
137
 
138
+ # add some permissions magic
139
+ pyramid_config.add_directive('add_wutta_permission_group',
140
+ 'wuttaweb.auth.add_permission_group')
141
+ pyramid_config.add_directive('add_wutta_permission',
142
+ 'wuttaweb.auth.add_permission')
143
+
138
144
  return pyramid_config
139
145
 
140
146
 
@@ -148,3 +148,93 @@ class WuttaSecurityPolicy:
148
148
  auth = app.get_auth_handler()
149
149
  user = self.identity(request)
150
150
  return auth.has_permission(self.db_session, user, permission)
151
+
152
+
153
+ def add_permission_group(pyramid_config, key, label=None, overwrite=True):
154
+ """
155
+ Pyramid directive to add a "permission group" to the app's
156
+ awareness.
157
+
158
+ The app must be made aware of all permissions, so they are exposed
159
+ when editing a
160
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
161
+ for discovering permissions is in
162
+ :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
163
+
164
+ This is usually called from within a master view's
165
+ :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
166
+ the permission group which applies to the view model.
167
+
168
+ A simple example of usage::
169
+
170
+ pyramid_config.add_permission_group('widgets', label="Widgets")
171
+
172
+ :param key: Unique key for the permission group. In the context
173
+ of a master view, this will be the same as
174
+ :attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
175
+
176
+ :param label: Optional label for the permission group. If not
177
+ specified, it is derived from ``key``.
178
+
179
+ :param overwrite: If the permission group was already established,
180
+ this flag controls whether the group's label should be
181
+ overwritten (with ``label``).
182
+
183
+ See also :func:`add_permission()`.
184
+ """
185
+ config = pyramid_config.get_settings()['wutta_config']
186
+ app = config.get_app()
187
+ def action():
188
+ perms = pyramid_config.get_settings().get('wutta_permissions', {})
189
+ if overwrite or key not in perms:
190
+ group = perms.setdefault(key, {'key': key})
191
+ group['label'] = label or app.make_title(key)
192
+ pyramid_config.add_settings({'wutta_permissions': perms})
193
+ pyramid_config.action(None, action)
194
+
195
+
196
+ def add_permission(pyramid_config, groupkey, key, label=None):
197
+ """
198
+ Pyramid directive to add a single "permission" to the app's
199
+ awareness.
200
+
201
+ The app must be made aware of all permissions, so they are exposed
202
+ when editing a
203
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
204
+ for discovering permissions is in
205
+ :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
206
+
207
+ This is usually called from within a master view's
208
+ :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
209
+ "known" permissions based on master view feature flags
210
+ (:attr:`~wuttaweb.views.master.MasterView.viewable`,
211
+ :attr:`~wuttaweb.views.master.MasterView.editable`, etc.).
212
+
213
+ A simple example of usage::
214
+
215
+ pyramid_config.add_permission('widgets', 'widgets.polish',
216
+ label="Polish all the widgets")
217
+
218
+ :param key: Unique key for the permission group. In the context
219
+ of a master view, this will be the same as
220
+ :attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
221
+
222
+ :param key: Unique key for the permission. This should be the
223
+ "complete" permission name which includes the permission
224
+ prefix.
225
+
226
+ :param label: Optional label for the permission. If not
227
+ specified, it is derived from ``key``.
228
+
229
+ See also :func:`add_permission_group()`.
230
+ """
231
+ def action():
232
+ config = pyramid_config.get_settings()['wutta_config']
233
+ app = config.get_app()
234
+ perms = pyramid_config.get_settings().get('wutta_permissions', {})
235
+ group = perms.setdefault(groupkey, {'key': groupkey})
236
+ group.setdefault('label', app.make_title(groupkey))
237
+ perm = group.setdefault('perms', {}).setdefault(key, {'key': key})
238
+ perm['label'] = label or app.make_title(key)
239
+ pyramid_config.add_settings({'wutta_permissions': perms})
240
+ pyramid_config.action(None, action)
@@ -120,6 +120,13 @@ class Form:
120
120
 
121
121
  See also :meth:`set_validator()`.
122
122
 
123
+ .. attribute:: defaults
124
+
125
+ Dict of default field values, used to construct the form in
126
+ :meth:`get_schema()`.
127
+
128
+ See also :meth:`set_default()`.
129
+
123
130
  .. attribute:: readonly
124
131
 
125
132
  Boolean indicating the form does not allow submit. In practice
@@ -248,6 +255,7 @@ class Form:
248
255
  nodes={},
249
256
  widgets={},
250
257
  validators={},
258
+ defaults={},
251
259
  readonly=False,
252
260
  readonly_fields=[],
253
261
  required_fields={},
@@ -271,6 +279,7 @@ class Form:
271
279
  self.nodes = nodes or {}
272
280
  self.widgets = widgets or {}
273
281
  self.validators = validators or {}
282
+ self.defaults = defaults or {}
274
283
  self.readonly = readonly
275
284
  self.readonly_fields = set(readonly_fields or [])
276
285
  self.required_fields = required_fields or {}
@@ -375,6 +384,23 @@ class Form:
375
384
  """
376
385
  self.fields = FieldList(fields)
377
386
 
387
+ def append(self, *keys):
388
+ """
389
+ Add some fields(s) to the form.
390
+
391
+ This is a convenience to allow adding multiple fields at
392
+ once::
393
+
394
+ form.append('first_field',
395
+ 'second_field',
396
+ 'third_field')
397
+
398
+ It will add each field to :attr:`fields`.
399
+ """
400
+ for key in keys:
401
+ if key not in self.fields:
402
+ self.fields.append(key)
403
+
378
404
  def remove(self, *keys):
379
405
  """
380
406
  Remove some fields(s) from the form.
@@ -409,16 +435,14 @@ class Form:
409
435
 
410
436
  Node overrides are tracked via :attr:`nodes`.
411
437
  """
438
+ from wuttaweb.forms.schema import ObjectNode
439
+
412
440
  if isinstance(nodeinfo, colander.SchemaNode):
413
441
  # assume nodeinfo is a complete node
414
442
  node = nodeinfo
415
443
 
416
444
  else: # assume nodeinfo is a schema type
417
445
  kwargs.setdefault('name', key)
418
-
419
- from wuttaweb.forms.schema import ObjectNode
420
-
421
- # node = colander.SchemaNode(nodeinfo, **kwargs)
422
446
  node = ObjectNode(nodeinfo, **kwargs)
423
447
 
424
448
  self.nodes[key] = node
@@ -471,6 +495,18 @@ class Form:
471
495
  if self.schema and key in self.schema:
472
496
  self.schema[key].validator = validator
473
497
 
498
+ def set_default(self, key, value):
499
+ """
500
+ Set/override the default value for a field.
501
+
502
+ :param key: Name of field.
503
+
504
+ :param validator: Default value for the field.
505
+
506
+ Default value overrides are tracked via :attr:`defaults`.
507
+ """
508
+ self.defaults[key] = value
509
+
474
510
  def set_readonly(self, key, readonly=True):
475
511
  """
476
512
  Enable or disable the "readonly" flag for a given field.
@@ -624,29 +660,22 @@ class Form:
624
660
 
625
661
  if self.model_class:
626
662
 
627
- # first define full list of 'includes' - final schema
628
- # should contain all of these fields
629
- includes = list(fields)
630
-
631
- # determine which we want ColanderAlchemy to handle
632
- auto_includes = []
633
- for key in includes:
634
-
635
- # skip if we already have a node defined
663
+ # collect list of field names and/or nodes
664
+ includes = []
665
+ for key in fields:
636
666
  if key in self.nodes:
637
- continue
638
-
639
- # we want the magic for this field
640
- auto_includes.append(key)
667
+ includes.append(self.nodes[key])
668
+ else:
669
+ includes.append(key)
641
670
 
642
671
  # make initial schema with ColanderAlchemy magic
643
672
  schema = SQLAlchemySchemaNode(self.model_class,
644
- includes=auto_includes)
673
+ includes=includes)
645
674
 
646
- # now fill in the blanks for non-magic fields
647
- for key in includes:
648
- if key not in auto_includes:
649
- node = self.nodes[key]
675
+ # fill in the blanks if anything got missed
676
+ for key in fields:
677
+ if key not in schema:
678
+ node = colander.SchemaNode(colander.String(), name=key)
650
679
  schema.add(node)
651
680
 
652
681
  else:
@@ -685,6 +714,11 @@ class Form:
685
714
  elif key in schema: # field-level
686
715
  schema[key].validator = validator
687
716
 
717
+ # apply default value overrides
718
+ for key, value in self.defaults.items():
719
+ if key in schema:
720
+ schema[key].default = value
721
+
688
722
  # apply required flags
689
723
  for key, required in self.required_fields.items():
690
724
  if key in schema:
@@ -775,7 +809,12 @@ class Form:
775
809
  output = render(template, context)
776
810
  return HTML.literal(output)
777
811
 
778
- def render_vue_field(self, fieldname, readonly=None):
812
+ def render_vue_field(
813
+ self,
814
+ fieldname,
815
+ readonly=None,
816
+ **kwargs,
817
+ ):
779
818
  """
780
819
  Render the given field completely, i.e. ``<b-field>`` wrapper
781
820
  with label and containing a widget.
@@ -791,6 +830,12 @@ class Form:
791
830
  message="something went wrong!">
792
831
  <!-- widget element(s) -->
793
832
  </b-field>
833
+
834
+ .. warning::
835
+
836
+ Any ``**kwargs`` received from caller are ignored by this
837
+ method. For now they are allowed, for sake of backwawrd
838
+ compatibility. This may change in the future.
794
839
  """
795
840
  # readonly comes from: caller, field flag, or form flag
796
841
  if readonly is None:
@@ -903,6 +948,20 @@ class Form:
903
948
 
904
949
  return model_data
905
950
 
951
+ # TODO: for tailbone compat, should document?
952
+ # (ideally should remove this and find a better way)
953
+ def get_vue_field_value(self, key):
954
+ """ """
955
+ if key not in self.fields:
956
+ return
957
+
958
+ dform = self.get_deform()
959
+ if key not in dform:
960
+ return
961
+
962
+ field = dform[key]
963
+ return make_json_safe(field.cstruct)
964
+
906
965
  def validate(self):
907
966
  """
908
967
  Try to validate the form, using data from the :attr:`request`.
@@ -59,13 +59,16 @@ class ObjectNode(colander.SchemaNode):
59
59
  :class:`ObjectRef`.
60
60
 
61
61
  If the node's type does not have a ``dictify()`` method, this
62
- will raise ``NotImplementeError``.
62
+ will just convert the object to a string and return that.
63
63
  """
64
64
  if hasattr(self.typ, 'dictify'):
65
65
  return self.typ.dictify(obj)
66
66
 
67
- class_name = self.typ.__class__.__name__
68
- raise NotImplementedError(f"you must define {class_name}.dictify()")
67
+ # TODO: this is better than raising an error, as it previously
68
+ # did, but seems like troubleshooting problems may often lead
69
+ # one here.. i suspect this needs to do something smarter but
70
+ # not sure what that is yet
71
+ return str(obj)
69
72
 
70
73
  def objectify(self, value):
71
74
  """
@@ -257,3 +260,101 @@ class PersonRef(ObjectRef):
257
260
  def sort_query(self, query):
258
261
  """ """
259
262
  return query.order_by(self.model_class.full_name)
263
+
264
+
265
+ class WuttaSet(colander.Set):
266
+ """
267
+ Custom schema type for :class:`python:set` fields.
268
+
269
+ This is a subclass of :class:`colander.Set`, but adds
270
+ Wutta-related params to the constructor.
271
+
272
+ :param request: Current :term:`request` object.
273
+
274
+ :param session: Optional :term:`db session` to use instead of
275
+ :class:`wuttaweb.db.Session`.
276
+ """
277
+
278
+ def __init__(self, request, session=None):
279
+ super().__init__()
280
+ self.request = request
281
+ self.config = self.request.wutta_config
282
+ self.app = self.config.get_app()
283
+ self.session = session or Session()
284
+
285
+
286
+ class RoleRefs(WuttaSet):
287
+ """
288
+ Form schema type for the User
289
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles`
290
+ association proxy field.
291
+
292
+ This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
293
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role` ``uuid``
294
+ values for underlying data format.
295
+ """
296
+
297
+ def widget_maker(self, **kwargs):
298
+ """
299
+ Constructs a default widget for the field.
300
+
301
+ :returns: Instance of
302
+ :class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
303
+ """
304
+ kwargs.setdefault('session', self.session)
305
+
306
+ if 'values' not in kwargs:
307
+ model = self.app.model
308
+ auth = self.app.get_auth_handler()
309
+ avoid = {
310
+ auth.get_role_authenticated(self.session),
311
+ auth.get_role_anonymous(self.session),
312
+ }
313
+ avoid = set([role.uuid for role in avoid])
314
+ roles = self.session.query(model.Role)\
315
+ .filter(~model.Role.uuid.in_(avoid))\
316
+ .order_by(model.Role.name)\
317
+ .all()
318
+ values = [(role.uuid, role.name) for role in roles]
319
+ kwargs['values'] = values
320
+
321
+ return widgets.RoleRefsWidget(self.request, **kwargs)
322
+
323
+
324
+ class Permissions(WuttaSet):
325
+ """
326
+ Form schema type for the Role
327
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
328
+ association proxy field.
329
+
330
+ This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
331
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Permission.permission`
332
+ values for underlying data format.
333
+
334
+ :param permissions: Dict with all possible permissions. Should be
335
+ in the same format as returned by
336
+ :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
337
+ """
338
+
339
+ def __init__(self, request, permissions, *args, **kwargs):
340
+ super().__init__(request, *args, **kwargs)
341
+ self.permissions = permissions
342
+
343
+ def widget_maker(self, **kwargs):
344
+ """
345
+ Constructs a default widget for the field.
346
+
347
+ :returns: Instance of
348
+ :class:`~wuttaweb.forms.widgets.PermissionsWidget`.
349
+ """
350
+ kwargs.setdefault('session', self.session)
351
+ kwargs.setdefault('permissions', self.permissions)
352
+
353
+ if 'values' not in kwargs:
354
+ values = []
355
+ for gkey, group in self.permissions.items():
356
+ for pkey, perm in group['perms'].items():
357
+ values.append((pkey, perm['label']))
358
+ kwargs['values'] = values
359
+
360
+ return widgets.PermissionsWidget(self.request, **kwargs)
@@ -0,0 +1,192 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024 Lance Edgar
6
+ #
7
+ # This file is part of Wutta Framework.
8
+ #
9
+ # Wutta Framework is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation, either version 3 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Wutta Framework is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17
+ # more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ ################################################################################
23
+ """
24
+ Form widgets
25
+
26
+ This module defines some custom widgets for use with WuttaWeb.
27
+
28
+ However for convenience it also makes other Deform widgets available
29
+ in the namespace:
30
+
31
+ * :class:`deform:deform.widget.Widget` (base class)
32
+ * :class:`deform:deform.widget.TextInputWidget`
33
+ * :class:`deform:deform.widget.TextAreaWidget`
34
+ * :class:`deform:deform.widget.PasswordWidget`
35
+ * :class:`deform:deform.widget.CheckedPasswordWidget`
36
+ * :class:`deform:deform.widget.SelectWidget`
37
+ * :class:`deform:deform.widget.CheckboxChoiceWidget`
38
+ """
39
+
40
+ import colander
41
+ from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
42
+ PasswordWidget, CheckedPasswordWidget,
43
+ SelectWidget, CheckboxChoiceWidget)
44
+ from webhelpers2.html import HTML
45
+
46
+ from wuttaweb.db import Session
47
+
48
+
49
+ class ObjectRefWidget(SelectWidget):
50
+ """
51
+ Widget for use with model "object reference" fields, e.g. foreign
52
+ key UUID => TargetModel instance.
53
+
54
+ While you may create instances of this widget directly, it
55
+ normally happens automatically when schema nodes of the
56
+ :class:`~wuttaweb.forms.schema.ObjectRef` (sub)type are part of
57
+ the form schema; via
58
+ :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`.
59
+
60
+ In readonly mode, this renders a ``<span>`` tag around the
61
+ :attr:`model_instance` (converted to string).
62
+
63
+ Otherwise it renders a select (dropdown) element allowing user to
64
+ choose from available records.
65
+
66
+ This is a subclass of :class:`deform:deform.widget.SelectWidget`
67
+ and uses these Deform templates:
68
+
69
+ * ``select``
70
+ * ``readonly/objectref``
71
+
72
+ .. attribute:: model_instance
73
+
74
+ Reference to the model record instance, i.e. the "far side" of
75
+ the foreign key relationship.
76
+
77
+ .. note::
78
+
79
+ You do not need to provide the ``model_instance`` when
80
+ constructing the widget. Rather, it is set automatically
81
+ when the :class:`~wuttaweb.forms.schema.ObjectRef` type
82
+ instance (associated with the node) is serialized.
83
+ """
84
+ readonly_template = 'readonly/objectref'
85
+
86
+ def __init__(self, request, *args, **kwargs):
87
+ super().__init__(*args, **kwargs)
88
+ self.request = request
89
+
90
+
91
+ class NotesWidget(TextAreaWidget):
92
+ """
93
+ Widget for use with "notes" fields.
94
+
95
+ In readonly mode, this shows the notes with a background to make
96
+ them stand out a bit more.
97
+
98
+ Otherwise it effectively shows a ``<textarea>`` input element.
99
+
100
+ This is a subclass of :class:`deform:deform.widget.TextAreaWidget`
101
+ and uses these Deform templates:
102
+
103
+ * ``textarea``
104
+ * ``readonly/notes``
105
+ """
106
+ readonly_template = 'readonly/notes'
107
+
108
+
109
+ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
110
+ """
111
+ Custom widget for :class:`python:set` fields.
112
+
113
+ This is a subclass of
114
+ :class:`deform:deform.widget.CheckboxChoiceWidget`, but adds
115
+ Wutta-related params to the constructor.
116
+
117
+ :param request: Current :term:`request` object.
118
+
119
+ :param session: Optional :term:`db session` to use instead of
120
+ :class:`wuttaweb.db.Session`.
121
+
122
+ It uses these Deform templates:
123
+
124
+ * ``checkbox_choice``
125
+ * ``readonly/checkbox_choice``
126
+ """
127
+
128
+ def __init__(self, request, session=None, *args, **kwargs):
129
+ super().__init__(*args, **kwargs)
130
+ self.request = request
131
+ self.config = self.request.wutta_config
132
+ self.app = self.config.get_app()
133
+ self.session = session or Session()
134
+
135
+
136
+ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
137
+ """
138
+ Widget for use with User
139
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
140
+
141
+ This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
142
+ """
143
+
144
+ def serialize(self, field, cstruct, **kw):
145
+ """ """
146
+ # special logic when field is editable
147
+ readonly = kw.get('readonly', self.readonly)
148
+ if not readonly:
149
+
150
+ # but does not apply if current user is root
151
+ if not self.request.is_root:
152
+ auth = self.app.get_auth_handler()
153
+ admin = auth.get_role_administrator(self.session)
154
+
155
+ # prune admin role from values list; it should not be
156
+ # one of the options since current user is not admin
157
+ values = kw.get('values', self.values)
158
+ values = [val for val in values
159
+ if val[0] != admin.uuid]
160
+ kw['values'] = values
161
+
162
+ # default logic from here
163
+ return super().serialize(field, cstruct, **kw)
164
+
165
+
166
+ class PermissionsWidget(WuttaCheckboxChoiceWidget):
167
+ """
168
+ Widget for use with Role
169
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
170
+ field.
171
+
172
+ This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses
173
+ these Deform templates:
174
+
175
+ * ``permissions``
176
+ * ``readonly/permissions``
177
+ """
178
+ template = 'permissions'
179
+ readonly_template = 'readonly/permissions'
180
+
181
+ def serialize(self, field, cstruct, **kw):
182
+ """ """
183
+ kw.setdefault('permissions', self.permissions)
184
+
185
+ if 'values' not in kw:
186
+ values = []
187
+ for gkey, group in self.permissions.items():
188
+ for pkey, perm in group['perms'].items():
189
+ values.append((pkey, perm['label']))
190
+ kw['values'] = values
191
+
192
+ return super().serialize(field, cstruct, **kw)