WuttaWeb 0.19.3__tar.gz → 0.20.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 (205) hide show
  1. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/CHANGELOG.md +12 -0
  2. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/PKG-INFO +2 -2
  3. wuttaweb-0.20.0/docs/api/wuttaweb.views.reports.rst +6 -0
  4. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/index.rst +1 -0
  5. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/pyproject.toml +2 -2
  6. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/forms/base.py +26 -1
  7. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/forms/vue_template.mako +11 -3
  8. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/grids/vue_template.mako +3 -0
  9. wuttaweb-0.20.0/src/wuttaweb/templates/reports/view.mako +61 -0
  10. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/master.py +41 -11
  11. wuttaweb-0.20.0/src/wuttaweb/views/reports.py +266 -0
  12. wuttaweb-0.20.0/tests/views/test_reports.py +231 -0
  13. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/.gitignore +0 -0
  14. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/COPYING.txt +0 -0
  15. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/README.md +0 -0
  16. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/Makefile +0 -0
  17. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/_static/.keepme +0 -0
  18. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.app.rst +0 -0
  19. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.auth.rst +0 -0
  20. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.cli.rst +0 -0
  21. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  22. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.conf.rst +0 -0
  23. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.db.continuum.rst +0 -0
  24. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.db.rst +0 -0
  25. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.db.sess.rst +0 -0
  26. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.emails.rst +0 -0
  27. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.forms.base.rst +0 -0
  28. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.forms.rst +0 -0
  29. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.forms.schema.rst +0 -0
  30. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  31. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.grids.base.rst +0 -0
  32. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.grids.filters.rst +0 -0
  33. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.grids.rst +0 -0
  34. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.handler.rst +0 -0
  35. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.helpers.rst +0 -0
  36. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.menus.rst +0 -0
  37. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.progress.rst +0 -0
  38. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.rst +0 -0
  39. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.static.rst +0 -0
  40. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.subscribers.rst +0 -0
  41. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.util.rst +0 -0
  42. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.auth.rst +0 -0
  43. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.base.rst +0 -0
  44. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.batch.rst +0 -0
  45. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.common.rst +0 -0
  46. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.email.rst +0 -0
  47. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.essential.rst +0 -0
  48. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.master.rst +0 -0
  49. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.people.rst +0 -0
  50. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.progress.rst +0 -0
  51. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.roles.rst +0 -0
  52. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.rst +0 -0
  53. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.settings.rst +0 -0
  54. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  55. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/api/wuttaweb.views.users.rst +0 -0
  56. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/conf.py +0 -0
  57. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/glossary.rst +0 -0
  58. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/make.bat +0 -0
  59. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/narr/cli/builtin.rst +0 -0
  60. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/narr/cli/index.rst +0 -0
  61. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/narr/templates/base.rst +0 -0
  62. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/narr/templates/index.rst +0 -0
  63. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/narr/templates/lookup.rst +0 -0
  64. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/docs/narr/templates/overview.rst +0 -0
  65. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/__init__.py +0 -0
  66. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/_version.py +0 -0
  67. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/app.py +0 -0
  68. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/auth.py +0 -0
  69. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/cli/__init__.py +0 -0
  70. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/cli/webapp.py +0 -0
  71. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/conf.py +0 -0
  72. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/db/__init__.py +0 -0
  73. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/db/continuum.py +0 -0
  74. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/db/sess.py +0 -0
  75. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  76. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  77. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/emails.py +0 -0
  78. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/forms/__init__.py +0 -0
  79. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/forms/schema.py +0 -0
  80. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/forms/widgets.py +0 -0
  81. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/grids/__init__.py +0 -0
  82. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/grids/base.py +0 -0
  83. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/grids/filters.py +0 -0
  84. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/handler.py +0 -0
  85. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/helpers.py +0 -0
  86. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/menus.py +0 -0
  87. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/progress.py +0 -0
  88. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/static/__init__.py +0 -0
  89. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  90. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/static/img/logo.png +0 -0
  91. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/static/img/testing.png +0 -0
  92. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/subscribers.py +0 -0
  93. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  94. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  95. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  96. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  97. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/base.mako +0 -0
  98. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  99. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/batch/view.mako +0 -0
  100. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/configure.mako +0 -0
  101. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  102. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  103. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  104. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  105. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  106. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  107. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  108. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  109. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  110. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  111. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  112. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  113. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  114. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  115. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  116. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  117. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  118. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  119. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  120. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/forbidden.mako +0 -0
  121. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/form.mako +0 -0
  122. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  123. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/home.mako +0 -0
  124. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  125. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/create.mako +0 -0
  126. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  127. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  128. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/form.mako +0 -0
  129. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/index.mako +0 -0
  130. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/master/view.mako +0 -0
  131. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/notfound.mako +0 -0
  132. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/page.mako +0 -0
  133. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  134. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/progress.mako +0 -0
  135. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/setup.mako +0 -0
  136. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/upgrade.mako +0 -0
  137. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  138. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  139. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/templates/wutta-components.mako +0 -0
  140. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/testing.py +0 -0
  141. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/util.py +0 -0
  142. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/__init__.py +0 -0
  143. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/auth.py +0 -0
  144. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/base.py +0 -0
  145. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/batch.py +0 -0
  146. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/common.py +0 -0
  147. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/email.py +0 -0
  148. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/essential.py +0 -0
  149. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/people.py +0 -0
  150. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/progress.py +0 -0
  151. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/roles.py +0 -0
  152. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/settings.py +0 -0
  153. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/upgrades.py +0 -0
  154. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/src/wuttaweb/views/users.py +0 -0
  155. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tasks.py +0 -0
  156. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/__init__.py +0 -0
  157. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/cli/__init__.py +0 -0
  158. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/cli/test_webapp.py +0 -0
  159. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/db/__init__.py +0 -0
  160. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/db/test_continuum.py +0 -0
  161. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/forms/test_base.py +0 -0
  162. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/forms/test_schema.py +0 -0
  163. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/forms/test_widgets.py +0 -0
  164. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/grids/__init__.py +0 -0
  165. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/grids/test_base.py +0 -0
  166. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/grids/test_filters.py +0 -0
  167. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  168. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  169. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_oruga.js +0 -0
  170. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  171. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  172. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_vue.js +0 -0
  173. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  174. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/buefy.css +0 -0
  175. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/buefy.js +0 -0
  176. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/fontawesome.js +0 -0
  177. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/vue.js +0 -0
  178. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/libcache/vue_resource.js +0 -0
  179. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_app.py +0 -0
  180. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_auth.py +0 -0
  181. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_emails.py +0 -0
  182. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_handler.py +0 -0
  183. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_helpers.py +0 -0
  184. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_menus.py +0 -0
  185. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_progress.py +0 -0
  186. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_static.py +0 -0
  187. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_subscribers.py +0 -0
  188. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/test_util.py +0 -0
  189. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/util.py +0 -0
  190. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/__init__.py +0 -0
  191. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test___init__.py +0 -0
  192. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_auth.py +0 -0
  193. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_base.py +0 -0
  194. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_batch.py +0 -0
  195. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_common.py +0 -0
  196. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_email.py +0 -0
  197. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_essential.py +0 -0
  198. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_master.py +0 -0
  199. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_people.py +0 -0
  200. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_progress.py +0 -0
  201. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_roles.py +0 -0
  202. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_settings.py +0 -0
  203. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_upgrades.py +0 -0
  204. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tests/views/test_users.py +0 -0
  205. {wuttaweb-0.19.3 → wuttaweb-0.20.0}/tox.ini +0 -0
@@ -5,6 +5,18 @@ 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.20.0 (2025-01-11)
9
+
10
+ ### Feat
11
+
12
+ - add basic views for Reports
13
+
14
+ ### Fix
15
+
16
+ - add `action_method` and `reset_url` params for Form class
17
+ - add placeholder when grid has no filters
18
+ - add `get_page_templates()` method for master view
19
+
8
20
  ## v0.19.3 (2025-01-09)
9
21
 
10
22
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.19.3
3
+ Version: 0.20.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
@@ -39,7 +39,7 @@ Requires-Dist: pyramid-tm
39
39
  Requires-Dist: pyramid>=2
40
40
  Requires-Dist: waitress
41
41
  Requires-Dist: webhelpers2
42
- Requires-Dist: wuttjamaican[db]>=0.19.3
42
+ Requires-Dist: wuttjamaican[db]>=0.20.0
43
43
  Requires-Dist: zope-sqlalchemy>=1.5
44
44
  Provides-Extra: continuum
45
45
  Requires-Dist: wutta-continuum; extra == 'continuum'
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.reports``
3
+ ==========================
4
+
5
+ .. automodule:: wuttaweb.views.reports
6
+ :members:
@@ -60,6 +60,7 @@ the narrative docs are pretty scant. That will eventually change.
60
60
  api/wuttaweb.views.master
61
61
  api/wuttaweb.views.people
62
62
  api/wuttaweb.views.progress
63
+ api/wuttaweb.views.reports
63
64
  api/wuttaweb.views.roles
64
65
  api/wuttaweb.views.settings
65
66
  api/wuttaweb.views.upgrades
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.19.3"
9
+ version = "0.20.0"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -44,7 +44,7 @@ dependencies = [
44
44
  "pyramid_tm",
45
45
  "waitress",
46
46
  "WebHelpers2",
47
- "WuttJamaican[db]>=0.19.3",
47
+ "WuttJamaican[db]>=0.20.0",
48
48
  "zope.sqlalchemy>=1.5",
49
49
  ]
50
50
 
@@ -161,10 +161,23 @@ class Form:
161
161
 
162
162
  See also :meth:`set_required()` and :meth:`is_required()`.
163
163
 
164
+ .. attribute:: action_method
165
+
166
+ HTTP method to use when submitting form; ``'post'`` is default.
167
+
164
168
  .. attribute:: action_url
165
169
 
166
170
  String URL to which the form should be submitted, if applicable.
167
171
 
172
+ .. attribute:: reset_url
173
+
174
+ String URL to which the reset button should "always" redirect,
175
+ if applicable.
176
+
177
+ This is null by default, in which case it will use standard
178
+ browser behavior for the form reset button (if shown). See
179
+ also :attr:`show_button_reset`.
180
+
168
181
  .. attribute:: cancel_url
169
182
 
170
183
  String URL to which the Cancel button should "always" redirect,
@@ -227,6 +240,9 @@ class Form:
227
240
  Flag indicating whether a Reset button should be shown.
228
241
  Default is ``False``.
229
242
 
243
+ Unless there is a :attr:`reset_url`, the reset button will use
244
+ standard behavior per the browser.
245
+
230
246
  .. attribute:: show_button_cancel
231
247
 
232
248
  Flag indicating whether a Cancel button should be shown.
@@ -266,7 +282,9 @@ class Form:
266
282
  readonly_fields=[],
267
283
  required_fields={},
268
284
  labels={},
285
+ action_method='post',
269
286
  action_url=None,
287
+ reset_url=None,
270
288
  cancel_url=None,
271
289
  cancel_url_fallback=None,
272
290
  vue_tagname='wutta-form',
@@ -290,9 +308,11 @@ class Form:
290
308
  self.readonly_fields = set(readonly_fields or [])
291
309
  self.required_fields = required_fields or {}
292
310
  self.labels = labels or {}
311
+ self.action_method = action_method
293
312
  self.action_url = action_url
294
313
  self.cancel_url = cancel_url
295
314
  self.cancel_url_fallback = cancel_url_fallback
315
+ self.reset_url = reset_url
296
316
  self.vue_tagname = vue_tagname
297
317
  self.align_buttons_right = align_buttons_right
298
318
  self.auto_disable_submit = auto_disable_submit
@@ -940,10 +960,15 @@ class Form:
940
960
  """
941
961
  context['form'] = self
942
962
  context['dform'] = self.get_deform()
943
- context.setdefault('form_attrs', {})
944
963
  context.setdefault('request', self.request)
945
964
  context['model_data'] = self.get_vue_model_data()
946
965
 
966
+ # set form method, enctype
967
+ context.setdefault('form_attrs', {})
968
+ context['form_attrs'].setdefault('method', self.action_method)
969
+ if self.action_method == 'post':
970
+ context['form_attrs'].setdefault('enctype', 'multipart/form-data')
971
+
947
972
  # auto disable button on submit
948
973
  if self.auto_disable_submit:
949
974
  context['form_attrs']['@submit'] = 'formSubmitting = true'
@@ -1,8 +1,10 @@
1
1
  ## -*- coding: utf-8; -*-
2
2
 
3
3
  <script type="text/x-template" id="${form.vue_tagname}-template">
4
- ${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
5
- ${h.csrf_token(request)}
4
+ ${h.form(form.action_url, **form_attrs)}
5
+ % if form.action_method == 'post':
6
+ ${h.csrf_token(request)}
7
+ % endif
6
8
 
7
9
  % if form.has_global_errors():
8
10
  % for msg in form.get_global_errors():
@@ -33,7 +35,13 @@
33
35
  % endif
34
36
 
35
37
  % if form.show_button_reset:
36
- <b-button native-type="reset">
38
+ <b-button
39
+ % if form.reset_url:
40
+ tag="a" href="${form.reset_url}"
41
+ % else:
42
+ native-type="reset"
43
+ % endif
44
+ >
37
45
  Reset
38
46
  </b-button>
39
47
  % endif
@@ -89,6 +89,9 @@
89
89
 
90
90
  </div>
91
91
  </form>
92
+
93
+ % else:
94
+ <div></div>
92
95
  % endif
93
96
 
94
97
  <div style="display: flex; flex-direction: column; justify-content: space-between;">
@@ -0,0 +1,61 @@
1
+ ## -*- coding: utf-8; mode: html; -*-
2
+ <%inherit file="/master/view.mako" />
3
+
4
+ <%def name="page_layout()">
5
+ ${parent.page_layout()}
6
+ % if report_data is not Undefined:
7
+ <br />
8
+ <a name="report-output"></a>
9
+ <div style="display: flex; justify-content: space-between;">
10
+ <div class="report-header">
11
+ ${self.report_output_header()}
12
+ </div>
13
+ <div class="report-tools">
14
+ ${self.report_tools()}
15
+ </div>
16
+ </div>
17
+ ${self.report_output_body()}
18
+ % endif
19
+ </%def>
20
+
21
+ <%def name="report_output_header()">
22
+ <h4 class="is-size-4"><a href="#report-output">{{ reportData.output_title }}</a></h4>
23
+ </%def>
24
+
25
+ <%def name="report_tools()"></%def>
26
+
27
+ <%def name="report_output_body()">
28
+ ${self.report_output_table()}
29
+ </%def>
30
+
31
+ <%def name="report_output_table()">
32
+ <b-table :data="reportData.data"
33
+ narrowed
34
+ hoverable>
35
+ % for column in report_columns:
36
+ <b-table-column field="${column['name']}"
37
+ label="${column['label']}"
38
+ % if column.get('numeric'):
39
+ numeric
40
+ % endif
41
+ v-slot="props">
42
+ <span v-html="props.row.${column['name']}"></span>
43
+ </b-table-column>
44
+ % endfor
45
+ </b-table>
46
+ </%def>
47
+
48
+ <%def name="modify_vue_vars()">
49
+ ${parent.modify_vue_vars()}
50
+ % if report_data is not Undefined:
51
+ <script>
52
+
53
+ ThisPageData.reportData = ${json.dumps(report_data)|n}
54
+
55
+ WholePageData.mountedHooks.push(function() {
56
+ location.href = '#report-output'
57
+ })
58
+
59
+ </script>
60
+ % endif
61
+ </%def>
@@ -1777,14 +1777,14 @@ class MasterView(View):
1777
1777
  context = self.get_template_context(context)
1778
1778
 
1779
1779
  # first try the template path most specific to this view
1780
- template_prefix = self.get_template_prefix()
1781
- mako_path = f'{template_prefix}/{template}.mako'
1780
+ page_templates = self.get_page_templates(template)
1781
+ mako_path = page_templates[0]
1782
1782
  try:
1783
1783
  return render_to_response(mako_path, context, request=self.request)
1784
1784
  except IOError:
1785
1785
 
1786
1786
  # failing that, try one or more fallback templates
1787
- for fallback in self.get_fallback_templates(template):
1787
+ for fallback in page_templates[1:]:
1788
1788
  try:
1789
1789
  return render_to_response(fallback, context, request=self.request)
1790
1790
  except IOError:
@@ -1815,21 +1815,51 @@ class MasterView(View):
1815
1815
  """
1816
1816
  return context
1817
1817
 
1818
+ def get_page_templates(self, template):
1819
+ """
1820
+ Returns a list of all templates which can be attempted, to
1821
+ render the current page. This is called by
1822
+ :meth:`render_to_response()`.
1823
+
1824
+ The list should be in order of preference, e.g. the first
1825
+ entry will be the most "specific" template, with subsequent
1826
+ entries becoming more generic.
1827
+
1828
+ In practice this method defines the first entry but calls
1829
+ :meth:`get_fallback_templates()` for the rest.
1830
+
1831
+ :param template: Base name for a template (without prefix), e.g.
1832
+ ``'view'``.
1833
+
1834
+ :returns: List of template paths to be tried, based on the
1835
+ specified template. For instance if ``template`` is
1836
+ ``'view'`` this will (by default) return::
1837
+
1838
+ [
1839
+ '/widgets/view.mako',
1840
+ '/master/view.mako',
1841
+ ]
1842
+
1843
+ """
1844
+ template_prefix = self.get_template_prefix()
1845
+ page_templates = [f'{template_prefix}/{template}.mako']
1846
+ page_templates.extend(self.get_fallback_templates(template))
1847
+ return page_templates
1848
+
1818
1849
  def get_fallback_templates(self, template):
1819
1850
  """
1820
1851
  Returns a list of "fallback" template paths which may be
1821
- attempted for rendering a view. This is used within
1822
- :meth:`render_to_response()` if the "first guess" template
1823
- file was not found.
1852
+ attempted for rendering the current page. See also
1853
+ :meth:`get_page_templates()`.
1824
1854
 
1825
1855
  :param template: Base name for a template (without prefix), e.g.
1826
- ``'custom'``.
1856
+ ``'view'``.
1827
1857
 
1828
- :returns: List of full template paths to be tried, based on
1829
- the specified template. For instance if ``template`` is
1830
- ``'custom'`` this will (by default) return::
1858
+ :returns: List of template paths to be tried, based on the
1859
+ specified template. For instance if ``template`` is
1860
+ ``'view'`` this will (by default) return::
1831
1861
 
1832
- ['/master/custom.mako']
1862
+ ['/master/view.mako']
1833
1863
  """
1834
1864
  return [f'/master/{template}.mako']
1835
1865
 
@@ -0,0 +1,266 @@
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
+ Report Views
25
+ """
26
+
27
+ import datetime
28
+ import logging
29
+ import os
30
+ import tempfile
31
+
32
+ import deform
33
+
34
+ from wuttaweb.views import MasterView
35
+
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+
40
+ class ReportView(MasterView):
41
+ """
42
+ Master view for :term:`reports <report>`; route prefix is
43
+ ``reports``.
44
+
45
+ Notable URLs provided by this class:
46
+
47
+ * ``/reports/``
48
+ * ``/reports/XXX``
49
+ """
50
+ model_title = "Report"
51
+ model_key = 'report_key'
52
+ filterable = False
53
+ sort_on_backend = False
54
+ creatable = False
55
+ editable = False
56
+ deletable = False
57
+ route_prefix = 'reports'
58
+ template_prefix = '/reports'
59
+
60
+ grid_columns = [
61
+ 'report_title',
62
+ 'help_text',
63
+ 'report_key',
64
+ ]
65
+
66
+ form_fields = [
67
+ 'help_text',
68
+ ]
69
+
70
+ def __init__(self, request, context=None):
71
+ super().__init__(request, context=context)
72
+ self.report_handler = self.app.get_report_handler()
73
+
74
+ def get_grid_data(self, columns=None, session=None):
75
+ """ """
76
+ data = []
77
+ for report in self.report_handler.get_reports().values():
78
+ data.append(self.normalize_report(report))
79
+ return data
80
+
81
+ def normalize_report(self, report):
82
+ """ """
83
+ return {
84
+ 'report_key': report.report_key,
85
+ 'report_title': report.report_title,
86
+ 'help_text': report.__doc__,
87
+ }
88
+
89
+ def configure_grid(self, g):
90
+ """ """
91
+ super().configure_grid(g)
92
+
93
+ # report_key
94
+ g.set_link('report_key')
95
+
96
+ # report_title
97
+ g.set_link('report_title')
98
+ g.set_searchable('report_title')
99
+
100
+ # help_text
101
+ g.set_searchable('help_text')
102
+
103
+ def get_instance(self):
104
+ """ """
105
+ key = self.request.matchdict['report_key']
106
+ report = self.report_handler.get_report(key)
107
+ if report:
108
+ return self.normalize_report(report)
109
+
110
+ raise self.notfound()
111
+
112
+ def get_instance_title(self, report):
113
+ """ """
114
+ return report['report_title']
115
+
116
+ def view(self):
117
+ """
118
+ This lets user "view" the report but in this context that
119
+ means showing them a form with report params, so they can run
120
+ it.
121
+ """
122
+ key = self.request.matchdict['report_key']
123
+ report = self.report_handler.get_report(key)
124
+ normal = self.normalize_report(report)
125
+
126
+ report_url = self.get_action_url('view', normal)
127
+ form = self.make_model_form(normal,
128
+ action_method='get',
129
+ action_url=report_url,
130
+ cancel_url=self.get_index_url(),
131
+ show_button_reset=True,
132
+ reset_url=report_url,
133
+ button_label_submit="Run Report",
134
+ button_icon_submit='arrow-circle-right')
135
+
136
+ context = {
137
+ 'instance': normal,
138
+ 'report': report,
139
+ 'form': form,
140
+ 'xref_buttons': self.get_xref_buttons(report),
141
+ }
142
+
143
+ if self.request.GET:
144
+ form.show_button_cancel = False
145
+ context = self.run_report(report, context)
146
+
147
+ return self.render_to_response('view', context)
148
+
149
+ def configure_form(self, f):
150
+ """ """
151
+ super().configure_form(f)
152
+ key = self.request.matchdict['report_key']
153
+ report = self.report_handler.get_report(key)
154
+
155
+ # help_text
156
+ f.set_readonly('help_text')
157
+
158
+ # add widget fields for all report params
159
+ schema = f.get_schema()
160
+ report.add_params(schema)
161
+ f.set_fields([node.name for node in schema.children])
162
+
163
+ def run_report(self, report, context):
164
+ """
165
+ Run the given report and update view template context.
166
+
167
+ This is called automatically from :meth:`view()`.
168
+
169
+ :param report:
170
+ :class:`~wuttjamaican:wuttjamaican.reports.Report` instance
171
+ to run.
172
+
173
+ :param context: Current view template context.
174
+
175
+ :returns: Final view template context.
176
+ """
177
+ form = context['form']
178
+ controls = list(self.request.GET.items())
179
+
180
+ # TODO: must re-inject help_text value for some reason,
181
+ # otherwise its absence screws things up. why?
182
+ controls.append(('help_text', report.__doc__))
183
+
184
+ dform = form.get_deform()
185
+ try:
186
+ params = dform.validate(controls)
187
+ except deform.ValidationFailure:
188
+ log.debug("form not valid: %s", dform.error)
189
+ return context
190
+
191
+ data = self.report_handler.make_report_data(report, params)
192
+
193
+ columns = self.normalize_columns(report.get_output_columns())
194
+ context['report_columns'] = columns
195
+
196
+ format_cols = [col for col in columns if col.get('formatter')]
197
+ if format_cols:
198
+ for record in data['data']:
199
+ for column in format_cols:
200
+ if column['name'] in record:
201
+ value = record[column['name']]
202
+ record[column['name']] = column['formatter'](value)
203
+
204
+ params.pop('help_text')
205
+ context['report_params'] = params
206
+ context['report_data'] = data
207
+ context['report_generated'] = datetime.datetime.now()
208
+ return context
209
+
210
+ def normalize_columns(self, columns):
211
+ """ """
212
+ normal = []
213
+ for column in columns:
214
+ if isinstance(column, str):
215
+ column = {'name': column}
216
+ column.setdefault('label', column['name'])
217
+ normal.append(column)
218
+ return normal
219
+
220
+ def get_download_data(self):
221
+ """ """
222
+ key = self.request.matchdict['report_key']
223
+ report = self.report_handler.get_report(key)
224
+ params = dict(self.request.GET)
225
+ columns = self.normalize_columns(report.get_output_columns())
226
+ data = self.report_handler.make_report_data(report, params)
227
+ return params, columns, data
228
+
229
+ def get_download_path(self, data, ext):
230
+ """ """
231
+ tempdir = tempfile.mkdtemp()
232
+ filename = f"{data['output_title']}.{ext}"
233
+ return os.path.join(tempdir, filename)
234
+
235
+ @classmethod
236
+ def defaults(cls, config):
237
+ """ """
238
+ cls._defaults(config)
239
+ cls._report_defaults(config)
240
+
241
+ @classmethod
242
+ def _report_defaults(cls, config):
243
+ permission_prefix = cls.get_permission_prefix()
244
+ model_title = cls.get_model_title()
245
+
246
+ # overwrite title for "view" perm since it also implies "run"
247
+ config.add_wutta_permission(permission_prefix,
248
+ f'{permission_prefix}.view',
249
+ f"View / run {model_title}")
250
+
251
+
252
+ # separate permission to download report files
253
+ config.add_wutta_permission(permission_prefix,
254
+ f'{permission_prefix}.download',
255
+ f"Download {model_title}")
256
+
257
+
258
+ def defaults(config, **kwargs):
259
+ base = globals()
260
+
261
+ ReportView = kwargs.get('ReportView', base['ReportView'])
262
+ ReportView.defaults(config)
263
+
264
+
265
+ def includeme(config):
266
+ defaults(config)