WuttaWeb 0.25.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.25.0 → wuttaweb-0.25.1}/CHANGELOG.md +11 -0
  2. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/PKG-INFO +3 -3
  3. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/pyproject.toml +3 -3
  4. wuttaweb-0.25.1/src/wuttaweb/diffs.py +199 -0
  5. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/schema.py +21 -9
  6. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/grids/base.py +3 -0
  7. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/view_version.mako +4 -0
  8. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/email.py +17 -5
  9. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/master.py +10 -1
  10. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/forms/test_schema.py +7 -0
  11. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/grids/test_base.py +12 -0
  12. wuttaweb-0.25.1/tests/test_diffs.py +132 -0
  13. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_master.py +1 -1
  14. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_settings.py +6 -3
  15. wuttaweb-0.25.0/src/wuttaweb/diffs.py +0 -224
  16. wuttaweb-0.25.0/tests/test_diffs.py +0 -222
  17. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/.gitignore +0 -0
  18. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/.hgignore +0 -0
  19. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/.pylintrc +0 -0
  20. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/COPYING.txt +0 -0
  21. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/README.md +0 -0
  22. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/Makefile +0 -0
  23. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/_static/.keepme +0 -0
  24. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.app.rst +0 -0
  25. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.auth.rst +0 -0
  26. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.cli.rst +0 -0
  27. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  28. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.conf.rst +0 -0
  29. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.db.continuum.rst +0 -0
  30. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.db.rst +0 -0
  31. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.db.sess.rst +0 -0
  32. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.diffs.rst +0 -0
  33. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.emails.rst +0 -0
  34. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.base.rst +0 -0
  35. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.rst +0 -0
  36. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.schema.rst +0 -0
  37. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  38. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.grids.base.rst +0 -0
  39. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.grids.filters.rst +0 -0
  40. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.grids.rst +0 -0
  41. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.handler.rst +0 -0
  42. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.helpers.rst +0 -0
  43. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.menus.rst +0 -0
  44. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.progress.rst +0 -0
  45. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.rst +0 -0
  46. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.static.rst +0 -0
  47. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.subscribers.rst +0 -0
  48. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.util.rst +0 -0
  49. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.auth.rst +0 -0
  50. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.base.rst +0 -0
  51. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.batch.rst +0 -0
  52. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.common.rst +0 -0
  53. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.email.rst +0 -0
  54. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.essential.rst +0 -0
  55. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.master.rst +0 -0
  56. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.people.rst +0 -0
  57. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.progress.rst +0 -0
  58. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.reports.rst +0 -0
  59. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.roles.rst +0 -0
  60. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.rst +0 -0
  61. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.settings.rst +0 -0
  62. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  63. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/api/wuttaweb.views.users.rst +0 -0
  64. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/conf.py +0 -0
  65. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/glossary.rst +0 -0
  66. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/index.rst +0 -0
  67. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/make.bat +0 -0
  68. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/narr/cli/builtin.rst +0 -0
  69. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/narr/cli/index.rst +0 -0
  70. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/narr/templates/base.rst +0 -0
  71. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/narr/templates/index.rst +0 -0
  72. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/narr/templates/lookup.rst +0 -0
  73. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/docs/narr/templates/overview.rst +0 -0
  74. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/__init__.py +0 -0
  75. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/_version.py +0 -0
  76. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/app.py +0 -0
  77. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/auth.py +0 -0
  78. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/cli/__init__.py +0 -0
  79. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/cli/webapp.py +0 -0
  80. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/conf.py +0 -0
  81. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/db/__init__.py +0 -0
  82. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/db/continuum.py +0 -0
  83. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/db/sess.py +0 -0
  84. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
  85. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
  86. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/emails.py +0 -0
  87. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/__init__.py +0 -0
  88. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/base.py +0 -0
  89. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/forms/widgets.py +0 -0
  90. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/grids/__init__.py +0 -0
  91. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/grids/filters.py +0 -0
  92. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/handler.py +0 -0
  93. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/helpers.py +0 -0
  94. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/menus.py +0 -0
  95. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/progress.py +0 -0
  96. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/static/__init__.py +0 -0
  97. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/static/img/favicon.ico +0 -0
  98. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/static/img/logo.png +0 -0
  99. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/static/img/testing.png +0 -0
  100. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/subscribers.py +0 -0
  101. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  102. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  103. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  104. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/auth/login.mako +0 -0
  105. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/base.mako +0 -0
  106. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/base_meta.mako +0 -0
  107. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/batch/view.mako +0 -0
  108. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/configure.mako +0 -0
  109. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  110. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  111. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  112. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  113. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  114. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  115. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/password.pt +0 -0
  116. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  117. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  118. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
  119. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  120. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  121. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  122. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  123. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  124. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/select.pt +0 -0
  125. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  126. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  127. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/deform/wutta_checked_password.pt +0 -0
  128. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/diff.mako +0 -0
  129. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/email/settings/view.mako +0 -0
  130. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/forbidden.mako +0 -0
  131. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/form.mako +0 -0
  132. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  133. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  134. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  135. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/home.mako +0 -0
  136. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/configure.mako +0 -0
  137. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/create.mako +0 -0
  138. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/create_row.mako +0 -0
  139. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/delete.mako +0 -0
  140. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/edit.mako +0 -0
  141. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/form.mako +0 -0
  142. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/index.mako +0 -0
  143. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/view.mako +0 -0
  144. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/master/view_versions.mako +0 -0
  145. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/notfound.mako +0 -0
  146. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/page.mako +0 -0
  147. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  148. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/progress.mako +0 -0
  149. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/reports/view.mako +0 -0
  150. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/setup.mako +0 -0
  151. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/base.mako +0 -0
  152. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/buefy-components.mako +0 -0
  153. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/buefy-plugin.mako +0 -0
  154. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/themes/butterfly/http-plugin.mako +0 -0
  155. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/upgrade.mako +0 -0
  156. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  157. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  158. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/users/view.mako +0 -0
  159. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/templates/wutta-components.mako +0 -0
  160. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/testing.py +0 -0
  161. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/util.py +0 -0
  162. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/__init__.py +0 -0
  163. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/auth.py +0 -0
  164. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/base.py +0 -0
  165. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/batch.py +0 -0
  166. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/common.py +0 -0
  167. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/essential.py +0 -0
  168. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/people.py +0 -0
  169. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/progress.py +0 -0
  170. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/reports.py +0 -0
  171. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/roles.py +0 -0
  172. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/settings.py +0 -0
  173. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/upgrades.py +0 -0
  174. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/src/wuttaweb/views/users.py +0 -0
  175. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tasks.py +0 -0
  176. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/__init__.py +0 -0
  177. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/cli/__init__.py +0 -0
  178. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/cli/test_webapp.py +0 -0
  179. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/db/__init__.py +0 -0
  180. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/db/test_continuum.py +0 -0
  181. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/forms/test_base.py +0 -0
  182. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/forms/test_widgets.py +0 -0
  183. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/grids/__init__.py +0 -0
  184. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/grids/test_filters.py +0 -0
  185. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  186. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  187. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_oruga.js +0 -0
  188. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_oruga_bulma.css +0 -0
  189. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_oruga_bulma.js +0 -0
  190. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_vue.js +0 -0
  191. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/bb_vue_fontawesome.js +0 -0
  192. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/buefy.css +0 -0
  193. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/buefy.js +0 -0
  194. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/fontawesome.js +0 -0
  195. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/vue.js +0 -0
  196. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/libcache/vue_resource.js +0 -0
  197. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_app.py +0 -0
  198. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_auth.py +0 -0
  199. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_emails.py +0 -0
  200. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_handler.py +0 -0
  201. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_helpers.py +0 -0
  202. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_menus.py +0 -0
  203. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_progress.py +0 -0
  204. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_static.py +0 -0
  205. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_subscribers.py +0 -0
  206. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/test_util.py +0 -0
  207. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/util.py +0 -0
  208. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/__init__.py +0 -0
  209. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test___init__.py +0 -0
  210. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_auth.py +0 -0
  211. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_base.py +0 -0
  212. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_batch.py +0 -0
  213. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_common.py +0 -0
  214. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_email.py +0 -0
  215. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_essential.py +0 -0
  216. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_people.py +0 -0
  217. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_progress.py +0 -0
  218. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_reports.py +0 -0
  219. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_roles.py +0 -0
  220. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_upgrades.py +0 -0
  221. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tests/views/test_users.py +0 -0
  222. {wuttaweb-0.25.0 → wuttaweb-0.25.1}/tox.ini +0 -0
@@ -5,6 +5,17 @@ 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
+
8
19
  ## v0.25.0 (2025-12-17)
9
20
 
10
21
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.25.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.26.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.25.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.26.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,28 +39,38 @@ 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
57
  def serialize(self, node, appstruct):
52
58
  if not appstruct:
53
59
  return colander.null
54
60
 
55
- request = node.widget.request
56
- config = request.wutta_config
57
- app = config.get_app()
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)
58
70
 
59
- dt = app.localtime(appstruct)
60
71
  if self.format:
61
- return dt.strftime(self.format)
62
- return dt.isoformat()
72
+ return appstruct.strftime(self.format)
73
+ return appstruct.isoformat()
63
74
 
64
75
  def deserialize( # pylint: disable=inconsistent-return-statements
65
76
  self, node, cstruct
@@ -72,6 +83,7 @@ class WuttaDateTime(colander.DateTime):
72
83
  "%Y-%m-%dT%I:%M %p",
73
84
  ]
74
85
 
86
+ # nb. request is always assumed to be present here
75
87
  request = node.widget.request
76
88
  config = request.wutta_config
77
89
  app = config.get_app()
@@ -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
 
@@ -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
 
@@ -62,6 +62,13 @@ class TestWuttaDateTime(WebTestCase):
62
62
  )
63
63
  self.assertEqual(result, "2024-12-11 02:33 PM")
64
64
 
65
+ # missing widget/request/config
66
+ typ = mod.WuttaDateTime()
67
+ node = colander.SchemaNode(typ)
68
+ result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
69
+ # nb. not possible to know which timezone is system-local
70
+ self.assertTrue(result.startswith("2024-12-"))
71
+
65
72
  def test_deserialize(self):
66
73
  tzlocal = get_timezone_by_name("America/Los_Angeles")
67
74
  with patch.object(self.app, "get_timezone", return_value=tzlocal):
@@ -1867,7 +1867,19 @@ class TestGrid(WebTestCase):
1867
1867
  context = grid.get_vue_context()
1868
1868
  self.assertEqual(context, {"data": [{"foo": "bar"}], "row_classes": {}})
1869
1869
 
1870
+ # non-declared columns are discarded
1871
+ mydata = [
1872
+ {"foo": "a", "bar": "b", "baz": "c"},
1873
+ ]
1874
+ grid = self.make_grid(columns=["bar"], data=mydata)
1875
+ context = grid.get_vue_context()
1876
+ self.assertEqual(context, {"data": [{"bar": "b"}], "row_classes": {}})
1877
+
1870
1878
  # if grid has actions, that list may be supplemented
1879
+ mydata = [
1880
+ {"foo": "bar"},
1881
+ ]
1882
+ grid = self.make_grid(columns=["foo"], data=mydata)
1871
1883
  grid.actions.append(mod.GridAction(self.request, "view", url="/blarg"))
1872
1884
  context = grid.get_vue_context()
1873
1885
  self.assertIsNot(context["data"], mydata)
@@ -0,0 +1,132 @@
1
+ # -*- coding: utf-8; -*-
2
+
3
+ from wuttaweb import diffs as mod
4
+ from wuttaweb.testing import WebTestCase, VersionWebTestCase
5
+
6
+
7
+ class TestWebDiff(WebTestCase):
8
+
9
+ def make_diff(self, *args, **kwargs):
10
+ return mod.WebDiff(self.config, *args, **kwargs)
11
+
12
+ def test_render_html(self):
13
+ old_data = {"foo": "bar"}
14
+ new_data = {"foo": "baz"}
15
+ diff = self.make_diff(old_data, new_data)
16
+ html = diff.render_html()
17
+ self.assertIn("<table", html)
18
+ self.assertIn("<tr>", html)
19
+ self.assertIn("&#39;bar&#39;", html)
20
+ self.assertIn(f'style="background-color: {diff.old_color}"', html)
21
+ self.assertIn("&#39;baz&#39;", html)
22
+ self.assertIn(f'style="background-color: {diff.new_color}"', html)
23
+ self.assertIn("</tr>", html)
24
+ self.assertIn("</table>", html)
25
+
26
+
27
+ class TestVersionDiff(VersionWebTestCase):
28
+
29
+ def make_diff(self, *args, **kwargs):
30
+ return mod.VersionDiff(self.config, *args, **kwargs)
31
+
32
+ def test_constructor(self):
33
+ import sqlalchemy_continuum as continuum
34
+
35
+ model = self.app.model
36
+ user = model.User(username="fred")
37
+ self.session.add(user)
38
+ self.session.commit()
39
+ user.username = "freddie"
40
+ self.session.commit()
41
+ self.session.delete(user)
42
+ self.session.commit()
43
+
44
+ txncls = continuum.transaction_class(model.User)
45
+ vercls = continuum.version_class(model.User)
46
+ versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
47
+ self.assertEqual(len(versions), 3)
48
+
49
+ version = versions[0]
50
+ diff = self.make_diff(version)
51
+ self.assertEqual(diff.nature, "create")
52
+ self.assertEqual(
53
+ diff.fields,
54
+ ["active", "person_uuid", "prevent_edit", "username", "uuid"],
55
+ )
56
+
57
+ version = versions[1]
58
+ diff = self.make_diff(version)
59
+ self.assertEqual(diff.nature, "update")
60
+ self.assertEqual(
61
+ diff.fields,
62
+ ["active", "person_uuid", "prevent_edit", "username", "uuid"],
63
+ )
64
+
65
+ version = versions[2]
66
+ diff = self.make_diff(version)
67
+ self.assertEqual(diff.nature, "delete")
68
+ self.assertEqual(
69
+ diff.fields,
70
+ ["active", "person_uuid", "prevent_edit", "username", "uuid"],
71
+ )
72
+
73
+ def test_render_version_value(self):
74
+ import sqlalchemy_continuum as continuum
75
+
76
+ model = self.app.model
77
+ person = model.Person(full_name="Fred Flintstone")
78
+ self.session.add(person)
79
+
80
+ # create, update, delete user
81
+ user = model.User(username="fred", person=person)
82
+ self.session.add(user)
83
+ self.session.commit()
84
+ user.username = "freddie"
85
+ self.session.commit()
86
+ self.session.delete(user)
87
+ self.session.commit()
88
+
89
+ txncls = continuum.transaction_class(model.User)
90
+ vercls = continuum.version_class(model.User)
91
+ versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
92
+ self.assertEqual(len(versions), 3)
93
+
94
+ # create (1st version)
95
+ version = versions[0]
96
+ diff = self.make_diff(version)
97
+ self.assertEqual(diff.nature, "create")
98
+ self.assertEqual(diff.render_old_value("username"), "")
99
+ self.assertIn("fred", diff.render_new_value("username"))
100
+ self.assertNotIn("freddie", diff.render_new_value("username"))
101
+ self.assertEqual(diff.render_old_value("person_uuid"), "")
102
+ # rendered person_uuid includes display name
103
+ html = diff.render_new_value("person_uuid")
104
+ self.assertIn(str(person.uuid), html)
105
+ self.assertIn("Fred Flintstone", html)
106
+
107
+ # update (2nd version)
108
+ version = versions[1]
109
+ diff = self.make_diff(version)
110
+ self.assertEqual(diff.nature, "update")
111
+ self.assertIn("fred", diff.render_old_value("username"))
112
+ self.assertNotIn("freddie", diff.render_old_value("username"))
113
+ self.assertIn("freddie", diff.render_new_value("username"))
114
+ # rendered person_uuid includes display name
115
+ html = diff.render_old_value("person_uuid")
116
+ self.assertIn(str(person.uuid), html)
117
+ self.assertIn("Fred Flintstone", html)
118
+ html = diff.render_new_value("person_uuid")
119
+ self.assertIn(str(person.uuid), html)
120
+ self.assertIn("Fred Flintstone", html)
121
+
122
+ # delete (3rd version)
123
+ version = versions[2]
124
+ diff = self.make_diff(version)
125
+ self.assertEqual(diff.nature, "delete")
126
+ self.assertIn("freddie", diff.render_old_value("username"))
127
+ self.assertEqual(diff.render_new_value("username"), "")
128
+ # rendered person_uuid includes display name
129
+ html = diff.render_old_value("person_uuid")
130
+ self.assertIn(str(person.uuid), html)
131
+ self.assertIn("Fred Flintstone", html)
132
+ self.assertEqual(diff.render_new_value("person_uuid"), "")
@@ -2115,7 +2115,7 @@ class TestVersionedMasterView(VersionWebTestCase):
2115
2115
  view = self.make_view()
2116
2116
  self.assertEqual(
2117
2117
  view.get_version_grid_columns(),
2118
- ["id", "issued_at", "user", "remote_addr"],
2118
+ ["id", "issued_at", "user", "remote_addr", "comment"],
2119
2119
  )
2120
2120
 
2121
2121
  # custom
@@ -1,5 +1,6 @@
1
1
  # -*- coding: utf-8; -*-
2
2
 
3
+ import sys
3
4
  from unittest.mock import patch
4
5
 
5
6
  import colander
@@ -57,9 +58,11 @@ class TestAppInfoView(WebTestCase):
57
58
  # invalid
58
59
  with patch.object(self.request, "GET", new={"tzname": "bad_name"}):
59
60
  result = view.configure_check_timezone()
60
- self.assertEqual(
61
- result["invalid"], "'No time zone found with key bad_name'"
62
- )
61
+ # nb. this check won't work for python 3.8
62
+ if sys.version_info >= (3, 9):
63
+ self.assertEqual(
64
+ result["invalid"], "'No time zone found with key bad_name'"
65
+ )
63
66
 
64
67
  # missing input
65
68
  with patch.object(self.request, "GET", new={}):