wagtail 7.2.1__py3-none-any.whl → 7.3rc1__py3-none-any.whl

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 (312) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/copy_for_translation.py +4 -2
  3. wagtail/admin/action_menu.py +4 -1
  4. wagtail/admin/api/actions/convert_alias.py +2 -2
  5. wagtail/admin/api/actions/copy.py +2 -2
  6. wagtail/admin/api/actions/copy_for_translation.py +2 -2
  7. wagtail/admin/api/actions/create_alias.py +2 -2
  8. wagtail/admin/api/actions/delete.py +1 -1
  9. wagtail/admin/api/actions/move.py +1 -1
  10. wagtail/admin/api/actions/publish.py +2 -2
  11. wagtail/admin/api/actions/revert_to_page_revision.py +2 -2
  12. wagtail/admin/api/actions/unpublish.py +1 -1
  13. wagtail/admin/api/filters.py +2 -2
  14. wagtail/admin/compare.py +22 -0
  15. wagtail/admin/forms/account.py +52 -1
  16. wagtail/admin/forms/comments.py +53 -0
  17. wagtail/admin/forms/models.py +36 -0
  18. wagtail/admin/forms/pages.py +7 -0
  19. wagtail/admin/forms/workflows.py +5 -2
  20. wagtail/admin/icons.py +4 -3
  21. wagtail/admin/locale/ar/LC_MESSAGES/django.mo +0 -0
  22. wagtail/admin/locale/ar/LC_MESSAGES/django.po +35 -0
  23. wagtail/admin/locale/en/LC_MESSAGES/django.po +262 -234
  24. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +72 -43
  25. wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
  26. wagtail/admin/locale/it/LC_MESSAGES/django.po +566 -6
  27. wagtail/admin/locale/it/LC_MESSAGES/djangojs.mo +0 -0
  28. wagtail/admin/locale/it/LC_MESSAGES/djangojs.po +40 -2
  29. wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
  30. wagtail/admin/locale/nl/LC_MESSAGES/django.po +2 -2
  31. wagtail/admin/locale/sv/LC_MESSAGES/django.mo +0 -0
  32. wagtail/admin/locale/sv/LC_MESSAGES/django.po +330 -15
  33. wagtail/admin/locale/sv/LC_MESSAGES/djangojs.mo +0 -0
  34. wagtail/admin/locale/sv/LC_MESSAGES/djangojs.po +3 -2
  35. wagtail/admin/panels/comment_panel.py +1 -51
  36. wagtail/admin/panels/title_field_panel.py +3 -1
  37. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  38. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  40. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -1
  42. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  44. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  45. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  46. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  47. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +1 -1
  48. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  49. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  50. wagtail/admin/templates/wagtailadmin/base.html +1 -1
  51. wagtail/admin/templates/wagtailadmin/generic/edit_partials.html +100 -0
  52. wagtail/admin/templates/wagtailadmin/generic/form.html +26 -5
  53. wagtail/admin/templates/wagtailadmin/generic/includes/_loaded_revision_inputs.html +3 -0
  54. wagtail/admin/templates/wagtailadmin/generic/listing_results.html +1 -17
  55. wagtail/admin/templates/wagtailadmin/pages/create.html +14 -4
  56. wagtail/admin/templates/wagtailadmin/pages/edit.html +16 -3
  57. wagtail/admin/templates/wagtailadmin/pages/edit_partials.html +31 -0
  58. wagtail/admin/templates/wagtailadmin/pages/index_results.html +1 -9
  59. wagtail/admin/templates/wagtailadmin/shared/autosave/indicator.html +36 -0
  60. wagtail/admin/templates/wagtailadmin/shared/breadcrumbs.html +1 -1
  61. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/list.html +2 -2
  62. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html +19 -3
  63. wagtail/admin/templates/wagtailadmin/shared/headers/_actions.html +5 -0
  64. wagtail/admin/templates/wagtailadmin/shared/headers/_history_icon_link.html +2 -2
  65. wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +8 -9
  66. wagtail/admin/templates/wagtailadmin/shared/listing/filter_partials.html +19 -0
  67. wagtail/admin/templates/wagtailadmin/shared/side_panel_toggle.html +3 -3
  68. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/side_panel.html +3 -0
  69. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +18 -6
  70. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/usage.html +11 -4
  71. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/workflow.html +22 -1
  72. wagtail/admin/templates/wagtailadmin/shared/side_panels/preview.html +1 -0
  73. wagtail/admin/templates/wagtailadmin/shared/side_panels.html +1 -2
  74. wagtail/admin/templates/wagtailadmin/shared/unsaved_changes_warning.html +20 -20
  75. wagtail/admin/templates/wagtailadmin/userbar/base.html +2 -0
  76. wagtail/admin/templates/wagtailadmin/userbar/item_accessibility.html +1 -1
  77. wagtail/admin/templatetags/wagtailadmin_tags.py +6 -14
  78. wagtail/admin/tests/pages/test_create_page.py +298 -1
  79. wagtail/admin/tests/pages/test_custom_listing.py +48 -2
  80. wagtail/admin/tests/pages/test_edit_page.py +721 -7
  81. wagtail/admin/tests/pages/test_explorer_view.py +9 -5
  82. wagtail/admin/tests/pages/test_page_search.py +15 -0
  83. wagtail/admin/tests/pages/test_revisions.py +4 -0
  84. wagtail/admin/tests/test_account_management.py +88 -2
  85. wagtail/admin/tests/test_collections_views.py +15 -15
  86. wagtail/admin/tests/test_compare.py +34 -0
  87. wagtail/admin/tests/test_editing_sessions.py +230 -8
  88. wagtail/admin/tests/test_forms.py +192 -1
  89. wagtail/admin/tests/test_icon_sprite.py +22 -2
  90. wagtail/admin/tests/test_page_chooser.py +13 -13
  91. wagtail/admin/tests/test_reports_views.py +4 -1
  92. wagtail/admin/tests/test_userbar.py +69 -5
  93. wagtail/admin/tests/test_views_generic.py +5 -5
  94. wagtail/admin/tests/test_workflows.py +14 -12
  95. wagtail/admin/tests/viewsets/test_model_viewset.py +13 -0
  96. wagtail/admin/ui/autosave.py +5 -0
  97. wagtail/admin/ui/editing_sessions.py +3 -0
  98. wagtail/admin/ui/side_panels.py +19 -20
  99. wagtail/admin/userbar.py +48 -27
  100. wagtail/admin/views/bulk_action/dispatcher.py +2 -2
  101. wagtail/admin/views/chooser.py +6 -6
  102. wagtail/admin/views/editing_sessions.py +20 -7
  103. wagtail/admin/views/generic/__init__.py +1 -0
  104. wagtail/admin/views/generic/chooser.py +5 -5
  105. wagtail/admin/views/generic/mixins.py +143 -26
  106. wagtail/admin/views/generic/models.py +157 -27
  107. wagtail/admin/views/generic/ordering.py +1 -1
  108. wagtail/admin/views/generic/preview.py +4 -4
  109. wagtail/admin/views/home.py +3 -1
  110. wagtail/admin/views/pages/create.py +77 -29
  111. wagtail/admin/views/pages/edit.py +160 -34
  112. wagtail/admin/views/pages/preview.py +4 -4
  113. wagtail/admin/views/pages/revisions.py +3 -0
  114. wagtail/admin/views/pages/search.py +4 -4
  115. wagtail/admin/views/pages/usage.py +2 -2
  116. wagtail/admin/views/tags.py +2 -2
  117. wagtail/admin/views/workflows.py +73 -54
  118. wagtail/admin/viewsets/model.py +34 -8
  119. wagtail/admin/widgets/button.py +11 -4
  120. wagtail/admin/widgets/chooser.py +6 -4
  121. wagtail/admin/widgets/slug.py +1 -1
  122. wagtail/api/v2/filters.py +27 -21
  123. wagtail/api/v2/pagination.py +4 -4
  124. wagtail/api/v2/views.py +7 -7
  125. wagtail/blocks/list_block.py +0 -8
  126. wagtail/blocks/migrations/migrate_operation.py +8 -1
  127. wagtail/blocks/stream_block.py +2 -10
  128. wagtail/blocks/struct_block.py +192 -12
  129. wagtail/compat.py +2 -2
  130. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +1 -1
  131. wagtail/contrib/forms/locale/it/LC_MESSAGES/django.mo +0 -0
  132. wagtail/contrib/forms/locale/it/LC_MESSAGES/django.po +40 -2
  133. wagtail/contrib/frontend_cache/backends/azure.py +6 -6
  134. wagtail/contrib/frontend_cache/backends/cloudfront.py +2 -2
  135. wagtail/contrib/frontend_cache/utils.py +1 -1
  136. wagtail/contrib/redirects/forms.py +1 -1
  137. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +14 -14
  138. wagtail/contrib/redirects/locale/it/LC_MESSAGES/django.mo +0 -0
  139. wagtail/contrib/redirects/locale/it/LC_MESSAGES/django.po +15 -2
  140. wagtail/contrib/redirects/locale/sv/LC_MESSAGES/django.mo +0 -0
  141. wagtail/contrib/redirects/locale/sv/LC_MESSAGES/django.po +16 -3
  142. wagtail/contrib/redirects/middleware.py +11 -7
  143. wagtail/contrib/redirects/models.py +17 -1
  144. wagtail/contrib/redirects/tests/test_import_admin_views.py +3 -3
  145. wagtail/contrib/redirects/tests/test_redirects.py +56 -8
  146. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +6 -6
  147. wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.mo +0 -0
  148. wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.po +43 -2
  149. wagtail/contrib/search_promotions/tests.py +1 -1
  150. wagtail/contrib/search_promotions/views/settings.py +24 -25
  151. wagtail/contrib/settings/jinja2tags.py +2 -2
  152. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +2 -2
  153. wagtail/contrib/settings/locale/it/LC_MESSAGES/django.mo +0 -0
  154. wagtail/contrib/settings/locale/it/LC_MESSAGES/django.po +6 -2
  155. wagtail/contrib/settings/locale/sv/LC_MESSAGES/django.mo +0 -0
  156. wagtail/contrib/settings/locale/sv/LC_MESSAGES/django.po +3 -2
  157. wagtail/contrib/settings/tests/generic/test_admin.py +118 -2
  158. wagtail/contrib/settings/tests/site_specific/test_admin.py +78 -15
  159. wagtail/contrib/settings/tests/site_specific/test_model.py +2 -2
  160. wagtail/contrib/settings/views.py +7 -0
  161. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  162. wagtail/contrib/simple_translation/locale/it/LC_MESSAGES/django.mo +0 -0
  163. wagtail/contrib/simple_translation/locale/it/LC_MESSAGES/django.po +5 -2
  164. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
  165. wagtail/contrib/styleguide/tests.py +2 -0
  166. wagtail/contrib/styleguide/views.py +4 -5
  167. wagtail/contrib/table_block/locale/ar/LC_MESSAGES/django.mo +0 -0
  168. wagtail/contrib/table_block/locale/ar/LC_MESSAGES/django.po +5 -1
  169. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  170. wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.mo +0 -0
  171. wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.po +5 -2
  172. wagtail/contrib/typed_table_block/blocks.py +37 -0
  173. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  174. wagtail/contrib/typed_table_block/locale/sv/LC_MESSAGES/django.mo +0 -0
  175. wagtail/contrib/typed_table_block/locale/sv/LC_MESSAGES/django.po +4 -3
  176. wagtail/contrib/typed_table_block/tests.py +108 -0
  177. wagtail/coreutils.py +4 -4
  178. wagtail/documents/__init__.py +4 -4
  179. wagtail/documents/locale/en/LC_MESSAGES/django.po +15 -15
  180. wagtail/documents/locale/it/LC_MESSAGES/django.mo +0 -0
  181. wagtail/documents/locale/it/LC_MESSAGES/django.po +32 -2
  182. wagtail/documents/locale/sv/LC_MESSAGES/django.mo +0 -0
  183. wagtail/documents/locale/sv/LC_MESSAGES/django.po +32 -4
  184. wagtail/documents/models.py +1 -1
  185. wagtail/documents/tests/test_admin_views.py +19 -4
  186. wagtail/documents/tests/test_search.py +0 -2
  187. wagtail/documents/tests/test_views.py +6 -4
  188. wagtail/documents/views/bulk_actions/add_tags.py +1 -1
  189. wagtail/embeds/finders/facebook.py +3 -3
  190. wagtail/embeds/finders/instagram.py +3 -3
  191. wagtail/embeds/finders/oembed.py +7 -2
  192. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  193. wagtail/embeds/oembed_providers.py +8 -0
  194. wagtail/embeds/tests/test_embeds.py +35 -0
  195. wagtail/images/__init__.py +4 -4
  196. wagtail/images/admin_urls.py +3 -3
  197. wagtail/images/blocks.py +1 -1
  198. wagtail/images/formats.py +2 -2
  199. wagtail/images/image_operations.py +2 -2
  200. wagtail/images/locale/en/LC_MESSAGES/django.po +44 -40
  201. wagtail/images/locale/it/LC_MESSAGES/django.mo +0 -0
  202. wagtail/images/locale/it/LC_MESSAGES/django.po +50 -4
  203. wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
  204. wagtail/images/locale/nl/LC_MESSAGES/django.po +3 -3
  205. wagtail/images/locale/sv/LC_MESSAGES/django.mo +0 -0
  206. wagtail/images/locale/sv/LC_MESSAGES/django.po +49 -6
  207. wagtail/images/models.py +11 -10
  208. wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
  209. wagtail/images/templates/wagtailimages/images/url_generator.html +17 -38
  210. wagtail/images/templates/wagtailimages/images/url_generator_output.html +28 -0
  211. wagtail/images/templatetags/wagtailimages_tags.py +4 -4
  212. wagtail/images/tests/test_admin_views.py +432 -59
  213. wagtail/images/tests/test_image_operations.py +2 -2
  214. wagtail/images/tests/test_models.py +0 -2
  215. wagtail/images/views/bulk_actions/add_tags.py +1 -1
  216. wagtail/images/views/images.py +72 -39
  217. wagtail/locale/en/LC_MESSAGES/django.po +103 -105
  218. wagtail/locale/it/LC_MESSAGES/django.mo +0 -0
  219. wagtail/locale/it/LC_MESSAGES/django.po +105 -2
  220. wagtail/locale/sv/LC_MESSAGES/django.mo +0 -0
  221. wagtail/locale/sv/LC_MESSAGES/django.po +95 -12
  222. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  223. wagtail/locales/locale/it/LC_MESSAGES/django.mo +0 -0
  224. wagtail/locales/locale/it/LC_MESSAGES/django.po +13 -2
  225. wagtail/locales/locale/sv/LC_MESSAGES/django.mo +0 -0
  226. wagtail/locales/locale/sv/LC_MESSAGES/django.po +4 -3
  227. wagtail/locales/tests.py +5 -5
  228. wagtail/models/i18n.py +4 -6
  229. wagtail/models/media.py +18 -0
  230. wagtail/models/pages.py +65 -11
  231. wagtail/models/reference_index.py +15 -0
  232. wagtail/models/revisions.py +40 -12
  233. wagtail/models/workflows.py +0 -3
  234. wagtail/permission_policies/base.py +2 -2
  235. wagtail/permission_policies/collections.py +2 -2
  236. wagtail/project_template/requirements.txt +2 -2
  237. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  238. wagtail/signal_handlers.py +1 -0
  239. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  240. wagtail/sites/locale/sv/LC_MESSAGES/django.mo +0 -0
  241. wagtail/sites/locale/sv/LC_MESSAGES/django.po +3 -2
  242. wagtail/sites/tests.py +7 -7
  243. wagtail/snippets/action_menu.py +2 -1
  244. wagtail/snippets/locale/en/LC_MESSAGES/django.po +23 -13
  245. wagtail/snippets/locale/sv/LC_MESSAGES/django.mo +0 -0
  246. wagtail/snippets/locale/sv/LC_MESSAGES/django.po +12 -1
  247. wagtail/snippets/tests/test_chooser_block.py +197 -0
  248. wagtail/snippets/tests/test_chooser_panel.py +149 -0
  249. wagtail/snippets/tests/test_chooser_views.py +375 -0
  250. wagtail/snippets/tests/test_chooser_widget.py +22 -0
  251. wagtail/snippets/tests/test_compare_revisions_view.py +167 -0
  252. wagtail/snippets/tests/test_copy_view.py +38 -0
  253. wagtail/snippets/tests/test_create_view.py +1159 -0
  254. wagtail/snippets/tests/test_delete_view.py +225 -0
  255. wagtail/snippets/tests/test_edit_view.py +2882 -0
  256. wagtail/snippets/tests/test_history_view.py +182 -0
  257. wagtail/snippets/tests/test_index_view.py +105 -0
  258. wagtail/snippets/tests/test_list_view.py +786 -0
  259. wagtail/snippets/tests/test_locking.py +28 -0
  260. wagtail/snippets/tests/test_permissions.py +167 -0
  261. wagtail/snippets/tests/test_preview.py +3 -3
  262. wagtail/snippets/tests/test_revert_view.py +343 -0
  263. wagtail/snippets/tests/test_snippet_models.py +112 -0
  264. wagtail/snippets/tests/test_unpublish_view.py +228 -0
  265. wagtail/snippets/tests/test_unschedule_view.py +151 -0
  266. wagtail/snippets/tests/test_viewset.py +38 -5
  267. wagtail/snippets/views/snippets.py +78 -30
  268. wagtail/snippets/widgets.py +2 -2
  269. wagtail/templatetags/wagtailcore_tags.py +2 -2
  270. wagtail/test/dummy_external_storage.py +1 -1
  271. wagtail/test/testapp/fixtures/test.json +22 -0
  272. wagtail/test/testapp/fixtures/test_empty.json +2 -0
  273. wagtail/test/testapp/fixtures/test_explorable_pages.json +10 -0
  274. wagtail/test/testapp/fixtures/test_specific.json +9 -0
  275. wagtail/test/testapp/migrations/0059_nopromotepage.py +25 -0
  276. wagtail/test/testapp/models.py +7 -0
  277. wagtail/test/testapp/views.py +5 -0
  278. wagtail/test/utils/page_tests.py +7 -7
  279. wagtail/test/utils/wagtail_factories/builder.py +2 -2
  280. wagtail/tests/streamfield_migrations/test_migrations.py +35 -0
  281. wagtail/tests/test_blocks.py +321 -61
  282. wagtail/tests/test_collection_model.py +12 -0
  283. wagtail/tests/test_page_model.py +190 -1
  284. wagtail/tests/test_page_privacy.py +5 -0
  285. wagtail/tests/test_reference_index.py +42 -0
  286. wagtail/tests/test_revision_model.py +118 -1
  287. wagtail/tests/test_utils.py +2 -2
  288. wagtail/users/locale/en/LC_MESSAGES/django.po +14 -14
  289. wagtail/users/locale/id_ID/LC_MESSAGES/django.mo +0 -0
  290. wagtail/users/locale/id_ID/LC_MESSAGES/django.po +6 -2
  291. wagtail/users/locale/it/LC_MESSAGES/django.mo +0 -0
  292. wagtail/users/locale/it/LC_MESSAGES/django.po +14 -2
  293. wagtail/users/locale/sv/LC_MESSAGES/django.mo +0 -0
  294. wagtail/users/locale/sv/LC_MESSAGES/django.po +48 -17
  295. wagtail/users/tests/test_admin_views.py +39 -25
  296. wagtail/users/utils.py +4 -1
  297. wagtail/users/views/groups.py +19 -5
  298. wagtail/users/wagtail_hooks.py +1 -1
  299. wagtail/utils/loading.py +2 -2
  300. wagtail-7.3rc1.data/data/AGENTS.md +21 -0
  301. wagtail-7.3rc1.data/data/CHANGELOG.txt +4941 -0
  302. wagtail-7.3rc1.data/data/CODE_OF_CONDUCT.md +71 -0
  303. wagtail-7.3rc1.data/data/CONTRIBUTORS.md +999 -0
  304. wagtail-7.3rc1.data/data/SPONSORS.md +55 -0
  305. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/METADATA +6 -6
  306. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/RECORD +310 -280
  307. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/WHEEL +1 -1
  308. wagtail/images/static/wagtailimages/js/image-url-generator.js +0 -1
  309. wagtail/snippets/tests/test_snippets.py +0 -6377
  310. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/entry_points.txt +0 -0
  311. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/licenses/LICENSE +0 -0
  312. {wagtail-7.2.1.dist-info → wagtail-7.3rc1.dist-info}/top_level.txt +0 -0
@@ -19,7 +19,13 @@ class BaseButton(Component):
19
19
  allow_in_dropdown = False
20
20
 
21
21
  def __init__(
22
- self, label="", url=None, classname="", icon_name=None, attrs={}, priority=1000
22
+ self,
23
+ label="",
24
+ url=None,
25
+ classname="",
26
+ icon_name=None,
27
+ attrs=None,
28
+ priority=1000,
23
29
  ):
24
30
  if label:
25
31
  self.label = label
@@ -33,7 +39,8 @@ class BaseButton(Component):
33
39
  self.icon_name = icon_name
34
40
 
35
41
  self.attrs = self.attrs.copy()
36
- self.attrs.update(attrs)
42
+ if attrs:
43
+ self.attrs.update(attrs)
37
44
  self.priority = priority
38
45
 
39
46
  def get_context_data(self, parent_context):
@@ -115,12 +122,12 @@ class HeaderButton(BaseButton):
115
122
  url=None,
116
123
  classname="",
117
124
  icon_name=None,
118
- attrs={},
125
+ attrs=None,
119
126
  icon_only=False,
120
127
  **kwargs,
121
128
  ):
122
129
  classname = f"{classname} w-header-button button".strip()
123
- attrs = attrs.copy()
130
+ attrs = attrs.copy() if attrs else {}
124
131
  if icon_only:
125
132
  controller = f"{attrs.get('data-controller', '')} w-tooltip".strip()
126
133
  attrs["data-controller"] = controller
@@ -157,8 +157,10 @@ class BaseChooser(widgets.Input):
157
157
  # so let's make sure it fails early in the process
158
158
  try:
159
159
  id_ = attrs["id"]
160
- except (KeyError, TypeError):
161
- raise TypeError("BaseChooser cannot be rendered without an 'id' attribute")
160
+ except (KeyError, TypeError) as e:
161
+ raise TypeError(
162
+ "BaseChooser cannot be rendered without an 'id' attribute"
163
+ ) from e
162
164
 
163
165
  value_data = self.get_value_data(value)
164
166
  widget_html = self.render_html(name, value_data, attrs)
@@ -241,12 +243,12 @@ class AdminPageChooser(BaseChooser):
241
243
  for model in target_models:
242
244
  try:
243
245
  cleaned_target_models.append(resolve_model_string(model))
244
- except (ValueError, LookupError):
246
+ except (ValueError, LookupError) as e:
245
247
  raise ImproperlyConfigured(
246
248
  "Could not resolve %r into a model. "
247
249
  "Model names should be in the form app_label.model_name"
248
250
  % (model,)
249
- )
251
+ ) from e
250
252
  else:
251
253
  cleaned_target_models = [Page]
252
254
 
@@ -27,7 +27,7 @@ class SlugInput(widgets.TextInput):
27
27
  str | None,
28
28
  ],
29
29
  ]
30
- ) = [],
30
+ ) = None,
31
31
  locale: object | None = None,
32
32
  ):
33
33
  default_attrs = {
wagtail/api/v2/filters.py CHANGED
@@ -45,7 +45,7 @@ class FieldsFilter(BaseFilterBackend):
45
45
  raise BadRequestError(
46
46
  "field filter error. '%s' is not a valid value for %s (%s)"
47
47
  % (value, field_name, str(e))
48
- )
48
+ ) from e
49
49
 
50
50
  if "\x00" in str(value):
51
51
  raise BadRequestError(
@@ -106,8 +106,10 @@ class OrderingFilter(BaseFilterBackend):
106
106
 
107
107
  try:
108
108
  return queryset.order_by(*validated_fields)
109
- except FieldError:
110
- raise BadRequestError(f"cannot order by '{order_param}' (invalid field)")
109
+ except FieldError as e:
110
+ raise BadRequestError(
111
+ f"cannot order by '{order_param}' (invalid field)"
112
+ ) from e
111
113
 
112
114
 
113
115
  class SearchFilter(BaseFilterBackend):
@@ -145,13 +147,13 @@ class SearchFilter(BaseFilterBackend):
145
147
  "cannot filter by '{}' while searching (field is not indexed)".format(
146
148
  e.field_name
147
149
  )
148
- )
150
+ ) from e
149
151
  except OrderByFieldError as e:
150
152
  raise BadRequestError(
151
153
  "cannot order by '{}' while searching (field is not indexed)".format(
152
154
  e.field_name
153
155
  )
154
- )
156
+ ) from e
155
157
 
156
158
  return queryset
157
159
 
@@ -170,13 +172,13 @@ class ChildOfFilter(BaseFilterBackend):
170
172
  raise ValueError()
171
173
 
172
174
  parent_page = view.get_base_queryset().get(id=parent_page_id)
173
- except ValueError:
175
+ except ValueError as e:
174
176
  if request.GET["child_of"] == "root":
175
177
  parent_page = view.get_root_page()
176
178
  else:
177
- raise BadRequestError("child_of must be a positive integer")
178
- except Page.DoesNotExist:
179
- raise BadRequestError("parent page doesn't exist")
179
+ raise BadRequestError("child_of must be a positive integer") from e
180
+ except Page.DoesNotExist as e:
181
+ raise BadRequestError("parent page doesn't exist") from e
180
182
 
181
183
  queryset = queryset.child_of(parent_page)
182
184
 
@@ -202,10 +204,10 @@ class AncestorOfFilter(BaseFilterBackend):
202
204
  raise ValueError()
203
205
 
204
206
  descendant_page = view.get_base_queryset().get(id=descendant_page_id)
205
- except ValueError:
206
- raise BadRequestError("ancestor_of must be a positive integer")
207
- except Page.DoesNotExist:
208
- raise BadRequestError("descendant page doesn't exist")
207
+ except ValueError as e:
208
+ raise BadRequestError("ancestor_of must be a positive integer") from e
209
+ except Page.DoesNotExist as e:
210
+ raise BadRequestError("descendant page doesn't exist") from e
209
211
 
210
212
  queryset = queryset.ancestor_of(descendant_page)
211
213
 
@@ -230,13 +232,15 @@ class DescendantOfFilter(BaseFilterBackend):
230
232
  raise ValueError()
231
233
 
232
234
  parent_page = view.get_base_queryset().get(id=parent_page_id)
233
- except ValueError:
235
+ except ValueError as e:
234
236
  if request.GET["descendant_of"] == "root":
235
237
  parent_page = view.get_root_page()
236
238
  else:
237
- raise BadRequestError("descendant_of must be a positive integer")
238
- except Page.DoesNotExist:
239
- raise BadRequestError("ancestor page doesn't exist")
239
+ raise BadRequestError(
240
+ "descendant_of must be a positive integer"
241
+ ) from e
242
+ except Page.DoesNotExist as e:
243
+ raise BadRequestError("ancestor page doesn't exist") from e
240
244
 
241
245
  queryset = queryset.descendant_of(parent_page)
242
246
 
@@ -257,13 +261,15 @@ class TranslationOfFilter(BaseFilterBackend):
257
261
  raise ValueError()
258
262
 
259
263
  page = view.get_base_queryset().get(id=page_id)
260
- except ValueError:
264
+ except ValueError as e:
261
265
  if request.GET["translation_of"] == "root":
262
266
  page = view.get_root_page()
263
267
  else:
264
- raise BadRequestError("translation_of must be a positive integer")
265
- except Page.DoesNotExist:
266
- raise BadRequestError("translation_of page doesn't exist")
268
+ raise BadRequestError(
269
+ "translation_of must be a positive integer"
270
+ ) from e
271
+ except Page.DoesNotExist as e:
272
+ raise BadRequestError("translation_of page doesn't exist") from e
267
273
 
268
274
  _filtered_by_child_of = getattr(queryset, "_filtered_by_child_of", None)
269
275
 
@@ -15,16 +15,16 @@ class WagtailPagination(BasePagination):
15
15
  offset = int(request.GET.get("offset", 0))
16
16
  if offset < 0:
17
17
  raise ValueError()
18
- except ValueError:
19
- raise BadRequestError("offset must be a positive integer")
18
+ except ValueError as e:
19
+ raise BadRequestError("offset must be a positive integer") from e
20
20
 
21
21
  try:
22
22
  limit_default = 20 if not limit_max else min(20, limit_max)
23
23
  limit = int(request.GET.get("limit", limit_default))
24
24
  if limit < 0:
25
25
  raise ValueError()
26
- except ValueError:
27
- raise BadRequestError("limit must be a positive integer")
26
+ except ValueError as e:
27
+ raise BadRequestError("limit must be a positive integer") from e
28
28
 
29
29
  if limit_max and limit > limit_max:
30
30
  raise BadRequestError("limit cannot be higher than %d" % limit_max)
wagtail/api/v2/views.py CHANGED
@@ -109,8 +109,8 @@ class BaseAPIViewSet(GenericViewSet):
109
109
  if obj is None:
110
110
  raise self.model.DoesNotExist
111
111
 
112
- except self.model.DoesNotExist:
113
- raise Http404("not found")
112
+ except self.model.DoesNotExist as e:
113
+ raise Http404("not found") from e
114
114
 
115
115
  # Generate redirect
116
116
  url = get_object_detail_url(
@@ -366,7 +366,7 @@ class BaseAPIViewSet(GenericViewSet):
366
366
  try:
367
367
  fields_config = parse_fields_parameter(request.GET["fields"])
368
368
  except ValueError as e:
369
- raise BadRequestError("fields error: %s" % str(e))
369
+ raise BadRequestError("fields error: %s" % str(e)) from e
370
370
  else:
371
371
  # Use default fields
372
372
  fields_config = []
@@ -549,10 +549,10 @@ class PagesAPIViewSet(BaseAPIViewSet):
549
549
  }
550
550
  try:
551
551
  site = Site.objects.get(**query)
552
- except Site.MultipleObjectsReturned:
552
+ except Site.MultipleObjectsReturned as e:
553
553
  raise BadRequestError(
554
554
  "Your query returned multiple sites. Try adding a port number to your site filter."
555
- )
555
+ ) from e
556
556
  else:
557
557
  # Otherwise, find the site from the request
558
558
  site = Site.find_for_request(self.request)
@@ -579,8 +579,8 @@ class PagesAPIViewSet(BaseAPIViewSet):
579
579
  try:
580
580
  models_type = request.GET.get("type", None)
581
581
  models = models_type and page_models_from_string(models_type) or []
582
- except (LookupError, ValueError):
583
- raise BadRequestError("type doesn't exist")
582
+ except (LookupError, ValueError) as e:
583
+ raise BadRequestError("type doesn't exist") from e
584
584
 
585
585
  if not models:
586
586
  if self.model == Page:
@@ -458,14 +458,6 @@ class ListBlockAdapter(Adapter):
458
458
  "classname": block.meta.form_classname,
459
459
  "attrs": block.meta.form_attrs or {},
460
460
  "collapsed": block.meta.collapsed,
461
- "strings": {
462
- "MOVE_UP": _("Move up"),
463
- "MOVE_DOWN": _("Move down"),
464
- "DRAG": _("Drag"),
465
- "DUPLICATE": _("Duplicate"),
466
- "DELETE": _("Delete"),
467
- "ADD": _("Add"),
468
- },
469
461
  }
470
462
  help_text = getattr(block.meta, "help_text", None)
471
463
  if help_text:
@@ -155,8 +155,15 @@ class MigrateStreamData(RunPython):
155
155
 
156
156
  updated_revisions_buffer = []
157
157
  for revision in revision_queryset.iterator(chunk_size=self.chunk_size):
158
+ try:
159
+ json_data = revision.content[self.field_name]
160
+ except KeyError:
161
+ # The requested field doesn't exist on this revision. The revision may
162
+ # have been created before the creation of the field. Don't process this
163
+ # revision.
164
+ continue
158
165
 
159
- raw_data = json.loads(revision.content[self.field_name])
166
+ raw_data = json.loads(json_data)
160
167
  for operation, block_path_str in self.operations_and_block_paths:
161
168
  try:
162
169
  raw_data = utils.apply_changes_to_raw_data(
@@ -812,10 +812,10 @@ class StreamValue(MutableSequence):
812
812
  def __reduce__(self):
813
813
  try:
814
814
  stream_field = self._stream_field
815
- except AttributeError:
815
+ except AttributeError as e:
816
816
  raise PickleError(
817
817
  "StreamValue can only be pickled if it is associated with a StreamField"
818
- )
818
+ ) from e
819
819
 
820
820
  return (
821
821
  self._deserialize_pickle_value,
@@ -845,14 +845,6 @@ class StreamBlockAdapter(Adapter):
845
845
  "minNum": block.meta.min_num,
846
846
  "blockCounts": block.meta.block_counts,
847
847
  "collapsed": block.meta.collapsed,
848
- "strings": {
849
- "MOVE_UP": _("Move up"),
850
- "MOVE_DOWN": _("Move down"),
851
- "DRAG": _("Drag"),
852
- "DUPLICATE": _("Duplicate"),
853
- "DELETE": _("Delete"),
854
- "ADD": _("Add"),
855
- },
856
848
  }
857
849
  help_text = getattr(block.meta, "help_text", None)
858
850
  if help_text:
@@ -1,15 +1,19 @@
1
1
  import collections
2
+ from typing import Union
2
3
 
3
4
  from django import forms
5
+ from django.core import checks
4
6
  from django.core.exceptions import ValidationError
5
7
  from django.forms.utils import ErrorList
6
8
  from django.template.loader import render_to_string
7
9
  from django.utils.functional import cached_property
8
10
  from django.utils.html import format_html, format_html_join
9
11
  from django.utils.safestring import mark_safe
12
+ from django.utils.translation import gettext_lazy as _
10
13
 
11
14
  from wagtail.admin.staticfiles import versioned_static
12
15
  from wagtail.admin.telepath import Adapter, register
16
+ from wagtail.coreutils import safe_snake_case
13
17
 
14
18
  from .base import (
15
19
  Block,
@@ -21,6 +25,7 @@ from .base import (
21
25
  )
22
26
 
23
27
  __all__ = [
28
+ "BlockGroup",
24
29
  "BaseStructBlock",
25
30
  "StructBlock",
26
31
  "StructValue",
@@ -70,6 +75,132 @@ class StructBlockValidationError(ValidationError):
70
75
  return result
71
76
 
72
77
 
78
+ @register
79
+ class BlockGroup:
80
+ """
81
+ A grouping of blocks within a :class:`StructBlock`'s form layout in the
82
+ editing interface. Can be used directly as the ``form_layout`` in
83
+ :class:`StructBlock`.Meta, or nested within another ``BlockGroup``.
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ children: list[Union[str, "BlockGroup"]],
89
+ settings: list[Union[str, "BlockGroup"]] = None,
90
+ heading="",
91
+ classname="",
92
+ help_text="",
93
+ icon="placeholder",
94
+ attrs: dict | None = None,
95
+ label_format: str | None = None,
96
+ ):
97
+ """
98
+ :param children: A list of block names or nested ``BlockGroup`` that will be
99
+ rendered in the main content area.
100
+ :type children: list[str | BlockGroup]
101
+
102
+ :param settings: A list of block names or nested ``BlockGroup`` that will be
103
+ rendered in the collapsible "settings" area that is hidden by default.
104
+ :type settings: list[str | BlockGroup]
105
+
106
+ The following attributes are only used when the ``BlockGroup`` is nested within
107
+ another ``BlockGroup``. For the top-level ``BlockGroup`` used as
108
+ ``Meta.form_layout`` in a :class:`StructBlock`, these attributes are ignored in
109
+ favor of the corresponding attributes on ``StructBlock.Meta``.
110
+
111
+ :param heading: The heading label of the collapsible panel for this block
112
+ group. For a top-level group, the ``StructBlock``'s ``label`` will be
113
+ used instead.
114
+ :type heading: str
115
+
116
+ :param classname: Additional CSS class name(s) to add to the block group's main
117
+ content area. To set the group to be initially collapsed, include the
118
+ ``collapsed`` class here.
119
+ :type classname: str
120
+
121
+ :param help_text: Help text to display below the block group's heading.
122
+ :type help_text: str
123
+
124
+ :param icon: The name of the icon to display alongside the block group's heading.
125
+ :type icon: str
126
+
127
+ :param attrs: A dictionary of HTML attributes to add to the block group's main content area.
128
+ :type attrs: dict
129
+
130
+ :param label_format: The summary label shown after the ``heading`` when the
131
+ block is collapsed in the editing interface. By default, the value of the
132
+ first child block is shown, but this can be customized by setting a string
133
+ here with block names contained in braces - for example ``label_format = "
134
+ {surname}, {first_name}"``. If you wish to hide the summary label entirely,
135
+ set this to the empty string ``""``.
136
+ :type label_format: str | None
137
+ """
138
+ self.children = children
139
+ self.settings = settings or []
140
+ self.heading = heading or _("Group")
141
+ self.clean_name = safe_snake_case(self.heading)
142
+ self.classname = classname
143
+ self.help_text = help_text
144
+ self.icon = icon
145
+ self.attrs = attrs or {}
146
+ self.label_format = label_format
147
+
148
+ telepath_adapter_name = "wagtail.blocks.BlockGroup"
149
+
150
+ @cached_property
151
+ def unique_children_and_settings(self):
152
+ # Ensure unique identifiers in the case of multiple BlockGroups with the
153
+ # same headings on the same level, either in children or settings.
154
+ used_names = set()
155
+
156
+ def group_with_unique_names(group):
157
+ results = []
158
+ for child in group:
159
+ base_name = child.clean_name if isinstance(child, BlockGroup) else child
160
+ candidate_name = base_name
161
+ suffix = 0
162
+ while candidate_name in used_names:
163
+ suffix += 1
164
+ candidate_name = "%s%d" % (base_name, suffix)
165
+ results.append((child, candidate_name))
166
+ used_names.add(candidate_name)
167
+ return results
168
+
169
+ children = group_with_unique_names(self.children)
170
+ settings = group_with_unique_names(self.settings)
171
+ return children, settings
172
+
173
+ def get_sorted_block_names(self):
174
+ """
175
+ Return a flat list of all block names in this ``BlockGroup`` and any
176
+ nested ``BlockGroups`` in the group's list order.
177
+ """
178
+ block_names = []
179
+ for child in self.children + self.settings:
180
+ if isinstance(child, BlockGroup):
181
+ block_names.extend(child.get_sorted_block_names())
182
+ else:
183
+ block_names.append(child)
184
+ return block_names
185
+
186
+ def js_opts(self):
187
+ children, settings = self.unique_children_and_settings
188
+ return {
189
+ "children": children,
190
+ "settings": settings,
191
+ "heading": self.heading,
192
+ "cleanName": self.clean_name,
193
+ "classname": self.classname,
194
+ "helpText": self.help_text,
195
+ "icon": self.icon,
196
+ "attrs": self.attrs,
197
+ "labelFormat": self.label_format,
198
+ }
199
+
200
+ def telepath_pack(self, context):
201
+ return (self.telepath_adapter_name, [self.js_opts()])
202
+
203
+
73
204
  class StructValue(collections.OrderedDict):
74
205
  """A class that generates a StructBlock value from provided sub-blocks"""
75
206
 
@@ -119,6 +250,17 @@ class BaseStructBlock(Block):
119
250
  block.set_name(name)
120
251
  self.child_blocks[name] = block
121
252
 
253
+ self.meta.form_layout = self.get_form_layout()
254
+
255
+ # Reorder child_blocks to match form_layout, appending any missing
256
+ # blocks to the end
257
+ sorted_block_names = self.meta.form_layout.get_sorted_block_names()
258
+ missing_block_names = self.child_blocks.keys() - set(sorted_block_names)
259
+ self.child_blocks = collections.OrderedDict(
260
+ (name, self.child_blocks[name])
261
+ for name in (sorted_block_names + list(missing_block_names))
262
+ )
263
+
122
264
  @classmethod
123
265
  def construct_from_lookup(cls, lookup, child_blocks, **kwargs):
124
266
  if child_blocks:
@@ -335,9 +477,27 @@ class BaseStructBlock(Block):
335
477
  for name, child_block in self.child_blocks.items():
336
478
  errors.extend(child_block.check(**kwargs))
337
479
  errors.extend(child_block._check_name(**kwargs))
480
+ errors.extend(self._check_form_layout())
338
481
 
339
482
  return errors
340
483
 
484
+ def _check_form_layout(self):
485
+ if self.meta.form_template and any(
486
+ isinstance(name, BlockGroup)
487
+ for name in (
488
+ self.meta.form_layout.children + self.meta.form_layout.settings
489
+ )
490
+ ):
491
+ return [
492
+ checks.Error(
493
+ f"{self.__class__.__name__}.Meta.form_layout cannot have "
494
+ "nested BlockGroups when using a custom form_template.",
495
+ obj=self,
496
+ id="wagtailcore.E007",
497
+ )
498
+ ]
499
+ return []
500
+
341
501
  def render_basic(self, value, context=None):
342
502
  return format_html(
343
503
  "<dl>\n{}\n</dl>",
@@ -363,24 +523,42 @@ class BaseStructBlock(Block):
363
523
  return super().get_description() or getattr(self.meta, "help_text", "")
364
524
 
365
525
  def get_form_context(self, value, prefix="", errors=None):
366
- return {
367
- "children": collections.OrderedDict(
368
- [
369
- (
370
- name,
371
- PlaceholderBoundBlock(
372
- block, value.get(name), prefix=f"{prefix}-{name}"
373
- ),
374
- )
375
- for name, block in self.child_blocks.items()
376
- ]
377
- ),
526
+ children = collections.OrderedDict(
527
+ [
528
+ (
529
+ name,
530
+ PlaceholderBoundBlock(
531
+ block, value.get(name), prefix=f"{prefix}-{name}"
532
+ ),
533
+ )
534
+ for name, block in self.child_blocks.items()
535
+ ]
536
+ )
537
+ # Move settings blocks into a separate dict for easier access in templates
538
+ settings = collections.OrderedDict(
539
+ [(name, children.pop(name)) for name in self.meta.form_layout.settings]
540
+ )
541
+ context = {
542
+ "children": children,
543
+ "settings": settings,
378
544
  "help_text": getattr(self.meta, "help_text", None),
379
545
  "classname": self.meta.form_classname,
380
546
  "collapsed": self.meta.collapsed,
381
547
  "block_definition": self,
382
548
  "prefix": prefix,
383
549
  }
550
+ return context
551
+
552
+ def get_form_layout(self) -> BlockGroup:
553
+ """
554
+ Return the :class:`BlockGroup` representing the form layout for this
555
+ ``StructBlock``.
556
+ """
557
+ if (form_layout := self.meta.form_layout) is None:
558
+ return BlockGroup(list(self.child_blocks.keys()))
559
+ if isinstance(form_layout, list):
560
+ return BlockGroup(form_layout)
561
+ return form_layout
384
562
 
385
563
  @cached_property
386
564
  def _has_default(self):
@@ -393,6 +571,7 @@ class BaseStructBlock(Block):
393
571
  value_class = StructValue
394
572
  label_format = None
395
573
  collapsed = False
574
+ form_layout: BlockGroup | list[Union[str, "BlockGroup"]] | None = None
396
575
  # No icon specified here, because that depends on the purpose that the
397
576
  # block is being used for. Feel encouraged to specify an icon in your
398
577
  # descendant block type
@@ -417,6 +596,7 @@ class StructBlockAdapter(Adapter):
417
596
  "classname": block.meta.form_classname,
418
597
  "collapsed": block.meta.collapsed,
419
598
  "attrs": block.meta.form_attrs or {},
599
+ "formLayout": block.meta.form_layout,
420
600
  }
421
601
 
422
602
  help_text = getattr(block.meta, "help_text", None)
wagtail/compat.py CHANGED
@@ -9,10 +9,10 @@ AUTH_USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User")
9
9
  # specifying the user model in the FakeORM
10
10
  try:
11
11
  AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME = AUTH_USER_MODEL.rsplit(".", 1)
12
- except ValueError:
12
+ except ValueError as e:
13
13
  raise ImproperlyConfigured(
14
14
  "AUTH_USER_MODEL must be of the form 'app_label.model_name'"
15
- )
15
+ ) from e
16
16
 
17
17
 
18
18
  try:
@@ -8,7 +8,7 @@ msgid ""
8
8
  msgstr ""
9
9
  "Project-Id-Version: PACKAGE VERSION\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
- "POT-Creation-Date: 2025-10-23 16:45+0100\n"
11
+ "POT-Creation-Date: 2026-01-21 18:00+0000\n"
12
12
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
13
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
14
  "Language-Team: LANGUAGE <LL@li.org>\n"