WuttaWeb 0.21.3__tar.gz → 0.21.4__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 (205) hide show
  1. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/CHANGELOG.md +6 -0
  2. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/PKG-INFO +1 -1
  3. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/pyproject.toml +1 -1
  4. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/grids/base.py +69 -0
  5. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/grids/filters.py +81 -1
  6. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/wutta-components.mako +11 -0
  7. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/grids/test_base.py +67 -0
  8. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/grids/test_filters.py +63 -0
  9. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/.gitignore +0 -0
  10. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/COPYING.txt +0 -0
  11. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/README.md +0 -0
  12. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/Makefile +0 -0
  13. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/_static/.keepme +0 -0
  14. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.app.rst +0 -0
  15. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.auth.rst +0 -0
  16. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.cli.rst +0 -0
  17. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  18. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.conf.rst +0 -0
  19. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.db.continuum.rst +0 -0
  20. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.db.rst +0 -0
  21. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.db.sess.rst +0 -0
  22. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.emails.rst +0 -0
  23. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.forms.base.rst +0 -0
  24. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.forms.rst +0 -0
  25. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.forms.schema.rst +0 -0
  26. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  27. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.grids.base.rst +0 -0
  28. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.grids.filters.rst +0 -0
  29. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.grids.rst +0 -0
  30. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.handler.rst +0 -0
  31. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.helpers.rst +0 -0
  32. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.menus.rst +0 -0
  33. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.progress.rst +0 -0
  34. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.rst +0 -0
  35. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.static.rst +0 -0
  36. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.subscribers.rst +0 -0
  37. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.util.rst +0 -0
  38. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.auth.rst +0 -0
  39. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.base.rst +0 -0
  40. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.batch.rst +0 -0
  41. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.common.rst +0 -0
  42. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.email.rst +0 -0
  43. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.essential.rst +0 -0
  44. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.master.rst +0 -0
  45. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.people.rst +0 -0
  46. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.progress.rst +0 -0
  47. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.reports.rst +0 -0
  48. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.roles.rst +0 -0
  49. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.rst +0 -0
  50. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.settings.rst +0 -0
  51. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  52. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/api/wuttaweb.views.users.rst +0 -0
  53. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/conf.py +0 -0
  54. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/glossary.rst +0 -0
  55. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/index.rst +0 -0
  56. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/make.bat +0 -0
  57. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/narr/cli/builtin.rst +0 -0
  58. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/narr/cli/index.rst +0 -0
  59. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/narr/templates/base.rst +0 -0
  60. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/narr/templates/index.rst +0 -0
  61. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/narr/templates/lookup.rst +0 -0
  62. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/docs/narr/templates/overview.rst +0 -0
  63. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/__init__.py +0 -0
  64. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/_version.py +0 -0
  65. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/app.py +0 -0
  66. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/auth.py +0 -0
  67. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/cli/__init__.py +0 -0
  68. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/cli/webapp.py +0 -0
  69. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/conf.py +0 -0
  70. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/db/__init__.py +0 -0
  71. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/db/continuum.py +0 -0
  72. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/db/sess.py +0 -0
  73. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  74. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  75. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/emails.py +0 -0
  76. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/forms/__init__.py +0 -0
  77. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/forms/base.py +0 -0
  78. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/forms/schema.py +0 -0
  79. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/forms/widgets.py +0 -0
  80. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/grids/__init__.py +0 -0
  81. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/handler.py +0 -0
  82. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/helpers.py +0 -0
  83. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/menus.py +0 -0
  84. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/progress.py +0 -0
  85. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/static/__init__.py +0 -0
  86. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/static/img/favicon.ico +0 -0
  87. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/static/img/logo.png +0 -0
  88. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/static/img/testing.png +0 -0
  89. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/subscribers.py +0 -0
  90. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  91. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  92. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  93. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/auth/login.mako +0 -0
  94. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/base.mako +0 -0
  95. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/base_meta.mako +0 -0
  96. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/batch/view.mako +0 -0
  97. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/configure.mako +0 -0
  98. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  99. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  100. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  101. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  102. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  103. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  104. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/password.pt +0 -0
  105. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  106. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  107. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  108. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  109. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  110. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  111. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  112. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  113. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/select.pt +0 -0
  114. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  115. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  116. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  117. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/forbidden.mako +0 -0
  118. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/form.mako +0 -0
  119. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  120. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  121. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  122. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/home.mako +0 -0
  123. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/configure.mako +0 -0
  124. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/create.mako +0 -0
  125. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/delete.mako +0 -0
  126. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/edit.mako +0 -0
  127. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/form.mako +0 -0
  128. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/index.mako +0 -0
  129. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/master/view.mako +0 -0
  130. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/notfound.mako +0 -0
  131. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/page.mako +0 -0
  132. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  133. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/progress.mako +0 -0
  134. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/reports/view.mako +0 -0
  135. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/setup.mako +0 -0
  136. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/upgrade.mako +0 -0
  137. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  138. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  139. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/testing.py +0 -0
  140. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/util.py +0 -0
  141. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/__init__.py +0 -0
  142. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/auth.py +0 -0
  143. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/base.py +0 -0
  144. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/batch.py +0 -0
  145. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/common.py +0 -0
  146. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/email.py +0 -0
  147. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/essential.py +0 -0
  148. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/master.py +0 -0
  149. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/people.py +0 -0
  150. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/progress.py +0 -0
  151. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/reports.py +0 -0
  152. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/roles.py +0 -0
  153. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/settings.py +0 -0
  154. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/upgrades.py +0 -0
  155. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/src/wuttaweb/views/users.py +0 -0
  156. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tasks.py +0 -0
  157. {wuttaweb-0.21.3/tests/views → wuttaweb-0.21.4/tests}/__init__.py +0 -0
  158. {wuttaweb-0.21.3/tests/grids → wuttaweb-0.21.4/tests/cli}/__init__.py +0 -0
  159. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/cli/test_webapp.py +0 -0
  160. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/db/__init__.py +0 -0
  161. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/db/test_continuum.py +0 -0
  162. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/forms/test_base.py +0 -0
  163. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/forms/test_schema.py +0 -0
  164. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/forms/test_widgets.py +0 -0
  165. {wuttaweb-0.21.3/tests/cli → wuttaweb-0.21.4/tests/grids}/__init__.py +0 -0
  166. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  167. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  168. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_oruga.js +0 -0
  169. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_oruga_bulma.css +0 -0
  170. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_oruga_bulma.js +0 -0
  171. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_vue.js +0 -0
  172. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/bb_vue_fontawesome.js +0 -0
  173. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/buefy.css +0 -0
  174. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/buefy.js +0 -0
  175. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/fontawesome.js +0 -0
  176. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/vue.js +0 -0
  177. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/libcache/vue_resource.js +0 -0
  178. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_app.py +0 -0
  179. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_auth.py +0 -0
  180. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_emails.py +0 -0
  181. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_handler.py +0 -0
  182. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_helpers.py +0 -0
  183. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_menus.py +0 -0
  184. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_progress.py +0 -0
  185. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_static.py +0 -0
  186. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_subscribers.py +0 -0
  187. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/test_util.py +0 -0
  188. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/util.py +0 -0
  189. {wuttaweb-0.21.3/tests → wuttaweb-0.21.4/tests/views}/__init__.py +0 -0
  190. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test___init__.py +0 -0
  191. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_auth.py +0 -0
  192. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_base.py +0 -0
  193. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_batch.py +0 -0
  194. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_common.py +0 -0
  195. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_email.py +0 -0
  196. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_essential.py +0 -0
  197. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_master.py +0 -0
  198. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_people.py +0 -0
  199. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_progress.py +0 -0
  200. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_reports.py +0 -0
  201. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_roles.py +0 -0
  202. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_settings.py +0 -0
  203. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_upgrades.py +0 -0
  204. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tests/views/test_users.py +0 -0
  205. {wuttaweb-0.21.3 → wuttaweb-0.21.4}/tox.ini +0 -0
@@ -5,6 +5,12 @@ 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.21.4 (2025-02-21)
9
+
10
+ ### Fix
11
+
12
+ - add value choice/enum support for grid filters
13
+
8
14
  ## v0.21.3 (2025-02-19)
9
15
 
10
16
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.21.3
3
+ Version: 0.21.4
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
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.21.3"
9
+ version = "0.21.4"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -119,6 +119,12 @@ class Grid:
119
119
  See also :meth:`set_renderer()` and
120
120
  :meth:`set_default_renderers()`.
121
121
 
122
+ .. attribute:: enums
123
+
124
+ Dict of "enum" collections, for supported columns.
125
+
126
+ See also :meth:`set_enum()`.
127
+
122
128
  .. attribute:: checkable
123
129
 
124
130
  Boolean indicating whether the grid should expose per-row
@@ -377,6 +383,7 @@ class Grid:
377
383
  data=None,
378
384
  labels={},
379
385
  renderers={},
386
+ enums={},
380
387
  checkable=False,
381
388
  row_class=None,
382
389
  actions=[],
@@ -458,6 +465,11 @@ class Grid:
458
465
  self.filters = {}
459
466
  self.set_filter_defaults(**(filter_defaults or {}))
460
467
 
468
+ # enums
469
+ self.enums = {}
470
+ for key in enums:
471
+ self.set_enum(key, enums[key])
472
+
461
473
  def get_columns(self):
462
474
  """
463
475
  Returns the official list of column names for the grid, or
@@ -720,6 +732,27 @@ class Grid:
720
732
  elif isinstance(column.type, sa.Boolean):
721
733
  self.set_renderer(key, self.render_boolean)
722
734
 
735
+ def set_enum(self, key, enum):
736
+ """
737
+ Set the "enum" collection for a given column.
738
+
739
+ This will set the column renderer to show the appropriate enum
740
+ value for each row in the grid. See also
741
+ :meth:`render_enum()`.
742
+
743
+ If the grid has a corresponding filter for the column, it will
744
+ be modified to show "choices" for values contained in the
745
+ enum.
746
+
747
+ :param key: Name of column.
748
+
749
+ :param enum: Instance of :class:`python:enum.Enum`.
750
+ """
751
+ self.enums[key] = enum
752
+ self.set_renderer(key, self.render_enum, enum=enum)
753
+ if key in self.filters:
754
+ self.filters[key].set_choices(enum)
755
+
723
756
  def set_link(self, key, link=True):
724
757
  """
725
758
  Explicitly enable or disable auto-link behavior for a given
@@ -1945,6 +1978,33 @@ class Grid:
1945
1978
  dt = getattr(obj, key)
1946
1979
  return self.app.render_datetime(dt)
1947
1980
 
1981
+ def render_enum(self, obj, key, value, enum=None):
1982
+ """
1983
+ Custom grid value renderer for "enum" fields.
1984
+
1985
+ See also :meth:`set_enum()`.
1986
+
1987
+ :param enum: Enum class for the field. This should be an
1988
+ instance of :class:`~python:enum.Enum`.
1989
+
1990
+ To use this feature for your grid::
1991
+
1992
+ from enum import Enum
1993
+
1994
+ class MyEnum(Enum):
1995
+ ONE = 1
1996
+ TWO = 2
1997
+ THREE = 3
1998
+
1999
+ grid.set_enum('my_enum_field', MyEnum)
2000
+ """
2001
+ if enum:
2002
+ raw_value = obj[key]
2003
+ if raw_value:
2004
+ return raw_value.value
2005
+
2006
+ return value
2007
+
1948
2008
  def render_percent(self, obj, key, value, **kwargs):
1949
2009
  """
1950
2010
  Column renderer for percentage values.
@@ -2176,6 +2236,13 @@ class Grid:
2176
2236
  """
2177
2237
  filters = []
2178
2238
  for filtr in self.filters.values():
2239
+
2240
+ choices = []
2241
+ choice_labels = {}
2242
+ if filtr.choices:
2243
+ choices = list(filtr.choices)
2244
+ choice_labels = dict(filtr.choices)
2245
+
2179
2246
  filters.append({
2180
2247
  'key': filtr.key,
2181
2248
  'data_type': filtr.data_type,
@@ -2185,6 +2252,8 @@ class Grid:
2185
2252
  'verb_labels': filtr.get_verb_labels(),
2186
2253
  'valueless_verbs': filtr.get_valueless_verbs(),
2187
2254
  'verb': filtr.verb,
2255
+ 'choices': choices,
2256
+ 'choice_labels': choice_labels,
2188
2257
  'value': filtr.value,
2189
2258
  'label': filtr.label,
2190
2259
  })
@@ -2,7 +2,7 @@
2
2
  ################################################################################
3
3
  #
4
4
  # wuttaweb -- Web App for Wutta Framework
5
- # Copyright © 2024 Lance Edgar
5
+ # Copyright © 2024-2025 Lance Edgar
6
6
  #
7
7
  # This file is part of Wutta Framework.
8
8
  #
@@ -26,6 +26,8 @@ Grid Filters
26
26
 
27
27
  import datetime
28
28
  import logging
29
+ from collections import OrderedDict
30
+ from enum import EnumType
29
31
 
30
32
  import sqlalchemy as sa
31
33
 
@@ -77,6 +79,7 @@ class GridFilter:
77
79
 
78
80
  * ``'string'``
79
81
  * ``'date'``
82
+ * ``'choice'``
80
83
 
81
84
  Note that this mainly applies to the "value input" used by the
82
85
  filter. There is no data type for boolean since it does not
@@ -94,6 +97,13 @@ class GridFilter:
94
97
 
95
98
  See also :attr:`value`.
96
99
 
100
+ .. attribute:: choices
101
+
102
+ OrderedDict of possible values for the filter.
103
+
104
+ This is safe to read from, but use :meth:`set_choices()` to
105
+ update it.
106
+
97
107
  .. attribute:: value
98
108
 
99
109
  Value for current filter, if :attr:`active` is true.
@@ -159,6 +169,7 @@ class GridFilter:
159
169
  key,
160
170
  label=None,
161
171
  verbs=None,
172
+ choices={},
162
173
  default_active=False,
163
174
  default_verb=None,
164
175
  default_value=None,
@@ -180,6 +191,9 @@ class GridFilter:
180
191
  if default_verb:
181
192
  self.default_verb = default_verb
182
193
 
194
+ # choices
195
+ self.set_choices(choices)
196
+
183
197
  # value
184
198
  self.default_value = default_value
185
199
  self.value = self.default_value
@@ -255,6 +269,72 @@ class GridFilter:
255
269
 
256
270
  return verb
257
271
 
272
+ def set_choices(self, choices):
273
+ """
274
+ Set the value choices for the filter.
275
+
276
+ If ``choices`` is non-empty, it is passed to
277
+ :meth:`normalize_choices()` and the result is assigned to
278
+ :attr:`choices`. Also, the :attr:`data_type` is set to
279
+ ``'choice'`` so the UI will present the value input as a
280
+ dropdown.
281
+
282
+ But if ``choices`` is empty, :attr:`choices` is set to an
283
+ empty dict, and :attr:`data_type` is set (back) to
284
+ ``'string'``.
285
+
286
+ :param choices: Collection of "choices" or ``None``.
287
+ """
288
+ if choices:
289
+ self.choices = self.normalize_choices(choices)
290
+ self.data_type = 'choice'
291
+ else:
292
+ self.choices = {}
293
+ self.data_type = 'string'
294
+
295
+ def normalize_choices(self, choices):
296
+ """
297
+ Normalize a collection of "choices" to standard ``OrderedDict``.
298
+
299
+ This is called automatically by :meth:`set_choices()`.
300
+
301
+ :param choices: A collection of "choices" in one of the following
302
+ formats:
303
+
304
+ * :class:`python:enum.Enum` class
305
+ * simple list, each value of which should be a string,
306
+ which is assumed to be able to serve as both key and
307
+ value (ordering of choices will be preserved)
308
+ * simple dict, keys and values of which will define the
309
+ choices (note that the final choices will be sorted by
310
+ key!)
311
+ * OrderedDict, keys and values of which will define the
312
+ choices (ordering of choices will be preserved)
313
+
314
+ :rtype: :class:`python:collections.OrderedDict`
315
+ """
316
+ normalized = choices
317
+
318
+ if isinstance(choices, EnumType):
319
+ normalized = OrderedDict([
320
+ (member.name, member.value)
321
+ for member in choices])
322
+
323
+ elif isinstance(choices, OrderedDict):
324
+ normalized = choices
325
+
326
+ elif isinstance(choices, dict):
327
+ normalized = OrderedDict([
328
+ (key, choices[key])
329
+ for key in sorted(choices)])
330
+
331
+ elif isinstance(choices, list):
332
+ normalized = OrderedDict([
333
+ (key, key)
334
+ for key in choices])
335
+
336
+ return normalized
337
+
258
338
  def apply_filter(self, data, verb=None, value=UNSPECIFIED):
259
339
  """
260
340
  Filter the given data set according to a verb/value pair.
@@ -486,6 +486,17 @@
486
486
  v-show="valuedVerb()"
487
487
  :is-small="isSmall" />
488
488
 
489
+ <b-select v-if="filter.data_type == 'choice'"
490
+ v-model="filter.value"
491
+ ref="filterValue"
492
+ v-show="valuedVerb()">
493
+ <option v-for="choice in filter.choices"
494
+ :key="choice"
495
+ :value="choice">
496
+ {{ filter.choice_labels[choice] || choice }}
497
+ </option>
498
+ </b-select>
499
+
489
500
  <wutta-filter-value v-else
490
501
  v-model="filter.value"
491
502
  ref="filterValue"
@@ -2,6 +2,8 @@
2
2
 
3
3
  import datetime
4
4
  import decimal
5
+ from collections import OrderedDict
6
+ from enum import Enum
5
7
  from unittest import TestCase
6
8
  from unittest.mock import patch, MagicMock
7
9
 
@@ -261,6 +263,36 @@ class TestGrid(WebTestCase):
261
263
  self.assertIn('created', grid.renderers)
262
264
  self.assertIsNot(grid.renderers['created'], myrender)
263
265
 
266
+ def test_set_enum(self):
267
+ model = self.app.model
268
+
269
+ class MockEnum(Enum):
270
+ FOO = 'foo'
271
+ BAR = 'bar'
272
+
273
+ # no enums by default
274
+ grid = self.make_grid(columns=['foo', 'bar'])
275
+ self.assertEqual(grid.enums, {})
276
+
277
+ # enum is set, but not filter choices
278
+ grid = self.make_grid(columns=['foo', 'bar'],
279
+ filterable=False,
280
+ enums={'foo': MockEnum})
281
+ self.assertIs(grid.enums['foo'], MockEnum)
282
+ self.assertEqual(grid.filters, {})
283
+
284
+ # both enum and filter choices are set
285
+ grid = self.make_grid(model_class=model.Setting,
286
+ filterable=True,
287
+ enums={'name': MockEnum})
288
+ self.assertIs(grid.enums['name'], MockEnum)
289
+ self.assertIn('name', grid.filters)
290
+ self.assertIn('value', grid.filters)
291
+ self.assertEqual(grid.filters['name'].choices, OrderedDict([
292
+ ('FOO', 'foo'),
293
+ ('BAR', 'bar'),
294
+ ]))
295
+
264
296
  def test_linked_columns(self):
265
297
  grid = self.make_grid(columns=['foo', 'bar'])
266
298
  self.assertEqual(grid.linked_columns, [])
@@ -1432,6 +1464,20 @@ class TestGrid(WebTestCase):
1432
1464
  value = decimal.Decimal('-42.42')
1433
1465
  self.assertEqual(grid.render_currency(obj, 'foo', value), '($42.42)')
1434
1466
 
1467
+ def test_render_enum(self):
1468
+ enum = self.app.enum
1469
+ grid = self.make_grid(columns=['foo', 'bar'])
1470
+ obj = {'status': None}
1471
+
1472
+ # null
1473
+ value = grid.render_enum(obj, 'status', None, enum=enum.UpgradeStatus)
1474
+ self.assertIsNone(value)
1475
+
1476
+ # normal
1477
+ obj['status'] = enum.UpgradeStatus.SUCCESS
1478
+ value = grid.render_enum(obj, 'status', 'SUCCESS', enum=enum.UpgradeStatus)
1479
+ self.assertEqual(value, 'success')
1480
+
1435
1481
  def test_render_percent(self):
1436
1482
  grid = self.make_grid(columns=['foo', 'bar'])
1437
1483
  obj = MagicMock()
@@ -1572,6 +1618,27 @@ class TestGrid(WebTestCase):
1572
1618
  grid.load_settings()
1573
1619
  filters = grid.get_vue_filters()
1574
1620
  self.assertEqual(len(filters), 2)
1621
+ name, value = filters
1622
+ self.assertEqual(name['choices'], [])
1623
+ self.assertEqual(name['choice_labels'], {})
1624
+ self.assertEqual(value['choices'], [])
1625
+ self.assertEqual(value['choice_labels'], {})
1626
+
1627
+ class MockEnum(Enum):
1628
+ FOO = 'foo'
1629
+ BAR = 'bar'
1630
+
1631
+ # with filter choices
1632
+ grid = self.make_grid(key='settings', model_class=model.Setting,
1633
+ filterable=True, enums={'name': MockEnum})
1634
+ grid.load_settings()
1635
+ filters = grid.get_vue_filters()
1636
+ self.assertEqual(len(filters), 2)
1637
+ name, value = filters
1638
+ self.assertEqual(name['choices'], ['FOO', 'BAR'])
1639
+ self.assertEqual(name['choice_labels'], {'FOO': 'foo', 'BAR': 'bar'})
1640
+ self.assertEqual(value['choices'], [])
1641
+ self.assertEqual(value['choice_labels'], {})
1575
1642
 
1576
1643
  def test_object_to_dict(self):
1577
1644
  grid = self.make_grid()
@@ -1,6 +1,8 @@
1
1
  # -*- coding: utf-8; -*-
2
2
 
3
3
  import datetime
4
+ from collections import OrderedDict
5
+ from enum import Enum
4
6
  from unittest import TestCase
5
7
  from unittest.mock import patch
6
8
 
@@ -136,6 +138,67 @@ class TestGridFilter(WebTestCase):
136
138
  self.assertIsInstance(verbs, list)
137
139
  self.assertIn('is_any', verbs)
138
140
 
141
+ def test_set_choices(self):
142
+ model = self.app.model
143
+
144
+ filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
145
+ self.assertEqual(filtr.choices, {})
146
+ self.assertEqual(filtr.data_type, 'string')
147
+
148
+ class MockEnum(Enum):
149
+ FOO = 'foo'
150
+ BAR = 'bar'
151
+
152
+ filtr.set_choices(MockEnum)
153
+ self.assertEqual(filtr.choices, OrderedDict([
154
+ ('FOO', 'foo'),
155
+ ('BAR', 'bar'),
156
+ ]))
157
+ self.assertEqual(filtr.data_type, 'choice')
158
+
159
+ filtr.set_choices(None)
160
+ self.assertEqual(filtr.choices, {})
161
+ self.assertEqual(filtr.data_type, 'string')
162
+
163
+ def test_normalize_choices(self):
164
+ model = self.app.model
165
+ filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
166
+
167
+ class MockEnum(Enum):
168
+ FOO = 'foo'
169
+ BAR = 'bar'
170
+
171
+ choices = filtr.normalize_choices(MockEnum)
172
+ self.assertEqual(choices, OrderedDict([
173
+ ('FOO', 'foo'),
174
+ ('BAR', 'bar'),
175
+ ]))
176
+
177
+ choices = filtr.normalize_choices(OrderedDict([
178
+ ('first', '1'),
179
+ ('second', '2'),
180
+ ]))
181
+ self.assertEqual(choices, OrderedDict([
182
+ ('first', '1'),
183
+ ('second', '2'),
184
+ ]))
185
+
186
+ choices = filtr.normalize_choices({
187
+ 'bbb': 'b',
188
+ 'aaa': 'a',
189
+ })
190
+ self.assertEqual(choices, OrderedDict([
191
+ ('aaa', 'a'),
192
+ ('bbb', 'b'),
193
+ ]))
194
+
195
+ choices = filtr.normalize_choices(['one', 'two', 'three'])
196
+ self.assertEqual(choices, OrderedDict([
197
+ ('one', 'one'),
198
+ ('two', 'two'),
199
+ ('three', 'three'),
200
+ ]))
201
+
139
202
  def test_apply_filter(self):
140
203
  model = self.app.model
141
204
  filtr = self.make_filter(model.Setting.value, factory=mod.StringAlchemyFilter)
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