WuttaWeb 0.18.0__tar.gz → 0.19.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 (201) hide show
  1. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/CHANGELOG.md +10 -0
  2. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/PKG-INFO +3 -3
  3. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/README.md +1 -1
  4. wuttaweb-0.19.0/docs/api/wuttaweb.emails.rst +6 -0
  5. wuttaweb-0.19.0/docs/api/wuttaweb.views.email.rst +6 -0
  6. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/index.rst +2 -0
  7. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/pyproject.toml +2 -2
  8. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/app.py +5 -2
  9. wuttaweb-0.19.0/src/wuttaweb/emails.py +48 -0
  10. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/forms/schema.py +34 -1
  11. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/forms/widgets.py +37 -0
  12. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/menus.py +6 -0
  13. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/base.mako +11 -7
  14. wuttaweb-0.19.0/src/wuttaweb/templates/deform/readonly/email_recips.pt +5 -0
  15. wuttaweb-0.19.0/src/wuttaweb/templates/email/settings/view.mako +39 -0
  16. wuttaweb-0.19.0/src/wuttaweb/views/email.py +298 -0
  17. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/essential.py +2 -0
  18. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/forms/test_schema.py +44 -0
  19. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/forms/test_widgets.py +50 -1
  20. wuttaweb-0.19.0/tests/test_emails.py +23 -0
  21. wuttaweb-0.19.0/tests/views/test_email.py +211 -0
  22. wuttaweb-0.19.0/tests/views/test_essential.py +10 -0
  23. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/.gitignore +0 -0
  24. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/COPYING.txt +0 -0
  25. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/Makefile +0 -0
  26. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/_static/.keepme +0 -0
  27. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.app.rst +0 -0
  28. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.auth.rst +0 -0
  29. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.cli.rst +0 -0
  30. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.cli.webapp.rst +0 -0
  31. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.conf.rst +0 -0
  32. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.db.continuum.rst +0 -0
  33. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.db.rst +0 -0
  34. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.db.sess.rst +0 -0
  35. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.base.rst +0 -0
  36. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.rst +0 -0
  37. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.schema.rst +0 -0
  38. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  39. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.grids.base.rst +0 -0
  40. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.grids.filters.rst +0 -0
  41. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.grids.rst +0 -0
  42. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.handler.rst +0 -0
  43. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.helpers.rst +0 -0
  44. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.menus.rst +0 -0
  45. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.progress.rst +0 -0
  46. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.rst +0 -0
  47. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.static.rst +0 -0
  48. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.subscribers.rst +0 -0
  49. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.util.rst +0 -0
  50. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.auth.rst +0 -0
  51. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.base.rst +0 -0
  52. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.batch.rst +0 -0
  53. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.common.rst +0 -0
  54. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.essential.rst +0 -0
  55. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.master.rst +0 -0
  56. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.people.rst +0 -0
  57. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.progress.rst +0 -0
  58. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.roles.rst +0 -0
  59. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.rst +0 -0
  60. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.settings.rst +0 -0
  61. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  62. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.users.rst +0 -0
  63. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/conf.py +0 -0
  64. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/glossary.rst +0 -0
  65. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/make.bat +0 -0
  66. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/narr/cli/builtin.rst +0 -0
  67. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/narr/cli/index.rst +0 -0
  68. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/narr/templates/base.rst +0 -0
  69. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/narr/templates/index.rst +0 -0
  70. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/narr/templates/lookup.rst +0 -0
  71. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/docs/narr/templates/overview.rst +0 -0
  72. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/__init__.py +0 -0
  73. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/_version.py +0 -0
  74. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/auth.py +0 -0
  75. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/cli/__init__.py +0 -0
  76. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/cli/webapp.py +0 -0
  77. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/conf.py +0 -0
  78. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/db/__init__.py +0 -0
  79. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/db/continuum.py +0 -0
  80. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/db/sess.py +0 -0
  81. {wuttaweb-0.18.0/src/wuttaweb/email/templates → wuttaweb-0.19.0/src/wuttaweb/email-templates}/feedback.html.mako +0 -0
  82. {wuttaweb-0.18.0/src/wuttaweb/email/templates → wuttaweb-0.19.0/src/wuttaweb/email-templates}/feedback.txt.mako +0 -0
  83. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/forms/__init__.py +0 -0
  84. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/forms/base.py +0 -0
  85. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/grids/__init__.py +0 -0
  86. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/grids/base.py +0 -0
  87. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/grids/filters.py +0 -0
  88. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/handler.py +0 -0
  89. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/helpers.py +0 -0
  90. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/progress.py +0 -0
  91. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/static/__init__.py +0 -0
  92. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  93. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/static/img/logo.png +0 -0
  94. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/static/img/testing.png +0 -0
  95. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/subscribers.py +0 -0
  96. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  97. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  98. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  99. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  100. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  101. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/batch/view.mako +0 -0
  102. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/configure.mako +0 -0
  103. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  104. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  105. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  106. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  107. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  108. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  109. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  110. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  111. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  112. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  113. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  114. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  115. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  116. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  117. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  118. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  119. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  120. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/forbidden.mako +0 -0
  121. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/form.mako +0 -0
  122. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  123. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  124. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  125. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/home.mako +0 -0
  126. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  127. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/create.mako +0 -0
  128. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  129. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  130. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/form.mako +0 -0
  131. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/index.mako +0 -0
  132. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/view.mako +0 -0
  133. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/notfound.mako +0 -0
  134. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/page.mako +0 -0
  135. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  136. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/progress.mako +0 -0
  137. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/setup.mako +0 -0
  138. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/upgrade.mako +0 -0
  139. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  140. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  141. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/templates/wutta-components.mako +0 -0
  142. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/util.py +0 -0
  143. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/__init__.py +0 -0
  144. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/auth.py +0 -0
  145. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/base.py +0 -0
  146. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/batch.py +0 -0
  147. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/common.py +0 -0
  148. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/master.py +0 -0
  149. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/people.py +0 -0
  150. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/progress.py +0 -0
  151. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/roles.py +0 -0
  152. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/settings.py +0 -0
  153. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/upgrades.py +0 -0
  154. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/src/wuttaweb/views/users.py +0 -0
  155. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tasks.py +0 -0
  156. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/__init__.py +0 -0
  157. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/cli/__init__.py +0 -0
  158. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/cli/test_webapp.py +0 -0
  159. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/db/__init__.py +0 -0
  160. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/db/test_continuum.py +0 -0
  161. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/forms/__init__.py +0 -0
  162. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/forms/test_base.py +0 -0
  163. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/grids/__init__.py +0 -0
  164. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/grids/test_base.py +0 -0
  165. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/grids/test_filters.py +0 -0
  166. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  167. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  168. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_oruga.js +0 -0
  169. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  170. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  171. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_vue.js +0 -0
  172. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  173. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/buefy.css +0 -0
  174. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/buefy.js +0 -0
  175. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/fontawesome.js +0 -0
  176. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/vue.js +0 -0
  177. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/libcache/vue_resource.js +0 -0
  178. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_app.py +0 -0
  179. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_auth.py +0 -0
  180. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_handler.py +0 -0
  181. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_helpers.py +0 -0
  182. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_menus.py +0 -0
  183. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_progress.py +0 -0
  184. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_static.py +0 -0
  185. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_subscribers.py +0 -0
  186. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/test_util.py +0 -0
  187. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/util.py +0 -0
  188. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/__init__.py +0 -0
  189. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test___init__.py +0 -0
  190. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_auth.py +0 -0
  191. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_base.py +0 -0
  192. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_batch.py +0 -0
  193. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_common.py +0 -0
  194. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_master.py +0 -0
  195. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_people.py +0 -0
  196. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_progress.py +0 -0
  197. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_roles.py +0 -0
  198. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_settings.py +0 -0
  199. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_upgrades.py +0 -0
  200. {wuttaweb-0.18.0 → wuttaweb-0.19.0}/tests/views/test_users.py +0 -0
  201. {wuttaweb-0.18.0 → wuttaweb-0.19.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.19.0 (2024-12-23)
9
+
10
+ ### Feat
11
+
12
+ - add feature to edit email settings, basic message preview
13
+
14
+ ### Fix
15
+
16
+ - move CRUD header buttons toward center of screen
17
+
8
18
  ## v0.18.0 (2024-12-18)
9
19
 
10
20
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.18.0
3
+ Version: 0.19.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.18.1
42
+ Requires-Dist: wuttjamaican[db]>=0.19.0
43
43
  Requires-Dist: zope-sqlalchemy>=1.5
44
44
  Provides-Extra: continuum
45
45
  Requires-Dist: wutta-continuum; extra == 'continuum'
@@ -53,7 +53,7 @@ Requires-Dist: tox; extra == 'tests'
53
53
  Description-Content-Type: text/markdown
54
54
 
55
55
 
56
- # wuttaweb
56
+ # WuttaWeb
57
57
 
58
58
  Web app for Wutta Framework
59
59
 
@@ -1,5 +1,5 @@
1
1
 
2
- # wuttaweb
2
+ # WuttaWeb
3
3
 
4
4
  Web app for Wutta Framework
5
5
 
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.emails``
3
+ ===================
4
+
5
+ .. automodule:: wuttaweb.emails
6
+ :members:
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.email``
3
+ ========================
4
+
5
+ .. automodule:: wuttaweb.views.email
6
+ :members:
@@ -35,6 +35,7 @@ the narrative docs are pretty scant. That will eventually change.
35
35
  api/wuttaweb.db
36
36
  api/wuttaweb.db.continuum
37
37
  api/wuttaweb.db.sess
38
+ api/wuttaweb.emails
38
39
  api/wuttaweb.forms
39
40
  api/wuttaweb.forms.base
40
41
  api/wuttaweb.forms.schema
@@ -54,6 +55,7 @@ the narrative docs are pretty scant. That will eventually change.
54
55
  api/wuttaweb.views.base
55
56
  api/wuttaweb.views.batch
56
57
  api/wuttaweb.views.common
58
+ api/wuttaweb.views.email
57
59
  api/wuttaweb.views.essential
58
60
  api/wuttaweb.views.master
59
61
  api/wuttaweb.views.people
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.18.0"
9
+ version = "0.19.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.18.1",
47
+ "WuttJamaican[db]>=0.19.0",
48
48
  "zope.sqlalchemy>=1.5",
49
49
  ]
50
50
 
@@ -43,9 +43,12 @@ log = logging.getLogger(__name__)
43
43
  class WebAppProvider(AppProvider):
44
44
  """
45
45
  The :term:`app provider` for WuttaWeb. This adds some methods to
46
- the :term:`app handler`, which are specific to web apps.
46
+ the :term:`app handler`, which are specific to web apps. It also
47
+ registers some :term:`email templates <email template>` for the
48
+ app, etc.
47
49
  """
48
- email_templates = 'wuttaweb:email/templates'
50
+ email_modules = ['wuttaweb.emails']
51
+ email_templates = ['wuttaweb:email-templates']
49
52
 
50
53
  def get_web_handler(self, **kwargs):
51
54
  """
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024 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
+ :term:`Email Settings <email setting>` for WuttaWeb
25
+ """
26
+
27
+ from wuttjamaican.email import EmailSetting
28
+
29
+
30
+ class feedback(EmailSetting):
31
+ """
32
+ Sent when user submits feedback via the web app.
33
+ """
34
+ default_subject = "User Feedback"
35
+
36
+ def sample_data(self):
37
+ """ """
38
+ model = self.app.model
39
+ person = model.Person(full_name="Barney Rubble")
40
+ user = model.User(username='barney', person=person)
41
+ return {
42
+ 'user': user,
43
+ 'user_name': str(person),
44
+ 'user_url': '#',
45
+ 'referrer': 'http://example.com/',
46
+ 'client_ip': '127.0.0.1',
47
+ 'message': "This app is cool but needs a new feature.\n\nAllow me to describe...",
48
+ }
@@ -30,9 +30,11 @@ import uuid as _uuid
30
30
  import colander
31
31
  import sqlalchemy as sa
32
32
 
33
+ from wuttjamaican.db.model import Person
34
+ from wuttjamaican.conf import parse_list
35
+
33
36
  from wuttaweb.db import Session
34
37
  from wuttaweb.forms import widgets
35
- from wuttjamaican.db.model import Person
36
38
 
37
39
 
38
40
  class WuttaDateTime(colander.DateTime):
@@ -569,5 +571,36 @@ class FileDownload(colander.String):
569
571
  return widgets.FileDownloadWidget(self.request, **kwargs)
570
572
 
571
573
 
574
+ class EmailRecipients(colander.String):
575
+ """
576
+ Custom schema type for :term:`email setting` recipient fields
577
+ (``To``, ``Cc``, ``Bcc``).
578
+ """
579
+
580
+ def serialize(self, node, appstruct):
581
+ if appstruct is colander.null:
582
+ return colander.null
583
+
584
+ return '\n'.join(parse_list(appstruct))
585
+
586
+ def deserialize(self, node, cstruct):
587
+ """ """
588
+ if cstruct is colander.null:
589
+ return colander.null
590
+
591
+ values = [value for value in parse_list(cstruct)
592
+ if value]
593
+ return ', '.join(values)
594
+
595
+ def widget_maker(self, **kwargs):
596
+ """
597
+ Constructs a default widget for the field.
598
+
599
+ :returns: Instance of
600
+ :class:`~wuttaweb.forms.widgets.EmailRecipientsWidget`.
601
+ """
602
+ return widgets.EmailRecipientsWidget(**kwargs)
603
+
604
+
572
605
  # nb. colanderalchemy schema overrides
573
606
  sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime}
@@ -51,6 +51,8 @@ from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
51
51
  DateTimeInputWidget, MoneyInputWidget)
52
52
  from webhelpers2.html import HTML
53
53
 
54
+ from wuttjamaican.conf import parse_list
55
+
54
56
  from wuttaweb.db import Session
55
57
  from wuttaweb.grids import Grid
56
58
 
@@ -423,6 +425,41 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
423
425
  return super().serialize(field, cstruct, **kw)
424
426
 
425
427
 
428
+ class EmailRecipientsWidget(TextAreaWidget):
429
+ """
430
+ Widget for :term:`email setting` recipient fields (``To``, ``Cc``,
431
+ ``Bcc``).
432
+
433
+ This is a subclass of
434
+ :class:`deform:deform.widget.TextAreaWidget`. It uses these
435
+ Deform templates:
436
+
437
+ * ``textarea``
438
+ * ``readonly/email_recips``
439
+
440
+ See also the :class:`~wuttaweb.forms.schema.EmailRecipients`
441
+ schema type, which uses this widget.
442
+ """
443
+ readonly_template = 'readonly/email_recips'
444
+
445
+ def serialize(self, field, cstruct, **kw):
446
+ """ """
447
+ readonly = kw.get('readonly', self.readonly)
448
+ if readonly:
449
+ kw['recips'] = parse_list(cstruct or '')
450
+
451
+ return super().serialize(field, cstruct, **kw)
452
+
453
+ def deserialize(self, field, pstruct):
454
+ """ """
455
+ if pstruct is colander.null:
456
+ return colander.null
457
+
458
+ values = [value for value in parse_list(pstruct)
459
+ if value]
460
+ return ', '.join(values)
461
+
462
+
426
463
  class BatchIdWidget(Widget):
427
464
  """
428
465
  Widget for use with the
@@ -174,6 +174,12 @@ class MenuHandler(GenericHandler):
174
174
  'perm': 'permissions.list',
175
175
  },
176
176
  {'type': 'sep'},
177
+ {
178
+ 'title': "Email Settings",
179
+ 'route': 'email_settings',
180
+ 'perm': 'email_settings.list',
181
+ },
182
+ {'type': 'sep'},
177
183
  {
178
184
  'title': "App Info",
179
185
  'route': 'appinfo',
@@ -277,17 +277,21 @@
277
277
  % if capture(self.content_title):
278
278
  <section id="content-title"
279
279
  class="has-background-primary">
280
- <div style="display: flex; align-items: center; padding: 0.5rem;">
280
+ <div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem;">
281
281
 
282
- <h1 class="title has-text-white"
283
- v-html="contentTitleHTML">
284
- </h1>
282
+ <div style="width: 60%; display: flex; gap: 0.5rem;">
283
+
284
+ <h1 class="title has-text-white"
285
+ v-html="contentTitleHTML">
286
+ </h1>
287
+
288
+ <div style="display: flex; gap: 0.5rem;">
289
+ ${self.render_instance_header_title_extras()}
290
+ </div>
285
291
 
286
- <div style="flex-grow: 1; display: flex; gap: 0.5rem;">
287
- ${self.render_instance_header_title_extras()}
288
292
  </div>
289
293
 
290
- <div style="display: flex; gap: 0.5rem;">
294
+ <div style="width: 40%; display: flex; gap: 0.5rem;">
291
295
  ${self.render_instance_header_buttons()}
292
296
  </div>
293
297
 
@@ -0,0 +1,5 @@
1
+ <ul>
2
+ <tal:loop tal:repeat="recip recips">
3
+ <li>${recip}</li>
4
+ </tal:loop>
5
+ </ul>
@@ -0,0 +1,39 @@
1
+ ## -*- coding: utf-8; -*-
2
+ <%inherit file="/master/view.mako" />
3
+
4
+ <%def name="tool_panels()">
5
+ ${parent.tool_panels()}
6
+ ${self.tool_panel_preview()}
7
+ </%def>
8
+
9
+ <%def name="tool_panel_preview()">
10
+ <wutta-tool-panel heading="Email Preview">
11
+
12
+ <b-button type="is-primary"
13
+ % if has_html_template:
14
+ tag="a" target="_blank"
15
+ href="${master.get_action_url('preview', setting)}?mode=html"
16
+ % else:
17
+ disabled
18
+ title="HTML template not found"
19
+ % endif
20
+ icon-pack="fas"
21
+ icon-left="external-link-alt">
22
+ Preview HTML
23
+ </b-button>
24
+
25
+ <b-button type="is-primary"
26
+ % if has_txt_template:
27
+ tag="a" target="_blank"
28
+ href="${master.get_action_url('preview', setting)}?mode=txt"
29
+ % else:
30
+ disabled
31
+ title="TXT template not found"
32
+ % endif
33
+ icon-pack="fas"
34
+ icon-left="external-link-alt">
35
+ Preview TXT
36
+ </b-button>
37
+
38
+ </wutta-tool-panel>
39
+ </%def>
@@ -0,0 +1,298 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024 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
+ Views for email settings
25
+ """
26
+
27
+ import colander
28
+
29
+ from wuttaweb.views import MasterView
30
+ from wuttaweb.forms.schema import EmailRecipients
31
+
32
+
33
+ class EmailSettingView(MasterView):
34
+ """
35
+ Master view for :term:`email settings <email setting>`.
36
+ """
37
+ model_name = 'email_setting'
38
+ model_key = 'key'
39
+ model_title = "Email Setting"
40
+ url_prefix = '/email/settings'
41
+ filterable = False
42
+ sortable = True
43
+ sort_on_backend = False
44
+ paginated = False
45
+ creatable = False
46
+ deletable = False
47
+
48
+ labels = {
49
+ 'key': "Email Key",
50
+ 'replyto': "Reply-To",
51
+ }
52
+
53
+ grid_columns = [
54
+ 'key',
55
+ 'subject',
56
+ 'to',
57
+ 'enabled',
58
+ ]
59
+
60
+ # TODO: why does this not work?
61
+ sort_defaults = 'key'
62
+
63
+ form_fields = [
64
+ 'key',
65
+ 'description',
66
+ 'subject',
67
+ 'sender',
68
+ 'replyto',
69
+ 'to',
70
+ 'cc',
71
+ 'bcc',
72
+ 'notes',
73
+ 'enabled',
74
+ ]
75
+
76
+ def __init__(self, request, context=None):
77
+ super().__init__(request, context=context)
78
+ self.email_handler = self.app.get_email_handler()
79
+
80
+ def get_grid_data(self, columns=None, session=None):
81
+ """
82
+ This view calls
83
+ :meth:`~wuttjamaican:wuttjamaican.email.EmailHandler.get_email_settings()`
84
+ on the :attr:`email_handler` to obtain its grid data.
85
+ """
86
+ data = []
87
+ for setting in self.email_handler.get_email_settings().values():
88
+ data.append(self.normalize_setting(setting))
89
+ return data
90
+
91
+ def normalize_setting(self, setting):
92
+ """ """
93
+ key = setting.__name__
94
+ return {
95
+ 'key': key,
96
+ 'description': setting.__doc__,
97
+ 'subject': self.email_handler.get_auto_subject(key, rendered=False, setting=setting),
98
+ 'sender': self.email_handler.get_auto_sender(key),
99
+ 'replyto': self.email_handler.get_auto_replyto(key) or colander.null,
100
+ 'to': self.email_handler.get_auto_to(key),
101
+ 'cc': self.email_handler.get_auto_cc(key),
102
+ 'bcc': self.email_handler.get_auto_bcc(key),
103
+ 'notes': self.email_handler.get_notes(key) or colander.null,
104
+ 'enabled': self.email_handler.is_enabled(key),
105
+ }
106
+
107
+ def configure_grid(self, g):
108
+ """ """
109
+ super().configure_grid(g)
110
+
111
+ # key
112
+ g.set_searchable('key')
113
+ g.set_link('key')
114
+
115
+ # subject
116
+ g.set_searchable('subject')
117
+ g.set_link('subject')
118
+
119
+ # to
120
+ g.set_renderer('to', self.render_to_short)
121
+
122
+ def render_to_short(self, setting, field, value):
123
+ """ """
124
+ recips = value
125
+ if not recips:
126
+ return
127
+
128
+ if len(recips) < 3:
129
+ return ', '.join(recips)
130
+
131
+ recips = ', '.join(recips[:2])
132
+ return f"{recips}, ..."
133
+
134
+ def get_instance(self):
135
+ """ """
136
+ key = self.request.matchdict['key']
137
+ setting = self.email_handler.get_email_setting(key, instance=False)
138
+ if setting:
139
+ return self.normalize_setting(setting)
140
+
141
+ raise self.notfound()
142
+
143
+ def get_instance_title(self, setting):
144
+ """ """
145
+ return setting['subject']
146
+
147
+ def configure_form(self, f):
148
+ """ """
149
+ super().configure_form(f)
150
+
151
+ # description
152
+ f.set_readonly('description')
153
+
154
+ # replyto
155
+ f.set_required('replyto', False)
156
+
157
+ # to
158
+ f.set_node('to', EmailRecipients())
159
+
160
+ # cc
161
+ f.set_node('cc', EmailRecipients())
162
+
163
+ # bcc
164
+ f.set_node('bcc', EmailRecipients())
165
+
166
+ # notes
167
+ f.set_widget('notes', 'notes')
168
+ f.set_required('notes', False)
169
+
170
+ # enabled
171
+ f.set_node('enabled', colander.Boolean())
172
+
173
+ def persist(self, setting):
174
+ """ """
175
+ session = self.Session()
176
+ key = self.request.matchdict['key']
177
+
178
+ def save(name, value):
179
+ self.app.save_setting(session, f'{self.config.appname}.email.{key}.{name}', value)
180
+
181
+ def delete(name):
182
+ self.app.delete_setting(session, f'{self.config.appname}.email.{key}.{name}')
183
+
184
+ # subject
185
+ if setting['subject']:
186
+ save('subject', setting['subject'])
187
+ else:
188
+ delete('subject')
189
+
190
+ # sender
191
+ if setting['sender']:
192
+ save('sender', setting['sender'])
193
+ else:
194
+ delete('sender')
195
+
196
+ # replyto
197
+ if setting['replyto']:
198
+ save('replyto', setting['replyto'])
199
+ else:
200
+ delete('replyto')
201
+
202
+ # to
203
+ if setting['to']:
204
+ save('to', setting['to'])
205
+ else:
206
+ delete('to')
207
+
208
+ # cc
209
+ if setting['cc']:
210
+ save('cc', setting['cc'])
211
+ else:
212
+ delete('cc')
213
+
214
+ # bcc
215
+ if setting['bcc']:
216
+ save('bcc', setting['bcc'])
217
+ else:
218
+ delete('bcc')
219
+
220
+ # notes
221
+ if setting['notes']:
222
+ save('notes', setting['notes'])
223
+ else:
224
+ delete('notes')
225
+
226
+ # enabled
227
+ save('enabled', 'true' if setting['enabled'] else 'false')
228
+
229
+ def render_to_response(self, template, context):
230
+ """ """
231
+ if self.viewing:
232
+ setting = context['instance']
233
+ context['setting'] = setting
234
+ context['has_html_template'] = self.email_handler.get_auto_body_template(
235
+ setting['key'], 'html')
236
+ context['has_txt_template'] = self.email_handler.get_auto_body_template(
237
+ setting['key'], 'txt')
238
+
239
+ return super().render_to_response(template, context)
240
+
241
+ def preview(self):
242
+ """
243
+ View for showing a rendered preview of a given email template.
244
+
245
+ This will render the email template according to the "mode"
246
+ requested - i.e. HTML or TXT.
247
+ """
248
+ key = self.request.matchdict['key']
249
+ setting = self.email_handler.get_email_setting(key)
250
+ context = setting.sample_data()
251
+ mode = self.request.params.get('mode', 'html')
252
+
253
+ if mode == 'txt':
254
+ body = self.email_handler.get_auto_txt_body(key, context)
255
+ self.request.response.content_type = 'text/plain'
256
+
257
+ else: # html
258
+ body = self.email_handler.get_auto_html_body(key, context)
259
+
260
+ self.request.response.text = body
261
+ return self.request.response
262
+
263
+ @classmethod
264
+ def defaults(cls, config):
265
+ """ """
266
+ cls._email_defaults(config)
267
+ cls._defaults(config)
268
+
269
+ @classmethod
270
+ def _email_defaults(cls, config):
271
+ """ """
272
+ route_prefix = cls.get_route_prefix()
273
+ permission_prefix = cls.get_permission_prefix()
274
+ model_title_plural = cls.get_model_title_plural()
275
+ instance_url_prefix = cls.get_instance_url_prefix()
276
+
277
+ # fix permission group
278
+ config.add_wutta_permission_group(permission_prefix,
279
+ model_title_plural,
280
+ overwrite=False)
281
+
282
+ # preview
283
+ config.add_route(f'{route_prefix}.preview',
284
+ f'{instance_url_prefix}/preview')
285
+ config.add_view(cls, attr='preview',
286
+ route_name=f'{route_prefix}.preview',
287
+ permission=f'{permission_prefix}.view')
288
+
289
+
290
+ def defaults(config, **kwargs):
291
+ base = globals()
292
+
293
+ EmailSettingView = kwargs.get('EmailSettingView', base['EmailSettingView'])
294
+ EmailSettingView.defaults(config)
295
+
296
+
297
+ def includeme(config):
298
+ defaults(config)
@@ -31,6 +31,7 @@ That will in turn include the following modules:
31
31
 
32
32
  * :mod:`wuttaweb.views.common`
33
33
  * :mod:`wuttaweb.views.auth`
34
+ * :mod:`wuttaweb.views.email`
34
35
  * :mod:`wuttaweb.views.settings`
35
36
  * :mod:`wuttaweb.views.progress`
36
37
  * :mod:`wuttaweb.views.people`
@@ -45,6 +46,7 @@ def defaults(config, **kwargs):
45
46
 
46
47
  config.include(mod('wuttaweb.views.common'))
47
48
  config.include(mod('wuttaweb.views.auth'))
49
+ config.include(mod('wuttaweb.views.email'))
48
50
  config.include(mod('wuttaweb.views.settings'))
49
51
  config.include(mod('wuttaweb.views.progress'))
50
52
  config.include(mod('wuttaweb.views.people'))