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
@@ -200,7 +200,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
200
200
  page, args, kwargs = site.root_page.localized.specific.route(
201
201
  self.dummy_request, path_components
202
202
  )
203
- except Http404:
203
+ except Http404 as e:
204
204
  msg = self._formatMessage(
205
205
  msg,
206
206
  'Failed to route to "%(route_path)s" for %(page_type)s "%(page)s". A Http404 was raised for path: "%(full_path)s".'
@@ -211,7 +211,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
211
211
  "full_path": path,
212
212
  },
213
213
  )
214
- raise self.failureException(msg)
214
+ raise self.failureException(msg) from e
215
215
 
216
216
  def assertPageIsRenderable(
217
217
  self,
@@ -268,7 +268,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
268
268
  "exc": e,
269
269
  },
270
270
  )
271
- raise self.failureException(msg)
271
+ raise self.failureException(msg) from e
272
272
  finally:
273
273
  if user:
274
274
  self.client.logout()
@@ -340,7 +340,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
340
340
  'Failed to load edit view via GET for %(page_type)s "%(page)s":\n%(exc)s'
341
341
  % {"page_type": type(page).__name__, "page": page, "exc": e},
342
342
  )
343
- raise self.failureException(msg)
343
+ raise self.failureException(msg) from e
344
344
  if response.status_code != 200:
345
345
  self.client.logout()
346
346
  msg = self._formatMessage(
@@ -370,7 +370,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
370
370
  'Failed to load edit view via POST for %(page_type)s "%(page)s":\n%(exc)s'
371
371
  % {"page_type": type(page).__name__, "page": page, "exc": e},
372
372
  )
373
- raise self.failureException(msg)
373
+ raise self.failureException(msg) from e
374
374
  finally:
375
375
  page.save() # undo any changes to page
376
376
  self.client.logout()
@@ -431,7 +431,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
431
431
  "exc": e,
432
432
  },
433
433
  )
434
- raise self.failureException(msg)
434
+ raise self.failureException(msg) from e
435
435
 
436
436
  try:
437
437
  self.client.get(preview_path, data={"mode": mode})
@@ -446,7 +446,7 @@ class WagtailPageTestCase(WagtailTestUtils, TestCase):
446
446
  "exc": e,
447
447
  },
448
448
  )
449
- raise self.failureException(msg)
449
+ raise self.failureException(msg) from e
450
450
  finally:
451
451
  self.client.logout()
452
452
 
@@ -78,12 +78,12 @@ class StreamBlockStepBuilder(BaseBlockStepBuilder):
78
78
  try:
79
79
  i, name, *params = k.split("__", maxsplit=2)
80
80
  key = int(i)
81
- except (ValueError, TypeError):
81
+ except (ValueError, TypeError) as e:
82
82
  raise InvalidDeclaration(
83
83
  "StreamFieldFactory declarations must be of the form "
84
84
  "<index>=<block_name>, <index>__<block_name>=value or "
85
85
  f"<index>__<block_name>__<param>=value, got: {k}"
86
- )
86
+ ) from e
87
87
  if key in indexed_block_names and indexed_block_names[key] != name:
88
88
  raise DuplicateDeclaration(
89
89
  f"Multiple declarations for index {key} at this level of nesting "
@@ -295,3 +295,38 @@ class StreamChildrenToListBlockOperationTestCase(BaseMigrationTest):
295
295
  self.assertEqual(len(new_block["value"]), 1)
296
296
  self.assertEqual(new_block["value"][0]["type"], "item")
297
297
  self.assertEqual(new_block["value"][0]["value"], "Char Block 1")
298
+
299
+
300
+ class TestRevisionHandling(BaseMigrationTest):
301
+ """
302
+ Test the handling of revisions in StreamField data migrations.
303
+ """
304
+
305
+ model = models.SamplePage
306
+ factory = factories.SamplePageFactory
307
+ has_revisions = False # We'll create them manually.
308
+ app_name = "streamfield_migration_tests"
309
+
310
+ def _get_test_instances(self):
311
+ return self.factory()
312
+
313
+ def setUp(self):
314
+ self.instance = self._get_test_instances()
315
+
316
+ def test_undefined_field_handled(self):
317
+ """
318
+ Revisions not having the targeted field are handled.
319
+
320
+ See https://github.com/wagtail/wagtail/issues/12408.
321
+ """
322
+
323
+ # Create a revision
324
+ revision = self.instance.save_revision()
325
+
326
+ # Delete the `content' field from the revision content, to simulate a revision
327
+ # created before the creation of the field.
328
+ del revision.content["content"]
329
+ revision.save()
330
+
331
+ # Should not throw a KeyError.
332
+ self.apply_migration()
@@ -17,13 +17,18 @@ from django.utils.safestring import SafeData, mark_safe
17
17
  from django.utils.translation import gettext_lazy as _
18
18
 
19
19
  from wagtail import blocks
20
+ from wagtail.admin.telepath import registry
20
21
  from wagtail.blocks.base import get_error_json_data
21
22
  from wagtail.blocks.definition_lookup import BlockDefinitionLookup
22
23
  from wagtail.blocks.field_block import FieldBlockAdapter
23
24
  from wagtail.blocks.list_block import ListBlockAdapter, ListBlockValidationError
24
25
  from wagtail.blocks.static_block import StaticBlockAdapter
25
26
  from wagtail.blocks.stream_block import StreamBlockAdapter, StreamBlockValidationError
26
- from wagtail.blocks.struct_block import StructBlockAdapter, StructBlockValidationError
27
+ from wagtail.blocks.struct_block import (
28
+ BlockGroup,
29
+ StructBlockAdapter,
30
+ StructBlockValidationError,
31
+ )
27
32
  from wagtail.models import Page
28
33
  from wagtail.rich_text import RichText
29
34
  from wagtail.test.testapp.blocks import LinkBlock as CustomLinkBlock
@@ -1924,6 +1929,82 @@ class TestMeta(unittest.TestCase):
1924
1929
  self.assertEqual(block.meta.label, "Child block")
1925
1930
 
1926
1931
 
1932
+ class TestBlockGroup(SimpleTestCase):
1933
+ @classmethod
1934
+ def setUpClass(cls):
1935
+ cls.adapter = registry.find_adapter(BlockGroup)
1936
+
1937
+ def test_adapt(self):
1938
+ group = BlockGroup(
1939
+ children=["title", "body"],
1940
+ settings=["theme"],
1941
+ heading="Content",
1942
+ classname="custom-class",
1943
+ help_text="Some help text",
1944
+ icon="folder",
1945
+ attrs={"data-example": "value"},
1946
+ label_format="Title: {title}, Theme: {theme}",
1947
+ )
1948
+ result = self.adapter.pack(group, None)
1949
+ self.assertEqual(result[0], "wagtail.blocks.BlockGroup")
1950
+ self.assertEqual(
1951
+ result[1],
1952
+ [
1953
+ {
1954
+ "children": [("title", "title"), ("body", "body")],
1955
+ "settings": [("theme", "theme")],
1956
+ "heading": "Content",
1957
+ "cleanName": "content",
1958
+ "classname": "custom-class",
1959
+ "helpText": "Some help text",
1960
+ "icon": "folder",
1961
+ "attrs": {"data-example": "value"},
1962
+ "labelFormat": "Title: {title}, Theme: {theme}",
1963
+ }
1964
+ ],
1965
+ )
1966
+
1967
+ def test_adapt_adjacent_block_groups_with_same_headings(self):
1968
+ form_layout = BlockGroup(
1969
+ children=[
1970
+ BlockGroup(children=["title", "body"], heading="Some heading"),
1971
+ BlockGroup(children=["image"], heading="Some heading"),
1972
+ "non_nested_block",
1973
+ # These will have default heading "Group"
1974
+ BlockGroup(children=["foo"]),
1975
+ BlockGroup(children=["bar"]),
1976
+ ],
1977
+ settings=[BlockGroup(children=["theme"], heading="Some heading")],
1978
+ )
1979
+ result = self.adapter.pack(form_layout, None)
1980
+ self.assertEqual(result[0], "wagtail.blocks.BlockGroup")
1981
+ self.assertEqual(
1982
+ result[1],
1983
+ [
1984
+ {
1985
+ # Children and settings are list of (child, unique_name) tuples
1986
+ # to ensure unique names of adjacent groups with same headings
1987
+ # for the purpose of generating collapsible panel element IDs
1988
+ "children": [
1989
+ (form_layout.children[0], "some_heading"),
1990
+ (form_layout.children[1], "some_heading1"),
1991
+ ("non_nested_block", "non_nested_block"),
1992
+ (form_layout.children[3], "group"),
1993
+ (form_layout.children[4], "group1"),
1994
+ ],
1995
+ "settings": [(form_layout.settings[0], "some_heading2")],
1996
+ "heading": "Group",
1997
+ "cleanName": "group",
1998
+ "classname": "",
1999
+ "helpText": "",
2000
+ "icon": "placeholder",
2001
+ "attrs": {},
2002
+ "labelFormat": None,
2003
+ }
2004
+ ],
2005
+ )
2006
+
2007
+
1927
2008
  class TestStructBlock(SimpleTestCase):
1928
2009
  def test_initialisation(self):
1929
2010
  block = blocks.StructBlock(
@@ -2121,6 +2202,72 @@ class TestStructBlock(SimpleTestCase):
2121
2202
  self.assertEqual(context["block_definition"], block)
2122
2203
  self.assertEqual(context["prefix"], "mylink")
2123
2204
 
2205
+ def test_get_form_context_with_settings(self):
2206
+ class LinkBlock(blocks.StructBlock):
2207
+ title = blocks.CharBlock()
2208
+ link = blocks.URLBlock()
2209
+ open_in_new_tab = blocks.BooleanBlock(required=False, default=False)
2210
+
2211
+ class Meta:
2212
+ form_layout = BlockGroup(
2213
+ children=["link", "title"],
2214
+ settings=["open_in_new_tab"],
2215
+ )
2216
+
2217
+ block = LinkBlock()
2218
+ context = block.get_form_context(
2219
+ block.to_python(
2220
+ {
2221
+ "title": "Django",
2222
+ "link": "http://djangoproject.com",
2223
+ "open_in_new_tab": True,
2224
+ }
2225
+ ),
2226
+ prefix="mylink",
2227
+ )
2228
+
2229
+ # The context separates children and settings according to the form layout
2230
+ children = context["children"]
2231
+ self.assertIsInstance(children, collections.OrderedDict)
2232
+ self.assertEqual(len(children), 2)
2233
+ self.assertIsInstance(children["title"], blocks.BoundBlock)
2234
+ self.assertIsInstance(children["link"], blocks.BoundBlock)
2235
+ # Should respect the order defined in the form layout
2236
+ self.assertEqual(
2237
+ [child.value for child in children.values()],
2238
+ ["http://djangoproject.com", "Django"],
2239
+ )
2240
+
2241
+ settings = context["settings"]
2242
+ self.assertIsInstance(settings, collections.OrderedDict)
2243
+ self.assertEqual(len(settings), 1)
2244
+ self.assertIsInstance(settings["open_in_new_tab"], blocks.BoundBlock)
2245
+
2246
+ def test_check_form_template_with_nested_block_groups(self):
2247
+ class LinkBlock(blocks.StructBlock):
2248
+ title = blocks.CharBlock()
2249
+ link = blocks.URLBlock()
2250
+ open_in_new_tab = blocks.BooleanBlock(required=False, default=False)
2251
+
2252
+ class Meta:
2253
+ form_layout = BlockGroup(
2254
+ children=[BlockGroup(children=["title", "link"])],
2255
+ settings=["open_in_new_tab"],
2256
+ )
2257
+ form_template = "tests/block_forms/struct_block_form_template.html"
2258
+
2259
+ block = LinkBlock()
2260
+ results = block.check()
2261
+
2262
+ self.assertEqual(len(results), 1)
2263
+ self.assertEqual(results[0].id, "wagtailcore.E007")
2264
+ self.assertEqual(results[0].obj, block)
2265
+ self.assertEqual(
2266
+ results[0].msg,
2267
+ "LinkBlock.Meta.form_layout cannot have nested BlockGroups "
2268
+ "when using a custom form_template.",
2269
+ )
2270
+
2124
2271
  def test_adapt(self):
2125
2272
  class LinkBlock(blocks.StructBlock):
2126
2273
  title = blocks.CharBlock(required=False)
@@ -2144,6 +2291,7 @@ class TestStructBlock(SimpleTestCase):
2144
2291
  "classname": "struct-block",
2145
2292
  "collapsed": False,
2146
2293
  "attrs": {},
2294
+ "formLayout": block.meta.form_layout,
2147
2295
  },
2148
2296
  )
2149
2297
 
@@ -2153,6 +2301,9 @@ class TestStructBlock(SimpleTestCase):
2153
2301
  self.assertEqual(title_field, block.child_blocks["title"])
2154
2302
  self.assertEqual(link_field, block.child_blocks["link"])
2155
2303
 
2304
+ # The default form layout lists the field names in order as children
2305
+ self.assertEqual(js_args[2]["formLayout"].children, ["title", "link"])
2306
+
2156
2307
  def test_adapt_with_form_template(self):
2157
2308
  class LinkBlock(blocks.StructBlock):
2158
2309
  title = blocks.CharBlock(required=False)
@@ -2178,6 +2329,7 @@ class TestStructBlock(SimpleTestCase):
2178
2329
  "classname": "struct-block",
2179
2330
  "collapsed": False,
2180
2331
  "attrs": {},
2332
+ "formLayout": block.meta.form_layout,
2181
2333
  "formTemplate": "<div>Hello</div>",
2182
2334
  },
2183
2335
  )
@@ -2256,6 +2408,7 @@ class TestStructBlock(SimpleTestCase):
2256
2408
  "classname": "struct-block",
2257
2409
  "collapsed": False,
2258
2410
  "attrs": {},
2411
+ "formLayout": block.meta.form_layout,
2259
2412
  "formTemplate": "<div>Hello</div>",
2260
2413
  },
2261
2414
  )
@@ -2313,6 +2466,7 @@ class TestStructBlock(SimpleTestCase):
2313
2466
  "classname": "struct-block",
2314
2467
  "collapsed": False,
2315
2468
  "attrs": {},
2469
+ "formLayout": block.meta.form_layout,
2316
2470
  "helpIcon": (
2317
2471
  '<svg class="icon icon-help default" aria-hidden="true">'
2318
2472
  '<use href="#icon-help"></use></svg>'
@@ -2343,6 +2497,7 @@ class TestStructBlock(SimpleTestCase):
2343
2497
  "classname": "struct-block",
2344
2498
  "collapsed": False,
2345
2499
  "attrs": {},
2500
+ "formLayout": block.meta.form_layout,
2346
2501
  "helpIcon": (
2347
2502
  '<svg class="icon icon-help default" aria-hidden="true">'
2348
2503
  '<use href="#icon-help"></use></svg>'
@@ -2366,6 +2521,167 @@ class TestStructBlock(SimpleTestCase):
2366
2521
 
2367
2522
  self.assertIs(js_args[2]["collapsed"], case)
2368
2523
 
2524
+ def test_adapt_with_list_form_layout(self):
2525
+ class LinkBlock(blocks.StructBlock):
2526
+ title = blocks.CharBlock()
2527
+ link = blocks.URLBlock()
2528
+
2529
+ class Meta:
2530
+ form_layout = ["link", "title"]
2531
+
2532
+ block = LinkBlock()
2533
+
2534
+ block.set_name("test_structblock")
2535
+ js_args = StructBlockAdapter().js_args(block)
2536
+
2537
+ # Should be converted to a BlockGroup instance,
2538
+ # which will be adapted on its own
2539
+ form_layout = js_args[2]["formLayout"]
2540
+ self.assertIsInstance(form_layout, BlockGroup)
2541
+ self.assertEqual(form_layout, block.meta.form_layout)
2542
+ self.assertEqual(form_layout.children, ["link", "title"])
2543
+
2544
+ def test_adapt_with_settings_blocks(self):
2545
+ class LinkBlock(blocks.StructBlock):
2546
+ title = blocks.CharBlock()
2547
+ link = blocks.URLBlock()
2548
+
2549
+ class Meta:
2550
+ form_layout = BlockGroup(
2551
+ children=["title"],
2552
+ settings=["link"],
2553
+ )
2554
+
2555
+ block = LinkBlock()
2556
+
2557
+ block.set_name("test_structblock")
2558
+ js_args = StructBlockAdapter().js_args(block)
2559
+
2560
+ # The form_layout is still a BlockGroup instance,
2561
+ # which will be adapted on its own
2562
+ form_layout = js_args[2]["formLayout"]
2563
+ self.assertIsInstance(form_layout, BlockGroup)
2564
+ self.assertEqual(form_layout, block.meta.form_layout)
2565
+ self.assertEqual(form_layout.children, ["title"])
2566
+ self.assertEqual(form_layout.settings, ["link"])
2567
+
2568
+ def test_with_nested_blockgroups_in_form_layout(self):
2569
+ class LinkBlock(blocks.StructBlock):
2570
+ title = blocks.CharBlock()
2571
+ link = blocks.URLBlock()
2572
+ description = blocks.TextBlock()
2573
+
2574
+ class Meta:
2575
+ form_layout = BlockGroup(
2576
+ children=[
2577
+ "link",
2578
+ BlockGroup(
2579
+ children=["title", "description"],
2580
+ heading="Details",
2581
+ ),
2582
+ ]
2583
+ )
2584
+
2585
+ block = LinkBlock()
2586
+
2587
+ block.set_name("test_structblock")
2588
+ js_args = StructBlockAdapter().js_args(block)
2589
+
2590
+ # The form_layout is still a BlockGroup instance,
2591
+ # which will be adapted on its own
2592
+ form_layout = js_args[2]["formLayout"]
2593
+ self.assertIsInstance(form_layout, BlockGroup)
2594
+ self.assertEqual(form_layout, block.meta.form_layout)
2595
+ self.assertEqual(form_layout.children[0], "link")
2596
+ self.assertIsInstance(form_layout.children[1], BlockGroup)
2597
+ self.assertEqual(form_layout.children[1].children, ["title", "description"])
2598
+
2599
+ def test_with_missing_blocks_in_form_layout(self):
2600
+ class LinkBlock(blocks.StructBlock):
2601
+ title = blocks.CharBlock()
2602
+ link = blocks.URLBlock()
2603
+ description = blocks.TextBlock()
2604
+
2605
+ class Meta:
2606
+ form_layout = BlockGroup(
2607
+ children=["link"],
2608
+ settings=["title"],
2609
+ )
2610
+
2611
+ block = LinkBlock()
2612
+
2613
+ block.set_name("test_structblock")
2614
+ js_args = StructBlockAdapter().js_args(block)
2615
+
2616
+ form_layout = js_args[2]["formLayout"]
2617
+ self.assertIsInstance(form_layout, BlockGroup)
2618
+
2619
+ # The form_layout remains as defined, even if some fields are missing
2620
+ self.assertEqual(form_layout, block.meta.form_layout)
2621
+ self.assertEqual(form_layout.children, ["link"])
2622
+ self.assertEqual(form_layout.settings, ["title"])
2623
+
2624
+ # However, it's still in block.child_blocks, appended to the end. This
2625
+ # ensures any code that relies on block.child_blocks to find all blocks
2626
+ # still works, even if the form_layout isn't configured properly.
2627
+ self.assertEqual(
2628
+ list(block.child_blocks.keys()),
2629
+ ["link", "title", "description"],
2630
+ )
2631
+ self.assertIsInstance(block.child_blocks["description"], blocks.TextBlock)
2632
+
2633
+ def test_adapt_with_get_form_layout(self):
2634
+ class LinkBlock(blocks.StructBlock):
2635
+ title = blocks.CharBlock()
2636
+ link = blocks.URLBlock()
2637
+
2638
+ class Meta:
2639
+ form_layout = BlockGroup(
2640
+ children=[
2641
+ "link",
2642
+ BlockGroup(
2643
+ children=["title"],
2644
+ heading="Details",
2645
+ ),
2646
+ ]
2647
+ )
2648
+
2649
+ class LinkBlockWithDescription(LinkBlock):
2650
+ description = blocks.TextBlock()
2651
+
2652
+ def get_form_layout(self):
2653
+ # Create a deep copy of the parent's form layout to include the
2654
+ # new 'description' field without mutating the parent's form layout
2655
+ form_layout = copy.deepcopy(super().get_form_layout())
2656
+ form_layout.children[1].children.append("description")
2657
+ return form_layout
2658
+
2659
+ block = LinkBlock()
2660
+ sub_block = LinkBlockWithDescription()
2661
+
2662
+ block.set_name("test_structblock")
2663
+ sub_block.set_name("test_structblockwithdescription")
2664
+ js_args = StructBlockAdapter().js_args(block)
2665
+ sub_js_args = StructBlockAdapter().js_args(sub_block)
2666
+
2667
+ form_layout = js_args[2]["formLayout"]
2668
+ self.assertIsInstance(form_layout, BlockGroup)
2669
+ self.assertEqual(form_layout, block.meta.form_layout)
2670
+ self.assertEqual(form_layout.children[0], "link")
2671
+ self.assertIsInstance(form_layout.children[1], BlockGroup)
2672
+ self.assertEqual(form_layout.children[1].children, ["title"])
2673
+
2674
+ # Different instances (including nested BlockGroups), to allow subclassing
2675
+ # without mutating parent class's form layout
2676
+ sub_form_layout = sub_js_args[2]["formLayout"]
2677
+ self.assertIsInstance(sub_form_layout, BlockGroup)
2678
+ self.assertNotEqual(form_layout, sub_form_layout)
2679
+ self.assertEqual(sub_form_layout, sub_block.meta.form_layout)
2680
+ self.assertEqual(sub_form_layout.children[0], "link")
2681
+ self.assertIsInstance(sub_form_layout.children[1], BlockGroup)
2682
+ self.assertNotEqual(form_layout.children[1], sub_form_layout.children[1])
2683
+ self.assertEqual(sub_form_layout.children[1].children, ["title", "description"])
2684
+
2369
2685
  def test_adapt_label_format(self):
2370
2686
  class LinkBlock(blocks.StructBlock):
2371
2687
  title = blocks.CharBlock()
@@ -3006,14 +3322,6 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
3006
3322
  "classname": None,
3007
3323
  "attrs": {},
3008
3324
  "collapsed": False,
3009
- "strings": {
3010
- "DELETE": "Delete",
3011
- "DUPLICATE": "Duplicate",
3012
- "MOVE_DOWN": "Move down",
3013
- "MOVE_UP": "Move up",
3014
- "DRAG": "Drag",
3015
- "ADD": "Add",
3016
- },
3017
3325
  },
3018
3326
  )
3019
3327
 
@@ -3043,14 +3351,6 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
3043
3351
  "collapsed": False,
3044
3352
  "minNum": 2,
3045
3353
  "maxNum": 5,
3046
- "strings": {
3047
- "DELETE": "Delete",
3048
- "DUPLICATE": "Duplicate",
3049
- "MOVE_DOWN": "Move down",
3050
- "MOVE_UP": "Move up",
3051
- "DRAG": "Drag",
3052
- "ADD": "Add",
3053
- },
3054
3354
  },
3055
3355
  )
3056
3356
 
@@ -3228,14 +3528,6 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
3228
3528
  "classname": "special-list-class",
3229
3529
  "attrs": {},
3230
3530
  "collapsed": False,
3231
- "strings": {
3232
- "DELETE": "Delete",
3233
- "DUPLICATE": "Duplicate",
3234
- "MOVE_DOWN": "Move down",
3235
- "MOVE_UP": "Move up",
3236
- "DRAG": "Drag",
3237
- "ADD": "Add",
3238
- },
3239
3531
  },
3240
3532
  )
3241
3533
 
@@ -3266,14 +3558,6 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
3266
3558
  "classname": "custom-list-class",
3267
3559
  "attrs": {},
3268
3560
  "collapsed": False,
3269
- "strings": {
3270
- "DELETE": "Delete",
3271
- "DUPLICATE": "Duplicate",
3272
- "MOVE_DOWN": "Move down",
3273
- "MOVE_UP": "Move up",
3274
- "DRAG": "Drag",
3275
- "ADD": "Add",
3276
- },
3277
3561
  },
3278
3562
  )
3279
3563
 
@@ -3689,10 +3973,10 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
3689
3973
  value = block.to_python([{"type": "paragraph", "value": "Hello"}])
3690
3974
  try:
3691
3975
  block.clean(value)
3692
- except blocks.StreamBlockValidationError:
3976
+ except blocks.StreamBlockValidationError as e:
3693
3977
  raise self.failureException(
3694
3978
  "%s was raised" % blocks.StreamBlockValidationError
3695
- )
3979
+ ) from e
3696
3980
 
3697
3981
  def test_not_required_does_not_raise_an_exception_if_empty(self):
3698
3982
  block = blocks.StreamBlock([("paragraph", blocks.CharBlock())], required=False)
@@ -3700,10 +3984,10 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
3700
3984
 
3701
3985
  try:
3702
3986
  block.clean(value)
3703
- except blocks.StreamBlockValidationError:
3987
+ except blocks.StreamBlockValidationError as e:
3704
3988
  raise self.failureException(
3705
3989
  "%s was raised" % blocks.StreamBlockValidationError
3706
- )
3990
+ ) from e
3707
3991
 
3708
3992
  def test_required_by_default(self):
3709
3993
  block = blocks.StreamBlock([("paragraph", blocks.CharBlock())])
@@ -3939,14 +4223,6 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
3939
4223
  "minNum": None,
3940
4224
  "blockCounts": {},
3941
4225
  "required": True,
3942
- "strings": {
3943
- "DELETE": "Delete",
3944
- "DUPLICATE": "Duplicate",
3945
- "MOVE_DOWN": "Move down",
3946
- "MOVE_UP": "Move up",
3947
- "DRAG": "Drag",
3948
- "ADD": "Add",
3949
- },
3950
4226
  },
3951
4227
  )
3952
4228
 
@@ -4716,14 +4992,6 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
4716
4992
  "required": True,
4717
4993
  "classname": "rocket-section",
4718
4994
  "attrs": {},
4719
- "strings": {
4720
- "DELETE": "Delete",
4721
- "DUPLICATE": "Duplicate",
4722
- "MOVE_DOWN": "Move down",
4723
- "MOVE_UP": "Move up",
4724
- "DRAG": "Drag",
4725
- "ADD": "Add",
4726
- },
4727
4995
  },
4728
4996
  )
4729
4997
 
@@ -4823,14 +5091,6 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
4823
5091
  "required": True,
4824
5092
  "classname": "profile-block-large",
4825
5093
  "attrs": {},
4826
- "strings": {
4827
- "DELETE": "Delete",
4828
- "DUPLICATE": "Duplicate",
4829
- "MOVE_DOWN": "Move down",
4830
- "MOVE_UP": "Move up",
4831
- "DRAG": "Drag",
4832
- "ADD": "Add",
4833
- },
4834
5094
  },
4835
5095
  )
4836
5096
 
@@ -109,3 +109,15 @@ class TestCollectionTreeOperations(TestCase):
109
109
  ),
110
110
  [self.evil_plans_collection],
111
111
  )
112
+
113
+ def test_collection_natural_key(self):
114
+ """Test Collection and natural key implementation"""
115
+
116
+ natural_key = self.holiday_photos_collection.natural_key()
117
+ expected_path = (self.root_collection.name, "Holiday photos")
118
+ self.assertEqual(natural_key, expected_path)
119
+
120
+ retrieved_collection = Collection.objects.get_by_natural_key(
121
+ self.root_collection.name, "Holiday photos"
122
+ )
123
+ self.assertEqual(retrieved_collection, self.holiday_photos_collection)