WuttaWeb 0.20.0__tar.gz → 0.20.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 (206) hide show
  1. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/CHANGELOG.md +18 -0
  2. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/PKG-INFO +2 -2
  3. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/pyproject.toml +5 -2
  4. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/grids/base.py +33 -5
  5. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/grids/filters.py +29 -0
  6. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/handler.py +75 -7
  7. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/appinfo/configure.mako +17 -0
  8. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/base.mako +1 -1
  9. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/grids/vue_template.mako +3 -0
  10. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/master.py +40 -12
  11. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/settings.py +12 -4
  12. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/grids/test_base.py +27 -1
  13. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/grids/test_filters.py +24 -0
  14. wuttaweb-0.20.2/tests/test_handler.py +149 -0
  15. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_master.py +65 -34
  16. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_reports.py +7 -2
  17. wuttaweb-0.20.0/tests/test_handler.py +0 -76
  18. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/.gitignore +0 -0
  19. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/COPYING.txt +0 -0
  20. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/README.md +0 -0
  21. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/Makefile +0 -0
  22. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/_static/.keepme +0 -0
  23. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.app.rst +0 -0
  24. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.auth.rst +0 -0
  25. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.cli.rst +0 -0
  26. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  27. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.conf.rst +0 -0
  28. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.db.continuum.rst +0 -0
  29. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.db.rst +0 -0
  30. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.db.sess.rst +0 -0
  31. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.emails.rst +0 -0
  32. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.base.rst +0 -0
  33. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.rst +0 -0
  34. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.schema.rst +0 -0
  35. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  36. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.grids.base.rst +0 -0
  37. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.grids.filters.rst +0 -0
  38. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.grids.rst +0 -0
  39. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.handler.rst +0 -0
  40. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.helpers.rst +0 -0
  41. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.menus.rst +0 -0
  42. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.progress.rst +0 -0
  43. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.rst +0 -0
  44. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.static.rst +0 -0
  45. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.subscribers.rst +0 -0
  46. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.util.rst +0 -0
  47. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.auth.rst +0 -0
  48. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.base.rst +0 -0
  49. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.batch.rst +0 -0
  50. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.common.rst +0 -0
  51. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.email.rst +0 -0
  52. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.essential.rst +0 -0
  53. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.master.rst +0 -0
  54. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.people.rst +0 -0
  55. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.progress.rst +0 -0
  56. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.reports.rst +0 -0
  57. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.roles.rst +0 -0
  58. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.rst +0 -0
  59. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.settings.rst +0 -0
  60. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  61. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.users.rst +0 -0
  62. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/conf.py +0 -0
  63. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/glossary.rst +0 -0
  64. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/index.rst +0 -0
  65. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/make.bat +0 -0
  66. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/cli/builtin.rst +0 -0
  67. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/cli/index.rst +0 -0
  68. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/base.rst +0 -0
  69. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/index.rst +0 -0
  70. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/lookup.rst +0 -0
  71. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/overview.rst +0 -0
  72. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/__init__.py +0 -0
  73. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/_version.py +0 -0
  74. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/app.py +0 -0
  75. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/auth.py +0 -0
  76. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/cli/__init__.py +0 -0
  77. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/cli/webapp.py +0 -0
  78. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/conf.py +0 -0
  79. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/db/__init__.py +0 -0
  80. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/db/continuum.py +0 -0
  81. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/db/sess.py +0 -0
  82. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  83. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  84. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/emails.py +0 -0
  85. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/__init__.py +0 -0
  86. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/base.py +0 -0
  87. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/schema.py +0 -0
  88. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/widgets.py +0 -0
  89. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/grids/__init__.py +0 -0
  90. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/helpers.py +0 -0
  91. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/menus.py +0 -0
  92. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/progress.py +0 -0
  93. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/__init__.py +0 -0
  94. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/img/favicon.ico +0 -0
  95. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/img/logo.png +0 -0
  96. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/img/testing.png +0 -0
  97. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/subscribers.py +0 -0
  98. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  99. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  100. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/auth/login.mako +0 -0
  101. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/base_meta.mako +0 -0
  102. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/batch/view.mako +0 -0
  103. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/configure.mako +0 -0
  104. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  105. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  106. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  107. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  108. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  109. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  110. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/password.pt +0 -0
  111. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  112. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  113. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  114. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  115. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  116. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  117. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  118. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  119. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/select.pt +0 -0
  120. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  121. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  122. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  123. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/forbidden.mako +0 -0
  124. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/form.mako +0 -0
  125. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  126. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  127. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/home.mako +0 -0
  128. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/configure.mako +0 -0
  129. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/create.mako +0 -0
  130. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/delete.mako +0 -0
  131. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/edit.mako +0 -0
  132. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/form.mako +0 -0
  133. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/index.mako +0 -0
  134. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/view.mako +0 -0
  135. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/notfound.mako +0 -0
  136. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/page.mako +0 -0
  137. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  138. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/progress.mako +0 -0
  139. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/reports/view.mako +0 -0
  140. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/setup.mako +0 -0
  141. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/upgrade.mako +0 -0
  142. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  143. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  144. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/wutta-components.mako +0 -0
  145. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/testing.py +0 -0
  146. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/util.py +0 -0
  147. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/__init__.py +0 -0
  148. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/auth.py +0 -0
  149. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/base.py +0 -0
  150. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/batch.py +0 -0
  151. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/common.py +0 -0
  152. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/email.py +0 -0
  153. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/essential.py +0 -0
  154. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/people.py +0 -0
  155. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/progress.py +0 -0
  156. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/reports.py +0 -0
  157. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/roles.py +0 -0
  158. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/upgrades.py +0 -0
  159. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/users.py +0 -0
  160. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tasks.py +0 -0
  161. {wuttaweb-0.20.0/tests/views → wuttaweb-0.20.2/tests}/__init__.py +0 -0
  162. {wuttaweb-0.20.0/tests/grids → wuttaweb-0.20.2/tests/cli}/__init__.py +0 -0
  163. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/cli/test_webapp.py +0 -0
  164. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/db/__init__.py +0 -0
  165. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/db/test_continuum.py +0 -0
  166. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/forms/test_base.py +0 -0
  167. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/forms/test_schema.py +0 -0
  168. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/forms/test_widgets.py +0 -0
  169. {wuttaweb-0.20.0/tests/cli → wuttaweb-0.20.2/tests/grids}/__init__.py +0 -0
  170. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  171. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  172. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_oruga.js +0 -0
  173. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_oruga_bulma.css +0 -0
  174. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_oruga_bulma.js +0 -0
  175. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_vue.js +0 -0
  176. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_vue_fontawesome.js +0 -0
  177. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/buefy.css +0 -0
  178. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/buefy.js +0 -0
  179. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/fontawesome.js +0 -0
  180. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/vue.js +0 -0
  181. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/vue_resource.js +0 -0
  182. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_app.py +0 -0
  183. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_auth.py +0 -0
  184. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_emails.py +0 -0
  185. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_helpers.py +0 -0
  186. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_menus.py +0 -0
  187. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_progress.py +0 -0
  188. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_static.py +0 -0
  189. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_subscribers.py +0 -0
  190. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_util.py +0 -0
  191. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/util.py +0 -0
  192. {wuttaweb-0.20.0/tests → wuttaweb-0.20.2/tests/views}/__init__.py +0 -0
  193. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test___init__.py +0 -0
  194. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_auth.py +0 -0
  195. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_base.py +0 -0
  196. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_batch.py +0 -0
  197. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_common.py +0 -0
  198. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_email.py +0 -0
  199. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_essential.py +0 -0
  200. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_people.py +0 -0
  201. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_progress.py +0 -0
  202. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_roles.py +0 -0
  203. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_settings.py +0 -0
  204. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_upgrades.py +0 -0
  205. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_users.py +0 -0
  206. {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tox.ini +0 -0
@@ -5,6 +5,24 @@ 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.2 (2025-01-14)
9
+
10
+ ### Fix
11
+
12
+ - improve support for composite `model_key` in MasterView
13
+ - let content header text be a bit longer
14
+ - add optional `target` attr for GridAction
15
+ - add `render_date()` method for grids
16
+
17
+ ## v0.20.1 (2025-01-13)
18
+
19
+ ### Fix
20
+
21
+ - expose setting to choose menu handler, in appinfo/configure
22
+ - use prop key instead of column name, for master view model key
23
+ - add grid filters specific to numeric, integer types
24
+ - use default value for config settings
25
+
8
26
  ## v0.20.0 (2025-01-11)
9
27
 
10
28
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.20.0
3
+ Version: 0.20.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
@@ -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.20.0
42
+ Requires-Dist: wuttjamaican[db]>=0.20.1
43
43
  Requires-Dist: zope-sqlalchemy>=1.5
44
44
  Provides-Extra: continuum
45
45
  Requires-Dist: wutta-continuum; extra == 'continuum'
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.20.0"
9
+ version = "0.20.2"
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.20.0",
47
+ "WuttJamaican[db]>=0.20.1",
48
48
  "zope.sqlalchemy>=1.5",
49
49
  ]
50
50
 
@@ -70,6 +70,9 @@ wuttaweb = "wuttaweb.conf:WuttaWebConfigExtension"
70
70
  [project.entry-points."wutta.typer_imports"]
71
71
  wuttaweb = "wuttaweb.cli"
72
72
 
73
+ [project.entry-points."wutta.web.menus"]
74
+ wuttaweb = "wuttaweb.menus:MenuHandler"
75
+
73
76
 
74
77
  [project.urls]
75
78
  Homepage = "https://wuttaproject.org/"
@@ -602,6 +602,7 @@ class Grid:
602
602
  * ``'batch_id'`` -> :meth:`render_batch_id()`
603
603
  * ``'boolean'`` -> :meth:`render_boolean()`
604
604
  * ``'currency'`` -> :meth:`render_currency()`
605
+ * ``'date'`` -> :meth:`render_date()`
605
606
  * ``'datetime'`` -> :meth:`render_datetime()`
606
607
  * ``'quantity'`` -> :meth:`render_quantity()`
607
608
 
@@ -611,6 +612,7 @@ class Grid:
611
612
  'batch_id': self.render_batch_id,
612
613
  'boolean': self.render_boolean,
613
614
  'currency': self.render_currency,
615
+ 'date': self.render_date,
614
616
  'datetime': self.render_datetime,
615
617
  'quantity': self.render_quantity,
616
618
  }
@@ -631,11 +633,13 @@ class Grid:
631
633
  data type implies a default renderer. This is only possible
632
634
  if :attr:`model_class` is set to a SQLAlchemy mapped class.
633
635
 
634
- This only looks for a couple of data types, and configures as
636
+ This only looks for a few data types, and configures as
635
637
  follows:
636
638
 
637
639
  * :class:`sqlalchemy:sqlalchemy.types.Boolean` ->
638
640
  :meth:`render_boolean()`
641
+ * :class:`sqlalchemy:sqlalchemy.types.Date` ->
642
+ :meth:`render_date()`
639
643
  * :class:`sqlalchemy:sqlalchemy.types.DateTime` ->
640
644
  :meth:`render_datetime()`
641
645
  """
@@ -651,7 +655,9 @@ class Grid:
651
655
  prop = getattr(attr, 'prop', None)
652
656
  if prop and isinstance(prop, orm.ColumnProperty):
653
657
  column = prop.columns[0]
654
- if isinstance(column.type, sa.DateTime):
658
+ if isinstance(column.type, sa.Date):
659
+ self.set_renderer(key, self.render_date)
660
+ elif isinstance(column.type, sa.DateTime):
655
661
  self.set_renderer(key, self.render_datetime)
656
662
  elif isinstance(column.type, sa.Boolean):
657
663
  self.set_renderer(key, self.render_boolean)
@@ -1820,7 +1826,7 @@ class Grid:
1820
1826
  This may be used automatically per
1821
1827
  :meth:`set_default_renderers()` or you can use it explicitly::
1822
1828
 
1823
- grid.set_renderer('foo', grid.render_boolean)
1829
+ grid.set_renderer('foo', 'boolean')
1824
1830
  """
1825
1831
  return self.app.render_boolean(value)
1826
1832
 
@@ -1839,6 +1845,22 @@ class Grid:
1839
1845
  """
1840
1846
  return self.app.render_currency(value, **kwargs)
1841
1847
 
1848
+ def render_date(self, obj, key, value):
1849
+ """
1850
+ Column renderer for :class:`python:datetime.date` values.
1851
+
1852
+ This calls
1853
+ :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_date()`
1854
+ for the return value.
1855
+
1856
+ This may be used automatically per
1857
+ :meth:`set_default_renderers()` or you can use it explicitly::
1858
+
1859
+ grid.set_renderer('foo', 'date')
1860
+ """
1861
+ dt = getattr(obj, key)
1862
+ return self.app.render_date(dt)
1863
+
1842
1864
  def render_datetime(self, obj, key, value):
1843
1865
  """
1844
1866
  Column renderer for :class:`python:datetime.datetime` values.
@@ -1850,7 +1872,7 @@ class Grid:
1850
1872
  This may be used automatically per
1851
1873
  :meth:`set_default_renderers()` or you can use it explicitly::
1852
1874
 
1853
- grid.set_renderer('foo', grid.render_datetime)
1875
+ grid.set_renderer('foo', 'datetime')
1854
1876
  """
1855
1877
  dt = getattr(obj, key)
1856
1878
  return self.app.render_datetime(dt)
@@ -1865,7 +1887,7 @@ class Grid:
1865
1887
 
1866
1888
  This is not used automatically but you can use it explicitly::
1867
1889
 
1868
- grid.set_renderer('foo', grid.render_quantity)
1890
+ grid.set_renderer('foo', 'quantity')
1869
1891
  """
1870
1892
  return self.app.render_quantity(value)
1871
1893
 
@@ -2258,6 +2280,10 @@ class GridAction:
2258
2280
 
2259
2281
  See also :meth:`get_url()`.
2260
2282
 
2283
+ .. attribute:: target
2284
+
2285
+ Optional ``target`` attribute for the ``<a>`` tag.
2286
+
2261
2287
  .. attribute:: icon
2262
2288
 
2263
2289
  Name of icon to be shown for the action link.
@@ -2275,6 +2301,7 @@ class GridAction:
2275
2301
  key,
2276
2302
  label=None,
2277
2303
  url=None,
2304
+ target=None,
2278
2305
  icon=None,
2279
2306
  link_class=None,
2280
2307
  ):
@@ -2283,6 +2310,7 @@ class GridAction:
2283
2310
  self.app = self.config.get_app()
2284
2311
  self.key = key
2285
2312
  self.url = url
2313
+ self.target = target
2286
2314
  self.label = label or self.app.make_title(key)
2287
2315
  self.icon = icon or key
2288
2316
  self.link_class = link_class or ''
@@ -465,6 +465,33 @@ class StringAlchemyFilter(AlchemyFilter):
465
465
  sa.and_(*criteria)))
466
466
 
467
467
 
468
+ class NumericAlchemyFilter(AlchemyFilter):
469
+ """
470
+ SQLAlchemy filter option for a numeric data column.
471
+
472
+ Subclass of :class:`AlchemyFilter`.
473
+ """
474
+ default_verbs = ['equal', 'not_equal',
475
+ 'greater_than', 'greater_equal',
476
+ 'less_than', 'less_equal']
477
+
478
+
479
+ class IntegerAlchemyFilter(NumericAlchemyFilter):
480
+ """
481
+ SQLAlchemy filter option for an integer data column.
482
+
483
+ Subclass of :class:`NumericAlchemyFilter`.
484
+ """
485
+
486
+ def coerce_value(self, value):
487
+ """ """
488
+ if value:
489
+ try:
490
+ return int(value)
491
+ except:
492
+ pass
493
+
494
+
468
495
  class BooleanAlchemyFilter(AlchemyFilter):
469
496
  """
470
497
  SQLAlchemy filter option for a boolean data column.
@@ -568,6 +595,8 @@ default_sqlalchemy_filters = {
568
595
  None: AlchemyFilter,
569
596
  sa.String: StringAlchemyFilter,
570
597
  sa.Text: StringAlchemyFilter,
598
+ sa.Numeric: NumericAlchemyFilter,
599
+ sa.Integer: IntegerAlchemyFilter,
571
600
  sa.Boolean: BooleanAlchemyFilter,
572
601
  sa.Date: DateAlchemyFilter,
573
602
  }
@@ -24,7 +24,10 @@
24
24
  Web Handler
25
25
  """
26
26
 
27
+ import warnings
28
+
27
29
  from wuttjamaican.app import GenericHandler
30
+ from wuttjamaican.util import load_entry_points
28
31
 
29
32
  from wuttaweb import static, forms, grids
30
33
 
@@ -106,22 +109,87 @@ class WebHandler(GenericHandler):
106
109
 
107
110
  def get_menu_handler(self, **kwargs):
108
111
  """
109
- Get the configured "menu" handler for the web app.
112
+ Get the configured :term:`menu handler` for the web app.
110
113
 
111
114
  Specify a custom handler in your config file like this:
112
115
 
113
116
  .. code-block:: ini
114
117
 
115
118
  [wutta.web]
116
- menus.handler_spec = poser.web.menus:PoserMenuHandler
119
+ menus.handler.spec = poser.web.menus:PoserMenuHandler
117
120
 
118
121
  :returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
119
122
  """
120
- if not hasattr(self, 'menu_handler'):
121
- spec = self.config.get(f'{self.appname}.web.menus.handler_spec',
122
- default='wuttaweb.menus:MenuHandler')
123
- self.menu_handler = self.app.load_object(spec)(self.config)
124
- return self.menu_handler
123
+ spec = self.config.get(f'{self.appname}.web.menus.handler.spec')
124
+ if not spec:
125
+ spec = self.config.get(f'{self.appname}.web.menus.handler_spec')
126
+ if spec:
127
+ warnings.warn(f"setting '{self.appname}.web.menus.handler_spec' is deprecated; "
128
+ f"please use '{self.appname}.web.menus.handler_spec' instead",
129
+ DeprecationWarning)
130
+ else:
131
+ spec = self.config.get(f'{self.appname}.web.menus.handler.default_spec',
132
+ default='wuttaweb.menus:MenuHandler')
133
+ factory = self.app.load_object(spec)
134
+ return factory(self.config)
135
+
136
+ def get_menu_handler_specs(self, default=None):
137
+ """
138
+ Get the :term:`spec` strings for all available :term:`menu
139
+ handlers <menu handler>`. See also
140
+ :meth:`get_menu_handler()`.
141
+
142
+ :param default: Default spec string(s) to include, even if not
143
+ registered. Can be a string or list of strings.
144
+
145
+ :returns: List of menu handler spec strings.
146
+
147
+ This will gather available spec strings from the following:
148
+
149
+ First, the ``default`` as provided by caller.
150
+
151
+ Second, the default spec from config, if set; for example:
152
+
153
+ .. code-block:: ini
154
+
155
+ [wutta.web]
156
+ menus.handler.default_spec = poser.web.menus:PoserMenuHandler
157
+
158
+ Third, each spec registered via entry points. For instance in
159
+ ``pyproject.toml``:
160
+
161
+ .. code-block:: toml
162
+
163
+ [project.entry-points."wutta.web.menus"]
164
+ poser = "poser.web.menus:PoserMenuHandler"
165
+
166
+ The final list will be "sorted" according to the above, with
167
+ the latter registered handlers being sorted alphabetically.
168
+ """
169
+ handlers = []
170
+
171
+ # defaults from caller
172
+ if isinstance(default, str):
173
+ handlers.append(default)
174
+ elif default:
175
+ handlers.extend(default)
176
+
177
+ # configured default, if applicable
178
+ default = self.config.get(f'{self.config.appname}.web.menus.handler.default_spec')
179
+ if default and default not in handlers:
180
+ handlers.append(default)
181
+
182
+ # registered via entry points
183
+ registered = []
184
+ for Handler in load_entry_points(f'{self.appname}.web.menus').values():
185
+ spec = Handler.get_spec()
186
+ if spec not in handlers:
187
+ registered.append(spec)
188
+ if registered:
189
+ registered.sort()
190
+ handlers.extend(registered)
191
+
192
+ return handlers
125
193
 
126
194
  def make_form(self, request, **kwargs):
127
195
  """
@@ -41,6 +41,21 @@
41
41
  </b-checkbox>
42
42
  </b-field>
43
43
 
44
+ <b-field label="Menu Handler">
45
+ <input type="hidden"
46
+ name="${app.appname}.web.menus.handler.spec"
47
+ :value="simpleSettings['${app.appname}.web.menus.handler.spec']" />
48
+ <b-select v-model="simpleSettings['${app.appname}.web.menus.handler.spec']"
49
+ @input="settingsNeedSaved = true">
50
+ <option :value="null">(use default)</option>
51
+ <option v-for="handler in menuHandlers"
52
+ :key="handler.spec"
53
+ :value="handler.spec">
54
+ {{ handler.spec }}
55
+ </option>
56
+ </b-select>
57
+ </b-field>
58
+
44
59
  </div>
45
60
 
46
61
  <h3 class="block is-size-3">User/Auth</h3>
@@ -241,6 +256,8 @@
241
256
  ${parent.modify_vue_vars()}
242
257
  <script>
243
258
 
259
+ ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
260
+
244
261
  ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
245
262
 
246
263
  ThisPageData.editWebLibraryShowDialog = false
@@ -155,7 +155,7 @@
155
155
  }
156
156
 
157
157
  #content-title h1 {
158
- max-width: 80%;
158
+ max-width: 85%;
159
159
  overflow: hidden;
160
160
  padding-left: 0.5rem;
161
161
  text-overflow: ellipsis;
@@ -180,6 +180,9 @@
180
180
  % for action in grid.actions:
181
181
  <a v-if="props.row._action_url_${action.key}"
182
182
  :href="props.row._action_url_${action.key}"
183
+ % if action.target:
184
+ target="${action.target}"
185
+ % endif
183
186
  class="${action.link_class}">
184
187
  ${action.render_icon_and_label()}
185
188
  </a>
@@ -1224,7 +1224,7 @@ class MasterView(View):
1224
1224
  elif simple.get('type') is bool:
1225
1225
  value = self.config.get_bool(name, default=simple.get('default', False))
1226
1226
  else:
1227
- value = self.config.get(name)
1227
+ value = self.config.get(name, default=simple.get('default'))
1228
1228
 
1229
1229
  normalized[name] = value
1230
1230
 
@@ -2176,6 +2176,28 @@ class MasterView(View):
2176
2176
  """
2177
2177
  return str(instance) or "(no title)"
2178
2178
 
2179
+ def get_action_route_kwargs(self, obj):
2180
+ """
2181
+ Get a dict of route kwargs for the given object.
2182
+
2183
+ This is called from :meth:`get_action_url()` and must return
2184
+ kwargs suitable for use with ``request.route_url()``.
2185
+
2186
+ In practice this should return a dict which has keys for each
2187
+ field from :meth:`get_model_key()` and values which come from
2188
+ the object.
2189
+
2190
+ :param obj: Model instance object.
2191
+
2192
+ :returns: The dict of route kwargs for the object.
2193
+ """
2194
+ try:
2195
+ return dict([(key, obj[key])
2196
+ for key in self.get_model_key()])
2197
+ except TypeError:
2198
+ return dict([(key, getattr(obj, key))
2199
+ for key in self.get_model_key()])
2200
+
2179
2201
  def get_action_url(self, action, obj, **kwargs):
2180
2202
  """
2181
2203
  Generate an "action" URL for the given model instance.
@@ -2183,22 +2205,21 @@ class MasterView(View):
2183
2205
  This is a shortcut which generates a route name based on
2184
2206
  :meth:`get_route_prefix()` and the ``action`` param.
2185
2207
 
2186
- It returns the URL based on generated route name and object's
2187
- model key values.
2208
+ It calls :meth:`get_action_route_kwargs()` and then passes
2209
+ those along with route name to ``request.route_url()``, and
2210
+ returns the result.
2188
2211
 
2189
2212
  :param action: String name for the action, which corresponds
2190
2213
  to part of some named route, e.g. ``'view'`` or ``'edit'``.
2191
2214
 
2192
2215
  :param obj: Model instance object.
2216
+
2217
+ :param \**kwargs: Additional kwargs to be passed to
2218
+ ``request.route_url()``, if needed.
2193
2219
  """
2194
- route_prefix = self.get_route_prefix()
2195
- try:
2196
- kw = dict([(key, obj[key])
2197
- for key in self.get_model_key()])
2198
- except TypeError:
2199
- kw = dict([(key, getattr(obj, key))
2200
- for key in self.get_model_key()])
2220
+ kw = self.get_action_route_kwargs(obj)
2201
2221
  kw.update(kwargs)
2222
+ route_prefix = self.get_route_prefix()
2202
2223
  return self.request.route_url(f'{route_prefix}.{action}', **kw)
2203
2224
 
2204
2225
  def get_action_url_view(self, obj, i):
@@ -2707,6 +2728,9 @@ class MasterView(View):
2707
2728
  represents a Wutta-based SQLAlchemy model, the return value
2708
2729
  for this method is: ``('uuid',)``
2709
2730
 
2731
+ Any class mapped via SQLAlchemy should be supported
2732
+ automatically, the keys are determined from class inspection.
2733
+
2710
2734
  But there is no "sane" default for other scenarios, in which
2711
2735
  case subclass should define :attr:`model_key`. If the model
2712
2736
  key cannot be determined, raises ``AttributeError``.
@@ -2721,8 +2745,12 @@ class MasterView(View):
2721
2745
 
2722
2746
  model_class = cls.get_model_class()
2723
2747
  if model_class:
2724
- mapper = sa.inspect(model_class)
2725
- return tuple([column.key for column in mapper.primary_key])
2748
+ # nb. we want the primary key but must avoid column names
2749
+ # in case mapped class uses different prop keys
2750
+ inspector = sa.inspect(model_class)
2751
+ keys = [col.name for col in inspector.primary_key]
2752
+ return tuple([prop.key for prop in inspector.column_attrs
2753
+ if all([col.name in keys for col in prop.columns])])
2726
2754
 
2727
2755
  raise AttributeError(f"you must define model_key for view class: {cls}")
2728
2756
 
@@ -129,6 +129,10 @@ class AppInfoView(MasterView):
129
129
  {'name': f'{self.config.appname}.node_title'},
130
130
  {'name': f'{self.config.appname}.production',
131
131
  'type': bool},
132
+ {'name': f'{self.config.appname}.web.menus.handler.spec'},
133
+ # nb. this is deprecated; we define so it is auto-deleted
134
+ # when we replace with newer setting
135
+ {'name': f'{self.config.appname}.web.menus.handler_spec'},
132
136
 
133
137
  # user/auth
134
138
  {'name': 'wuttaweb.home_redirect_to_login',
@@ -164,11 +168,15 @@ class AppInfoView(MasterView):
164
168
 
165
169
  def configure_get_context(self, **kwargs):
166
170
  """ """
167
-
168
- # normal context
169
171
  context = super().configure_get_context(**kwargs)
170
172
 
171
- # we will add `weblibs` to context, based on config values
173
+ # add registered menu handlers
174
+ web = self.app.get_web_handler()
175
+ handlers = web.get_menu_handler_specs()
176
+ handlers = [{'spec': spec} for spec in handlers]
177
+ context['menu_handlers'] = handlers
178
+
179
+ # add `weblibs` to context, based on config values
172
180
  weblibs = self.get_weblibs()
173
181
  for key in weblibs:
174
182
  title = weblibs[key]
@@ -192,8 +200,8 @@ class AppInfoView(MasterView):
192
200
  'live_url': get_liburl(self.request, key,
193
201
  prefix=self.weblib_config_prefix),
194
202
  }
195
-
196
203
  context['weblibs'] = list(weblibs.values())
204
+
197
205
  return context
198
206
 
199
207
 
@@ -213,7 +213,7 @@ class TestGrid(WebTestCase):
213
213
  obj = MagicMock(foo=42.00)
214
214
  self.assertEqual(grid.renderers['foo'](obj, 'foo', 42.00), '42')
215
215
 
216
- def test_set_default_renderer(self):
216
+ def test_set_default_renderers(self):
217
217
  model = self.app.model
218
218
 
219
219
  # no defaults for "plain" schema
@@ -249,6 +249,18 @@ class TestGrid(WebTestCase):
249
249
  self.assertIn('executing', grid.renderers)
250
250
  self.assertIs(grid.renderers['executing'], myrender)
251
251
 
252
+ # nb. as of writing we have no Date columns in default schema,
253
+ # so must invent one to test that type
254
+ class SomeFoolery(model.Base):
255
+ __tablename__ = 'somefoolery'
256
+ id = sa.Column(sa.Integer(), primary_key=True)
257
+ created = sa.Column(sa.Date())
258
+
259
+ # renderer set for date mapped field
260
+ grid = self.make_grid(model_class=SomeFoolery)
261
+ self.assertIn('created', grid.renderers)
262
+ self.assertIsNot(grid.renderers['created'], myrender)
263
+
252
264
  def test_linked_columns(self):
253
265
  grid = self.make_grid(columns=['foo', 'bar'])
254
266
  self.assertEqual(grid.linked_columns, [])
@@ -1415,6 +1427,20 @@ class TestGrid(WebTestCase):
1415
1427
  # zero is *not* empty string (with this renderer)
1416
1428
  self.assertEqual(grid.render_quantity(obj, 'foo', 0), "0")
1417
1429
 
1430
+ def test_render_date(self):
1431
+ grid = self.make_grid(columns=['foo', 'bar'])
1432
+
1433
+ # null
1434
+ obj = MagicMock(dt=None)
1435
+ result = grid.render_date(obj, 'dt', None)
1436
+ self.assertIsNone(result)
1437
+
1438
+ # typical
1439
+ dt = datetime.date(2025, 1, 13)
1440
+ obj = MagicMock(dt=dt)
1441
+ result = grid.render_date(obj, 'dt', str(dt))
1442
+ self.assertEqual(result, '2025-01-13')
1443
+
1418
1444
  def test_render_datetime(self):
1419
1445
  grid = self.make_grid(columns=['foo', 'bar'])
1420
1446
 
@@ -326,6 +326,30 @@ class TestStringAlchemyFilter(WebTestCase):
326
326
  self.assertEqual(filtered_query.count(), 6)
327
327
 
328
328
 
329
+ class TestIntegerAlchemyFilter(WebTestCase):
330
+
331
+ def make_filter(self, model_property, **kwargs):
332
+ factory = kwargs.pop('factory', mod.IntegerAlchemyFilter)
333
+ kwargs['model_property'] = model_property
334
+ return factory(self.request, model_property.key, **kwargs)
335
+
336
+ def test_coerce_value(self):
337
+ model = self.app.model
338
+ filtr = self.make_filter(model.Upgrade.exit_code)
339
+
340
+ # null
341
+ self.assertIsNone(filtr.coerce_value(None))
342
+ self.assertIsNone(filtr.coerce_value(''))
343
+
344
+ # typical
345
+ self.assertEqual(filtr.coerce_value('42'), 42)
346
+ self.assertEqual(filtr.coerce_value('-42'), -42)
347
+
348
+ # invalid
349
+ self.assertIsNone(filtr.coerce_value('42.12'))
350
+ self.assertIsNone(filtr.coerce_value('bogus'))
351
+
352
+
329
353
  class TestBooleanAlchemyFilter(WebTestCase):
330
354
 
331
355
  def setUp(self):