WuttaWeb 0.6.0__tar.gz → 0.7.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.7.0}/CHANGELOG.md +19 -0
  2. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/PKG-INFO +2 -2
  3. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/pyproject.toml +2 -2
  4. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/app.py +6 -0
  5. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/auth.py +90 -0
  6. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/forms/base.py +80 -19
  7. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/forms/schema.py +98 -0
  8. wuttaweb-0.7.0/src/wuttaweb/forms/widgets.py +192 -0
  9. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/grids/base.py +80 -3
  10. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/menus.py +2 -3
  11. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/subscribers.py +57 -8
  12. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/base.mako +15 -12
  13. wuttaweb-0.7.0/src/wuttaweb/templates/deform/checkbox_choice.pt +18 -0
  14. wuttaweb-0.7.0/src/wuttaweb/templates/deform/permissions.pt +23 -0
  15. wuttaweb-0.7.0/src/wuttaweb/templates/deform/readonly/notes.pt +7 -0
  16. wuttaweb-0.7.0/src/wuttaweb/templates/deform/readonly/objectref.pt +1 -0
  17. wuttaweb-0.7.0/src/wuttaweb/templates/deform/readonly/permissions.pt +18 -0
  18. wuttaweb-0.7.0/src/wuttaweb/templates/deform/textarea.pt +11 -0
  19. wuttaweb-0.7.0/src/wuttaweb/templates/forbidden.mako +26 -0
  20. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/form.mako +9 -5
  21. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/grids/vue_template.mako +2 -1
  22. wuttaweb-0.7.0/src/wuttaweb/templates/notfound.mako +23 -0
  23. wuttaweb-0.7.0/src/wuttaweb/templates/people/view_profile.mako +12 -0
  24. wuttaweb-0.7.0/src/wuttaweb/templates/setup.mako +20 -0
  25. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/util.py +13 -8
  26. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/auth.py +13 -8
  27. wuttaweb-0.7.0/src/wuttaweb/views/common.py +223 -0
  28. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/master.py +212 -51
  29. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/people.py +43 -0
  30. wuttaweb-0.7.0/src/wuttaweb/views/roles.py +271 -0
  31. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/settings.py +4 -0
  32. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/users.py +86 -2
  33. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/forms/test_base.py +67 -0
  34. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/forms/test_schema.py +55 -0
  35. wuttaweb-0.7.0/tests/forms/test_widgets.py +115 -0
  36. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/grids/test_base.py +33 -2
  37. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_auth.py +24 -0
  38. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_menus.py +24 -9
  39. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_subscribers.py +132 -1
  40. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_util.py +8 -0
  41. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/util.py +11 -5
  42. wuttaweb-0.7.0/tests/views/test___init__.py +10 -0
  43. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/views/test_auth.py +45 -52
  44. wuttaweb-0.7.0/tests/views/test_common.py +95 -0
  45. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/views/test_master.py +249 -35
  46. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/views/test_people.py +34 -0
  47. wuttaweb-0.7.0/tests/views/test_roles.py +242 -0
  48. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/views/test_settings.py +4 -0
  49. wuttaweb-0.7.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.7.0}/.gitignore +0 -0
  59. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/COPYING.txt +0 -0
  60. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/README.md +0 -0
  61. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/Makefile +0 -0
  62. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/_static/.keepme +0 -0
  63. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/index.rst +0 -0
  64. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/app.rst +0 -0
  65. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/auth.rst +0 -0
  66. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/db.rst +0 -0
  67. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/forms.base.rst +0 -0
  68. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/forms.rst +0 -0
  69. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/forms.schema.rst +0 -0
  70. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/forms.widgets.rst +0 -0
  71. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/grids.base.rst +0 -0
  72. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/grids.rst +0 -0
  73. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/handler.rst +0 -0
  74. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/helpers.rst +0 -0
  75. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/index.rst +0 -0
  76. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/menus.rst +0 -0
  77. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/static.rst +0 -0
  78. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/subscribers.rst +0 -0
  79. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/util.rst +0 -0
  80. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.auth.rst +0 -0
  81. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.base.rst +0 -0
  82. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.common.rst +0 -0
  83. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.essential.rst +0 -0
  84. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.master.rst +0 -0
  85. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.people.rst +0 -0
  86. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.roles.rst +0 -0
  87. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.rst +0 -0
  88. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.settings.rst +0 -0
  89. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/api/wuttaweb/views.users.rst +0 -0
  90. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/conf.py +0 -0
  91. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/glossary.rst +0 -0
  92. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/index.rst +0 -0
  93. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/make.bat +0 -0
  94. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/docs/narr/index.rst +0 -0
  95. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/__init__.py +0 -0
  96. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/_version.py +0 -0
  97. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/db.py +0 -0
  98. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/forms/__init__.py +0 -0
  99. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/grids/__init__.py +0 -0
  100. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/handler.py +0 -0
  101. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/helpers.py +0 -0
  102. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/static/__init__.py +0 -0
  103. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  104. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/static/img/logo.png +0 -0
  105. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/static/img/testing.png +0 -0
  106. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  107. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  108. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  109. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  110. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  111. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/configure.mako +0 -0
  112. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  113. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  114. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  115. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  116. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  117. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  118. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/home.mako +0 -0
  119. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  120. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/create.mako +0 -0
  121. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  122. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  123. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/form.mako +0 -0
  124. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/index.mako +0 -0
  125. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/master/view.mako +0 -0
  126. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/page.mako +0 -0
  127. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/templates/wutta-components.mako +0 -0
  128. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/__init__.py +0 -0
  129. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/base.py +0 -0
  130. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/src/wuttaweb/views/essential.py +0 -0
  131. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tasks.py +0 -0
  132. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/__init__.py +0 -0
  133. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/forms/__init__.py +0 -0
  134. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/grids/__init__.py +0 -0
  135. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  136. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  137. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_oruga.js +0 -0
  138. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  139. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  140. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_vue.js +0 -0
  141. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  142. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/buefy.css +0 -0
  143. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/buefy.js +0 -0
  144. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/fontawesome.js +0 -0
  145. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/vue.js +0 -0
  146. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/libcache/vue_resource.js +0 -0
  147. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_app.py +0 -0
  148. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_handler.py +0 -0
  149. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_helpers.py +0 -0
  150. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/test_static.py +0 -0
  151. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/views/__init__.py +0 -0
  152. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tests/views/test_base.py +0 -0
  153. {wuttaweb-0.6.0 → wuttaweb-0.7.0}/tox.ini +0 -0
@@ -5,6 +5,25 @@ 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.7.0 (2024-08-15)
9
+
10
+ ### Feat
11
+
12
+ - add sane views for 403 Forbidden and 404 Not Found
13
+ - add permission checks for menus, view routes
14
+ - add first-time setup page to create admin user
15
+ - expose User password for editing in master views
16
+ - expose Role permissions for editing
17
+ - expose User "roles" for editing
18
+ - improve widget, rendering for Role notes
19
+
20
+ ### Fix
21
+
22
+ - add stub for `PersonView.make_user()`
23
+ - allow arbitrary kwargs for `Form.render_vue_field()`
24
+ - make some tweaks for better tailbone compatibility
25
+ - prevent delete for built-in roles
26
+
8
27
  ## v0.6.0 (2024-08-13)
9
28
 
10
29
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: WuttaWeb
3
- Version: 0.6.0
3
+ Version: 0.7.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.11.1
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.7.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.11.1",
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.
@@ -471,6 +497,18 @@ class Form:
471
497
  if self.schema and key in self.schema:
472
498
  self.schema[key].validator = validator
473
499
 
500
+ def set_default(self, key, value):
501
+ """
502
+ Set/override the default value for a field.
503
+
504
+ :param key: Name of field.
505
+
506
+ :param validator: Default value for the field.
507
+
508
+ Default value overrides are tracked via :attr:`defaults`.
509
+ """
510
+ self.defaults[key] = value
511
+
474
512
  def set_readonly(self, key, readonly=True):
475
513
  """
476
514
  Enable or disable the "readonly" flag for a given field.
@@ -624,29 +662,22 @@ class Form:
624
662
 
625
663
  if self.model_class:
626
664
 
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
665
+ # collect list of field names and/or nodes
666
+ includes = []
667
+ for key in fields:
636
668
  if key in self.nodes:
637
- continue
638
-
639
- # we want the magic for this field
640
- auto_includes.append(key)
669
+ includes.append(self.nodes[key])
670
+ else:
671
+ includes.append(key)
641
672
 
642
673
  # make initial schema with ColanderAlchemy magic
643
674
  schema = SQLAlchemySchemaNode(self.model_class,
644
- includes=auto_includes)
675
+ includes=includes)
645
676
 
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]
677
+ # fill in the blanks if anything got missed
678
+ for key in fields:
679
+ if key not in schema:
680
+ node = colander.SchemaNode(colander.String(), name=key)
650
681
  schema.add(node)
651
682
 
652
683
  else:
@@ -685,6 +716,11 @@ class Form:
685
716
  elif key in schema: # field-level
686
717
  schema[key].validator = validator
687
718
 
719
+ # apply default value overrides
720
+ for key, value in self.defaults.items():
721
+ if key in schema:
722
+ schema[key].default = value
723
+
688
724
  # apply required flags
689
725
  for key, required in self.required_fields.items():
690
726
  if key in schema:
@@ -775,7 +811,12 @@ class Form:
775
811
  output = render(template, context)
776
812
  return HTML.literal(output)
777
813
 
778
- def render_vue_field(self, fieldname, readonly=None):
814
+ def render_vue_field(
815
+ self,
816
+ fieldname,
817
+ readonly=None,
818
+ **kwargs,
819
+ ):
779
820
  """
780
821
  Render the given field completely, i.e. ``<b-field>`` wrapper
781
822
  with label and containing a widget.
@@ -791,6 +832,12 @@ class Form:
791
832
  message="something went wrong!">
792
833
  <!-- widget element(s) -->
793
834
  </b-field>
835
+
836
+ .. warning::
837
+
838
+ Any ``**kwargs`` received from caller are ignored by this
839
+ method. For now they are allowed, for sake of backwawrd
840
+ compatibility. This may change in the future.
794
841
  """
795
842
  # readonly comes from: caller, field flag, or form flag
796
843
  if readonly is None:
@@ -903,6 +950,20 @@ class Form:
903
950
 
904
951
  return model_data
905
952
 
953
+ # TODO: for tailbone compat, should document?
954
+ # (ideally should remove this and find a better way)
955
+ def get_vue_field_value(self, key):
956
+ """ """
957
+ if key not in self.fields:
958
+ return
959
+
960
+ dform = self.get_deform()
961
+ if key not in dform:
962
+ return
963
+
964
+ field = dform[key]
965
+ return make_json_safe(field.cstruct)
966
+
906
967
  def validate(self):
907
968
  """
908
969
  Try to validate the form, using data from the :attr:`request`.
@@ -257,3 +257,101 @@ class PersonRef(ObjectRef):
257
257
  def sort_query(self, query):
258
258
  """ """
259
259
  return query.order_by(self.model_class.full_name)
260
+
261
+
262
+ class WuttaSet(colander.Set):
263
+ """
264
+ Custom schema type for :class:`python:set` fields.
265
+
266
+ This is a subclass of :class:`colander.Set`, but adds
267
+ Wutta-related params to the constructor.
268
+
269
+ :param request: Current :term:`request` object.
270
+
271
+ :param session: Optional :term:`db session` to use instead of
272
+ :class:`wuttaweb.db.Session`.
273
+ """
274
+
275
+ def __init__(self, request, session=None):
276
+ super().__init__()
277
+ self.request = request
278
+ self.config = self.request.wutta_config
279
+ self.app = self.config.get_app()
280
+ self.session = session or Session()
281
+
282
+
283
+ class RoleRefs(WuttaSet):
284
+ """
285
+ Form schema type for the User
286
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles`
287
+ association proxy field.
288
+
289
+ This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
290
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role` ``uuid``
291
+ values for underlying data format.
292
+ """
293
+
294
+ def widget_maker(self, **kwargs):
295
+ """
296
+ Constructs a default widget for the field.
297
+
298
+ :returns: Instance of
299
+ :class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
300
+ """
301
+ kwargs.setdefault('session', self.session)
302
+
303
+ if 'values' not in kwargs:
304
+ model = self.app.model
305
+ auth = self.app.get_auth_handler()
306
+ avoid = {
307
+ auth.get_role_authenticated(self.session),
308
+ auth.get_role_anonymous(self.session),
309
+ }
310
+ avoid = set([role.uuid for role in avoid])
311
+ roles = self.session.query(model.Role)\
312
+ .filter(~model.Role.uuid.in_(avoid))\
313
+ .order_by(model.Role.name)\
314
+ .all()
315
+ values = [(role.uuid, role.name) for role in roles]
316
+ kwargs['values'] = values
317
+
318
+ return widgets.RoleRefsWidget(self.request, **kwargs)
319
+
320
+
321
+ class Permissions(WuttaSet):
322
+ """
323
+ Form schema type for the Role
324
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
325
+ association proxy field.
326
+
327
+ This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
328
+ :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Permission.permission`
329
+ values for underlying data format.
330
+
331
+ :param permissions: Dict with all possible permissions. Should be
332
+ in the same format as returned by
333
+ :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
334
+ """
335
+
336
+ def __init__(self, request, permissions, *args, **kwargs):
337
+ super().__init__(request, *args, **kwargs)
338
+ self.permissions = permissions
339
+
340
+ def widget_maker(self, **kwargs):
341
+ """
342
+ Constructs a default widget for the field.
343
+
344
+ :returns: Instance of
345
+ :class:`~wuttaweb.forms.widgets.PermissionsWidget`.
346
+ """
347
+ kwargs.setdefault('session', self.session)
348
+ kwargs.setdefault('permissions', self.permissions)
349
+
350
+ if 'values' not in kwargs:
351
+ values = []
352
+ for gkey, group in self.permissions.items():
353
+ for pkey, perm in group['perms'].items():
354
+ values.append((pkey, perm['label']))
355
+ kwargs['values'] = values
356
+
357
+ 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)