WuttaWeb 0.25.1__tar.gz → 0.26.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 (240) hide show
  1. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/CHANGELOG.md +19 -0
  2. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/PKG-INFO +3 -2
  3. wuttaweb-0.26.0/docs/api/wuttaweb.views.alembic.rst +6 -0
  4. wuttaweb-0.26.0/docs/api/wuttaweb.views.tables.rst +6 -0
  5. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/conf.py +1 -0
  6. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/index.rst +2 -0
  7. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/pyproject.toml +3 -2
  8. wuttaweb-0.26.0/src/wuttaweb/code-templates/new-table.mako +68 -0
  9. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/forms/base.py +175 -24
  10. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/forms/widgets.py +74 -1
  11. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/grids/base.py +12 -7
  12. wuttaweb-0.26.0/src/wuttaweb/templates/alembic/dashboard.mako +234 -0
  13. wuttaweb-0.26.0/src/wuttaweb/templates/alembic/migrations/configure.mako +31 -0
  14. wuttaweb-0.26.0/src/wuttaweb/templates/alembic/migrations/create.mako +70 -0
  15. wuttaweb-0.26.0/src/wuttaweb/templates/alembic/migrations/index.mako +25 -0
  16. wuttaweb-0.26.0/src/wuttaweb/templates/alembic/migrations/view.mako +17 -0
  17. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/appinfo/index.mako +20 -0
  18. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/checkbox.pt +4 -2
  19. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/form.mako +8 -6
  20. wuttaweb-0.26.0/src/wuttaweb/templates/forms/vue_buttons.mako +43 -0
  21. wuttaweb-0.26.0/src/wuttaweb/templates/forms/vue_fields.mako +4 -0
  22. wuttaweb-0.26.0/src/wuttaweb/templates/forms/vue_template.mako +54 -0
  23. wuttaweb-0.26.0/src/wuttaweb/templates/tables/app/create.mako +1035 -0
  24. wuttaweb-0.26.0/src/wuttaweb/templates/tables/app/index.mako +25 -0
  25. wuttaweb-0.26.0/src/wuttaweb/templates/upgrades/index.mako +14 -0
  26. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/wutta-components.mako +67 -0
  27. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/testing.py +5 -3
  28. wuttaweb-0.26.0/src/wuttaweb/views/alembic.py +565 -0
  29. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/batch.py +2 -2
  30. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/common.py +3 -3
  31. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/essential.py +23 -1
  32. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/master.py +268 -146
  33. wuttaweb-0.26.0/src/wuttaweb/views/tables.py +406 -0
  34. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/users.py +1 -0
  35. wuttaweb-0.26.0/tests/forms/main_template.mako +9 -0
  36. wuttaweb-0.26.0/tests/forms/main_template_with_fields.mako +11 -0
  37. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/forms/test_base.py +43 -29
  38. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/forms/test_widgets.py +127 -0
  39. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/grids/test_base.py +13 -0
  40. wuttaweb-0.26.0/tests/views/test_alembic.py +460 -0
  41. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_master.py +127 -48
  42. wuttaweb-0.26.0/tests/views/test_tables.py +326 -0
  43. wuttaweb-0.25.1/src/wuttaweb/templates/forms/vue_template.mako +0 -99
  44. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/.gitignore +0 -0
  45. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/.hgignore +0 -0
  46. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/.pylintrc +0 -0
  47. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/COPYING.txt +0 -0
  48. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/README.md +0 -0
  49. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/Makefile +0 -0
  50. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/_static/.keepme +0 -0
  51. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.app.rst +0 -0
  52. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.auth.rst +0 -0
  53. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.cli.rst +0 -0
  54. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  55. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.conf.rst +0 -0
  56. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.db.continuum.rst +0 -0
  57. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.db.rst +0 -0
  58. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.db.sess.rst +0 -0
  59. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.diffs.rst +0 -0
  60. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.emails.rst +0 -0
  61. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.forms.base.rst +0 -0
  62. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.forms.rst +0 -0
  63. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.forms.schema.rst +0 -0
  64. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  65. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.grids.base.rst +0 -0
  66. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.grids.filters.rst +0 -0
  67. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.grids.rst +0 -0
  68. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.handler.rst +0 -0
  69. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.helpers.rst +0 -0
  70. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.menus.rst +0 -0
  71. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.progress.rst +0 -0
  72. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.rst +0 -0
  73. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.static.rst +0 -0
  74. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.subscribers.rst +0 -0
  75. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.util.rst +0 -0
  76. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.auth.rst +0 -0
  77. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.base.rst +0 -0
  78. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.batch.rst +0 -0
  79. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.common.rst +0 -0
  80. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.email.rst +0 -0
  81. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.essential.rst +0 -0
  82. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.master.rst +0 -0
  83. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.people.rst +0 -0
  84. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.progress.rst +0 -0
  85. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.reports.rst +0 -0
  86. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.roles.rst +0 -0
  87. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.rst +0 -0
  88. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.settings.rst +0 -0
  89. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  90. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/api/wuttaweb.views.users.rst +0 -0
  91. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/glossary.rst +0 -0
  92. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/make.bat +0 -0
  93. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/narr/cli/builtin.rst +0 -0
  94. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/narr/cli/index.rst +0 -0
  95. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/narr/templates/base.rst +0 -0
  96. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/narr/templates/index.rst +0 -0
  97. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/narr/templates/lookup.rst +0 -0
  98. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/docs/narr/templates/overview.rst +0 -0
  99. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/__init__.py +0 -0
  100. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/_version.py +0 -0
  101. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/app.py +0 -0
  102. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/auth.py +0 -0
  103. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/cli/__init__.py +0 -0
  104. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/cli/webapp.py +0 -0
  105. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/conf.py +0 -0
  106. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/db/__init__.py +0 -0
  107. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/db/continuum.py +0 -0
  108. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/db/sess.py +0 -0
  109. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/diffs.py +0 -0
  110. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  111. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  112. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/emails.py +0 -0
  113. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/forms/__init__.py +0 -0
  114. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/forms/schema.py +0 -0
  115. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/grids/__init__.py +0 -0
  116. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/grids/filters.py +0 -0
  117. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/handler.py +0 -0
  118. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/helpers.py +0 -0
  119. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/menus.py +0 -0
  120. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/progress.py +0 -0
  121. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/static/__init__.py +0 -0
  122. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  123. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/static/img/logo.png +0 -0
  124. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/static/img/testing.png +0 -0
  125. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/subscribers.py +0 -0
  126. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  127. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  128. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  129. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/base.mako +0 -0
  130. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  131. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/batch/view.mako +0 -0
  132. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/configure.mako +0 -0
  133. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  134. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  135. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  136. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  137. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  138. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  139. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  140. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  141. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  142. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  143. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  144. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  145. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  146. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  147. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  148. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  149. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  150. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/deform/wutta_checked_password.pt +0 -0
  151. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/diff.mako +0 -0
  152. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  153. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/forbidden.mako +0 -0
  154. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  155. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  156. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/home.mako +0 -0
  157. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  158. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/create.mako +0 -0
  159. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/create_row.mako +0 -0
  160. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  161. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  162. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/form.mako +0 -0
  163. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/index.mako +0 -0
  164. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/view.mako +0 -0
  165. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/view_version.mako +0 -0
  166. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/master/view_versions.mako +0 -0
  167. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/notfound.mako +0 -0
  168. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/page.mako +0 -0
  169. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  170. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/progress.mako +0 -0
  171. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/reports/view.mako +0 -0
  172. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/setup.mako +0 -0
  173. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/themes/butterfly/base.mako +0 -0
  174. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/themes/butterfly/buefy-components.mako +0 -0
  175. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/themes/butterfly/buefy-plugin.mako +0 -0
  176. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/themes/butterfly/http-plugin.mako +0 -0
  177. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/upgrade.mako +0 -0
  178. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  179. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  180. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/templates/users/view.mako +0 -0
  181. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/util.py +0 -0
  182. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/__init__.py +0 -0
  183. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/auth.py +0 -0
  184. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/base.py +0 -0
  185. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/email.py +0 -0
  186. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/people.py +0 -0
  187. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/progress.py +0 -0
  188. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/reports.py +0 -0
  189. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/roles.py +0 -0
  190. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/settings.py +0 -0
  191. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/src/wuttaweb/views/upgrades.py +0 -0
  192. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tasks.py +0 -0
  193. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/__init__.py +0 -0
  194. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/cli/__init__.py +0 -0
  195. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/cli/test_webapp.py +0 -0
  196. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/db/__init__.py +0 -0
  197. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/db/test_continuum.py +0 -0
  198. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/forms/test_schema.py +0 -0
  199. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/grids/__init__.py +0 -0
  200. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/grids/test_filters.py +0 -0
  201. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  202. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  203. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_oruga.js +0 -0
  204. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  205. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  206. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_vue.js +0 -0
  207. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  208. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/buefy.css +0 -0
  209. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/buefy.js +0 -0
  210. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/fontawesome.js +0 -0
  211. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/vue.js +0 -0
  212. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/libcache/vue_resource.js +0 -0
  213. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_app.py +0 -0
  214. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_auth.py +0 -0
  215. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_diffs.py +0 -0
  216. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_emails.py +0 -0
  217. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_handler.py +0 -0
  218. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_helpers.py +0 -0
  219. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_menus.py +0 -0
  220. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_progress.py +0 -0
  221. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_static.py +0 -0
  222. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_subscribers.py +0 -0
  223. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/test_util.py +0 -0
  224. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/util.py +0 -0
  225. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/__init__.py +0 -0
  226. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test___init__.py +0 -0
  227. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_auth.py +0 -0
  228. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_base.py +0 -0
  229. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_batch.py +0 -0
  230. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_common.py +0 -0
  231. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_email.py +0 -0
  232. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_essential.py +0 -0
  233. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_people.py +0 -0
  234. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_progress.py +0 -0
  235. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_reports.py +0 -0
  236. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_roles.py +0 -0
  237. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_settings.py +0 -0
  238. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_upgrades.py +0 -0
  239. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tests/views/test_users.py +0 -0
  240. {wuttaweb-0.25.1 → wuttaweb-0.26.0}/tox.ini +0 -0
@@ -5,6 +5,25 @@ All notable changes to wuttaweb will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v0.26.0 (2025-12-28)
9
+
10
+ ### Feat
11
+
12
+ - add "wizard" for creating new table/model/revision
13
+ - add support for Create Alembic Migration
14
+ - add CopyableTextWidget and `<wutta-copyable-text>` component
15
+ - overhaul how form vue template is rendered
16
+ - add basic views for Alembic Migrations, Dashboard
17
+ - add basic Table views
18
+
19
+ ### Fix
20
+
21
+ - let checkbox widget show static text instead of Yes/No
22
+ - rename form-saving methods etc. for consistency in MasterView
23
+ - temporarily avoid make_uuid()
24
+ - remove password filter option for Users grid
25
+ - use smarter default for `grid.sort_multiple` based on model class
26
+
8
27
  ## v0.25.1 (2025-12-20)
9
28
 
10
29
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.25.1
3
+ Version: 0.26.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
@@ -37,9 +37,10 @@ Requires-Dist: pyramid-fanstatic
37
37
  Requires-Dist: pyramid-mako
38
38
  Requires-Dist: pyramid-tm
39
39
  Requires-Dist: pyramid>=2
40
+ Requires-Dist: sqlalchemy-utils
40
41
  Requires-Dist: waitress
41
42
  Requires-Dist: webhelpers2
42
- Requires-Dist: wuttjamaican[db]>=0.27.0
43
+ Requires-Dist: wuttjamaican[db]>=0.28.0
43
44
  Requires-Dist: zope-sqlalchemy>=1.5
44
45
  Provides-Extra: continuum
45
46
  Requires-Dist: wutta-continuum>=0.3.0; extra == 'continuum'
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.alembic``
3
+ ==========================
4
+
5
+ .. automodule:: wuttaweb.views.alembic
6
+ :members:
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.tables``
3
+ =========================
4
+
5
+ .. automodule:: wuttaweb.views.tables
6
+ :members:
@@ -28,6 +28,7 @@ templates_path = ["_templates"]
28
28
  exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
29
29
 
30
30
  intersphinx_mapping = {
31
+ "alembic": ("https://alembic.sqlalchemy.org/en/latest/", None),
31
32
  "colander": ("https://docs.pylonsproject.org/projects/colander/en/latest/", None),
32
33
  "deform": ("https://docs.pylonsproject.org/projects/deform/en/latest/", None),
33
34
  "fanstatic": ("https://www.fanstatic.org/en/latest/", None),
@@ -58,6 +58,7 @@ the narrative docs are pretty scant. That will eventually change.
58
58
  api/wuttaweb.subscribers
59
59
  api/wuttaweb.util
60
60
  api/wuttaweb.views
61
+ api/wuttaweb.views.alembic
61
62
  api/wuttaweb.views.auth
62
63
  api/wuttaweb.views.base
63
64
  api/wuttaweb.views.batch
@@ -70,6 +71,7 @@ the narrative docs are pretty scant. That will eventually change.
70
71
  api/wuttaweb.views.reports
71
72
  api/wuttaweb.views.roles
72
73
  api/wuttaweb.views.settings
74
+ api/wuttaweb.views.tables
73
75
  api/wuttaweb.views.upgrades
74
76
  api/wuttaweb.views.users
75
77
 
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.25.1"
9
+ version = "0.26.0"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -42,9 +42,10 @@ dependencies = [
42
42
  "pyramid_fanstatic",
43
43
  "pyramid_mako",
44
44
  "pyramid_tm",
45
+ "SQLAlchemy-Utils",
45
46
  "waitress",
46
47
  "WebHelpers2",
47
- "WuttJamaican[db]>=0.27.0",
48
+ "WuttJamaican[db]>=0.28.0",
48
49
  "zope.sqlalchemy>=1.5",
49
50
  ]
50
51
 
@@ -0,0 +1,68 @@
1
+ ## -*- coding: utf-8; mode: python; -*-
2
+ # -*- coding: utf-8; -*-
3
+ """
4
+ Model definition for ${model_title_plural}
5
+ """
6
+
7
+ import sqlalchemy as sa
8
+ from sqlalchemy import orm
9
+
10
+ from wuttjamaican.db import model
11
+
12
+
13
+ class ${model_name}(model.Base):
14
+ """
15
+ ${description}
16
+ """
17
+ __tablename__ = "${table_name}"
18
+ % if any([c["data_type"]["type"] == "_fk_uuid_" for c in columns]):
19
+ __table_args__ = (
20
+ % for column in columns:
21
+ % if column["data_type"]["type"] == "_fk_uuid_":
22
+ sa.ForeignKeyConstraint(["${column['name']}"], ["${column['data_type']['reference']}.uuid"],
23
+ name="${table_name}_fk_${column['data_type']['reference']}"),
24
+ % endif
25
+ % endfor
26
+ )
27
+ % endif
28
+ % if versioned:
29
+ % if all([c["versioned"] for c in columns]):
30
+ __versioned__ = {}
31
+ % else:
32
+ __versioned__ = {
33
+ "exclude": [
34
+ % for column in columns:
35
+ % if not column["versioned"]:
36
+ "${column['name']}",
37
+ % endif
38
+ % endfor
39
+ ],
40
+ }
41
+ % endif
42
+ % endif
43
+ __wutta_hint__ = {
44
+ "model_title": "${model_title}",
45
+ "model_title_plural": "${model_title_plural}",
46
+ }
47
+ % for column in columns:
48
+
49
+ % if column["name"] == "uuid":
50
+ uuid = model.uuid_column()
51
+ % else:
52
+ ${column["name"]} = sa.Column(${column["formatted_data_type"]}, nullable=${column["nullable"]}, doc="""
53
+ ${column["description"] or ""}
54
+ """)
55
+ % if column["data_type"]["type"] == "_fk_uuid_" and column["relationship"]:
56
+ ${column["relationship"]["name"]} = orm.relationship(
57
+ "${column['relationship']['reference_model']}",
58
+ doc="""
59
+ ${column["description"] or ""}
60
+ """)
61
+ % endif
62
+ % endif
63
+ % endfor
64
+
65
+ # TODO: you usually should define the __str__() method
66
+
67
+ # def __str__(self):
68
+ # return self.name or ""
@@ -275,6 +275,10 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
275
275
  deform_form = None
276
276
  validated = None
277
277
 
278
+ vue_template = "/forms/vue_template.mako"
279
+ fields_template = "/forms/vue_fields.mako"
280
+ buttons_template = "/forms/vue_buttons.mako"
281
+
278
282
  def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
279
283
  self,
280
284
  request,
@@ -331,6 +335,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
331
335
  self.show_button_cancel = show_button_cancel
332
336
  self.button_label_cancel = button_label_cancel
333
337
  self.auto_disable_cancel = auto_disable_cancel
338
+ self.form_attrs = {}
334
339
 
335
340
  self.config = self.request.wutta_config
336
341
  self.app = self.config.get_app()
@@ -806,7 +811,10 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
806
811
  # get fields
807
812
  fields = self.get_fields()
808
813
  if not fields:
809
- raise NotImplementedError
814
+ raise ValueError(
815
+ "could not determine fields list; "
816
+ "please set model_class or fields explicitly"
817
+ )
810
818
 
811
819
  if self.model_class:
812
820
 
@@ -939,7 +947,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
939
947
  """
940
948
  return HTML.tag(self.vue_tagname, **kwargs)
941
949
 
942
- def render_vue_template(self, template="/forms/vue_template.mako", **context):
950
+ def render_vue_template(self, template=None, **context):
943
951
  """
944
952
  Render the Vue template block for the form.
945
953
 
@@ -954,8 +962,8 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
954
962
  </script>
955
963
 
956
964
  <script>
957
- WuttaFormData = {}
958
- WuttaForm = {
965
+ const WuttaFormData = {}
966
+ const WuttaForm = {
959
967
  template: 'wutta-form-template',
960
968
  }
961
969
  </script>
@@ -969,36 +977,121 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
969
977
  Actual output will of course depend on form attributes, i.e.
970
978
  :attr:`vue_tagname` and :attr:`fields` list etc.
971
979
 
972
- :param template: Path to Mako template which is used to render
973
- the output.
980
+ Default logic will also invoke (indirectly):
981
+
982
+ * :meth:`render_vue_fields()`
983
+ * :meth:`render_vue_buttons()`
984
+
985
+ :param template: Optional template path to override the class
986
+ default.
987
+
988
+ :returns: HTML literal
974
989
  """
990
+ context = self.get_vue_context(**context)
991
+ html = render(template or self.vue_template, context)
992
+ return HTML.literal(html)
993
+
994
+ def get_vue_context(self, **context): # pylint: disable=missing-function-docstring
975
995
  context["form"] = self
976
996
  context["dform"] = self.get_deform()
977
997
  context.setdefault("request", self.request)
978
998
  context["model_data"] = self.get_vue_model_data()
979
999
 
980
1000
  # set form method, enctype
981
- context.setdefault("form_attrs", {})
982
- context["form_attrs"].setdefault("method", self.action_method)
1001
+ form_attrs = context.setdefault("form_attrs", dict(self.form_attrs))
1002
+ form_attrs.setdefault("method", self.action_method)
983
1003
  if self.action_method == "post":
984
- context["form_attrs"].setdefault("enctype", "multipart/form-data")
1004
+ form_attrs.setdefault("enctype", "multipart/form-data")
985
1005
 
986
1006
  # auto disable button on submit
987
1007
  if self.auto_disable_submit:
988
- context["form_attrs"]["@submit"] = "formSubmitting = true"
1008
+ form_attrs["@submit"] = "formSubmitting = true"
1009
+
1010
+ # duplicate entire context for sake of fields/buttons template
1011
+ context["form_context"] = context
1012
+
1013
+ return context
1014
+
1015
+ def render_vue_fields(self, context, template=None, **kwargs):
1016
+ """
1017
+ Render the fields section within the form template.
1018
+
1019
+ This is normally invoked from within the form's
1020
+ ``vue_template`` like this:
1021
+
1022
+ .. code-block:: none
1023
+
1024
+ ${form.render_vue_fields(form_context)}
1025
+
1026
+ There is a default ``fields_template`` but that is only the
1027
+ last resort. Logic will first look for a
1028
+ ``form_vue_fields()`` def within the *main template* being
1029
+ rendered for the page.
1030
+
1031
+ An example will surely help:
1032
+
1033
+ .. code-block:: mako
1034
+
1035
+ <%inherit file="/master/edit.mako" />
1036
+
1037
+ <%def name="form_vue_fields()">
1038
+
1039
+ <p>this is my custom fields section:</p>
1040
+
1041
+ ${form.render_vue_field("myfield")}
1042
+
1043
+ </%def>
1044
+
1045
+ This keeps the custom fields section within the main page
1046
+ template as opposed to yet another file. But if your page
1047
+ template has no ``form_vue_fields()`` def, then the class
1048
+ default template is used. (Unless the ``template`` param
1049
+ is specified.)
1050
+
1051
+ See also :meth:`render_vue_template()` and
1052
+ :meth:`render_vue_buttons()`.
1053
+
1054
+ :param context: This must be the original context as provided
1055
+ to the form's ``vue_template``. See example above.
1056
+
1057
+ :param template: Optional template path to use instead of the
1058
+ defaults described above.
1059
+
1060
+ :returns: HTML literal
1061
+ """
1062
+ context.update(kwargs)
1063
+ html = False
1064
+
1065
+ if not template:
1066
+
1067
+ if main_template := context.get("main_template"):
1068
+ try:
1069
+ vue_fields = main_template.get_def("form_vue_fields")
1070
+ except AttributeError:
1071
+ pass
1072
+ else:
1073
+ html = vue_fields.render(**context)
989
1074
 
990
- output = render(template, context)
991
- return HTML.literal(output)
1075
+ if html is False:
1076
+ template = self.fields_template
1077
+
1078
+ if html is False:
1079
+ html = render(template, context)
1080
+
1081
+ return HTML.literal(html)
992
1082
 
993
1083
  def render_vue_field( # pylint: disable=unused-argument,too-many-locals
994
1084
  self,
995
1085
  fieldname,
996
1086
  readonly=None,
1087
+ label=True,
1088
+ horizontal=True,
997
1089
  **kwargs,
998
1090
  ):
999
1091
  """
1000
1092
  Render the given field completely, i.e. ``<b-field>`` wrapper
1001
- with label and containing a widget.
1093
+ with label and a widget, with validation errors flagged as
1094
+ needed.
1002
1095
 
1003
1096
  Actual output will depend on the field attributes etc.
1004
1097
  Typical output might look like:
@@ -1009,14 +1102,23 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
1009
1102
  horizontal
1010
1103
  type="is-danger"
1011
1104
  message="something went wrong!">
1012
- <!-- widget element(s) -->
1105
+ <b-input name="foo"
1106
+ v-model="${form.get_field_vmodel('foo')}" />
1013
1107
  </b-field>
1014
1108
 
1015
- .. warning::
1109
+ :param fieldname: Name of field to render.
1110
+
1111
+ :param readonly: Optional override for readonly flag.
1016
1112
 
1017
- Any ``**kwargs`` received from caller are ignored by this
1018
- method. For now they are allowed, for sake of backwawrd
1019
- compatibility. This may change in the future.
1113
+ :param label: Whether to include/set the field label.
1114
+
1115
+ :param horizontal: Boolean value for the ``horizontal`` flag
1116
+ on the field.
1117
+
1118
+ :param \\**kwargs: Remaining kwargs are passed to widget's
1119
+ ``serialize()`` method.
1120
+
1121
+ :returns: HTML literal
1020
1122
  """
1021
1123
  # readonly comes from: caller, field flag, or form flag
1022
1124
  if readonly is None:
@@ -1034,10 +1136,9 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
1034
1136
 
1035
1137
  # render proper widget if field is in deform/schema
1036
1138
  field = dform[fieldname]
1037
- kw = {}
1038
1139
  if readonly:
1039
- kw["readonly"] = True
1040
- html = field.serialize(**kw)
1140
+ kwargs["readonly"] = True
1141
+ html = field.serialize(**kwargs)
1041
1142
 
1042
1143
  else:
1043
1144
  # render static text if field not in deform/schema
@@ -1052,12 +1153,13 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
1052
1153
  html = HTML.literal(html or "&nbsp;")
1053
1154
 
1054
1155
  # render field label
1055
- label = self.get_label(fieldname)
1156
+ if label:
1157
+ label = self.get_label(fieldname)
1056
1158
 
1057
1159
  # b-field attrs
1058
1160
  attrs = {
1059
- ":horizontal": "true",
1060
- "label": label,
1161
+ ":horizontal": "true" if horizontal else "false",
1162
+ "label": label or "",
1061
1163
  }
1062
1164
 
1063
1165
  # next we will build array of messages to display..some
@@ -1085,6 +1187,36 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
1085
1187
 
1086
1188
  return HTML.tag("b-field", c=[html], **attrs)
1087
1189
 
1190
+ def render_vue_buttons(self, context, template=None, **kwargs):
1191
+ """
1192
+ Render the buttons section within the form template.
1193
+
1194
+ This is normally invoked from within the form's
1195
+ ``vue_template`` like this:
1196
+
1197
+ .. code-block:: none
1198
+
1199
+ ${form.render_vue_buttons(form_context)}
1200
+
1201
+ .. note::
1202
+
1203
+ This method does not yet inspect the main page template,
1204
+ unlike :meth:`render_vue_fields()`.
1205
+
1206
+ See also :meth:`render_vue_template()`.
1207
+
1208
+ :param context: This must be the original context as provided
1209
+ to the form's ``vue_template``. See example above.
1210
+
1211
+ :param template: Optional template path to override the class
1212
+ default.
1213
+
1214
+ :returns: HTML literal
1215
+ """
1216
+ context.update(kwargs)
1217
+ html = render(template or self.buttons_template, context)
1218
+ return HTML.literal(html)
1219
+
1088
1220
  def render_vue_finalize(self):
1089
1221
  """
1090
1222
  Render the Vue "finalize" script for the form.
@@ -1103,6 +1235,25 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
1103
1235
  """
1104
1236
  return render_vue_finalize(self.vue_tagname, self.vue_component)
1105
1237
 
1238
+ def get_field_vmodel(self, field):
1239
+ """
1240
+ Convenience to return the ``v-model`` data reference for the
1241
+ given field. For instance:
1242
+
1243
+ .. code-block:: none
1244
+
1245
+ <b-input name="myfield"
1246
+ v-model="${form.get_field_vmodel('myfield')}" />
1247
+
1248
+ <div v-show="${form.get_field_vmodel('myfield')} == 'easter'">
1249
+ easter egg!
1250
+ </div>
1251
+
1252
+ :returns: JS-valid string referencing the field value
1253
+ """
1254
+ dform = self.get_deform()
1255
+ return f"modelData.{dform[field].oid}"
1256
+
1106
1257
  def get_vue_model_data(self):
1107
1258
  """
1108
1259
  Returns a dict with form model data. Values may be nested
@@ -60,7 +60,7 @@ from deform.widget import ( # pylint: disable=unused-import
60
60
  DateTimeInputWidget,
61
61
  MoneyInputWidget,
62
62
  )
63
- from webhelpers2.html import HTML
63
+ from webhelpers2.html import HTML, tags
64
64
 
65
65
  from wuttjamaican.conf import parse_list
66
66
 
@@ -147,6 +147,24 @@ class NotesWidget(TextAreaWidget):
147
147
  readonly_template = "readonly/notes"
148
148
 
149
149
 
150
+ class CopyableTextWidget(Widget): # pylint: disable=abstract-method
151
+ """
152
+ A readonly text widget which adds a "copy" icon/link just after
153
+ the text.
154
+ """
155
+
156
+ def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
157
+ """ """
158
+ if not cstruct:
159
+ return colander.null
160
+
161
+ return HTML.tag("wutta-copyable-text", **{"text": cstruct})
162
+
163
+ def deserialize(self, field, pstruct): # pylint: disable=empty-docstring
164
+ """ """
165
+ raise NotImplementedError
166
+
167
+
150
168
  class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
151
169
  """
152
170
  Custom widget for :class:`python:set` fields.
@@ -537,3 +555,58 @@ class BatchIdWidget(Widget): # pylint: disable=abstract-method
537
555
 
538
556
  batch_id = int(cstruct)
539
557
  return f"{batch_id:08d}"
558
+
559
+
560
+ class AlembicRevisionWidget(Widget): # pylint: disable=missing-class-docstring
561
+ """
562
+ Widget to show an Alembic revision identifier, with link to view
563
+ the revision.
564
+ """
565
+
566
+ def __init__(self, request, *args, **kwargs):
567
+ super().__init__(*args, **kwargs)
568
+ self.request = request
569
+
570
+ def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
571
+ """ """
572
+ if not cstruct:
573
+ return colander.null
574
+
575
+ return tags.link_to(
576
+ cstruct, self.request.route_url("alembic.migrations.view", revision=cstruct)
577
+ )
578
+
579
+ def deserialize(self, field, pstruct): # pylint: disable=empty-docstring
580
+ """ """
581
+ raise NotImplementedError
582
+
583
+
584
+ class AlembicRevisionsWidget(Widget):
585
+ """
586
+ Widget to show list of Alembic revision identifiers, with links to
587
+ view each revision.
588
+ """
589
+
590
+ def __init__(self, request, *args, **kwargs):
591
+ super().__init__(*args, **kwargs)
592
+ self.request = request
593
+ self.config = self.request.wutta_config
594
+
595
+ def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
596
+ """ """
597
+ if not cstruct:
598
+ return colander.null
599
+
600
+ revisions = []
601
+ for rev in self.config.parse_list(cstruct):
602
+ revisions.append(
603
+ tags.link_to(
604
+ rev, self.request.route_url("alembic.migrations.view", revision=rev)
605
+ )
606
+ )
607
+
608
+ return ", ".join(revisions)
609
+
610
+ def deserialize(self, field, pstruct): # pylint: disable=empty-docstring
611
+ """ """
612
+ raise NotImplementedError
@@ -186,8 +186,8 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
186
186
  .. attribute:: sort_multiple
187
187
 
188
188
  Boolean indicating whether "multi-column" sorting is allowed.
189
- Default is ``True``; if this is ``False`` then only one column
190
- may be sorted at a time.
189
+ This is true by default, where possible. If false then only
190
+ one column may be sorted at a time.
191
191
 
192
192
  Only relevant if :attr:`sortable` is true, but applies to both
193
193
  frontend and backend sorting.
@@ -382,7 +382,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
382
382
  joined = None
383
383
  pager = None
384
384
 
385
- def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
385
+ def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches,too-many-statements
386
386
  self,
387
387
  request,
388
388
  vue_tagname="wutta-grid",
@@ -399,7 +399,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
399
399
  linked_columns=None,
400
400
  hidden_columns=None,
401
401
  sortable=False,
402
- sort_multiple=True,
402
+ sort_multiple=None,
403
403
  sort_on_backend=True,
404
404
  sorters=None,
405
405
  sort_defaults=None,
@@ -441,7 +441,12 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
441
441
 
442
442
  # sorting
443
443
  self.sortable = sortable
444
- self.sort_multiple = sort_multiple
444
+ if sort_multiple is not None:
445
+ self.sort_multiple = sort_multiple
446
+ elif self.request.use_oruga:
447
+ self.sort_multiple = False
448
+ else:
449
+ self.sort_multiple = bool(self.model_class)
445
450
  if self.sort_multiple and self.request.use_oruga:
446
451
  log.warning(
447
452
  "grid.sort_multiple is not implemented for Oruga-based templates"
@@ -848,7 +853,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
848
853
  """
849
854
  if tools and isinstance(tools, list):
850
855
  if not any(isinstance(t, (tuple, list)) for t in tools):
851
- tools = [(self.app.make_uuid(), t) for t in tools]
856
+ tools = [(self.app.make_true_uuid().hex, t) for t in tools]
852
857
  self.tools = OrderedDict(tools or [])
853
858
 
854
859
  def add_tool(self, html, key=None):
@@ -864,7 +869,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
864
869
  See also :meth:`set_tools()`.
865
870
  """
866
871
  if not key:
867
- key = self.app.make_uuid()
872
+ key = self.app.make_true_uuid().hex
868
873
  self.tools[key] = html
869
874
 
870
875
  ##############################