WuttaWeb 0.24.0__tar.gz → 0.25.1__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 (222) hide show
  1. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/CHANGELOG.md +21 -0
  2. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/PKG-INFO +3 -3
  3. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/pyproject.toml +3 -3
  4. wuttaweb-0.25.1/src/wuttaweb/diffs.py +199 -0
  5. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/schema.py +37 -6
  6. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/widgets.py +3 -1
  7. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/grids/base.py +3 -0
  8. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/appinfo/configure.mako +78 -0
  9. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/appinfo/index.mako +3 -0
  10. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/view_version.mako +4 -0
  11. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/email.py +17 -5
  12. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/master.py +10 -1
  13. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/settings.py +48 -0
  14. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/forms/test_schema.py +79 -18
  15. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/forms/test_widgets.py +29 -11
  16. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/grids/test_base.py +28 -10
  17. wuttaweb-0.25.1/tests/test_diffs.py +132 -0
  18. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_master.py +5 -1
  19. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_settings.py +23 -0
  20. wuttaweb-0.24.0/src/wuttaweb/diffs.py +0 -224
  21. wuttaweb-0.24.0/tests/test_diffs.py +0 -222
  22. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/.gitignore +0 -0
  23. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/.hgignore +0 -0
  24. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/.pylintrc +0 -0
  25. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/COPYING.txt +0 -0
  26. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/README.md +0 -0
  27. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/Makefile +0 -0
  28. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/_static/.keepme +0 -0
  29. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.app.rst +0 -0
  30. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.auth.rst +0 -0
  31. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.cli.rst +0 -0
  32. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  33. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.conf.rst +0 -0
  34. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.db.continuum.rst +0 -0
  35. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.db.rst +0 -0
  36. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.db.sess.rst +0 -0
  37. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.diffs.rst +0 -0
  38. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.emails.rst +0 -0
  39. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.base.rst +0 -0
  40. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.rst +0 -0
  41. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.schema.rst +0 -0
  42. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  43. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.grids.base.rst +0 -0
  44. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.grids.filters.rst +0 -0
  45. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.grids.rst +0 -0
  46. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.handler.rst +0 -0
  47. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.helpers.rst +0 -0
  48. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.menus.rst +0 -0
  49. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.progress.rst +0 -0
  50. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.rst +0 -0
  51. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.static.rst +0 -0
  52. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.subscribers.rst +0 -0
  53. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.util.rst +0 -0
  54. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.auth.rst +0 -0
  55. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.base.rst +0 -0
  56. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.batch.rst +0 -0
  57. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.common.rst +0 -0
  58. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.email.rst +0 -0
  59. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.essential.rst +0 -0
  60. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.master.rst +0 -0
  61. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.people.rst +0 -0
  62. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.progress.rst +0 -0
  63. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.reports.rst +0 -0
  64. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.roles.rst +0 -0
  65. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.rst +0 -0
  66. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.settings.rst +0 -0
  67. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  68. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.users.rst +0 -0
  69. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/conf.py +0 -0
  70. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/glossary.rst +0 -0
  71. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/index.rst +0 -0
  72. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/make.bat +0 -0
  73. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/narr/cli/builtin.rst +0 -0
  74. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/narr/cli/index.rst +0 -0
  75. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/narr/templates/base.rst +0 -0
  76. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/narr/templates/index.rst +0 -0
  77. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/narr/templates/lookup.rst +0 -0
  78. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/docs/narr/templates/overview.rst +0 -0
  79. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/__init__.py +0 -0
  80. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/_version.py +0 -0
  81. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/app.py +0 -0
  82. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/auth.py +0 -0
  83. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/cli/__init__.py +0 -0
  84. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/cli/webapp.py +0 -0
  85. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/conf.py +0 -0
  86. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/db/__init__.py +0 -0
  87. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/db/continuum.py +0 -0
  88. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/db/sess.py +0 -0
  89. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  90. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  91. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/emails.py +0 -0
  92. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/__init__.py +0 -0
  93. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/base.py +0 -0
  94. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/grids/__init__.py +0 -0
  95. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/grids/filters.py +0 -0
  96. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/handler.py +0 -0
  97. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/helpers.py +0 -0
  98. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/menus.py +0 -0
  99. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/progress.py +0 -0
  100. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/static/__init__.py +0 -0
  101. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/static/img/favicon.ico +0 -0
  102. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/static/img/logo.png +0 -0
  103. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/static/img/testing.png +0 -0
  104. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/subscribers.py +0 -0
  105. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  106. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/auth/login.mako +0 -0
  107. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/base.mako +0 -0
  108. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/base_meta.mako +0 -0
  109. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/batch/view.mako +0 -0
  110. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/configure.mako +0 -0
  111. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  112. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  113. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  114. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  115. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  116. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  117. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/password.pt +0 -0
  118. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  119. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  120. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  121. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  122. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  123. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  124. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  125. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  126. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/select.pt +0 -0
  127. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  128. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  129. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/wutta_checked_password.pt +0 -0
  130. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/diff.mako +0 -0
  131. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  132. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/forbidden.mako +0 -0
  133. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/form.mako +0 -0
  134. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  135. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  136. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  137. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/home.mako +0 -0
  138. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/configure.mako +0 -0
  139. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/create.mako +0 -0
  140. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/create_row.mako +0 -0
  141. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/delete.mako +0 -0
  142. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/edit.mako +0 -0
  143. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/form.mako +0 -0
  144. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/index.mako +0 -0
  145. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/view.mako +0 -0
  146. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/view_versions.mako +0 -0
  147. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/notfound.mako +0 -0
  148. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/page.mako +0 -0
  149. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  150. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/progress.mako +0 -0
  151. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/reports/view.mako +0 -0
  152. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/setup.mako +0 -0
  153. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/base.mako +0 -0
  154. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/buefy-components.mako +0 -0
  155. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/buefy-plugin.mako +0 -0
  156. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/http-plugin.mako +0 -0
  157. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/upgrade.mako +0 -0
  158. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  159. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  160. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/users/view.mako +0 -0
  161. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/wutta-components.mako +0 -0
  162. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/testing.py +0 -0
  163. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/util.py +0 -0
  164. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/__init__.py +0 -0
  165. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/auth.py +0 -0
  166. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/base.py +0 -0
  167. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/batch.py +0 -0
  168. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/common.py +0 -0
  169. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/essential.py +0 -0
  170. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/people.py +0 -0
  171. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/progress.py +0 -0
  172. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/reports.py +0 -0
  173. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/roles.py +0 -0
  174. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/upgrades.py +0 -0
  175. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/src/wuttaweb/views/users.py +0 -0
  176. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tasks.py +0 -0
  177. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/__init__.py +0 -0
  178. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/cli/__init__.py +0 -0
  179. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/cli/test_webapp.py +0 -0
  180. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/db/__init__.py +0 -0
  181. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/db/test_continuum.py +0 -0
  182. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/forms/test_base.py +0 -0
  183. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/grids/__init__.py +0 -0
  184. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/grids/test_filters.py +0 -0
  185. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  186. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  187. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_oruga.js +0 -0
  188. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_oruga_bulma.css +0 -0
  189. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_oruga_bulma.js +0 -0
  190. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_vue.js +0 -0
  191. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/bb_vue_fontawesome.js +0 -0
  192. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/buefy.css +0 -0
  193. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/buefy.js +0 -0
  194. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/fontawesome.js +0 -0
  195. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/vue.js +0 -0
  196. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/libcache/vue_resource.js +0 -0
  197. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_app.py +0 -0
  198. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_auth.py +0 -0
  199. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_emails.py +0 -0
  200. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_handler.py +0 -0
  201. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_helpers.py +0 -0
  202. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_menus.py +0 -0
  203. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_progress.py +0 -0
  204. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_static.py +0 -0
  205. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_subscribers.py +0 -0
  206. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/test_util.py +0 -0
  207. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/util.py +0 -0
  208. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/__init__.py +0 -0
  209. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test___init__.py +0 -0
  210. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_auth.py +0 -0
  211. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_base.py +0 -0
  212. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_batch.py +0 -0
  213. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_common.py +0 -0
  214. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_email.py +0 -0
  215. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_essential.py +0 -0
  216. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_people.py +0 -0
  217. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_progress.py +0 -0
  218. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_reports.py +0 -0
  219. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_roles.py +0 -0
  220. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_upgrades.py +0 -0
  221. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tests/views/test_users.py +0 -0
  222. {wuttaweb-0.24.0 → wuttaweb-0.25.1}/tox.ini +0 -0
@@ -5,6 +5,27 @@ 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.25.1 (2025-12-20)
9
+
10
+ ### Fix
11
+
12
+ - add `WebDiff` class now that `Diff` lives in wuttjamaican
13
+ - expose fallback key for email settings
14
+ - expose transaction comment for version history
15
+ - show display text for related objects, in version diff
16
+ - discard non-declared field values for grid vue data
17
+ - prevent error in DateTime schema type if no widget/request set
18
+
19
+ ## v0.25.0 (2025-12-17)
20
+
21
+ ### Feat
22
+
23
+ - add "complete" (sic) timezone support
24
+
25
+ ### Fix
26
+
27
+ - add local timezone awareness for datetime fields
28
+
8
29
  ## v0.24.0 (2025-12-15)
9
30
 
10
31
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.24.0
3
+ Version: 0.25.1
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,10 +39,10 @@ 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.25.0
42
+ Requires-Dist: wuttjamaican[db]>=0.27.0
43
43
  Requires-Dist: zope-sqlalchemy>=1.5
44
44
  Provides-Extra: continuum
45
- Requires-Dist: wutta-continuum>=0.2.2; extra == 'continuum'
45
+ Requires-Dist: wutta-continuum>=0.3.0; extra == 'continuum'
46
46
  Provides-Extra: docs
47
47
  Requires-Dist: furo; extra == 'docs'
48
48
  Requires-Dist: sphinx; extra == 'docs'
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.24.0"
9
+ version = "0.25.1"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -44,13 +44,13 @@ dependencies = [
44
44
  "pyramid_tm",
45
45
  "waitress",
46
46
  "WebHelpers2",
47
- "WuttJamaican[db]>=0.25.0",
47
+ "WuttJamaican[db]>=0.27.0",
48
48
  "zope.sqlalchemy>=1.5",
49
49
  ]
50
50
 
51
51
 
52
52
  [project.optional-dependencies]
53
- continuum = ["Wutta-Continuum>=0.2.2"]
53
+ continuum = ["Wutta-Continuum>=0.3.0"]
54
54
  docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
55
55
  tests = ["pylint", "pytest", "pytest-cov", "tox"]
56
56
 
@@ -0,0 +1,199 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024-2025 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
+ Tools for displaying simple data diffs
25
+ """
26
+
27
+ import sqlalchemy as sa
28
+
29
+ from pyramid.renderers import render
30
+ from webhelpers2.html import HTML
31
+
32
+ from wuttjamaican.diffs import Diff
33
+
34
+
35
+ class WebDiff(Diff):
36
+ """
37
+ Simple diff class for the web app.
38
+
39
+ This is based on the
40
+ :class:`~wuttjamaican:wuttjamaican.diffs.Diff` class; it just
41
+ tweaks :meth:`render_html()` to use the web template lookup
42
+ engine.
43
+ """
44
+
45
+ cell_padding = None
46
+
47
+ def render_html(self, template="/diff.mako", **kwargs):
48
+ """
49
+ Render the diff as HTML table.
50
+
51
+ :param template: Name of template to render, if you need to
52
+ override the default.
53
+
54
+ :param \\**kwargs: Remaining kwargs are passed as context to
55
+ the template renderer.
56
+
57
+ :returns: HTML literal string
58
+ """
59
+ context = kwargs
60
+ context["diff"] = self
61
+ html = render(template, context)
62
+ return HTML.literal(html)
63
+
64
+
65
+ class VersionDiff(WebDiff):
66
+ """
67
+ Special diff class for use with version history views. While
68
+ based on :class:`WebDiff`, this class uses a different signature
69
+ for the constructor.
70
+
71
+ :param config: The app :term:`config object`.
72
+
73
+ :param version: Reference to a Continuum version record object.
74
+
75
+ :param \\**kwargs: Remaining kwargs are passed as-is to the
76
+ :class:`WebDiff` constructor.
77
+ """
78
+
79
+ def __init__(self, config, version, **kwargs):
80
+ import sqlalchemy_continuum as continuum # pylint: disable=import-outside-toplevel
81
+ from wutta_continuum.util import ( # pylint: disable=import-outside-toplevel
82
+ render_operation_type,
83
+ )
84
+
85
+ self.version = version
86
+ self.model_class = continuum.parent_class(type(self.version))
87
+ self.mapper = sa.inspect(self.model_class)
88
+ self.version_mapper = sa.inspect(type(self.version))
89
+ self.title = kwargs.pop("title", self.model_class.__name__)
90
+
91
+ self.operation_title = render_operation_type(self.version.operation_type)
92
+
93
+ if "nature" not in kwargs:
94
+ if (
95
+ version.previous
96
+ and version.operation_type == continuum.Operation.DELETE
97
+ ):
98
+ kwargs["nature"] = "delete"
99
+ elif version.previous:
100
+ kwargs["nature"] = "update"
101
+ else:
102
+ kwargs["nature"] = "create"
103
+
104
+ if "fields" not in kwargs:
105
+ kwargs["fields"] = self.get_default_fields()
106
+
107
+ old_data = {}
108
+ new_data = {}
109
+ for field in kwargs["fields"]:
110
+ if version.previous:
111
+ old_data[field] = getattr(version.previous, field)
112
+ new_data[field] = getattr(version, field)
113
+
114
+ super().__init__(config, old_data, new_data, **kwargs)
115
+
116
+ def get_default_fields(self): # pylint: disable=missing-function-docstring
117
+ fields = sorted(self.version_mapper.columns.keys())
118
+
119
+ unwanted = [
120
+ "transaction_id",
121
+ "end_transaction_id",
122
+ "operation_type",
123
+ ]
124
+
125
+ return [field for field in fields if field not in unwanted]
126
+
127
+ def render_version_value(self, version, field, value):
128
+ """
129
+ Render the cell value HTML for a given version + field.
130
+
131
+ This method is used to render both sides of the diff (old +
132
+ new values). It will just render the field value using a
133
+ monospace font by default. However:
134
+
135
+ If the field is involved in a mapper relationship (i.e. it is
136
+ the "foreign key" to a related table), the logic here will
137
+ also (try to) traverse that show display text for the related
138
+ object (if found).
139
+
140
+ :param version: Reference to the Continuum version object.
141
+
142
+ :param field: Name of the field, as string.
143
+
144
+ :param value: Raw value for the field, as obtained from the
145
+ version object.
146
+
147
+ :returns: Rendered cell value as HTML literal
148
+ """
149
+ # first render normal span; this is our fallback but also may
150
+ # be embedded within a more complex result.
151
+ text = HTML.tag("span", c=[repr(value)], style="font-family: monospace;")
152
+
153
+ # loop thru all mapped relationship props
154
+ for prop in self.mapper.relationships:
155
+
156
+ # we only want singletons
157
+ if prop.uselist:
158
+ continue
159
+
160
+ # loop thru columns for prop
161
+ # nb. there should always be just one colum for a
162
+ # singleton prop, but technically a list is used, so no
163
+ # harm in looping i assume..
164
+ for col in prop.local_columns:
165
+
166
+ # we only want the matching column
167
+ if col.name != field:
168
+ continue
169
+
170
+ # grab "related version" reference via prop key. this
171
+ # would be like a UserVersion for instance.
172
+ if ref := getattr(version, prop.key):
173
+
174
+ # grab "related object" reference. this would be
175
+ # like a User for instance.
176
+ if ref := getattr(ref, "version_parent", None):
177
+
178
+ # render text w/ related object as bold string
179
+ style = (
180
+ "margin-left: 2rem; font-style: italic; font-weight: bold;"
181
+ )
182
+ return HTML.tag(
183
+ "span",
184
+ c=[text, HTML.tag("span", c=[str(ref)], style=style)],
185
+ )
186
+
187
+ return text
188
+
189
+ def render_old_value(self, field):
190
+ if self.nature == "create":
191
+ return ""
192
+ value = self.old_value(field)
193
+ return self.render_version_value(self.version.previous, field, value)
194
+
195
+ def render_new_value(self, field):
196
+ if self.nature == "delete":
197
+ return ""
198
+ value = self.new_value(field)
199
+ return self.render_version_value(self.version, field, value)
@@ -31,6 +31,7 @@ import colander
31
31
  import sqlalchemy as sa
32
32
 
33
33
  from wuttjamaican.conf import parse_list
34
+ from wuttjamaican.util import localtime
34
35
 
35
36
  from wuttaweb.db import Session
36
37
  from wuttaweb.forms import widgets
@@ -38,20 +39,42 @@ from wuttaweb.forms import widgets
38
39
 
39
40
  class WuttaDateTime(colander.DateTime):
40
41
  """
41
- Custom schema type for ``datetime`` fields.
42
+ Custom schema type for :class:`~python:datetime.datetime` fields.
42
43
 
43
44
  This should be used automatically for
44
- :class:`sqlalchemy:sqlalchemy.types.DateTime` columns unless you
45
- register another default.
45
+ :class:`~sqlalchemy:sqlalchemy.types.DateTime` ORM columns unless
46
+ you register another default.
46
47
 
47
48
  This schema type exists for sake of convenience, when working with
48
49
  the Buefy datepicker + timepicker widgets.
50
+
51
+ It also follows the datetime handling "rules" as outlined in
52
+ :doc:`wuttjamaican:narr/datetime`. On the Python side, values
53
+ should be naive/UTC datetime objects. On the HTTP side, values
54
+ will be ISO-format strings representing aware/local time.
49
55
  """
50
56
 
51
- def deserialize( # pylint: disable=inconsistent-return-statements,empty-docstring
57
+ def serialize(self, node, appstruct):
58
+ if not appstruct:
59
+ return colander.null
60
+
61
+ # nb. request should be present when it matters
62
+ if node.widget and node.widget.request:
63
+ request = node.widget.request
64
+ config = request.wutta_config
65
+ app = config.get_app()
66
+ appstruct = app.localtime(appstruct)
67
+ else:
68
+ # but if not, fallback to config-less logic
69
+ appstruct = localtime(appstruct)
70
+
71
+ if self.format:
72
+ return appstruct.strftime(self.format)
73
+ return appstruct.isoformat()
74
+
75
+ def deserialize( # pylint: disable=inconsistent-return-statements
52
76
  self, node, cstruct
53
77
  ):
54
- """ """
55
78
  if not cstruct:
56
79
  return colander.null
57
80
 
@@ -60,9 +83,17 @@ class WuttaDateTime(colander.DateTime):
60
83
  "%Y-%m-%dT%I:%M %p",
61
84
  ]
62
85
 
86
+ # nb. request is always assumed to be present here
87
+ request = node.widget.request
88
+ config = request.wutta_config
89
+ app = config.get_app()
90
+
63
91
  for fmt in formats:
64
92
  try:
65
- return datetime.datetime.strptime(cstruct, fmt)
93
+ dt = datetime.datetime.strptime(cstruct, fmt)
94
+ if not dt.tzinfo:
95
+ dt = app.localtime(dt, from_utc=False)
96
+ return app.make_utc(dt)
66
97
  except Exception: # pylint: disable=broad-exception-caught
67
98
  pass
68
99
 
@@ -252,7 +252,9 @@ class WuttaDateTimeWidget(DateTimeInputWidget):
252
252
  def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring
253
253
  """ """
254
254
  readonly = kw.get("readonly", self.readonly)
255
- if readonly and cstruct:
255
+ if readonly:
256
+ if not cstruct:
257
+ return ""
256
258
  dt = datetime.datetime.fromisoformat(cstruct)
257
259
  return self.app.render_datetime(dt)
258
260
 
@@ -2390,6 +2390,9 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
2390
2390
  # convert record to new dict
2391
2391
  record = self.object_to_dict(record)
2392
2392
 
2393
+ # discard non-declared fields
2394
+ record = {field: record[field] for field in record if field in self.columns}
2395
+
2393
2396
  # make all values safe for json
2394
2397
  record = make_json_safe(record, warn=False)
2395
2398
 
@@ -48,6 +48,16 @@
48
48
  </b-checkbox>
49
49
  </b-field>
50
50
 
51
+ <b-field label="Time Zone"
52
+ :message="timezoneFieldMessage"
53
+ :type="timezoneFieldType">
54
+ <b-input name="${app.appname}.timezone.default"
55
+ v-model="simpleSettings['${app.appname}.timezone.default']"
56
+ ## TODO: ideally could use @change here but it does not work..?
57
+ ##@change="timezoneCheck()"
58
+ @input="timezoneCheck(); settingsNeedSaved = true" />
59
+ </b-field>
60
+
51
61
  <b-field label="Menu Handler">
52
62
  <input type="hidden"
53
63
  name="${app.appname}.web.menus.handler.spec"
@@ -270,6 +280,74 @@
270
280
 
271
281
  ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
272
282
 
283
+ ThisPageData.timezoneChecking = false
284
+ ThisPageData.timezoneInvalid = false
285
+ ThisPageData.timezoneError = false
286
+
287
+ ThisPage.computed.timezoneFieldMessage = function() {
288
+ if (this.timezoneChecking) {
289
+ return "Working, please wait..."
290
+ }
291
+ if (this.timezoneInvalid) {
292
+ return this.timezoneInvalid
293
+ }
294
+ if (this.timezoneError) {
295
+ return this.timezoneError
296
+ }
297
+ return "RESTART REQUIRED IF YOU CHANGE THIS. The system (default) timezone is: ${default_timezone}"
298
+ }
299
+
300
+ ThisPage.computed.timezoneFieldType = function() {
301
+ if (this.timezoneChecking) {
302
+ return 'is-warning'
303
+ }
304
+ if (this.timezoneInvalid || this.timezoneError) {
305
+ return 'is-danger'
306
+ }
307
+ }
308
+
309
+ ThisPage.methods.timezoneCheck = function() {
310
+ if (this.timezoneChecking) {
311
+ return
312
+ }
313
+
314
+ this.timezoneError = false
315
+
316
+ if (!this.simpleSettings['${config.appname}.timezone.default']) {
317
+ this.timezoneInvalid = false
318
+
319
+ } else {
320
+ this.timezoneChecking = true
321
+ const url = '${url(f"{route_prefix}.check_timezone")}'
322
+ const params = {
323
+ tzname: this.simpleSettings['${config.appname}.timezone.default'],
324
+ }
325
+ this.wuttaGET(url, params, response => {
326
+ this.timezoneInvalid = response.data.invalid
327
+ this.timezoneChecking = false
328
+ }, response => {
329
+ this.timezoneError = response?.data?.error || "unknown error"
330
+ this.timezoneChecking = false
331
+ })
332
+ }
333
+ }
334
+
335
+ ThisPage.methods.timezoneValidate = function() {
336
+ if (this.timezoneChecking) {
337
+ return "Still checking time zone, please try again in a moment."
338
+ }
339
+
340
+ if (this.timezoneError) {
341
+ return "Error checking time zone! Please reload page and try again."
342
+ }
343
+
344
+ if (this.timezoneInvalid) {
345
+ return "The time zone is invalid!"
346
+ }
347
+ }
348
+
349
+ ThisPageData.validators.push(ThisPage.methods.timezoneValidate)
350
+
273
351
  ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
274
352
 
275
353
  ThisPageData.editWebLibraryShowDialog = false
@@ -19,6 +19,9 @@
19
19
  <b-field horizontal label="Node Title">
20
20
  <span>${app.get_node_title()}</span>
21
21
  </b-field>
22
+ <b-field horizontal label="Time Zone">
23
+ <span>${app.get_timezone_name()}</span>
24
+ </b-field>
22
25
  <b-field horizontal label="Production Mode">
23
26
  <span>${"Yes" if config.production() else "No"}</span>
24
27
  </b-field>
@@ -24,6 +24,10 @@
24
24
  <span>${transaction.id}</span>
25
25
  </b-field>
26
26
 
27
+ <b-field label="Comment" horizontal>
28
+ <span>${transaction.meta.get("comment", "")}</span>
29
+ </b-field>
30
+
27
31
  </div>
28
32
 
29
33
  <div style="padding: 2rem;">
@@ -63,6 +63,7 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
63
63
 
64
64
  form_fields = [
65
65
  "key",
66
+ "fallback_key",
66
67
  "description",
67
68
  "subject",
68
69
  "sender",
@@ -92,9 +93,11 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
92
93
  def normalize_setting(self, setting): # pylint: disable=empty-docstring
93
94
  """ """
94
95
  key = setting.__name__
96
+ setting = setting(self.config)
95
97
  return {
96
98
  "key": key,
97
- "description": setting.__doc__,
99
+ "fallback_key": setting.fallback_key or "",
100
+ "description": setting.get_description() or "",
98
101
  "subject": self.email_handler.get_auto_subject(
99
102
  key, rendered=False, setting=setting
100
103
  ),
@@ -158,8 +161,12 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
158
161
  f = form
159
162
  super().configure_form(f)
160
163
 
164
+ # fallback_key
165
+ f.set_readonly("fallback_key")
166
+
161
167
  # description
162
168
  f.set_readonly("description")
169
+ f.set_widget("description", "notes")
163
170
 
164
171
  # replyto
165
172
  f.set_required("replyto", False)
@@ -247,11 +254,12 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
247
254
  if self.viewing:
248
255
  setting = context["instance"]
249
256
  context["setting"] = setting
257
+
250
258
  context["has_html_template"] = self.email_handler.get_auto_body_template(
251
- setting["key"], "html"
259
+ setting["key"], "html", fallback_key=setting["fallback_key"]
252
260
  )
253
261
  context["has_txt_template"] = self.email_handler.get_auto_body_template(
254
- setting["key"], "txt"
262
+ setting["key"], "txt", fallback_key=setting["fallback_key"]
255
263
  )
256
264
 
257
265
  return super().render_to_response(template, context)
@@ -269,11 +277,15 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
269
277
  mode = self.request.params.get("mode", "html")
270
278
 
271
279
  if mode == "txt":
272
- body = self.email_handler.get_auto_txt_body(key, context)
280
+ body = self.email_handler.get_auto_txt_body(
281
+ key, context, fallback_key=setting.fallback_key
282
+ )
273
283
  self.request.response.content_type = "text/plain"
274
284
 
275
285
  else: # html
276
- body = self.email_handler.get_auto_html_body(key, context)
286
+ body = self.email_handler.get_auto_html_body(
287
+ key, context, fallback_key=setting.fallback_key
288
+ )
277
289
 
278
290
  self.request.response.text = body
279
291
  return self.request.response
@@ -1145,6 +1145,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
1145
1145
  "issued_at",
1146
1146
  "user",
1147
1147
  "remote_addr",
1148
+ "comment",
1148
1149
  ]
1149
1150
 
1150
1151
  def get_version_grid_data(self, instance):
@@ -1197,6 +1198,14 @@ class MasterView(View): # pylint: disable=too-many-public-methods
1197
1198
  # remote_addr
1198
1199
  g.set_label("remote_addr", "IP Address")
1199
1200
 
1201
+ # comment
1202
+ g.set_renderer("comment", self.render_version_comment)
1203
+
1204
+ def render_version_comment( # pylint: disable=missing-function-docstring,unused-argument
1205
+ self, txn, key, value
1206
+ ):
1207
+ return txn.meta.get("comment", "")
1208
+
1200
1209
  def view_version(self): # pylint: disable=too-many-locals
1201
1210
  """
1202
1211
  View to show diff details for a particular object version.
@@ -1260,7 +1269,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
1260
1269
  )
1261
1270
 
1262
1271
  version_diffs = [
1263
- VersionDiff(version)
1272
+ VersionDiff(self.config, version)
1264
1273
  for version in self.get_relevant_versions(txn, instance)
1265
1274
  ]
1266
1275
 
@@ -24,6 +24,7 @@
24
24
  Views for app settings
25
25
  """
26
26
 
27
+ import datetime
27
28
  import json
28
29
  import os
29
30
  import sys
@@ -31,6 +32,7 @@ import subprocess
31
32
  from collections import OrderedDict
32
33
 
33
34
  from wuttjamaican.db.model import Setting
35
+ from wuttjamaican.util import get_timezone_by_name
34
36
  from wuttaweb.views import MasterView
35
37
  from wuttaweb.util import get_libver, get_liburl
36
38
 
@@ -134,6 +136,7 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
134
136
  {"name": f"{self.config.appname}.node_title"},
135
137
  {"name": f"{self.config.appname}.production", "type": bool},
136
138
  {"name": "wuttaweb.themes.expose_picker", "type": bool},
139
+ {"name": f"{self.config.appname}.timezone.default"},
137
140
  {"name": f"{self.config.appname}.web.menus.handler.spec"},
138
141
  # nb. this is deprecated; we define so it is auto-deleted
139
142
  # when we replace with newer setting
@@ -174,12 +177,31 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
174
177
 
175
178
  return simple_settings
176
179
 
180
+ def configure_check_timezone(self):
181
+ """
182
+ AJAX view to validate a user-specified timezone name.
183
+
184
+ Route name for this is: ``appinfo.check_timezone``
185
+ """
186
+ tzname = self.request.GET.get("tzname")
187
+ if not tzname:
188
+ return {"invalid": "Must provide 'tzname' parameter."}
189
+ try:
190
+ get_timezone_by_name(tzname)
191
+ return {"invalid": False}
192
+ except Exception as err: # pylint: disable=broad-exception-caught
193
+ return {"invalid": str(err)}
194
+
177
195
  def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
178
196
  self, **kwargs
179
197
  ):
180
198
  """ """
181
199
  context = super().configure_get_context(**kwargs)
182
200
 
201
+ # default system timezone
202
+ dt = datetime.datetime.now().astimezone()
203
+ context["default_timezone"] = dt.tzname()
204
+
183
205
  # add registered menu handlers
184
206
  web = self.app.get_web_handler()
185
207
  handlers = web.get_menu_handler_specs()
@@ -222,6 +244,32 @@ class AppInfoView(MasterView): # pylint: disable=abstract-method
222
244
 
223
245
  return context
224
246
 
247
+ @classmethod
248
+ def defaults(cls, config): # pylint: disable=empty-docstring
249
+ """ """
250
+ cls._defaults(config)
251
+ cls._appinfo_defaults(config)
252
+
253
+ @classmethod
254
+ def _appinfo_defaults(cls, config):
255
+ route_prefix = cls.get_route_prefix()
256
+ permission_prefix = cls.get_permission_prefix()
257
+ url_prefix = cls.get_url_prefix()
258
+
259
+ # check timezone
260
+ config.add_route(
261
+ f"{route_prefix}.check_timezone",
262
+ f"{url_prefix}/check-timezone",
263
+ request_method="GET",
264
+ )
265
+ config.add_view(
266
+ cls,
267
+ attr="configure_check_timezone",
268
+ route_name=f"{route_prefix}.check_timezone",
269
+ permission=f"{permission_prefix}.configure",
270
+ renderer="json",
271
+ )
272
+
225
273
 
226
274
  class SettingView(MasterView): # pylint: disable=abstract-method
227
275
  """