WuttaWeb 0.16.1__tar.gz → 0.17.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 (190) hide show
  1. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/CHANGELOG.md +32 -0
  2. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/PKG-INFO +3 -2
  3. wuttaweb-0.17.0/docs/api/wuttaweb.views.batch.rst +6 -0
  4. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/conf.py +2 -0
  5. wuttaweb-0.17.0/docs/glossary.rst +30 -0
  6. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/index.rst +1 -0
  7. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/pyproject.toml +7 -2
  8. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/forms/base.py +132 -14
  9. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/forms/schema.py +77 -3
  10. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/forms/widgets.py +99 -3
  11. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/grids/base.py +48 -1
  12. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/grids/filters.py +35 -4
  13. wuttaweb-0.17.0/src/wuttaweb/handler.py +144 -0
  14. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/menus.py +6 -1
  15. wuttaweb-0.17.0/src/wuttaweb/static/__init__.py +74 -0
  16. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/subscribers.py +8 -3
  17. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/base.mako +10 -0
  18. wuttaweb-0.17.0/src/wuttaweb/templates/base_meta.mako +23 -0
  19. wuttaweb-0.17.0/src/wuttaweb/templates/batch/view.mako +124 -0
  20. wuttaweb-0.17.0/src/wuttaweb/templates/deform/dateinput.pt +6 -0
  21. wuttaweb-0.17.0/src/wuttaweb/templates/deform/datetimeinput.pt +10 -0
  22. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/readonly/objectref.pt +1 -1
  23. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/form.mako +21 -0
  24. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/forms/vue_template.mako +8 -0
  25. wuttaweb-0.17.0/src/wuttaweb/templates/master/view.mako +52 -0
  26. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/page.mako +5 -1
  27. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/wutta-components.mako +163 -0
  28. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/util.py +19 -2
  29. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/base.py +5 -3
  30. wuttaweb-0.17.0/src/wuttaweb/views/batch.py +404 -0
  31. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/master.py +278 -12
  32. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/roles.py +64 -2
  33. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/upgrades.py +1 -2
  34. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/users.py +1 -2
  35. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/forms/test_base.py +88 -0
  36. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/forms/test_schema.py +63 -2
  37. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/forms/test_widgets.py +92 -5
  38. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/grids/test_base.py +38 -0
  39. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/grids/test_filters.py +47 -3
  40. wuttaweb-0.17.0/tests/test_handler.py +76 -0
  41. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_util.py +31 -0
  42. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/util.py +5 -0
  43. wuttaweb-0.17.0/tests/views/test_batch.py +373 -0
  44. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_master.py +159 -2
  45. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_roles.py +31 -0
  46. wuttaweb-0.16.1/docs/glossary.rst +0 -10
  47. wuttaweb-0.16.1/src/wuttaweb/handler.py +0 -57
  48. wuttaweb-0.16.1/src/wuttaweb/static/__init__.py +0 -37
  49. wuttaweb-0.16.1/src/wuttaweb/templates/base_meta.mako +0 -23
  50. wuttaweb-0.16.1/src/wuttaweb/templates/master/view.mako +0 -9
  51. wuttaweb-0.16.1/tests/test_handler.py +0 -20
  52. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/.gitignore +0 -0
  53. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/COPYING.txt +0 -0
  54. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/README.md +0 -0
  55. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/Makefile +0 -0
  56. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/_static/.keepme +0 -0
  57. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.app.rst +0 -0
  58. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.auth.rst +0 -0
  59. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.conf.rst +0 -0
  60. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.db.continuum.rst +0 -0
  61. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.db.rst +0 -0
  62. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.db.sess.rst +0 -0
  63. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.forms.base.rst +0 -0
  64. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.forms.rst +0 -0
  65. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.forms.schema.rst +0 -0
  66. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.forms.widgets.rst +0 -0
  67. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.grids.base.rst +0 -0
  68. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.grids.filters.rst +0 -0
  69. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.grids.rst +0 -0
  70. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.handler.rst +0 -0
  71. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.helpers.rst +0 -0
  72. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.menus.rst +0 -0
  73. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.progress.rst +0 -0
  74. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.rst +0 -0
  75. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.static.rst +0 -0
  76. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.subscribers.rst +0 -0
  77. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.util.rst +0 -0
  78. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.auth.rst +0 -0
  79. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.base.rst +0 -0
  80. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.common.rst +0 -0
  81. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.essential.rst +0 -0
  82. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.master.rst +0 -0
  83. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.people.rst +0 -0
  84. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.progress.rst +0 -0
  85. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.roles.rst +0 -0
  86. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.rst +0 -0
  87. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.settings.rst +0 -0
  88. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.upgrades.rst +0 -0
  89. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/api/wuttaweb.views.users.rst +0 -0
  90. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/make.bat +0 -0
  91. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/narr/templates/base.rst +0 -0
  92. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/narr/templates/index.rst +0 -0
  93. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/narr/templates/lookup.rst +0 -0
  94. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/docs/narr/templates/overview.rst +0 -0
  95. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/__init__.py +0 -0
  96. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/_version.py +0 -0
  97. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/app.py +0 -0
  98. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/auth.py +0 -0
  99. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/conf.py +0 -0
  100. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/db/__init__.py +0 -0
  101. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/db/continuum.py +0 -0
  102. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/db/sess.py +0 -0
  103. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/email/templates/feedback.html.mako +0 -0
  104. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/email/templates/feedback.txt.mako +0 -0
  105. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/forms/__init__.py +0 -0
  106. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/grids/__init__.py +0 -0
  107. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/helpers.py +0 -0
  108. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/progress.py +0 -0
  109. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/static/img/favicon.ico +0 -0
  110. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/static/img/logo.png +0 -0
  111. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/static/img/testing.png +0 -0
  112. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/appinfo/configure.mako +0 -0
  113. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/appinfo/index.mako +0 -0
  114. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/auth/change_password.mako +0 -0
  115. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/auth/login.mako +0 -0
  116. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/configure.mako +0 -0
  117. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
  118. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
  119. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
  120. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
  121. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/password.pt +0 -0
  122. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/permissions.pt +0 -0
  123. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
  124. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
  125. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
  126. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
  127. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
  128. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/select.pt +0 -0
  129. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/textarea.pt +0 -0
  130. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/deform/textinput.pt +0 -0
  131. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/forbidden.mako +0 -0
  132. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/grids/table_element.mako +0 -0
  133. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/grids/vue_template.mako +0 -0
  134. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/home.mako +0 -0
  135. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/master/configure.mako +0 -0
  136. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/master/create.mako +0 -0
  137. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/master/delete.mako +0 -0
  138. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/master/edit.mako +0 -0
  139. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/master/form.mako +0 -0
  140. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/master/index.mako +0 -0
  141. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/notfound.mako +0 -0
  142. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/people/view_profile.mako +0 -0
  143. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/progress.mako +0 -0
  144. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/setup.mako +0 -0
  145. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/upgrade.mako +0 -0
  146. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
  147. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/templates/upgrades/view.mako +0 -0
  148. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/__init__.py +0 -0
  149. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/auth.py +0 -0
  150. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/common.py +0 -0
  151. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/essential.py +0 -0
  152. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/people.py +0 -0
  153. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/progress.py +0 -0
  154. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/src/wuttaweb/views/settings.py +0 -0
  155. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tasks.py +0 -0
  156. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/__init__.py +0 -0
  157. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/db/__init__.py +0 -0
  158. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/db/test_continuum.py +0 -0
  159. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/forms/__init__.py +0 -0
  160. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/grids/__init__.py +0 -0
  161. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
  162. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
  163. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_oruga.js +0 -0
  164. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_oruga_bulma.css +0 -0
  165. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_oruga_bulma.js +0 -0
  166. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_vue.js +0 -0
  167. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/bb_vue_fontawesome.js +0 -0
  168. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/buefy.css +0 -0
  169. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/buefy.js +0 -0
  170. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/fontawesome.js +0 -0
  171. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/vue.js +0 -0
  172. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/libcache/vue_resource.js +0 -0
  173. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_app.py +0 -0
  174. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_auth.py +0 -0
  175. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_helpers.py +0 -0
  176. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_menus.py +0 -0
  177. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_progress.py +0 -0
  178. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_static.py +0 -0
  179. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/test_subscribers.py +0 -0
  180. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/__init__.py +0 -0
  181. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test___init__.py +0 -0
  182. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_auth.py +0 -0
  183. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_base.py +0 -0
  184. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_common.py +0 -0
  185. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_people.py +0 -0
  186. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_progress.py +0 -0
  187. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_settings.py +0 -0
  188. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_upgrades.py +0 -0
  189. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tests/views/test_users.py +0 -0
  190. {wuttaweb-0.16.1 → wuttaweb-0.17.0}/tox.ini +0 -0
@@ -5,6 +5,38 @@ 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.17.0 (2024-12-15)
9
+
10
+ ### Feat
11
+
12
+ - add basic support for batch execution
13
+ - add basic support for rows grid for master, batch views
14
+ - add basic master view class for batches
15
+
16
+ ### Fix
17
+
18
+ - add handling for decimal values and lists, in `make_json_safe()`
19
+ - fix behavior when editing Roles for a User
20
+ - add basic views for raw Permissions
21
+ - improve support for date, datetime fields in grids, forms
22
+ - add way to set field widgets using pseudo-type
23
+ - add support for date, datetime form fields
24
+ - make dropdown widgets as wide as other text fields in main form
25
+ - add fallback instance title
26
+ - display "global" errors at top of form, if present
27
+ - add `make_form()` and `make_grid()` methods on web handler
28
+ - correct "empty option" behavior for `ObjectRef` schema type
29
+ - use fanstatic to serve built-in images by default
30
+
31
+ ## v0.16.2 (2024-12-10)
32
+
33
+ ### Fix
34
+
35
+ - add `GridWidget` and `form.set_grid()` for convenience
36
+ - add "is false or null" grid filter, for nullable bool columns
37
+ - remove Person column for `Person.users` grid display
38
+ - flatten UUID to str for `make_json_safe()`
39
+
8
40
  ## v0.16.1 (2024-12-08)
9
41
 
10
42
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: WuttaWeb
3
- Version: 0.16.1
3
+ Version: 0.17.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
@@ -26,6 +26,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
26
  Requires-Python: >=3.8
27
27
  Requires-Dist: colanderalchemy
28
28
  Requires-Dist: humanize
29
+ Requires-Dist: markdown
29
30
  Requires-Dist: paginate
30
31
  Requires-Dist: paginate-sqlalchemy
31
32
  Requires-Dist: pyramid-beaker
@@ -36,7 +37,7 @@ Requires-Dist: pyramid-tm
36
37
  Requires-Dist: pyramid>=2
37
38
  Requires-Dist: waitress
38
39
  Requires-Dist: webhelpers2
39
- Requires-Dist: wuttjamaican[db]>=0.17.1
40
+ Requires-Dist: wuttjamaican[db]>=0.18.0
40
41
  Requires-Dist: zope-sqlalchemy>=1.5
41
42
  Provides-Extra: continuum
42
43
  Requires-Dist: wutta-continuum; extra == 'continuum'
@@ -0,0 +1,6 @@
1
+
2
+ ``wuttaweb.views.batch``
3
+ ========================
4
+
5
+ .. automodule:: wuttaweb.views.batch
6
+ :members:
@@ -29,9 +29,11 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
29
29
  intersphinx_mapping = {
30
30
  'colander': ('https://docs.pylonsproject.org/projects/colander/en/latest/', None),
31
31
  'deform': ('https://docs.pylonsproject.org/projects/deform/en/latest/', None),
32
+ 'fanstatic': ('https://www.fanstatic.org/en/latest/', None),
32
33
  'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
33
34
  'python': ('https://docs.python.org/3/', None),
34
35
  'rattail-manual': ('https://rattailproject.org/docs/rattail-manual/', None),
36
+ 'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None),
35
37
  'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
36
38
  'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
37
39
  'wutta-continuum': ('https://rattailproject.org/docs/wutta-continuum/', None),
@@ -0,0 +1,30 @@
1
+ .. _glossary:
2
+
3
+ Glossary
4
+ ========
5
+
6
+ .. glossary::
7
+ :sorted:
8
+
9
+ grid
10
+ This refers to a "table of data, with features" essentially.
11
+ Sometimes it may be displayed as a simple table with no features,
12
+ or sometimes it has sortable columns, search filters and other
13
+ tools.
14
+
15
+ See also the :class:`~wuttaweb.grids.base.Grid` base class.
16
+
17
+ menu handler
18
+ This is the :term:`handler` responsible for constructing the main
19
+ app menu at top of page.
20
+
21
+ The menu handler is accessed by way of the :term:`web handler`.
22
+
23
+ See also the :class:`~wuttaweb.menus.MenuHandler` base class.
24
+
25
+ web handler
26
+ This is the :term:`handler` responsible for overall web layer
27
+ customizations, e.g. logo image and menu overrides. Although
28
+ the latter it delegates to the :term:`menu handler`.
29
+
30
+ See also the :class:`~wuttaweb.handler.WebHandler` base class.
@@ -49,6 +49,7 @@ the narrative docs are pretty scant. That will eventually change.
49
49
  api/wuttaweb.views
50
50
  api/wuttaweb.views.auth
51
51
  api/wuttaweb.views.base
52
+ api/wuttaweb.views.batch
52
53
  api/wuttaweb.views.common
53
54
  api/wuttaweb.views.essential
54
55
  api/wuttaweb.views.master
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "WuttaWeb"
9
- version = "0.16.1"
9
+ version = "0.17.0"
10
10
  description = "Web App for Wutta Framework"
11
11
  readme = "README.md"
12
12
  authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -32,6 +32,7 @@ requires-python = ">= 3.8"
32
32
  dependencies = [
33
33
  "ColanderAlchemy",
34
34
  "humanize",
35
+ "markdown",
35
36
  "paginate",
36
37
  "paginate_sqlalchemy",
37
38
  "pyramid>=2",
@@ -42,7 +43,7 @@ dependencies = [
42
43
  "pyramid_tm",
43
44
  "waitress",
44
45
  "WebHelpers2",
45
- "WuttJamaican[db]>=0.17.1",
46
+ "WuttJamaican[db]>=0.18.0",
46
47
  "zope.sqlalchemy>=1.5",
47
48
  ]
48
49
 
@@ -53,6 +54,10 @@ docs = ["Sphinx", "furo"]
53
54
  tests = ["pytest-cov", "tox"]
54
55
 
55
56
 
57
+ [project.entry-points."fanstatic.libraries"]
58
+ wuttaweb_img = "wuttaweb.static:img"
59
+
60
+
56
61
  [project.entry-points."paste.app_factory"]
57
62
  main = "wuttaweb.app:main"
58
63
 
@@ -27,6 +27,9 @@ Base form classes
27
27
  import logging
28
28
  from collections import OrderedDict
29
29
 
30
+ import sqlalchemy as sa
31
+ from sqlalchemy import orm
32
+
30
33
  import colander
31
34
  import deform
32
35
  from colanderalchemy import SQLAlchemySchemaNode
@@ -311,6 +314,7 @@ class Form:
311
314
  self.model_class = type(self.model_instance)
312
315
 
313
316
  self.set_fields(fields or self.get_fields())
317
+ self.set_default_widgets()
314
318
 
315
319
  # nb. this tracks grid JSON data for inclusion in page template
316
320
  self.grid_vue_context = OrderedDict()
@@ -457,23 +461,122 @@ class Form:
457
461
  if self.schema:
458
462
  self.schema[key] = node
459
463
 
460
- def set_widget(self, key, widget):
464
+ def set_widget(self, key, widget, **kwargs):
461
465
  """
462
466
  Set/override the widget for a field.
463
467
 
468
+ You can specify a widget instance or else a named "type" of
469
+ widget, in which case that is passed along to
470
+ :meth:`make_widget()`.
471
+
464
472
  :param key: Name of field.
465
473
 
466
- :param widget: Instance of
467
- :class:`deform:deform.widget.Widget`.
474
+ :param widget: Either a :class:`deform:deform.widget.Widget`
475
+ instance, or else a widget "type" name.
476
+
477
+ :param \**kwargs: Any remaining kwargs are passed along to
478
+ :meth:`make_widget()` - if applicable.
468
479
 
469
480
  Widget overrides are tracked via :attr:`widgets`.
470
481
  """
482
+ if not isinstance(widget, deform.widget.Widget):
483
+ widget_obj = self.make_widget(widget, **kwargs)
484
+ if not widget_obj:
485
+ raise ValueError(f"widget type not supported: {widget}")
486
+ widget = widget_obj
487
+
471
488
  self.widgets[key] = widget
472
489
 
473
490
  # update schema if necessary
474
491
  if self.schema and key in self.schema:
475
492
  self.schema[key].widget = widget
476
493
 
494
+ def make_widget(self, widget_type, **kwargs):
495
+ """
496
+ Make and return a new field widget of the given type.
497
+
498
+ This has built-in support for the following types (although
499
+ subclass can override as needed):
500
+
501
+ * ``'notes'`` => :class:`~wuttaweb.forms.widgets.NotesWidget`
502
+
503
+ See also :meth:`set_widget()` which may call this method
504
+ automatically.
505
+
506
+ :param widget_type: Which of the above (or custom) widget
507
+ type to create.
508
+
509
+ :param \**kwargs: Remaining kwargs are passed as-is to the
510
+ widget factory.
511
+
512
+ :returns: New widget instance, or ``None`` if e.g. it could
513
+ not determine how to create the widget.
514
+ """
515
+ from wuttaweb.forms import widgets
516
+
517
+ if widget_type == 'notes':
518
+ return widgets.NotesWidget(**kwargs)
519
+
520
+ def set_default_widgets(self):
521
+ """
522
+ Set default field widgets, where applicable.
523
+
524
+ This will add new entries to :attr:`widgets` for columns
525
+ whose data type implies a default widget should be used.
526
+ This is generally only possible if :attr:`model_class` is set
527
+ to a valid SQLAlchemy mapped class.
528
+
529
+ As of writing this only looks for
530
+ :class:`sqlalchemy:sqlalchemy.types.DateTime` fields and if
531
+ any are found, they are configured to use
532
+ :class:`~wuttaweb.forms.widgets.WuttaDateTimeWidget()`.
533
+ """
534
+ from wuttaweb.forms import widgets
535
+
536
+ if not self.model_class:
537
+ return
538
+
539
+ for key in self.fields:
540
+ if key in self.widgets:
541
+ continue
542
+
543
+ attr = getattr(self.model_class, key, None)
544
+ if attr:
545
+ prop = getattr(attr, 'prop', None)
546
+ if prop and isinstance(prop, orm.ColumnProperty):
547
+ column = prop.columns[0]
548
+ if isinstance(column.type, sa.DateTime):
549
+ # self.set_renderer(key, self.render_datetime)
550
+ self.set_widget(key, widgets.WuttaDateTimeWidget(self.request))
551
+
552
+ def set_grid(self, key, grid):
553
+ """
554
+ Establish a :term:`grid` to be displayed for a field. This
555
+ uses a :class:`~wuttaweb.forms.widgets.GridWidget` to wrap the
556
+ rendered grid.
557
+
558
+ :param key: Name of field.
559
+
560
+ :param widget: :class:`~wuttaweb.grids.base.Grid` instance,
561
+ pre-configured and (usually) with data.
562
+ """
563
+ from wuttaweb.forms.widgets import GridWidget
564
+
565
+ widget = GridWidget(self.request, grid)
566
+ self.set_widget(key, widget)
567
+ self.add_grid_vue_context(grid)
568
+
569
+ def add_grid_vue_context(self, grid):
570
+ """ """
571
+ if not grid.key:
572
+ raise ValueError("grid must have a key!")
573
+
574
+ if grid.key in self.grid_vue_context:
575
+ log.warning("grid data with key '%s' already registered, "
576
+ "but will be replaced", grid.key)
577
+
578
+ self.grid_vue_context[grid.key] = grid.get_vue_context()
579
+
477
580
  def set_validator(self, key, validator):
478
581
  """
479
582
  Set/override the validator for a field, or the form.
@@ -848,17 +951,6 @@ class Form:
848
951
  output = render(template, context)
849
952
  return HTML.literal(output)
850
953
 
851
- def add_grid_vue_context(self, grid):
852
- """ """
853
- if not grid.key:
854
- raise ValueError("grid must have a key!")
855
-
856
- if grid.key in self.grid_vue_context:
857
- log.warning("grid data with key '%s' already registered, "
858
- "but will be replaced", grid.key)
859
-
860
- self.grid_vue_context[grid.key] = grid.get_vue_context()
861
-
862
954
  def render_vue_field(
863
955
  self,
864
956
  fieldname,
@@ -1094,6 +1186,32 @@ class Form:
1094
1186
 
1095
1187
  return self.validated
1096
1188
 
1189
+ def has_global_errors(self):
1190
+ """
1191
+ Convenience function to check if the form has any "global"
1192
+ (not field-level) errors.
1193
+
1194
+ See also :meth:`get_global_errors()`.
1195
+
1196
+ :returns: ``True`` if global errors present, else ``False``.
1197
+ """
1198
+ dform = self.get_deform()
1199
+ return bool(dform.error)
1200
+
1201
+ def get_global_errors(self):
1202
+ """
1203
+ Returns a list of "global" (not field-level) error messages
1204
+ for the form.
1205
+
1206
+ See also :meth:`has_global_errors()`.
1207
+
1208
+ :returns: List of error messages (possibly empty).
1209
+ """
1210
+ dform = self.get_deform()
1211
+ if dform.error is None:
1212
+ return []
1213
+ return dform.error.messages()
1214
+
1097
1215
  def get_field_errors(self, field):
1098
1216
  """
1099
1217
  Return a list of error messages for the given field.
@@ -24,15 +24,48 @@
24
24
  Form schema types
25
25
  """
26
26
 
27
+ import datetime
27
28
  import uuid as _uuid
28
29
 
29
30
  import colander
31
+ import sqlalchemy as sa
30
32
 
31
33
  from wuttaweb.db import Session
32
34
  from wuttaweb.forms import widgets
33
35
  from wuttjamaican.db.model import Person
34
36
 
35
37
 
38
+ class WuttaDateTime(colander.DateTime):
39
+ """
40
+ Custom schema type for ``datetime`` fields.
41
+
42
+ This should be used automatically for
43
+ :class:`sqlalchemy:sqlalchemy.types.DateTime` columns unless you
44
+ register another default.
45
+
46
+ This schema type exists for sake of convenience, when working with
47
+ the Buefy datepicker + timepicker widgets.
48
+ """
49
+
50
+ def deserialize(self, node, cstruct):
51
+ """ """
52
+ if not cstruct:
53
+ return colander.null
54
+
55
+ formats = [
56
+ '%Y-%m-%dT%H:%M:%S',
57
+ '%Y-%m-%dT%I:%M %p',
58
+ ]
59
+
60
+ for fmt in formats:
61
+ try:
62
+ return datetime.datetime.strptime(cstruct, fmt)
63
+ except:
64
+ pass
65
+
66
+ node.raise_invalid("Invalid date and/or time")
67
+
68
+
36
69
  class ObjectNode(colander.SchemaNode):
37
70
  """
38
71
  Custom schema node class which adds methods for compatibility with
@@ -207,6 +240,11 @@ class ObjectRef(colander.SchemaType):
207
240
 
208
241
  def serialize(self, node, appstruct):
209
242
  """ """
243
+ # nb. normalize to empty option if no object ref, so that
244
+ # works as expected
245
+ if self.empty_option and not appstruct:
246
+ return self.empty_option[0]
247
+
210
248
  if appstruct is colander.null:
211
249
  return colander.null
212
250
 
@@ -214,7 +252,7 @@ class ObjectRef(colander.SchemaType):
214
252
  node.model_instance = appstruct
215
253
 
216
254
  # serialize to uuid
217
- return appstruct.uuid
255
+ return appstruct.uuid.hex
218
256
 
219
257
  def deserialize(self, node, cstruct):
220
258
  """ """
@@ -296,7 +334,7 @@ class ObjectRef(colander.SchemaType):
296
334
  if 'values' not in kwargs:
297
335
  query = self.get_query()
298
336
  objects = query.all()
299
- values = [(obj.uuid, str(obj))
337
+ values = [(obj.uuid.hex, str(obj))
300
338
  for obj in objects]
301
339
  if self.empty_option:
302
340
  values.insert(0, self.empty_option)
@@ -344,6 +382,30 @@ class PersonRef(ObjectRef):
344
382
  return self.request.route_url('people.view', uuid=person.uuid)
345
383
 
346
384
 
385
+ class RoleRef(ObjectRef):
386
+ """
387
+ Custom schema type for a
388
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role` reference
389
+ field.
390
+
391
+ This is a subclass of :class:`ObjectRef`.
392
+ """
393
+
394
+ @property
395
+ def model_class(self):
396
+ """ """
397
+ model = self.app.model
398
+ return model.Role
399
+
400
+ def sort_query(self, query):
401
+ """ """
402
+ return query.order_by(self.model_class.name)
403
+
404
+ def get_object_url(self, role):
405
+ """ """
406
+ return self.request.route_url('roles.view', uuid=role.uuid)
407
+
408
+
347
409
  class UserRef(ObjectRef):
348
410
  """
349
411
  Custom schema type for a
@@ -391,16 +453,24 @@ class RoleRefs(WuttaSet):
391
453
  if 'values' not in kwargs:
392
454
  model = self.app.model
393
455
  auth = self.app.get_auth_handler()
456
+
457
+ # avoid built-ins which cannot be assigned to users
394
458
  avoid = {
395
459
  auth.get_role_authenticated(self.session),
396
460
  auth.get_role_anonymous(self.session),
397
461
  }
398
462
  avoid = set([role.uuid for role in avoid])
463
+
464
+ # also avoid admin unless current user is root
465
+ if not self.request.is_root:
466
+ avoid.add(auth.get_role_administrator(self.session).uuid)
467
+
468
+ # everything else can be (un)assigned for users
399
469
  roles = self.session.query(model.Role)\
400
470
  .filter(~model.Role.uuid.in_(avoid))\
401
471
  .order_by(model.Role.name)\
402
472
  .all()
403
- values = [(role.uuid, role.name) for role in roles]
473
+ values = [(role.uuid.hex, role.name) for role in roles]
404
474
  kwargs['values'] = values
405
475
 
406
476
  return widgets.RoleRefsWidget(self.request, **kwargs)
@@ -497,3 +567,7 @@ class FileDownload(colander.String):
497
567
  """ """
498
568
  kwargs.setdefault('url', self.url)
499
569
  return widgets.FileDownloadWidget(self.request, **kwargs)
570
+
571
+
572
+ # nb. colanderalchemy schema overrides
573
+ sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime}
@@ -36,9 +36,11 @@ in the namespace:
36
36
  * :class:`deform:deform.widget.CheckboxWidget`
37
37
  * :class:`deform:deform.widget.SelectWidget`
38
38
  * :class:`deform:deform.widget.CheckboxChoiceWidget`
39
+ * :class:`deform:deform.widget.DateTimeInputWidget`
39
40
  * :class:`deform:deform.widget.MoneyInputWidget`
40
41
  """
41
42
 
43
+ import datetime
42
44
  import os
43
45
 
44
46
  import colander
@@ -46,7 +48,7 @@ import humanize
46
48
  from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
47
49
  PasswordWidget, CheckedPasswordWidget,
48
50
  CheckboxWidget, SelectWidget, CheckboxChoiceWidget,
49
- MoneyInputWidget)
51
+ DateTimeInputWidget, MoneyInputWidget)
50
52
  from webhelpers2.html import HTML
51
53
 
52
54
  from wuttaweb.db import Session
@@ -102,7 +104,7 @@ class ObjectRefWidget(SelectWidget):
102
104
  # add url, only if rendering readonly
103
105
  readonly = kw.get('readonly', self.readonly)
104
106
  if readonly:
105
- if 'url' not in values and self.url and field.schema.model_instance:
107
+ if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None):
106
108
  values['url'] = self.url(field.schema.model_instance)
107
109
 
108
110
  return values
@@ -153,6 +155,43 @@ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
153
155
  self.session = session or Session()
154
156
 
155
157
 
158
+ class WuttaDateTimeWidget(DateTimeInputWidget):
159
+ """
160
+ Custom widget for :class:`python:datetime.datetime` fields.
161
+
162
+ The main purpose of this widget is to leverage
163
+ :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()`
164
+ for the readonly display.
165
+
166
+ It is automatically used for SQLAlchemy mapped classes where the
167
+ field maps to a :class:`sqlalchemy:sqlalchemy.types.DateTime`
168
+ column. For other (non-mapped) datetime fields, you may have to
169
+ use it explicitly via
170
+ :meth:`~wuttaweb.forms.base.Form.set_widget()`.
171
+
172
+ This is a subclass of
173
+ :class:`deform:deform.widget.DateTimeInputWidget` and uses these
174
+ Deform templates:
175
+
176
+ * ``datetimeinput``
177
+ """
178
+
179
+ def __init__(self, request, *args, **kwargs):
180
+ super().__init__(*args, **kwargs)
181
+ self.request = request
182
+ self.config = self.request.wutta_config
183
+ self.app = self.config.get_app()
184
+
185
+ def serialize(self, field, cstruct, **kw):
186
+ """ """
187
+ readonly = kw.get('readonly', self.readonly)
188
+ if readonly and cstruct:
189
+ dt = datetime.datetime.fromisoformat(cstruct)
190
+ return self.app.render_datetime(dt)
191
+
192
+ return super().serialize(field, cstruct, **kw)
193
+
194
+
156
195
  class FileDownloadWidget(Widget):
157
196
  """
158
197
  Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
@@ -210,6 +249,44 @@ class FileDownloadWidget(Widget):
210
249
  return humanize.naturalsize(size)
211
250
 
212
251
 
252
+ class GridWidget(Widget):
253
+ """
254
+ Widget for fields whose data is represented by a :term:`grid`.
255
+
256
+ This is a subclass of :class:`deform:deform.widget.Widget` but
257
+ does not use any Deform templates.
258
+
259
+ This widget only supports "readonly" mode, is not editable. It is
260
+ merely a convenience around the grid itself, which does the heavy
261
+ lifting.
262
+
263
+ Instead of creating this widget directly you probably should call
264
+ :meth:`~wuttaweb.forms.base.Form.set_grid()` on your form.
265
+
266
+ :param request: Current :term:`request` object.
267
+
268
+ :param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to
269
+ display the field data.
270
+ """
271
+
272
+ def __init__(self, request, grid, *args, **kwargs):
273
+ super().__init__(*args, **kwargs)
274
+ self.request = request
275
+ self.grid = grid
276
+
277
+ def serialize(self, field, cstruct, **kw):
278
+ """
279
+ This widget simply calls
280
+ :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on
281
+ the ``grid`` to serialize.
282
+ """
283
+ readonly = kw.get('readonly', self.readonly)
284
+ if not readonly:
285
+ raise NotImplementedError("edit not allowed for this widget")
286
+
287
+ return self.grid.render_table_element()
288
+
289
+
213
290
  class RoleRefsWidget(WuttaCheckboxChoiceWidget):
214
291
  """
215
292
  Widget for use with User
@@ -280,7 +357,7 @@ class UserRefsWidget(WuttaCheckboxChoiceWidget):
280
357
  raise NotImplementedError("edit not allowed for this widget")
281
358
 
282
359
  model = self.app.model
283
- columns = ['person', 'username', 'active']
360
+ columns = ['username', 'active']
284
361
 
285
362
  # generate data set for users
286
363
  users = []
@@ -344,3 +421,22 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
344
421
  kw['values'] = values
345
422
 
346
423
  return super().serialize(field, cstruct, **kw)
424
+
425
+
426
+ class BatchIdWidget(Widget):
427
+ """
428
+ Widget for use with the
429
+ :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
430
+ field of a :term:`batch` model.
431
+
432
+ This widget is "always" read-only and renders the Batch ID as
433
+ zero-padded 8-char string
434
+ """
435
+
436
+ def serialize(self, field, cstruct, **kw):
437
+ """ """
438
+ if cstruct is colander.null:
439
+ return colander.null
440
+
441
+ batch_id = int(cstruct)
442
+ return f'{batch_id:08d}'