WuttaWeb 0.16.0__tar.gz → 0.16.2__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 (179) hide show
  1. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/CHANGELOG.md +15 -0
  2. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/PKG-INFO +2 -2
  3. wuttaweb-0.16.2/docs/glossary.rst +15 -0
  4. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/pyproject.toml +2 -2
  5. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/forms/base.py +28 -11
  6. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/forms/schema.py +12 -3
  7. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/forms/widgets.py +39 -1
  8. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/grids/base.py +1 -1
  9. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/grids/filters.py +35 -4
  10. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/base.mako +1 -1
  11. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/util.py +5 -0
  12. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/forms/test_base.py +14 -0
  13. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/forms/test_schema.py +1 -1
  14. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/forms/test_widgets.py +30 -4
  15. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/grids/test_filters.py +47 -3
  16. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_auth.py +2 -1
  17. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_util.py +6 -0
  18. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_common.py +1 -1
  19. wuttaweb-0.16.0/docs/glossary.rst +0 -10
  20. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/.gitignore +0 -0
  21. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/COPYING.txt +0 -0
  22. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/README.md +0 -0
  23. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/Makefile +0 -0
  24. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/_static/.keepme +0 -0
  25. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.app.rst +0 -0
  26. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.auth.rst +0 -0
  27. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.conf.rst +0 -0
  28. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.db.continuum.rst +0 -0
  29. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.db.rst +0 -0
  30. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.db.sess.rst +0 -0
  31. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.forms.base.rst +0 -0
  32. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.forms.rst +0 -0
  33. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.forms.schema.rst +0 -0
  34. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  35. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.grids.base.rst +0 -0
  36. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.grids.filters.rst +0 -0
  37. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.grids.rst +0 -0
  38. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.handler.rst +0 -0
  39. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.helpers.rst +0 -0
  40. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.menus.rst +0 -0
  41. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.progress.rst +0 -0
  42. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.rst +0 -0
  43. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.static.rst +0 -0
  44. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.subscribers.rst +0 -0
  45. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.util.rst +0 -0
  46. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.auth.rst +0 -0
  47. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.base.rst +0 -0
  48. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.common.rst +0 -0
  49. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.essential.rst +0 -0
  50. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.master.rst +0 -0
  51. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.people.rst +0 -0
  52. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.progress.rst +0 -0
  53. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.roles.rst +0 -0
  54. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.rst +0 -0
  55. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.settings.rst +0 -0
  56. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  57. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/api/wuttaweb.views.users.rst +0 -0
  58. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/conf.py +0 -0
  59. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/index.rst +0 -0
  60. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/make.bat +0 -0
  61. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/narr/templates/base.rst +0 -0
  62. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/narr/templates/index.rst +0 -0
  63. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/narr/templates/lookup.rst +0 -0
  64. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/docs/narr/templates/overview.rst +0 -0
  65. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/__init__.py +0 -0
  66. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/_version.py +0 -0
  67. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/app.py +0 -0
  68. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/auth.py +0 -0
  69. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/conf.py +0 -0
  70. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/db/__init__.py +0 -0
  71. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/db/continuum.py +0 -0
  72. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/db/sess.py +0 -0
  73. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/email/templates/feedback.html.mako +0 -0
  74. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/email/templates/feedback.txt.mako +0 -0
  75. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/forms/__init__.py +0 -0
  76. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/grids/__init__.py +0 -0
  77. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/handler.py +0 -0
  78. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/helpers.py +0 -0
  79. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/menus.py +0 -0
  80. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/progress.py +0 -0
  81. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/static/__init__.py +0 -0
  82. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/static/img/favicon.ico +0 -0
  83. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/static/img/logo.png +0 -0
  84. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/static/img/testing.png +0 -0
  85. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/subscribers.py +0 -0
  86. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  87. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  88. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  89. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/auth/login.mako +0 -0
  90. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/base_meta.mako +0 -0
  91. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/configure.mako +0 -0
  92. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  93. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  94. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  95. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  96. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/password.pt +0 -0
  97. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  98. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  99. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  100. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  101. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  102. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  103. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  104. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/select.pt +0 -0
  105. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  106. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  107. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/forbidden.mako +0 -0
  108. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/form.mako +0 -0
  109. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  110. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  111. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  112. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/home.mako +0 -0
  113. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/configure.mako +0 -0
  114. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/create.mako +0 -0
  115. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/delete.mako +0 -0
  116. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/edit.mako +0 -0
  117. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/form.mako +0 -0
  118. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/index.mako +0 -0
  119. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/master/view.mako +0 -0
  120. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/notfound.mako +0 -0
  121. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/page.mako +0 -0
  122. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  123. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/progress.mako +0 -0
  124. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/setup.mako +0 -0
  125. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/upgrade.mako +0 -0
  126. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  127. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  128. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/templates/wutta-components.mako +0 -0
  129. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/__init__.py +0 -0
  130. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/auth.py +0 -0
  131. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/base.py +0 -0
  132. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/common.py +0 -0
  133. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/essential.py +0 -0
  134. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/master.py +0 -0
  135. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/people.py +0 -0
  136. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/progress.py +0 -0
  137. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/roles.py +0 -0
  138. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/settings.py +0 -0
  139. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/upgrades.py +0 -0
  140. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/src/wuttaweb/views/users.py +0 -0
  141. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tasks.py +0 -0
  142. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/__init__.py +0 -0
  143. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/db/__init__.py +0 -0
  144. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/db/test_continuum.py +0 -0
  145. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/forms/__init__.py +0 -0
  146. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/grids/__init__.py +0 -0
  147. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/grids/test_base.py +0 -0
  148. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  149. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  150. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_oruga.js +0 -0
  151. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_oruga_bulma.css +0 -0
  152. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_oruga_bulma.js +0 -0
  153. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_vue.js +0 -0
  154. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/bb_vue_fontawesome.js +0 -0
  155. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/buefy.css +0 -0
  156. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/buefy.js +0 -0
  157. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/fontawesome.js +0 -0
  158. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/vue.js +0 -0
  159. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/libcache/vue_resource.js +0 -0
  160. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_app.py +0 -0
  161. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_handler.py +0 -0
  162. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_helpers.py +0 -0
  163. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_menus.py +0 -0
  164. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_progress.py +0 -0
  165. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_static.py +0 -0
  166. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/test_subscribers.py +0 -0
  167. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/util.py +0 -0
  168. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/__init__.py +0 -0
  169. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test___init__.py +0 -0
  170. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_auth.py +0 -0
  171. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_base.py +0 -0
  172. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_master.py +0 -0
  173. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_people.py +0 -0
  174. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_progress.py +0 -0
  175. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_roles.py +0 -0
  176. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_settings.py +0 -0
  177. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_upgrades.py +0 -0
  178. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tests/views/test_users.py +0 -0
  179. {wuttaweb-0.16.0 → wuttaweb-0.16.2}/tox.ini +0 -0
@@ -5,6 +5,21 @@ 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.16.2 (2024-12-10)
9
+
10
+ ### Fix
11
+
12
+ - add `GridWidget` and `form.set_grid()` for convenience
13
+ - add "is false or null" grid filter, for nullable bool columns
14
+ - remove Person column for `Person.users` grid display
15
+ - flatten UUID to str for `make_json_safe()`
16
+
17
+ ## v0.16.1 (2024-12-08)
18
+
19
+ ### Fix
20
+
21
+ - refactor to reflect usage of proper UUID values
22
+
8
23
  ## v0.16.0 (2024-12-05)
9
24
 
10
25
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: WuttaWeb
3
- Version: 0.16.0
3
+ Version: 0.16.2
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
@@ -36,7 +36,7 @@ Requires-Dist: pyramid-tm
36
36
  Requires-Dist: pyramid>=2
37
37
  Requires-Dist: waitress
38
38
  Requires-Dist: webhelpers2
39
- Requires-Dist: wuttjamaican[db]>=0.16.1
39
+ Requires-Dist: wuttjamaican[db]>=0.17.1
40
40
  Requires-Dist: zope-sqlalchemy>=1.5
41
41
  Provides-Extra: continuum
42
42
  Requires-Dist: wutta-continuum; extra == 'continuum'
@@ -0,0 +1,15 @@
1
+ .. _glossary:
2
+
3
+ Glossary
4
+ ========
5
+
6
+ .. glossary::
7
+ :sorted:
8
+
9
+ grid
10
+ This refers to a "table of data, with features" essentially.
11
+ Sometimes it may be displayed as a simple table with no features,
12
+ or sometimes it has sortable columns, search filters and other
13
+ tools.
14
+
15
+ See also the :class:`~wuttaweb.grids.base.Grid` base class.
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.16.0"
9
+ version = "0.16.2"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -42,7 +42,7 @@ dependencies = [
42
42
  "pyramid_tm",
43
43
  "waitress",
44
44
  "WebHelpers2",
45
- "WuttJamaican[db]>=0.16.1",
45
+ "WuttJamaican[db]>=0.17.1",
46
46
  "zope.sqlalchemy>=1.5",
47
47
  ]
48
48
 
@@ -474,6 +474,34 @@ class Form:
474
474
  if self.schema and key in self.schema:
475
475
  self.schema[key].widget = widget
476
476
 
477
+ def set_grid(self, key, grid):
478
+ """
479
+ Establish a :term:`grid` to be displayed for a field. This
480
+ uses a :class:`~wuttaweb.forms.widgets.GridWidget` to wrap the
481
+ rendered grid.
482
+
483
+ :param key: Name of field.
484
+
485
+ :param widget: :class:`~wuttaweb.grids.base.Grid` instance,
486
+ pre-configured and (usually) with data.
487
+ """
488
+ from wuttaweb.forms.widgets import GridWidget
489
+
490
+ widget = GridWidget(self.request, grid)
491
+ self.set_widget(key, widget)
492
+ self.add_grid_vue_context(grid)
493
+
494
+ def add_grid_vue_context(self, grid):
495
+ """ """
496
+ if not grid.key:
497
+ raise ValueError("grid must have a key!")
498
+
499
+ if grid.key in self.grid_vue_context:
500
+ log.warning("grid data with key '%s' already registered, "
501
+ "but will be replaced", grid.key)
502
+
503
+ self.grid_vue_context[grid.key] = grid.get_vue_context()
504
+
477
505
  def set_validator(self, key, validator):
478
506
  """
479
507
  Set/override the validator for a field, or the form.
@@ -848,17 +876,6 @@ class Form:
848
876
  output = render(template, context)
849
877
  return HTML.literal(output)
850
878
 
851
- def add_grid_vue_context(self, grid):
852
- """ """
853
- if not grid.key:
854
- raise ValueError("grid must have a key!")
855
-
856
- if grid.key in self.grid_vue_context:
857
- log.warning("grid data with key '%s' already registered, "
858
- "but will be replaced", grid.key)
859
-
860
- self.grid_vue_context[grid.key] = grid.get_vue_context()
861
-
862
879
  def render_vue_field(
863
880
  self,
864
881
  fieldname,
@@ -24,6 +24,8 @@
24
24
  Form schema types
25
25
  """
26
26
 
27
+ import uuid as _uuid
28
+
27
29
  import colander
28
30
 
29
31
  from wuttaweb.db import Session
@@ -212,7 +214,7 @@ class ObjectRef(colander.SchemaType):
212
214
  node.model_instance = appstruct
213
215
 
214
216
  # serialize to uuid
215
- return appstruct.uuid
217
+ return appstruct.uuid.hex
216
218
 
217
219
  def deserialize(self, node, cstruct):
218
220
  """ """
@@ -246,7 +248,14 @@ class ObjectRef(colander.SchemaType):
246
248
 
247
249
  # fetch object from DB
248
250
  model = self.app.model
249
- obj = self.session.get(self.model_class, value)
251
+ obj = None
252
+ if isinstance(value, _uuid.UUID):
253
+ obj = self.session.get(self.model_class, value)
254
+ else:
255
+ try:
256
+ obj = self.session.get(self.model_class, _uuid.UUID(value))
257
+ except ValueError:
258
+ pass
250
259
 
251
260
  # raise error if not found
252
261
  if not obj:
@@ -287,7 +296,7 @@ class ObjectRef(colander.SchemaType):
287
296
  if 'values' not in kwargs:
288
297
  query = self.get_query()
289
298
  objects = query.all()
290
- values = [(obj.uuid, str(obj))
299
+ values = [(obj.uuid.hex, str(obj))
291
300
  for obj in objects]
292
301
  if self.empty_option:
293
302
  values.insert(0, self.empty_option)
@@ -210,6 +210,44 @@ class FileDownloadWidget(Widget):
210
210
  return humanize.naturalsize(size)
211
211
 
212
212
 
213
+ class GridWidget(Widget):
214
+ """
215
+ Widget for fields whose data is represented by a :term:`grid`.
216
+
217
+ This is a subclass of :class:`deform:deform.widget.Widget` but
218
+ does not use any Deform templates.
219
+
220
+ This widget only supports "readonly" mode, is not editable. It is
221
+ merely a convenience around the grid itself, which does the heavy
222
+ lifting.
223
+
224
+ Instead of creating this widget directly you probably should call
225
+ :meth:`~wuttaweb.forms.base.Form.set_grid()` on your form.
226
+
227
+ :param request: Current :term:`request` object.
228
+
229
+ :param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to
230
+ display the field data.
231
+ """
232
+
233
+ def __init__(self, request, grid, *args, **kwargs):
234
+ super().__init__(*args, **kwargs)
235
+ self.request = request
236
+ self.grid = grid
237
+
238
+ def serialize(self, field, cstruct, **kw):
239
+ """
240
+ This widget simply calls
241
+ :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on
242
+ the ``grid`` to serialize.
243
+ """
244
+ readonly = kw.get('readonly', self.readonly)
245
+ if not readonly:
246
+ raise NotImplementedError("edit not allowed for this widget")
247
+
248
+ return self.grid.render_table_element()
249
+
250
+
213
251
  class RoleRefsWidget(WuttaCheckboxChoiceWidget):
214
252
  """
215
253
  Widget for use with User
@@ -280,7 +318,7 @@ class UserRefsWidget(WuttaCheckboxChoiceWidget):
280
318
  raise NotImplementedError("edit not allowed for this widget")
281
319
 
282
320
  model = self.app.model
283
- columns = ['person', 'username', 'active']
321
+ columns = ['username', 'active']
284
322
 
285
323
  # generate data set for users
286
324
  users = []
@@ -56,7 +56,7 @@ Elements of :attr:`~Grid.sort_defaults` will be of this type.
56
56
 
57
57
  class Grid:
58
58
  """
59
- Base class for all grids.
59
+ Base class for all :term:`grids <grid>`.
60
60
 
61
61
  :param request: Reference to current :term:`request` object.
62
62
 
@@ -116,20 +116,22 @@ class GridFilter:
116
116
  'is_any': "is any",
117
117
  'equal': "equal to",
118
118
  'not_equal': "not equal to",
119
- 'is_null': "is null",
120
- 'is_not_null': "is not null",
121
119
  'is_true': "is true",
122
120
  'is_false': "is false",
121
+ 'is_false_null': "is false or null",
122
+ 'is_null': "is null",
123
+ 'is_not_null': "is not null",
123
124
  'contains': "contains",
124
125
  'does_not_contain': "does not contain",
125
126
  }
126
127
 
127
128
  valueless_verbs = [
128
129
  'is_any',
129
- 'is_null',
130
- 'is_not_null',
131
130
  'is_true',
132
131
  'is_false',
132
+ 'is_false_null',
133
+ 'is_null',
134
+ 'is_not_null',
133
135
  ]
134
136
 
135
137
  def __init__(
@@ -416,6 +418,27 @@ class BooleanAlchemyFilter(AlchemyFilter):
416
418
  """
417
419
  default_verbs = ['is_true', 'is_false']
418
420
 
421
+ def get_verbs(self):
422
+ """ """
423
+
424
+ # get basic verbs from caller, or default list
425
+ verbs = getattr(self, 'verbs', self.default_verbs)
426
+ if callable(verbs):
427
+ verbs = verbs()
428
+ verbs = list(verbs)
429
+
430
+ # add some more if column is nullable
431
+ if self.nullable:
432
+ for verb in ('is_false_null', 'is_null', 'is_not_null'):
433
+ if verb not in verbs:
434
+ verbs.append(verb)
435
+
436
+ # add wildcard
437
+ if 'is_any' not in verbs:
438
+ verbs.append('is_any')
439
+
440
+ return verbs
441
+
419
442
  def coerce_value(self, value):
420
443
  """ """
421
444
  if value is not None:
@@ -435,6 +458,14 @@ class BooleanAlchemyFilter(AlchemyFilter):
435
458
  """
436
459
  return query.filter(self.model_property == False)
437
460
 
461
+ def filter_is_false_null(self, query, value):
462
+ """
463
+ Filter data with "is false or null" condition. The value is
464
+ ignored.
465
+ """
466
+ return query.filter(sa.or_(self.model_property == False,
467
+ self.model_property == None))
468
+
438
469
 
439
470
  default_sqlalchemy_filters = {
440
471
  None: AlchemyFilter,
@@ -563,7 +563,7 @@
563
563
 
564
564
  const WuttaFeedbackFormData = {
565
565
  referrer: null,
566
- userUUID: ${json.dumps(request.user.uuid if request.user else None)|n},
566
+ userUUID: ${json.dumps(request.user.uuid.hex if request.user else None)|n},
567
567
  userName: ${json.dumps(str(request.user) if request.user else None)|n},
568
568
  showDialog: false,
569
569
  sendingFeedback: false,
@@ -27,6 +27,7 @@ Web Utilities
27
27
  import importlib
28
28
  import json
29
29
  import logging
30
+ import uuid as _uuid
30
31
  import warnings
31
32
 
32
33
  import sqlalchemy as sa
@@ -531,6 +532,10 @@ def make_json_safe(value, key=None, warn=True):
531
532
  parent[key] = make_json_safe(value, key=key, warn=warn)
532
533
  value = parent
533
534
 
535
+ # convert UUID to str
536
+ if isinstance(value, _uuid.UUID):
537
+ value = value.hex
538
+
534
539
  # ensure JSON-compatibility, warn if problems
535
540
  try:
536
541
  json.dumps(value)
@@ -142,6 +142,20 @@ class TestForm(TestCase):
142
142
  self.assertIs(form.widgets['foo'], new_widget)
143
143
  self.assertIs(schema['foo'].widget, new_widget)
144
144
 
145
+ def test_set_grid(self):
146
+ form = self.make_form(fields=['foo', 'bar'])
147
+ self.assertNotIn('foo', form.widgets)
148
+ self.assertNotIn('foogrid', form.grid_vue_context)
149
+
150
+ grid = Grid(self.request, key='foogrid',
151
+ columns=['a', 'b'],
152
+ data=[{'a': 1, 'b': 2}, {'a': 3, 'b': 4}])
153
+
154
+ form.set_grid('foo', grid)
155
+ self.assertIn('foo', form.widgets)
156
+ self.assertIsInstance(form.widgets['foo'], widgets.GridWidget)
157
+ self.assertIn('foogrid', form.grid_vue_context)
158
+
145
159
  def test_set_validator(self):
146
160
  form = self.make_form(fields=['foo', 'bar'])
147
161
  self.assertEqual(form.validators, {})
@@ -100,7 +100,7 @@ class TestObjectRef(DataTestCase):
100
100
  self.assertIsNotNone(person.uuid)
101
101
  typ = mod.ObjectRef(self.request)
102
102
  value = typ.serialize(node, person)
103
- self.assertEqual(value, person.uuid)
103
+ self.assertEqual(value, person.uuid.hex)
104
104
 
105
105
  def test_deserialize(self):
106
106
  model = self.app.model
@@ -6,6 +6,7 @@ import colander
6
6
  import deform
7
7
  from pyramid import testing
8
8
 
9
+ from wuttaweb import grids
9
10
  from wuttaweb.forms import widgets as mod
10
11
  from wuttaweb.forms.schema import FileDownload, PersonRef, RoleRefs, UserRefs, Permissions
11
12
  from tests.util import WebTestCase
@@ -117,6 +118,31 @@ class TestFileDownloadWidget(WebTestCase):
117
118
  self.assertEqual(html2, html)
118
119
 
119
120
 
121
+ class TestGridWidget(WebTestCase):
122
+
123
+ def make_field(self, node, **kwargs):
124
+ # TODO: not sure why default renderer is in use even though
125
+ # pyramid_deform was included in setup? but this works..
126
+ kwargs.setdefault('renderer', deform.Form.default_renderer)
127
+ return deform.Field(node, **kwargs)
128
+
129
+ def test_serialize(self):
130
+ grid = grids.Grid(self.request,
131
+ columns=['foo', 'bar'],
132
+ data=[{'foo': 1, 'bar': 2}, {'foo': 3, 'bar': 4}])
133
+
134
+ node = colander.SchemaNode(colander.String())
135
+ widget = mod.GridWidget(self.request, grid)
136
+ field = self.make_field(node)
137
+
138
+ # readonly works okay
139
+ html = widget.serialize(field, None, readonly=True)
140
+ self.assertIn('<b-table ', html)
141
+
142
+ # but otherwise, error
143
+ self.assertRaises(NotImplementedError, widget.serialize, field, None)
144
+
145
+
120
146
  class TestRoleRefsWidget(WebTestCase):
121
147
 
122
148
  def make_field(self, node, **kwargs):
@@ -146,14 +172,14 @@ class TestRoleRefsWidget(WebTestCase):
146
172
 
147
173
  # editable values list *excludes* admin (by default)
148
174
  html = widget.serialize(field, {admin.uuid, blokes.uuid})
149
- self.assertNotIn(admin.uuid, html)
150
- self.assertIn(blokes.uuid, html)
175
+ self.assertNotIn(str(admin.uuid), html)
176
+ self.assertIn(str(blokes.uuid), html)
151
177
 
152
178
  # but admin is included for root user
153
179
  self.request.is_root = True
154
180
  html = widget.serialize(field, {admin.uuid, blokes.uuid})
155
- self.assertIn(admin.uuid, html)
156
- self.assertIn(blokes.uuid, html)
181
+ self.assertIn(str(admin.uuid), html)
182
+ self.assertIn(str(blokes.uuid), html)
157
183
 
158
184
 
159
185
  class TestUserRefsWidget(WebTestCase):
@@ -328,9 +328,15 @@ class TestBooleanAlchemyFilter(WebTestCase):
328
328
 
329
329
  model = self.app.model
330
330
  self.sample_data = [
331
- {'username': 'alice', 'active': True},
332
- {'username': 'bob', 'active': True},
333
- {'username': 'charlie', 'active': False},
331
+ {'username': 'alice',
332
+ 'prevent_edit': False,
333
+ 'active': True},
334
+ {'username': 'bob',
335
+ 'prevent_edit': True,
336
+ 'active': True},
337
+ {'username': 'charlie',
338
+ 'active': False,
339
+ 'prevent_edit': None},
334
340
  ]
335
341
  for user in self.sample_data:
336
342
  user = model.User(**user)
@@ -343,6 +349,34 @@ class TestBooleanAlchemyFilter(WebTestCase):
343
349
  kwargs['model_property'] = model_property
344
350
  return factory(self.request, model_property.key, **kwargs)
345
351
 
352
+ def test_get_verbs(self):
353
+ model = self.app.model
354
+
355
+ # bool field, not nullable
356
+ filtr = self.make_filter(model.User.active,
357
+ factory=mod.BooleanAlchemyFilter,
358
+ nullable=False)
359
+ self.assertFalse(hasattr(filtr, 'verbs'))
360
+ self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])
361
+
362
+ # by default, returns default verbs (plus 'is_any')
363
+ self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])
364
+
365
+ # default verbs can be a callable
366
+ filtr.default_verbs = lambda: ['foo', 'bar']
367
+ self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])
368
+
369
+ # bool field, *nullable*
370
+ filtr = self.make_filter(model.User.active,
371
+ factory=mod.BooleanAlchemyFilter,
372
+ nullable=True)
373
+ self.assertFalse(hasattr(filtr, 'verbs'))
374
+ self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])
375
+
376
+ # effective verbs also include is_false_null
377
+ self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_false_null',
378
+ 'is_null', 'is_not_null', 'is_any'])
379
+
346
380
  def test_coerce_value(self):
347
381
  model = self.app.model
348
382
  filtr = self.make_filter(model.User.active)
@@ -377,6 +411,16 @@ class TestBooleanAlchemyFilter(WebTestCase):
377
411
  self.assertIsNot(filtered_query, self.sample_query)
378
412
  self.assertEqual(filtered_query.count(), 1)
379
413
 
414
+ def test_filter_is_false_null(self):
415
+ model = self.app.model
416
+ filtr = self.make_filter(model.User.prevent_edit)
417
+ self.assertEqual(self.sample_query.count(), 3)
418
+
419
+ # nb. only one account is marked with "prevent edit"
420
+ filtered_query = filtr.filter_is_false_null(self.sample_query, None)
421
+ self.assertIsNot(filtered_query, self.sample_query)
422
+ self.assertEqual(filtered_query.count(), 2)
423
+
380
424
 
381
425
  class TestVerbNotSupported(TestCase):
382
426
 
@@ -1,5 +1,6 @@
1
1
  # -*- coding: utf-8; -*-
2
2
 
3
+ import uuid as _uuid
3
4
  from unittest import TestCase
4
5
  from unittest.mock import MagicMock
5
6
 
@@ -90,7 +91,7 @@ class TestWuttaSecurityPolicy(TestCase):
90
91
 
91
92
  # invalid identity yields no user
92
93
  self.policy = self.make_policy()
93
- self.policy.remember(self.request, 'bogus-user-uuid')
94
+ self.policy.remember(self.request, _uuid.uuid4()) # random uuid
94
95
  user = self.policy.identity(self.request)
95
96
  self.assertIsNone(user)
96
97
 
@@ -1,6 +1,7 @@
1
1
  # -*- coding: utf-8; -*-
2
2
 
3
3
  import json
4
+ import uuid as _uuid
4
5
  from unittest import TestCase
5
6
  from unittest.mock import patch, MagicMock
6
7
 
@@ -564,6 +565,11 @@ class TestMakeJsonSafe(TestCase):
564
565
  value = mod.make_json_safe(person, key='person')
565
566
  self.assertEqual(value, "Betty Boop")
566
567
 
568
+ def test_uuid(self):
569
+ uuid = _uuid.uuid4()
570
+ value = mod.make_json_safe(uuid)
571
+ self.assertEqual(value, uuid.hex)
572
+
567
573
  def test_dict(self):
568
574
  model = self.app.model
569
575
  person = model.Person(full_name="Betty Boop")
@@ -88,7 +88,7 @@ class TestCommonView(WebTestCase):
88
88
 
89
89
  # basic send, with user
90
90
  self.request.user = user
91
- self.request.POST['user_uuid'] = user.uuid
91
+ self.request.POST['user_uuid'] = str(user.uuid)
92
92
  with patch.object(mod, 'Session', return_value=self.session):
93
93
  context = view.feedback()
94
94
  self.assertEqual(context, {'ok': True})
@@ -1,10 +0,0 @@
1
- .. _glossary:
2
-
3
- Glossary
4
- ========
5
-
6
- .. glossary::
7
- :sorted:
8
-
9
- view
10
- TODO
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes