WuttaWeb 0.17.2__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 (202) hide show
  1. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/CHANGELOG.md +17 -0
  2. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/PKG-INFO +5 -3
  3. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/README.md +1 -1
  4. wuttaweb-0.19.0/docs/api/wuttaweb.cli.rst +6 -0
  5. wuttaweb-0.19.0/docs/api/wuttaweb.cli.webapp.rst +6 -0
  6. wuttaweb-0.19.0/docs/api/wuttaweb.emails.rst +6 -0
  7. wuttaweb-0.19.0/docs/api/wuttaweb.views.email.rst +6 -0
  8. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/conf.py +1 -0
  9. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/index.rst +5 -0
  10. wuttaweb-0.19.0/docs/narr/cli/builtin.rst +28 -0
  11. wuttaweb-0.19.0/docs/narr/cli/index.rst +14 -0
  12. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/pyproject.toml +7 -6
  13. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/app.py +104 -26
  14. wuttaweb-0.19.0/src/wuttaweb/cli/__init__.py +28 -0
  15. wuttaweb-0.19.0/src/wuttaweb/cli/webapp.py +88 -0
  16. wuttaweb-0.19.0/src/wuttaweb/emails.py +48 -0
  17. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/forms/schema.py +34 -1
  18. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/forms/widgets.py +37 -0
  19. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/menus.py +6 -0
  20. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/base.mako +11 -7
  21. wuttaweb-0.19.0/src/wuttaweb/templates/deform/readonly/email_recips.pt +5 -0
  22. wuttaweb-0.19.0/src/wuttaweb/templates/email/settings/view.mako +39 -0
  23. wuttaweb-0.19.0/src/wuttaweb/views/email.py +298 -0
  24. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/essential.py +2 -0
  25. wuttaweb-0.19.0/tests/cli/test_webapp.py +104 -0
  26. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/forms/test_schema.py +44 -0
  27. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/forms/test_widgets.py +50 -1
  28. wuttaweb-0.19.0/tests/test_app.py +131 -0
  29. wuttaweb-0.19.0/tests/test_emails.py +23 -0
  30. wuttaweb-0.19.0/tests/views/__init__.py +0 -0
  31. wuttaweb-0.19.0/tests/views/test_email.py +211 -0
  32. wuttaweb-0.19.0/tests/views/test_essential.py +10 -0
  33. wuttaweb-0.17.2/tests/test_app.py +0 -61
  34. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/.gitignore +0 -0
  35. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/COPYING.txt +0 -0
  36. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/Makefile +0 -0
  37. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/_static/.keepme +0 -0
  38. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.app.rst +0 -0
  39. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.auth.rst +0 -0
  40. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.conf.rst +0 -0
  41. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.db.continuum.rst +0 -0
  42. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.db.rst +0 -0
  43. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.db.sess.rst +0 -0
  44. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.base.rst +0 -0
  45. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.rst +0 -0
  46. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.schema.rst +0 -0
  47. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  48. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.grids.base.rst +0 -0
  49. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.grids.filters.rst +0 -0
  50. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.grids.rst +0 -0
  51. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.handler.rst +0 -0
  52. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.helpers.rst +0 -0
  53. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.menus.rst +0 -0
  54. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.progress.rst +0 -0
  55. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.rst +0 -0
  56. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.static.rst +0 -0
  57. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.subscribers.rst +0 -0
  58. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.util.rst +0 -0
  59. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.auth.rst +0 -0
  60. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.base.rst +0 -0
  61. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.batch.rst +0 -0
  62. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.common.rst +0 -0
  63. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.essential.rst +0 -0
  64. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.master.rst +0 -0
  65. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.people.rst +0 -0
  66. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.progress.rst +0 -0
  67. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.roles.rst +0 -0
  68. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.rst +0 -0
  69. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.settings.rst +0 -0
  70. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  71. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/api/wuttaweb.views.users.rst +0 -0
  72. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/glossary.rst +0 -0
  73. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/make.bat +0 -0
  74. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/narr/templates/base.rst +0 -0
  75. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/narr/templates/index.rst +0 -0
  76. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/narr/templates/lookup.rst +0 -0
  77. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/docs/narr/templates/overview.rst +0 -0
  78. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/__init__.py +0 -0
  79. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/_version.py +0 -0
  80. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/auth.py +0 -0
  81. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/conf.py +0 -0
  82. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/db/__init__.py +0 -0
  83. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/db/continuum.py +0 -0
  84. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/db/sess.py +0 -0
  85. {wuttaweb-0.17.2/src/wuttaweb/email/templates → wuttaweb-0.19.0/src/wuttaweb/email-templates}/feedback.html.mako +0 -0
  86. {wuttaweb-0.17.2/src/wuttaweb/email/templates → wuttaweb-0.19.0/src/wuttaweb/email-templates}/feedback.txt.mako +0 -0
  87. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/forms/__init__.py +0 -0
  88. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/forms/base.py +0 -0
  89. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/grids/__init__.py +0 -0
  90. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/grids/base.py +0 -0
  91. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/grids/filters.py +0 -0
  92. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/handler.py +0 -0
  93. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/helpers.py +0 -0
  94. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/progress.py +0 -0
  95. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/static/__init__.py +0 -0
  96. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  97. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/static/img/logo.png +0 -0
  98. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/static/img/testing.png +0 -0
  99. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/subscribers.py +0 -0
  100. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  101. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  102. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  103. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  104. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/base_meta.mako +0 -0
  105. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/batch/view.mako +0 -0
  106. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/configure.mako +0 -0
  107. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  108. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  109. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  110. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
  111. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
  112. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  113. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  114. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  115. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  116. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  117. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  118. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
  119. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  120. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  121. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  122. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  123. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  124. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/forbidden.mako +0 -0
  125. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/form.mako +0 -0
  126. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
  127. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  128. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  129. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/home.mako +0 -0
  130. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  131. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/create.mako +0 -0
  132. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  133. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  134. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/form.mako +0 -0
  135. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/index.mako +0 -0
  136. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/master/view.mako +0 -0
  137. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/notfound.mako +0 -0
  138. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/page.mako +0 -0
  139. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  140. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/progress.mako +0 -0
  141. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/setup.mako +0 -0
  142. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/upgrade.mako +0 -0
  143. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  144. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  145. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/templates/wutta-components.mako +0 -0
  146. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/util.py +0 -0
  147. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/__init__.py +0 -0
  148. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/auth.py +0 -0
  149. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/base.py +0 -0
  150. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/batch.py +0 -0
  151. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/common.py +0 -0
  152. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/master.py +0 -0
  153. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/people.py +0 -0
  154. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/progress.py +0 -0
  155. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/roles.py +0 -0
  156. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/settings.py +0 -0
  157. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/upgrades.py +0 -0
  158. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/src/wuttaweb/views/users.py +0 -0
  159. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tasks.py +0 -0
  160. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/__init__.py +0 -0
  161. {wuttaweb-0.17.2/tests/db → wuttaweb-0.19.0/tests/cli}/__init__.py +0 -0
  162. {wuttaweb-0.17.2/tests/forms → wuttaweb-0.19.0/tests/db}/__init__.py +0 -0
  163. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/db/test_continuum.py +0 -0
  164. {wuttaweb-0.17.2/tests/grids → wuttaweb-0.19.0/tests/forms}/__init__.py +0 -0
  165. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/forms/test_base.py +0 -0
  166. {wuttaweb-0.17.2/tests/views → wuttaweb-0.19.0/tests/grids}/__init__.py +0 -0
  167. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/grids/test_base.py +0 -0
  168. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/grids/test_filters.py +0 -0
  169. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  170. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  171. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_oruga.js +0 -0
  172. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  173. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  174. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_vue.js +0 -0
  175. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  176. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/buefy.css +0 -0
  177. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/buefy.js +0 -0
  178. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/fontawesome.js +0 -0
  179. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/vue.js +0 -0
  180. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/libcache/vue_resource.js +0 -0
  181. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_auth.py +0 -0
  182. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_handler.py +0 -0
  183. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_helpers.py +0 -0
  184. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_menus.py +0 -0
  185. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_progress.py +0 -0
  186. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_static.py +0 -0
  187. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_subscribers.py +0 -0
  188. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/test_util.py +0 -0
  189. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/util.py +0 -0
  190. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test___init__.py +0 -0
  191. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_auth.py +0 -0
  192. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_base.py +0 -0
  193. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_batch.py +0 -0
  194. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_common.py +0 -0
  195. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_master.py +0 -0
  196. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_people.py +0 -0
  197. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_progress.py +0 -0
  198. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_roles.py +0 -0
  199. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_settings.py +0 -0
  200. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_upgrades.py +0 -0
  201. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tests/views/test_users.py +0 -0
  202. {wuttaweb-0.17.2 → wuttaweb-0.19.0}/tox.ini +0 -0
@@ -5,6 +5,23 @@ 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
+
18
+ ## v0.18.0 (2024-12-18)
19
+
20
+ ### Feat
21
+
22
+ - add basic support for running in ASGI context
23
+ - add support for running via uvicorn; `wutta webapp` command
24
+
8
25
  ## v0.17.2 (2024-12-17)
9
26
 
10
27
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WuttaWeb
3
- Version: 0.17.2
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
@@ -25,6 +25,7 @@ Classifier: Programming Language :: Python :: 3.11
25
25
  Classifier: Topic :: Internet :: WWW/HTTP
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Requires-Python: >=3.8
28
+ Requires-Dist: asgiref
28
29
  Requires-Dist: colanderalchemy
29
30
  Requires-Dist: humanize
30
31
  Requires-Dist: markdown
@@ -38,20 +39,21 @@ Requires-Dist: pyramid-tm
38
39
  Requires-Dist: pyramid>=2
39
40
  Requires-Dist: waitress
40
41
  Requires-Dist: webhelpers2
41
- Requires-Dist: wuttjamaican[db]>=0.18.0
42
+ Requires-Dist: wuttjamaican[db]>=0.19.0
42
43
  Requires-Dist: zope-sqlalchemy>=1.5
43
44
  Provides-Extra: continuum
44
45
  Requires-Dist: wutta-continuum; extra == 'continuum'
45
46
  Provides-Extra: docs
46
47
  Requires-Dist: furo; extra == 'docs'
47
48
  Requires-Dist: sphinx; extra == 'docs'
49
+ Requires-Dist: sphinxcontrib-programoutput; extra == 'docs'
48
50
  Provides-Extra: tests
49
51
  Requires-Dist: pytest-cov; extra == 'tests'
50
52
  Requires-Dist: tox; extra == 'tests'
51
53
  Description-Content-Type: text/markdown
52
54
 
53
55
 
54
- # wuttaweb
56
+ # WuttaWeb
55
57
 
56
58
  Web app for Wutta Framework
57
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.cli``
3
+ ================
4
+
5
+ .. automodule:: wuttaweb.cli
6
+ :members:
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.cli.webapp``
3
+ =======================
4
+
5
+ .. automodule:: wuttaweb.cli.webapp
6
+ :members:
@@ -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:
@@ -21,6 +21,7 @@ extensions = [
21
21
  'sphinx.ext.intersphinx',
22
22
  'sphinx.ext.viewcode',
23
23
  'sphinx.ext.todo',
24
+ 'sphinxcontrib.programoutput',
24
25
  ]
25
26
 
26
27
  templates_path = ['_templates']
@@ -19,6 +19,7 @@ the narrative docs are pretty scant. That will eventually change.
19
19
  :caption: Documentation:
20
20
 
21
21
  glossary
22
+ narr/cli/index
22
23
  narr/templates/index
23
24
 
24
25
  .. toctree::
@@ -28,10 +29,13 @@ the narrative docs are pretty scant. That will eventually change.
28
29
  api/wuttaweb
29
30
  api/wuttaweb.app
30
31
  api/wuttaweb.auth
32
+ api/wuttaweb.cli
33
+ api/wuttaweb.cli.webapp
31
34
  api/wuttaweb.conf
32
35
  api/wuttaweb.db
33
36
  api/wuttaweb.db.continuum
34
37
  api/wuttaweb.db.sess
38
+ api/wuttaweb.emails
35
39
  api/wuttaweb.forms
36
40
  api/wuttaweb.forms.base
37
41
  api/wuttaweb.forms.schema
@@ -51,6 +55,7 @@ the narrative docs are pretty scant. That will eventually change.
51
55
  api/wuttaweb.views.base
52
56
  api/wuttaweb.views.batch
53
57
  api/wuttaweb.views.common
58
+ api/wuttaweb.views.email
54
59
  api/wuttaweb.views.essential
55
60
  api/wuttaweb.views.master
56
61
  api/wuttaweb.views.people
@@ -0,0 +1,28 @@
1
+
2
+ ===================
3
+ Built-in Commands
4
+ ===================
5
+
6
+ Below are the :term:`subcommands <subcommand>` which come with
7
+ WuttaWeb.
8
+
9
+
10
+ .. _wutta-webapp:
11
+
12
+ ``wutta webapp``
13
+ ----------------
14
+
15
+ Run the web app, according to config file(s).
16
+
17
+ This command is a convenience only; under the hood it can run `uvicorn
18
+ <https://www.uvicorn.org/#uvicornrun>`_ but by default will run
19
+ whatever :ref:`pserve <pyramid:pserve_script>` is setup to do (which
20
+ usually is `waitress
21
+ <https://docs.pylonsproject.org/projects/waitress/en/latest/index.html>`_).
22
+
23
+ Ultimately it's all up to config, so run different web apps with
24
+ different config files.
25
+
26
+ Defined in: :mod:`wuttaweb.cli.webapp`
27
+
28
+ .. program-output:: wutta webapp --help
@@ -0,0 +1,14 @@
1
+
2
+ ==============
3
+ Command Line
4
+ ==============
5
+
6
+ There isn't much to the command line for WuttaWeb, but here it is.
7
+
8
+ For more general info about CLI see
9
+ :doc:`wuttjamaican:narr/cli/index`.
10
+
11
+ .. toctree::
12
+ :maxdepth: 2
13
+
14
+ builtin
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.17.2"
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"}]
@@ -30,6 +30,7 @@ classifiers = [
30
30
  ]
31
31
  requires-python = ">= 3.8"
32
32
  dependencies = [
33
+ "asgiref",
33
34
  "ColanderAlchemy",
34
35
  "humanize",
35
36
  "markdown",
@@ -43,32 +44,32 @@ dependencies = [
43
44
  "pyramid_tm",
44
45
  "waitress",
45
46
  "WebHelpers2",
46
- "WuttJamaican[db]>=0.18.0",
47
+ "WuttJamaican[db]>=0.19.0",
47
48
  "zope.sqlalchemy>=1.5",
48
49
  ]
49
50
 
50
51
 
51
52
  [project.optional-dependencies]
52
53
  continuum = ["Wutta-Continuum"]
53
- docs = ["Sphinx", "furo"]
54
+ docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
54
55
  tests = ["pytest-cov", "tox"]
55
56
 
56
57
 
57
58
  [project.entry-points."fanstatic.libraries"]
58
59
  wuttaweb_img = "wuttaweb.static:img"
59
60
 
60
-
61
61
  [project.entry-points."paste.app_factory"]
62
62
  main = "wuttaweb.app:main"
63
63
 
64
-
65
64
  [project.entry-points."wutta.app.providers"]
66
65
  wuttaweb = "wuttaweb.app:WebAppProvider"
67
66
 
68
-
69
67
  [project.entry-points."wutta.config.extensions"]
70
68
  wuttaweb = "wuttaweb.conf:WuttaWebConfigExtension"
71
69
 
70
+ [project.entry-points."wutta.typer_imports"]
71
+ wuttaweb = "wuttaweb.cli"
72
+
72
73
 
73
74
  [project.urls]
74
75
  Homepage = "https://wuttaproject.org/"
@@ -24,23 +24,31 @@
24
24
  Application
25
25
  """
26
26
 
27
+ import logging
27
28
  import os
28
29
 
29
30
  from wuttjamaican.app import AppProvider
30
31
  from wuttjamaican.conf import make_config
31
32
 
33
+ from asgiref.wsgi import WsgiToAsgi
32
34
  from pyramid.config import Configurator
33
35
 
34
36
  import wuttaweb.db
35
37
  from wuttaweb.auth import WuttaSecurityPolicy
36
38
 
37
39
 
40
+ log = logging.getLogger(__name__)
41
+
42
+
38
43
  class WebAppProvider(AppProvider):
39
44
  """
40
45
  The :term:`app provider` for WuttaWeb. This adds some methods to
41
- 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.
42
49
  """
43
- email_templates = 'wuttaweb:email/templates'
50
+ email_modules = ['wuttaweb.emails']
51
+ email_templates = ['wuttaweb:email-templates']
44
52
 
45
53
  def get_web_handler(self, **kwargs):
46
54
  """
@@ -87,17 +95,20 @@ def make_wutta_config(settings, config_maker=None, **kwargs):
87
95
 
88
96
  If this config file path cannot be discovered, an error is raised.
89
97
  """
90
- # validate config file path
91
- path = settings.get('wutta.config')
92
- if not path or not os.path.exists(path):
93
- raise ValueError("Please set 'wutta.config' in [app:main] "
94
- "section of config to the path of your "
95
- "config file. Lame, but necessary.")
96
-
97
- # make config, add to settings
98
- config_maker = config_maker or make_config
99
- wutta_config = config_maker(path, **kwargs)
100
- settings['wutta_config'] = wutta_config
98
+ wutta_config = settings.get('wutta_config')
99
+ if not wutta_config:
100
+
101
+ # validate config file path
102
+ path = settings.get('wutta.config')
103
+ if not path or not os.path.exists(path):
104
+ raise ValueError("Please set 'wutta.config' in [app:main] "
105
+ "section of config to the path of your "
106
+ "config file. Lame, but necessary.")
107
+
108
+ # make config, add to settings
109
+ config_maker = config_maker or make_config
110
+ wutta_config = config_maker(path, **kwargs)
111
+ settings['wutta_config'] = wutta_config
101
112
 
102
113
  # configure database sessions
103
114
  if hasattr(wutta_config, 'appdb_engine'):
@@ -148,19 +159,18 @@ def make_pyramid_config(settings):
148
159
 
149
160
  def main(global_config, **settings):
150
161
  """
151
- This function returns a Pyramid WSGI application.
152
-
153
- Typically there is no need to call this function directly, but it
154
- may be configured as the web app entry point like so:
155
-
156
- .. code-block:: ini
157
-
158
- [app:main]
159
- use = egg:wuttaweb
160
-
161
- The app returned by this function is quite minimal, so most apps
162
- will need to define their own ``main()`` function, and use that
163
- instead.
162
+ Make and return the WSGI application, per given settings.
163
+
164
+ This function is designed to be called via Paste, hence it does
165
+ require params and therefore can't be used directly as app factory
166
+ for general WSGI servers. For the latter see
167
+ :func:`make_wsgi_app()` instead.
168
+
169
+ And this *particular* function is not even that useful, it only
170
+ constructs an app with minimal views built-in to WuttaWeb. Most
171
+ apps will define their own ``main()`` function (e.g. as
172
+ ``poser.web.app:main``), similar to this one but with additional
173
+ views and other config.
164
174
  """
165
175
  wutta_config = make_wutta_config(settings)
166
176
  pyramid_config = make_pyramid_config(settings)
@@ -170,3 +180,71 @@ def main(global_config, **settings):
170
180
  pyramid_config.include('wuttaweb.views')
171
181
 
172
182
  return pyramid_config.make_wsgi_app()
183
+
184
+
185
+ def make_wsgi_app(main_app=None, config=None):
186
+ """
187
+ Make and return a WSGI app, using the given Paste app factory.
188
+
189
+ See also :func:`make_asgi_app()` for the ASGI equivalent.
190
+
191
+ This function could be used directly for general WSGI servers
192
+ (e.g. uvicorn), ***if*** you just want the built-in :func:`main()`
193
+ app factory.
194
+
195
+ But most likely you do not, in which case you must define your own
196
+ function and call this one with your preferred app factory::
197
+
198
+ from wuttaweb.app import make_wsgi_app
199
+
200
+ def my_main(global_config, **settings):
201
+ # TODO: build your app
202
+ pass
203
+
204
+ def make_my_wsgi_app():
205
+ return make_wsgi_app(my_main)
206
+
207
+ So ``make_my_wsgi_app()`` could then be used as-is for general
208
+ WSGI servers. However, note that this approach will require
209
+ setting the ``WUTTA_CONFIG_FILES`` environment variable, unless
210
+ running via :ref:`wutta-webapp`.
211
+
212
+ :param main_app: Either a Paste-compatible app factory, or
213
+ :term:`spec` for one. If not specified, the built-in
214
+ :func:`main()` is assumed.
215
+
216
+ :param config: Optional :term:`config object`. If not specified,
217
+ one is created based on ``WUTTA_CONFIG_FILES`` environment
218
+ variable.
219
+ """
220
+ if not config:
221
+ config = make_config()
222
+ app = config.get_app()
223
+
224
+ # extract pyramid settings
225
+ settings = config.get_dict('app:main')
226
+
227
+ # keep same config object
228
+ settings['wutta_config'] = config
229
+
230
+ # determine the app factory
231
+ if isinstance(main_app, str):
232
+ make_wsgi_app = app.load_object(main_app)
233
+ elif callable(main_app):
234
+ make_wsgi_app = main_app
235
+ else:
236
+ raise ValueError("main_app must be spec or callable")
237
+
238
+ # construct a pyramid app "per usual"
239
+ return make_wsgi_app({}, **settings)
240
+
241
+
242
+ def make_asgi_app(main_app=None, config=None):
243
+ """
244
+ Make and return a ASGI app, using the given Paste app factory.
245
+
246
+ This works the same as :func:`make_wsgi_app()` and should be
247
+ called in the same way etc.
248
+ """
249
+ wsgi_app = make_wsgi_app(main_app, config=config)
250
+ return WsgiToAsgi(wsgi_app)
@@ -0,0 +1,28 @@
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
+ WuttaWeb - ``wutta`` subcommands
25
+ """
26
+
27
+ # nb. must bring in all modules for discovery to work
28
+ from . import webapp
@@ -0,0 +1,88 @@
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
+ See also: :ref:`wutta-webapp`
25
+ """
26
+
27
+ import os
28
+ import sys
29
+ from typing_extensions import Annotated
30
+
31
+ import typer
32
+ from pyramid.scripts import pserve
33
+
34
+ from wuttjamaican.cli import wutta_typer
35
+
36
+
37
+ @wutta_typer.command()
38
+ def webapp(
39
+ ctx: typer.Context,
40
+ auto_reload: Annotated[
41
+ bool,
42
+ typer.Option('--reload', '-r',
43
+ help="Auto-reload web app when files change.")] = False,
44
+ ):
45
+ """
46
+ Run the configured web app
47
+ """
48
+ config = ctx.parent.wutta_config
49
+
50
+ # we'll need config file(s) to specify for web app
51
+ if not config.files_read:
52
+ sys.stderr.write("no config files found!\n")
53
+ sys.exit(1)
54
+
55
+ runner = config.get(f'{config.appname}.web.app.runner', default='pserve')
56
+ if runner == 'pserve':
57
+
58
+ # run pserve
59
+ argv = ['pserve', f'file+ini:{config.files_read[0]}']
60
+ if ctx.params['auto_reload']:
61
+ argv.append('--reload')
62
+ pserve.main(argv=argv)
63
+
64
+ elif runner == 'uvicorn':
65
+
66
+ import uvicorn
67
+
68
+ # need service details from config
69
+ spec = config.require(f'{config.appname}.web.app.spec')
70
+ kw = {
71
+ 'host': config.get(f'{config.appname}.web.app.host', default='127.0.0.1'),
72
+ 'port': config.get_int(f'{config.appname}.web.app.port', default=8000),
73
+ 'reload': ctx.params['auto_reload'],
74
+ 'reload_dirs': config.get_list(f'{config.appname}.web.app.reload_dirs'),
75
+ 'factory': config.get_bool(f'{config.appname}.web.app.factory', default=False),
76
+ 'interface': config.get(f'{config.appname}.web.app.interface', default='auto'),
77
+ }
78
+
79
+ # also must inject our config files to env, since there is no
80
+ # other way to specify when running via uvicorn
81
+ os.environ['WUTTA_CONFIG_FILES'] = os.pathsep.join(config.files_read)
82
+
83
+ # run uvicorn
84
+ uvicorn.run(spec, **kw)
85
+
86
+ else:
87
+ sys.stderr.write(f"unknown web app runner: {runner}\n")
88
+ sys.exit(2)
@@ -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