WuttaWeb 0.24.0__tar.gz → 0.25.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 (220) hide show
  1. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/CHANGELOG.md +10 -0
  2. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/PKG-INFO +2 -2
  3. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/pyproject.toml +2 -2
  4. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/forms/schema.py +22 -3
  5. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/forms/widgets.py +3 -1
  6. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/appinfo/configure.mako +78 -0
  7. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/appinfo/index.mako +3 -0
  8. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/settings.py +48 -0
  9. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/forms/test_schema.py +72 -18
  10. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/forms/test_widgets.py +29 -11
  11. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/grids/test_base.py +16 -10
  12. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_master.py +4 -0
  13. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_settings.py +20 -0
  14. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/.gitignore +0 -0
  15. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/.hgignore +0 -0
  16. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/.pylintrc +0 -0
  17. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/COPYING.txt +0 -0
  18. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/README.md +0 -0
  19. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/Makefile +0 -0
  20. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/_static/.keepme +0 -0
  21. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.app.rst +0 -0
  22. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.auth.rst +0 -0
  23. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.cli.rst +0 -0
  24. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  25. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.conf.rst +0 -0
  26. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.db.continuum.rst +0 -0
  27. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.db.rst +0 -0
  28. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.db.sess.rst +0 -0
  29. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.diffs.rst +0 -0
  30. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.emails.rst +0 -0
  31. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.forms.base.rst +0 -0
  32. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.forms.rst +0 -0
  33. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.forms.schema.rst +0 -0
  34. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  35. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.grids.base.rst +0 -0
  36. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.grids.filters.rst +0 -0
  37. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.grids.rst +0 -0
  38. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.handler.rst +0 -0
  39. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.helpers.rst +0 -0
  40. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.menus.rst +0 -0
  41. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.progress.rst +0 -0
  42. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.rst +0 -0
  43. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.static.rst +0 -0
  44. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.subscribers.rst +0 -0
  45. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.util.rst +0 -0
  46. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.auth.rst +0 -0
  47. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.base.rst +0 -0
  48. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.batch.rst +0 -0
  49. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.common.rst +0 -0
  50. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.email.rst +0 -0
  51. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.essential.rst +0 -0
  52. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.master.rst +0 -0
  53. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.people.rst +0 -0
  54. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.progress.rst +0 -0
  55. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.reports.rst +0 -0
  56. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.roles.rst +0 -0
  57. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.rst +0 -0
  58. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.settings.rst +0 -0
  59. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  60. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/api/wuttaweb.views.users.rst +0 -0
  61. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/conf.py +0 -0
  62. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/glossary.rst +0 -0
  63. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/index.rst +0 -0
  64. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/make.bat +0 -0
  65. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/narr/cli/builtin.rst +0 -0
  66. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/narr/cli/index.rst +0 -0
  67. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/narr/templates/base.rst +0 -0
  68. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/narr/templates/index.rst +0 -0
  69. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/narr/templates/lookup.rst +0 -0
  70. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/docs/narr/templates/overview.rst +0 -0
  71. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/__init__.py +0 -0
  72. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/_version.py +0 -0
  73. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/app.py +0 -0
  74. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/auth.py +0 -0
  75. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/cli/__init__.py +0 -0
  76. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/cli/webapp.py +0 -0
  77. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/conf.py +0 -0
  78. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/db/__init__.py +0 -0
  79. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/db/continuum.py +0 -0
  80. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/db/sess.py +0 -0
  81. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/diffs.py +0 -0
  82. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  83. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  84. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/emails.py +0 -0
  85. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/forms/__init__.py +0 -0
  86. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/forms/base.py +0 -0
  87. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/grids/__init__.py +0 -0
  88. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/grids/base.py +0 -0
  89. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/grids/filters.py +0 -0
  90. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/handler.py +0 -0
  91. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/helpers.py +0 -0
  92. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/menus.py +0 -0
  93. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/progress.py +0 -0
  94. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/static/__init__.py +0 -0
  95. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  96. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/static/img/logo.png +0 -0
  97. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/static/img/testing.png +0 -0
  98. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/subscribers.py +0 -0
  99. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  100. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  101. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/base.mako +0 -0
  102. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  103. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/batch/view.mako +0 -0
  104. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/configure.mako +0 -0
  105. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  106. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  107. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  108. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  109. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  110. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  111. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  112. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  113. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  114. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  115. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  116. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  117. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  118. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  119. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  120. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  121. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  122. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  123. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/deform/wutta_checked_password.pt +0 -0
  124. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/diff.mako +0 -0
  125. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  126. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/forbidden.mako +0 -0
  127. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/form.mako +0 -0
  128. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  129. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  130. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  131. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/home.mako +0 -0
  132. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  133. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/create.mako +0 -0
  134. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/create_row.mako +0 -0
  135. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  136. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  137. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/form.mako +0 -0
  138. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/index.mako +0 -0
  139. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/view.mako +0 -0
  140. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/view_version.mako +0 -0
  141. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/master/view_versions.mako +0 -0
  142. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/notfound.mako +0 -0
  143. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/page.mako +0 -0
  144. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  145. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/progress.mako +0 -0
  146. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/reports/view.mako +0 -0
  147. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/setup.mako +0 -0
  148. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/themes/butterfly/base.mako +0 -0
  149. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/themes/butterfly/buefy-components.mako +0 -0
  150. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/themes/butterfly/buefy-plugin.mako +0 -0
  151. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/themes/butterfly/http-plugin.mako +0 -0
  152. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/upgrade.mako +0 -0
  153. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  154. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  155. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/users/view.mako +0 -0
  156. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/templates/wutta-components.mako +0 -0
  157. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/testing.py +0 -0
  158. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/util.py +0 -0
  159. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/__init__.py +0 -0
  160. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/auth.py +0 -0
  161. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/base.py +0 -0
  162. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/batch.py +0 -0
  163. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/common.py +0 -0
  164. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/email.py +0 -0
  165. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/essential.py +0 -0
  166. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/master.py +0 -0
  167. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/people.py +0 -0
  168. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/progress.py +0 -0
  169. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/reports.py +0 -0
  170. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/roles.py +0 -0
  171. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/upgrades.py +0 -0
  172. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/src/wuttaweb/views/users.py +0 -0
  173. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tasks.py +0 -0
  174. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/__init__.py +0 -0
  175. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/cli/__init__.py +0 -0
  176. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/cli/test_webapp.py +0 -0
  177. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/db/__init__.py +0 -0
  178. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/db/test_continuum.py +0 -0
  179. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/forms/test_base.py +0 -0
  180. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/grids/__init__.py +0 -0
  181. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/grids/test_filters.py +0 -0
  182. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  183. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  184. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_oruga.js +0 -0
  185. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  186. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  187. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_vue.js +0 -0
  188. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  189. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/buefy.css +0 -0
  190. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/buefy.js +0 -0
  191. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/fontawesome.js +0 -0
  192. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/vue.js +0 -0
  193. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/libcache/vue_resource.js +0 -0
  194. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_app.py +0 -0
  195. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_auth.py +0 -0
  196. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_diffs.py +0 -0
  197. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_emails.py +0 -0
  198. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_handler.py +0 -0
  199. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_helpers.py +0 -0
  200. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_menus.py +0 -0
  201. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_progress.py +0 -0
  202. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_static.py +0 -0
  203. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_subscribers.py +0 -0
  204. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/test_util.py +0 -0
  205. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/util.py +0 -0
  206. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/__init__.py +0 -0
  207. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test___init__.py +0 -0
  208. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_auth.py +0 -0
  209. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_base.py +0 -0
  210. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_batch.py +0 -0
  211. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_common.py +0 -0
  212. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_email.py +0 -0
  213. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_essential.py +0 -0
  214. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_people.py +0 -0
  215. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_progress.py +0 -0
  216. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_reports.py +0 -0
  217. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_roles.py +0 -0
  218. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_upgrades.py +0 -0
  219. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tests/views/test_users.py +0 -0
  220. {wuttaweb-0.24.0 → wuttaweb-0.25.0}/tox.ini +0 -0
@@ -5,6 +5,16 @@ 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.0 (2025-12-17)
9
+
10
+ ### Feat
11
+
12
+ - add "complete" (sic) timezone support
13
+
14
+ ### Fix
15
+
16
+ - add local timezone awareness for datetime fields
17
+
8
18
  ## v0.24.0 (2025-12-15)
9
19
 
10
20
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.24.0
3
+ Version: 0.25.0
4
4
  Summary: Web App for Wutta Framework
5
5
  Project-URL: Homepage, https://wuttaproject.org/
6
6
  Project-URL: Repository, https://forgejo.wuttaproject.org/wutta/wuttaweb
@@ -39,7 +39,7 @@ Requires-Dist: pyramid-tm
39
39
  Requires-Dist: pyramid>=2
40
40
  Requires-Dist: waitress
41
41
  Requires-Dist: webhelpers2
42
- Requires-Dist: wuttjamaican[db]>=0.25.0
42
+ Requires-Dist: wuttjamaican[db]>=0.26.0
43
43
  Requires-Dist: zope-sqlalchemy>=1.5
44
44
  Provides-Extra: continuum
45
45
  Requires-Dist: wutta-continuum>=0.2.2; extra == 'continuum'
@@ -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.0"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -44,7 +44,7 @@ dependencies = [
44
44
  "pyramid_tm",
45
45
  "waitress",
46
46
  "WebHelpers2",
47
- "WuttJamaican[db]>=0.25.0",
47
+ "WuttJamaican[db]>=0.26.0",
48
48
  "zope.sqlalchemy>=1.5",
49
49
  ]
50
50
 
@@ -48,10 +48,22 @@ class WuttaDateTime(colander.DateTime):
48
48
  the Buefy datepicker + timepicker widgets.
49
49
  """
50
50
 
51
- def deserialize( # pylint: disable=inconsistent-return-statements,empty-docstring
51
+ def serialize(self, node, appstruct):
52
+ if not appstruct:
53
+ return colander.null
54
+
55
+ request = node.widget.request
56
+ config = request.wutta_config
57
+ app = config.get_app()
58
+
59
+ dt = app.localtime(appstruct)
60
+ if self.format:
61
+ return dt.strftime(self.format)
62
+ return dt.isoformat()
63
+
64
+ def deserialize( # pylint: disable=inconsistent-return-statements
52
65
  self, node, cstruct
53
66
  ):
54
- """ """
55
67
  if not cstruct:
56
68
  return colander.null
57
69
 
@@ -60,9 +72,16 @@ class WuttaDateTime(colander.DateTime):
60
72
  "%Y-%m-%dT%I:%M %p",
61
73
  ]
62
74
 
75
+ request = node.widget.request
76
+ config = request.wutta_config
77
+ app = config.get_app()
78
+
63
79
  for fmt in formats:
64
80
  try:
65
- return datetime.datetime.strptime(cstruct, fmt)
81
+ dt = datetime.datetime.strptime(cstruct, fmt)
82
+ if not dt.tzinfo:
83
+ dt = app.localtime(dt, from_utc=False)
84
+ return app.make_utc(dt)
66
85
  except Exception: # pylint: disable=broad-exception-caught
67
86
  pass
68
87
 
@@ -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
 
@@ -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,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
  """
@@ -11,32 +11,86 @@ from pyramid import testing
11
11
  from sqlalchemy import orm
12
12
 
13
13
  from wuttjamaican.conf import WuttaConfig
14
+ from wuttjamaican.util import get_timezone_by_name
14
15
  from wuttjamaican.testing import DataTestCase
15
16
  from wuttaweb.forms import schema as mod
16
17
  from wuttaweb.forms import widgets
17
18
  from wuttaweb.testing import WebTestCase
18
19
 
19
20
 
20
- class TestWuttaDateTime(TestCase):
21
+ class TestWuttaDateTime(WebTestCase):
21
22
 
22
- def test_deserialize(self):
23
- typ = mod.WuttaDateTime()
24
- node = colander.SchemaNode(typ)
25
-
26
- result = typ.deserialize(node, colander.null)
27
- self.assertIs(result, colander.null)
28
-
29
- result = typ.deserialize(node, "2024-12-11T10:33 PM")
30
- self.assertIsInstance(result, datetime.datetime)
31
- self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
32
- self.assertIsNone(result.tzinfo)
33
-
34
- result = typ.deserialize(node, "2024-12-11T22:33:00")
35
- self.assertIsInstance(result, datetime.datetime)
36
- self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
37
- self.assertIsNone(result.tzinfo)
23
+ def test_serialize(self):
24
+ tzlocal = get_timezone_by_name("America/Los_Angeles")
25
+ with patch.object(self.app, "get_timezone", return_value=tzlocal):
26
+ typ = mod.WuttaDateTime()
27
+ node = colander.SchemaNode(
28
+ typ, widget=widgets.WuttaDateTimeWidget(self.request)
29
+ )
30
+
31
+ # null
32
+ self.assertIs(typ.serialize(node, colander.null), colander.null)
33
+ self.assertIs(typ.serialize(node, None), colander.null)
34
+ self.assertIs(typ.serialize(node, ""), colander.null)
35
+
36
+ # naive, UTC
37
+ result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
38
+ self.assertEqual(result, "2024-12-11T14:33:00-08:00")
39
+
40
+ # aware, UTC
41
+ result = typ.serialize(
42
+ node,
43
+ datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
44
+ )
45
+ self.assertEqual(result, "2024-12-11T14:33:00-08:00")
46
+
47
+ # aware, local
48
+ result = typ.serialize(
49
+ node,
50
+ datetime.datetime(2024, 12, 11, 14, 33, tzinfo=tzlocal),
51
+ )
52
+ self.assertEqual(result, "2024-12-11T14:33:00-08:00")
53
+
54
+ # custom format
55
+ typ = mod.WuttaDateTime(format="%Y-%m-%d %I:%M %p")
56
+ node = colander.SchemaNode(
57
+ typ, widget=widgets.WuttaDateTimeWidget(self.request)
58
+ )
59
+ result = typ.serialize(
60
+ node,
61
+ datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
62
+ )
63
+ self.assertEqual(result, "2024-12-11 02:33 PM")
38
64
 
39
- self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
65
+ def test_deserialize(self):
66
+ tzlocal = get_timezone_by_name("America/Los_Angeles")
67
+ with patch.object(self.app, "get_timezone", return_value=tzlocal):
68
+ typ = mod.WuttaDateTime()
69
+ node = colander.SchemaNode(
70
+ typ, widget=widgets.WuttaDateTimeWidget(self.request)
71
+ )
72
+
73
+ # null
74
+ self.assertIs(typ.deserialize(node, colander.null), colander.null)
75
+ self.assertIs(typ.deserialize(node, None), colander.null)
76
+ self.assertIs(typ.deserialize(node, ""), colander.null)
77
+
78
+ # format #1
79
+ result = typ.deserialize(node, "2024-12-11T22:33:00")
80
+ self.assertIsInstance(result, datetime.datetime)
81
+ self.assertEqual(
82
+ result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
83
+ )
84
+
85
+ # format #2
86
+ result = typ.deserialize(node, "2024-12-11T10:33 PM")
87
+ self.assertIsInstance(result, datetime.datetime)
88
+ self.assertEqual(
89
+ result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
90
+ )
91
+
92
+ # invalid
93
+ self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
40
94
 
41
95
 
42
96
  class TestObjectNode(DataTestCase):
@@ -8,6 +8,7 @@ import colander
8
8
  import deform
9
9
  from pyramid import testing
10
10
 
11
+ from wuttjamaican.util import get_timezone_by_name
11
12
  from wuttaweb import grids
12
13
  from wuttaweb.forms import widgets as mod
13
14
  from wuttaweb.forms import schema
@@ -145,19 +146,36 @@ class TestWuttaDateTimeWidget(WebTestCase):
145
146
  def make_widget(self, **kwargs):
146
147
  return mod.WuttaDateTimeWidget(self.request, **kwargs)
147
148
 
148
- def test_serialize(self):
149
- node = colander.SchemaNode(WuttaDateTime())
150
- field = self.make_field(node)
151
- widget = self.make_widget()
152
- dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=datetime.timezone.utc)
149
+ def test_serialize_editable(self):
150
+ tzlocal = get_timezone_by_name("America/New_York")
151
+ with patch.object(self.app, "get_timezone", return_value=tzlocal):
152
+ widget = self.make_widget()
153
+ self.assertFalse(widget.readonly)
154
+ node = colander.SchemaNode(WuttaDateTime(), widget=widget)
155
+ field = self.make_field(node)
153
156
 
154
- # editable widget has normal picker html
155
- result = widget.serialize(field, str(dt))
156
- self.assertIn("<wutta-datepicker", result)
157
+ # nb. input data (from schema type) is always "local, zone-aware, isoformat"
158
+ dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=tzlocal)
159
+ result = widget.serialize(field, dt.isoformat())
160
+ self.assertIn("<wutta-datepicker", result)
161
+
162
+ def test_serialize_readonly(self):
163
+ tzlocal = get_timezone_by_name("America/New_York")
164
+ with patch.object(self.app, "get_timezone", return_value=tzlocal):
165
+ widget = self.make_widget(readonly=True)
166
+ self.assertTrue(widget.readonly)
167
+ node = colander.SchemaNode(WuttaDateTime(), widget=widget)
168
+ field = self.make_field(node)
157
169
 
158
- # readonly is rendered per app convention
159
- result = widget.serialize(field, str(dt), readonly=True)
160
- self.assertEqual(result, "2024-12-12 13:49+0000")
170
+ # null
171
+ self.assertEqual(widget.serialize(field, colander.null), "")
172
+ self.assertEqual(widget.serialize(field, None), "")
173
+ self.assertEqual(widget.serialize(field, ""), "")
174
+
175
+ # input data (from schema type) is always "local, zone-aware, isoformat"
176
+ dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=tzlocal)
177
+ result = widget.serialize(field, dt.isoformat())
178
+ self.assertEqual(result, "2024-12-12 13:49-0500")
161
179
 
162
180
 
163
181
  class TestWuttaMoneyInputWidget(WebTestCase):
@@ -14,6 +14,7 @@ from paginate_sqlalchemy import SqlalchemyOrmPage
14
14
  from pyramid import testing
15
15
 
16
16
  from wuttjamaican.conf import WuttaConfig
17
+ from wuttjamaican.util import get_timezone_by_name
17
18
  from wuttaweb.grids import base as mod
18
19
  from wuttaweb.grids.filters import (
19
20
  GridFilter,
@@ -1654,16 +1655,21 @@ class TestGrid(WebTestCase):
1654
1655
  self.assertEqual(result, "2025-01-13")
1655
1656
 
1656
1657
  def test_render_datetime(self):
1657
- grid = self.make_grid(columns=["foo", "bar"])
1658
-
1659
- obj = MagicMock(dt=None)
1660
- result = grid.render_datetime(obj, "dt", None)
1661
- self.assertEqual(result, "")
1662
-
1663
- dt = datetime.datetime(2024, 12, 12, 13, 44, tzinfo=datetime.timezone.utc)
1664
- obj = MagicMock(dt=dt)
1665
- result = grid.render_datetime(obj, "dt", str(dt))
1666
- self.assertEqual(result, "2024-12-12 13:44+0000")
1658
+ tzlocal = get_timezone_by_name("America/Los_Angeles")
1659
+ with patch.object(self.app, "get_timezone", return_value=tzlocal):
1660
+ grid = self.make_grid(columns=["foo", "bar"])
1661
+
1662
+ # null
1663
+ obj = MagicMock(dt=None)
1664
+ result = grid.render_datetime(obj, "dt", None)
1665
+ self.assertEqual(result, "")
1666
+
1667
+ # normal (naive utc)
1668
+ dt = datetime.datetime(2024, 12, 12, 13, 44)
1669
+ obj = MagicMock(dt=dt)
1670
+ result = grid.render_datetime(obj, "dt", str(dt))
1671
+ self.assertEqual(result, "2024-12-12 05:44-0800")
1672
+ self.assertNotEqual(result, str(dt))
1667
1673
 
1668
1674
  def test_render_vue_tag(self):
1669
1675
  grid = self.make_grid(columns=["foo", "bar"])
@@ -1663,6 +1663,9 @@ class TestMasterView(WebTestCase):
1663
1663
  def test_configure(self):
1664
1664
  self.pyramid_config.include("wuttaweb.views.common")
1665
1665
  self.pyramid_config.include("wuttaweb.views.auth")
1666
+ self.pyramid_config.add_route(
1667
+ "appinfo.check_timezone", "/appinfo/check-timezone"
1668
+ )
1666
1669
  model = self.app.model
1667
1670
 
1668
1671
  # mock settings
@@ -1697,6 +1700,7 @@ class TestMasterView(WebTestCase):
1697
1700
  def get_context(**kw):
1698
1701
  kw = original_context(**kw)
1699
1702
  kw["menu_handlers"] = []
1703
+ kw["default_timezone"] = "UTC"
1700
1704
  return kw
1701
1705
 
1702
1706
  with patch.object(view, "configure_get_context", new=get_context):
@@ -46,6 +46,26 @@ class TestAppInfoView(WebTestCase):
46
46
  view = self.make_view()
47
47
  context = view.configure_get_context()
48
48
 
49
+ def test_configure_check_timezone(self):
50
+ view = self.make_view()
51
+
52
+ # normal
53
+ with patch.object(self.request, "GET", new={"tzname": "America/Chicago"}):
54
+ result = view.configure_check_timezone()
55
+ self.assertFalse(result["invalid"])
56
+
57
+ # invalid
58
+ with patch.object(self.request, "GET", new={"tzname": "bad_name"}):
59
+ result = view.configure_check_timezone()
60
+ self.assertEqual(
61
+ result["invalid"], "'No time zone found with key bad_name'"
62
+ )
63
+
64
+ # missing input
65
+ with patch.object(self.request, "GET", new={}):
66
+ result = view.configure_check_timezone()
67
+ self.assertEqual(result["invalid"], "Must provide 'tzname' parameter.")
68
+
49
69
 
50
70
  class TestSettingView(WebTestCase):
51
71
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes