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
@@ -7,13 +7,13 @@ from django.conf import settings
7
7
  from django.contrib.auth.models import Group, Permission
8
8
  from django.core import mail
9
9
  from django.core.files.base import ContentFile
10
- from django.http import HttpRequest, HttpResponse
10
+ from django.http import HttpRequest, HttpResponse, JsonResponse
11
11
  from django.test import TestCase, modify_settings, override_settings
12
12
  from django.urls import reverse
13
13
  from django.utils import timezone
14
14
  from django.utils.translation import gettext_lazy as _
15
15
 
16
- from wagtail.admin.action_menu import ActionMenuItem
16
+ from wagtail.admin.action_menu import ActionMenuItem, PublishMenuItem
17
17
  from wagtail.admin.admin_url_finder import AdminURLFinder
18
18
  from wagtail.exceptions import PageClassNotFoundError
19
19
  from wagtail.models import (
@@ -119,6 +119,28 @@ class TestPageEdit(WagtailTestUtils, TestCase):
119
119
  # Login
120
120
  self.user = self.login()
121
121
 
122
+ def get_publish_button_label(self, response):
123
+ soup = self.get_soup(response.content)
124
+ publish_button = soup.select_one('.w-dropdown-button > [name="action-publish"]')
125
+ if publish_button is None:
126
+ publish_button = soup.select_one('[name="action-publish"]')
127
+ self.assertIsNotNone(publish_button)
128
+ label = publish_button.select_one('[data-w-progress-target="label"]')
129
+ self.assertIsNotNone(label)
130
+ return label.get_text(strip=True)
131
+
132
+ def schedule_child_page(self, go_live_at):
133
+ edit_url = reverse("wagtailadmin_pages:edit", args=(self.child_page.id,))
134
+ post_data = {
135
+ "title": self.child_page.title,
136
+ "content": self.child_page.content,
137
+ "slug": self.child_page.slug,
138
+ "go_live_at": submittable_timestamp(go_live_at),
139
+ }
140
+ self.client.post(edit_url, post_data, follow=True)
141
+ self.child_page.refresh_from_db(fields=["go_live_at"])
142
+ return edit_url
143
+
122
144
  def assertSchedulingDialogRendered(self, response, edit_url):
123
145
  # Should show the "Edit schedule" button
124
146
  html = response.content.decode()
@@ -220,6 +242,106 @@ class TestPageEdit(WagtailTestUtils, TestCase):
220
242
  expected_url = "/admin/pages/%d/edit/" % self.event_page.id
221
243
  self.assertEqual(url_finder.get_edit_url(self.event_page), expected_url)
222
244
 
245
+ # Autosave defaults to enabled with 500ms interval
246
+ soup = self.get_soup(response.content)
247
+ form = soup.select_one("form[data-edit-form]")
248
+ self.assertIsNotNone(form)
249
+ self.assertIn("w-autosave", form["data-controller"].split())
250
+ self.assertTrue(
251
+ {
252
+ "w-unsaved:add->w-autosave#save:prevent",
253
+ "w-autosave:success->w-unsaved#clear",
254
+ }.issubset(form["data-action"].split())
255
+ )
256
+ self.assertEqual(form.attrs.get("data-w-autosave-interval-value"), "500")
257
+
258
+ def test_loaded_revision_id_and_timestamp_included_in_form(self):
259
+ # Ensure there's a revision for the page
260
+ self.event_page.title = "Updated event page"
261
+ revision = self.event_page.save_revision()
262
+ self.assertEqual(self.event_page.revisions.count(), 1)
263
+
264
+ response = self.client.get(
265
+ reverse("wagtailadmin_pages:edit", args=(self.event_page.id,)),
266
+ )
267
+ self.assertEqual(response.status_code, 200)
268
+ soup = self.get_soup(response.content)
269
+ form = soup.select_one("form[data-edit-form]")
270
+ self.assertIsNotNone(form)
271
+ loaded_revision = form.select_one("input[name='loaded_revision_id']")
272
+ self.assertIsNotNone(loaded_revision)
273
+ self.assertEqual(int(loaded_revision["value"]), revision.pk)
274
+ loaded_timestamp = form.select_one("input[name='loaded_revision_created_at']")
275
+ self.assertIsNotNone(loaded_timestamp)
276
+ self.assertEqual(loaded_timestamp["value"], revision.created_at.isoformat())
277
+
278
+ @override_settings(WAGTAIL_AUTOSAVE_INTERVAL=0)
279
+ def test_autosave_disabled(self):
280
+ response = self.client.get(
281
+ reverse("wagtailadmin_pages:edit", args=(self.event_page.id,))
282
+ )
283
+ self.assertEqual(response.status_code, 200)
284
+ soup = self.get_soup(response.content)
285
+ form = soup.select_one("form[data-edit-form]")
286
+ self.assertIsNotNone(form)
287
+ self.assertNotIn("w-autosave", form["data-controller"].split())
288
+ self.assertNotIn("w-autosave", form["data-action"])
289
+ self.assertIsNone(form.attrs.get("data-w-autosave-interval-value"))
290
+
291
+ @override_settings(WAGTAIL_AUTOSAVE_INTERVAL=2000)
292
+ def test_autosave_custom_interval(self):
293
+ response = self.client.get(
294
+ reverse("wagtailadmin_pages:edit", args=(self.event_page.id,))
295
+ )
296
+ self.assertEqual(response.status_code, 200)
297
+ soup = self.get_soup(response.content)
298
+ form = soup.select_one("form[data-edit-form]")
299
+ self.assertIsNotNone(form)
300
+ self.assertIn("w-autosave", form["data-controller"].split())
301
+ self.assertTrue(
302
+ {
303
+ "w-unsaved:add->w-autosave#save:prevent",
304
+ "w-autosave:success->w-unsaved#clear",
305
+ }.issubset(form["data-action"].split())
306
+ )
307
+ self.assertEqual(form.attrs.get("data-w-autosave-interval-value"), "2000")
308
+
309
+ def test_publish_button_shows_schedule_label_for_future_go_live(self):
310
+ go_live_at = timezone.now() + datetime.timedelta(hours=1)
311
+
312
+ response = self.client.get(self.schedule_child_page(go_live_at))
313
+ self.assertEqual(response.status_code, 200)
314
+
315
+ publish_menu_item = next(
316
+ item
317
+ for item in response.context["action_menu"].menu_items
318
+ if getattr(item, "name", "") == "action-publish"
319
+ )
320
+ publish_context = publish_menu_item.get_context_data(
321
+ response.context["action_menu"].context
322
+ )
323
+
324
+ self.assertTrue(publish_context["is_scheduled"])
325
+ self.assertEqual(self.get_publish_button_label(response), "Schedule to publish")
326
+
327
+ def test_publish_button_shows_publish_label_for_past_schedule(self):
328
+ go_live_at = timezone.now() - datetime.timedelta(hours=1)
329
+
330
+ response = self.client.get(self.schedule_child_page(go_live_at))
331
+ self.assertEqual(response.status_code, 200)
332
+
333
+ publish_menu_item = next(
334
+ item
335
+ for item in response.context["action_menu"].menu_items
336
+ if getattr(item, "name", "") == "action-publish"
337
+ )
338
+ publish_context = publish_menu_item.get_context_data(
339
+ response.context["action_menu"].context
340
+ )
341
+
342
+ self.assertFalse(publish_context["is_scheduled"])
343
+ self.assertEqual(self.get_publish_button_label(response), "Publish")
344
+
223
345
  def test_construct_page_action_menu_hook_with_custom_default_button(self):
224
346
  class CustomDefaultItem(ActionMenuItem):
225
347
  label = "Custom button"
@@ -468,6 +590,292 @@ class TestPageEdit(WagtailTestUtils, TestCase):
468
590
  # The draft_title should have a new title
469
591
  self.assertEqual(child_page_new.draft_title, post_data["title"])
470
592
 
593
+ def test_page_edit_post_with_json_response(self):
594
+ self.assertEqual(self.child_page.revisions.count(), 1)
595
+ loaded_revision = self.child_page.get_latest_revision()
596
+ # Tests simple editing
597
+ post_data = {
598
+ "title": "I've been edited!",
599
+ "content": "Some content",
600
+ "slug": "hello-world",
601
+ "loaded_revision_id": loaded_revision.pk,
602
+ "loaded_revision_created_at": loaded_revision.created_at.isoformat(),
603
+ }
604
+ response = self.client.post(
605
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
606
+ post_data,
607
+ headers={"Accept": "application/json"},
608
+ )
609
+
610
+ # Should be a 200 OK JSON response
611
+ self.assertEqual(response.status_code, 200)
612
+ self.assertEqual(response["Content-Type"], "application/json")
613
+ response_json = response.json()
614
+ self.assertEqual(response_json["success"], True)
615
+ self.assertEqual(response_json["pk"], self.child_page.pk)
616
+ self.assertEqual(response_json["field_updates"], {})
617
+
618
+ # Should create a new revision to be overwritten later
619
+ self.assertEqual(self.child_page.revisions.count(), 2)
620
+ self.assertNotEqual(response_json["revision_id"], loaded_revision.pk)
621
+ revision = self.child_page.revisions.get(pk=response_json["revision_id"])
622
+ self.assertEqual(
623
+ response_json["revision_created_at"],
624
+ revision.created_at.isoformat(),
625
+ )
626
+ self.assertEqual(revision.content["title"], "I've been edited!")
627
+ soup = self.get_soup(response_json["html"])
628
+ status_side_panel = soup.find(
629
+ "template",
630
+ {
631
+ "data-controller": "w-teleport",
632
+ "data-w-teleport-target-value": "[data-side-panel='status']",
633
+ "data-w-teleport-mode-value": "innerHTML",
634
+ },
635
+ )
636
+ self.assertIsNotNone(status_side_panel)
637
+ breadcrumbs = soup.find(
638
+ "template",
639
+ {
640
+ "data-controller": "w-teleport",
641
+ "data-w-teleport-target-value": "header [data-w-breadcrumbs]",
642
+ "data-w-teleport-mode-value": "outerHTML",
643
+ },
644
+ )
645
+ self.assertIsNotNone(breadcrumbs)
646
+ form_title_heading = soup.find(
647
+ "template",
648
+ {
649
+ "data-controller": "w-teleport",
650
+ "data-w-teleport-target-value": "#header-title span",
651
+ "data-w-teleport-mode-value": "textContent",
652
+ },
653
+ )
654
+ self.assertIsNone(form_title_heading)
655
+ header_title = soup.find(
656
+ "template",
657
+ {
658
+ "data-controller": "w-teleport",
659
+ "data-w-teleport-target-value": "head title",
660
+ "data-w-teleport-mode-value": "textContent",
661
+ },
662
+ )
663
+ self.assertIsNotNone(header_title)
664
+ self.assertEqual(
665
+ header_title.text.strip(),
666
+ # Looks a bit off because get_admin_display_title for SimplePage
667
+ # adds (simple page) suffix
668
+ "Editing Simple page: I've been edited! (simple page)",
669
+ )
670
+
671
+ # The page should have "has_unpublished_changes" flag set
672
+ child_page_new = SimplePage.objects.get(id=self.child_page.id)
673
+ self.assertTrue(child_page_new.has_unpublished_changes)
674
+
675
+ # Page fields should not be changed (because we just created a new draft)
676
+ self.assertEqual(child_page_new.title, self.child_page.title)
677
+ self.assertEqual(child_page_new.content, self.child_page.content)
678
+ self.assertEqual(child_page_new.slug, self.child_page.slug)
679
+
680
+ # The draft_title should have a new title
681
+ self.assertEqual(child_page_new.draft_title, post_data["title"])
682
+
683
+ def test_save_outdated_revision_with_json_response(self):
684
+ self.assertEqual(self.child_page.revisions.count(), 1)
685
+ loaded_revision = self.child_page.get_latest_revision()
686
+ self.child_page.title = "Someone else edited after the page is loaded"
687
+ other_revision = self.child_page.save_revision(user=self.user)
688
+ self.assertEqual(self.child_page.revisions.count(), 2)
689
+
690
+ post_data = {
691
+ "title": "Just another edit submitted after the other edit is done",
692
+ "content": "Some content",
693
+ "slug": "hello-world",
694
+ "loaded_revision_id": loaded_revision.pk,
695
+ }
696
+ response = self.client.post(
697
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
698
+ post_data,
699
+ headers={"Accept": "application/json"},
700
+ )
701
+
702
+ # Instead of creating a new revision for autosave (which means the user
703
+ # would unknowingly replace a newer revision), we return an error
704
+ # response that should be a 400 response
705
+ self.assertEqual(response.status_code, 400)
706
+ self.assertEqual(response["Content-Type"], "application/json")
707
+ self.assertEqual(
708
+ response.json(),
709
+ {
710
+ "success": False,
711
+ "error_code": "invalid_revision",
712
+ "error_message": "Saving will overwrite a newer version.",
713
+ },
714
+ )
715
+
716
+ # Page fields should still be from the published version
717
+ self.child_page.refresh_from_db()
718
+ self.assertEqual(self.child_page.title, "Hello world!")
719
+
720
+ # The initially loaded revision, and the actual latest revision,
721
+ # should both be unchanged
722
+ self.assertEqual(self.child_page.revisions.count(), 2)
723
+ loaded_revision.refresh_from_db()
724
+ self.assertEqual(loaded_revision.content["title"], "Hello world!")
725
+ other_revision.refresh_from_db()
726
+ self.assertEqual(
727
+ other_revision.content["title"],
728
+ "Someone else edited after the page is loaded",
729
+ )
730
+ self.assertEqual(self.child_page.get_latest_revision().id, other_revision.id)
731
+
732
+ def test_save_outdated_revision_timestamp_with_json_response(self):
733
+ self.assertEqual(self.child_page.revisions.count(), 1)
734
+ loaded_revision = self.child_page.get_latest_revision()
735
+ loaded_revision_created_at = loaded_revision.created_at.isoformat()
736
+ # Simulate the loaded revision being updated via another session's autosave,
737
+ # which means the revision is overwritten with new content and created_at
738
+ self.child_page.title = "Someone else edited after the page is loaded"
739
+ self.child_page.save_revision(overwrite_revision=loaded_revision)
740
+ self.assertEqual(self.child_page.revisions.count(), 1)
741
+
742
+ post_data = {
743
+ "title": "Just another edit submitted after the other edit is done",
744
+ "content": "Some content",
745
+ "slug": "hello-world",
746
+ "loaded_revision_id": loaded_revision.pk,
747
+ "loaded_revision_created_at": loaded_revision_created_at,
748
+ }
749
+ response = self.client.post(
750
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
751
+ post_data,
752
+ headers={"Accept": "application/json"},
753
+ )
754
+
755
+ # Instead of creating a new revision for autosave (which means the user
756
+ # would unknowingly replace the updated revision), we return an error
757
+ # response that should be a 400 response
758
+ self.assertEqual(response.status_code, 400)
759
+ self.assertEqual(response["Content-Type"], "application/json")
760
+ self.assertEqual(
761
+ response.json(),
762
+ {
763
+ "success": False,
764
+ "error_code": "invalid_revision",
765
+ "error_message": "Saving will overwrite a newer version.",
766
+ },
767
+ )
768
+
769
+ # Page fields should still be from the published version
770
+ self.child_page.refresh_from_db()
771
+ self.assertEqual(self.child_page.title, "Hello world!")
772
+
773
+ # The initially loaded revision should prefer the other session's autosave
774
+ self.assertEqual(self.child_page.revisions.count(), 1)
775
+ loaded_revision.refresh_from_db()
776
+ self.assertEqual(
777
+ loaded_revision.content["title"],
778
+ "Someone else edited after the page is loaded",
779
+ )
780
+ self.assertEqual(self.child_page.get_latest_revision().id, loaded_revision.id)
781
+
782
+ def test_page_edit_post_with_overwrite_revision_and_json_response(self):
783
+ self.assertEqual(self.child_page.revisions.count(), 1)
784
+ loaded_revision = self.child_page.get_latest_revision()
785
+ self.child_page.title = "A changed title"
786
+ revision = self.child_page.save_revision(user=self.user)
787
+ self.assertEqual(self.child_page.revisions.count(), 2)
788
+
789
+ post_data = {
790
+ "title": "I've been edited again!",
791
+ "content": "Some content",
792
+ "slug": "hello-world",
793
+ # The page was originally loaded with loaded_revision, but
794
+ # a successful autosave created a new revision which we now
795
+ # want to overwrite with a new autosave request
796
+ "loaded_revision_id": loaded_revision.pk,
797
+ "overwrite_revision_id": revision.id,
798
+ }
799
+ response = self.client.post(
800
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
801
+ post_data,
802
+ headers={"Accept": "application/json"},
803
+ )
804
+
805
+ # Should be a 200 OK JSON response
806
+ self.assertEqual(response.status_code, 200)
807
+ self.assertEqual(response["Content-Type"], "application/json")
808
+ revision.refresh_from_db()
809
+ response_json = response.json()
810
+ self.assertEqual(response_json["success"], True)
811
+ self.assertEqual(response_json["pk"], self.child_page.pk)
812
+ self.assertEqual(response_json["revision_id"], revision.pk)
813
+ self.assertEqual(
814
+ response_json["revision_created_at"],
815
+ revision.created_at.isoformat(),
816
+ )
817
+
818
+ # The page should have "has_unpublished_changes" flag set
819
+ child_page_new = SimplePage.objects.get(id=self.child_page.id)
820
+ self.assertTrue(child_page_new.has_unpublished_changes)
821
+
822
+ # Page fields should still be from the published version
823
+ self.assertEqual(child_page_new.title, "Hello world!")
824
+
825
+ # The draft_title should have a new title
826
+ self.assertEqual(child_page_new.draft_title, "I've been edited again!")
827
+
828
+ # There should still be only two revisions, but the latest one should be overwritten
829
+ self.assertEqual(self.child_page.revisions.count(), 2)
830
+ self.assertEqual(self.child_page.get_latest_revision().id, revision.id)
831
+ revision.refresh_from_db()
832
+ self.assertEqual(revision.content["title"], "I've been edited again!")
833
+
834
+ def test_overwrite_non_latest_revision(self):
835
+ self.child_page.title = "A changed title"
836
+ user_revision = self.child_page.save_revision(user=self.user)
837
+ self.child_page.title = "Someone else's changed title"
838
+ later_revision = self.child_page.save_revision()
839
+ self.assertEqual(self.child_page.revisions.count(), 3)
840
+
841
+ post_data = {
842
+ "title": "I've been edited again!",
843
+ "content": "Some content",
844
+ "slug": "hello-world",
845
+ "overwrite_revision_id": user_revision.id,
846
+ }
847
+ response = self.client.post(
848
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
849
+ post_data,
850
+ headers={"Accept": "application/json"},
851
+ )
852
+
853
+ # Should be a 400 response
854
+ self.assertEqual(response.status_code, 400)
855
+ self.assertEqual(response["Content-Type"], "application/json")
856
+ self.assertEqual(
857
+ response.json(),
858
+ {
859
+ "success": False,
860
+ "error_code": "invalid_revision",
861
+ "error_message": "Saving will overwrite a newer version.",
862
+ },
863
+ )
864
+
865
+ # Page fields should still be from the published version
866
+ self.child_page.refresh_from_db()
867
+ self.assertEqual(self.child_page.title, "Hello world!")
868
+
869
+ # The passed revision for overwriting, and the actual latest revision, should both be unchanged
870
+ self.assertEqual(self.child_page.revisions.count(), 3)
871
+ user_revision.refresh_from_db()
872
+ self.assertEqual(user_revision.content["title"], "A changed title")
873
+ later_revision.refresh_from_db()
874
+ self.assertEqual(
875
+ later_revision.content["title"], "Someone else's changed title"
876
+ )
877
+ self.assertEqual(self.child_page.get_latest_revision().id, later_revision.id)
878
+
471
879
  def test_page_edit_post_unpublished_page(self):
472
880
  # Based on test_page_edit_post(), but tests changes on a draft page vs. live page.
473
881
  post_data = {
@@ -655,6 +1063,39 @@ class TestPageEdit(WagtailTestUtils, TestCase):
655
1063
  child_page_new = SimplePage.objects.get(id=self.child_page.id)
656
1064
  self.assertFalse(child_page_new.has_unpublished_changes)
657
1065
 
1066
+ def test_page_edit_post_when_locked_with_json_response(self):
1067
+ # Tests that trying to edit a locked page results in an error
1068
+
1069
+ # Lock the page
1070
+ self.child_page.locked = True
1071
+ self.child_page.save()
1072
+
1073
+ # Post
1074
+ post_data = {
1075
+ "title": "I've been edited!",
1076
+ "content": "Some content",
1077
+ "slug": "hello-world",
1078
+ }
1079
+ response = self.client.post(
1080
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
1081
+ post_data,
1082
+ headers={"Accept": "application/json"},
1083
+ )
1084
+
1085
+ self.assertEqual(response.status_code, 400)
1086
+ self.assertEqual(
1087
+ response.json(),
1088
+ {
1089
+ "success": False,
1090
+ "error_code": "locked",
1091
+ "error_message": "The page could not be saved as it is locked.",
1092
+ },
1093
+ )
1094
+
1095
+ # The page shouldn't have "has_unpublished_changes" flag set
1096
+ child_page_new = SimplePage.objects.get(id=self.child_page.id)
1097
+ self.assertFalse(child_page_new.has_unpublished_changes)
1098
+
658
1099
  def test_edit_post_scheduled(self):
659
1100
  # put go_live_at and expire_at several days away from the current date, to avoid
660
1101
  # false matches in content__ tests
@@ -1979,6 +2420,44 @@ class TestPageEdit(WagtailTestUtils, TestCase):
1979
2420
  self.assertEqual(response.status_code, 200)
1980
2421
  self.assertEqual(response.content, b"Overridden!")
1981
2422
 
2423
+ def test_before_edit_page_hook_with_json_response(self):
2424
+ def non_json_hook_func(request, page):
2425
+ self.assertIsInstance(request, HttpRequest)
2426
+ self.assertEqual(page.id, self.child_page.id)
2427
+
2428
+ return HttpResponse("Overridden!")
2429
+
2430
+ def json_hook_func(request, page):
2431
+ self.assertIsInstance(request, HttpRequest)
2432
+ self.assertEqual(page.id, self.child_page.id)
2433
+
2434
+ return JsonResponse({"status": "purple"})
2435
+
2436
+ with self.register_hook("before_edit_page", non_json_hook_func):
2437
+ response = self.client.get(
2438
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
2439
+ headers={"Accept": "application/json"},
2440
+ )
2441
+
2442
+ self.assertEqual(response.status_code, 400)
2443
+ self.assertEqual(
2444
+ response.json(),
2445
+ {
2446
+ "success": False,
2447
+ "error_code": "blocked_by_hook",
2448
+ "error_message": "Request to edit page was blocked by hook.",
2449
+ },
2450
+ )
2451
+
2452
+ with self.register_hook("before_edit_page", json_hook_func):
2453
+ response = self.client.get(
2454
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
2455
+ headers={"Accept": "application/json"},
2456
+ )
2457
+
2458
+ self.assertEqual(response.status_code, 200)
2459
+ self.assertEqual(response.json(), {"status": "purple"})
2460
+
1982
2461
  def test_before_edit_page_hook_post(self):
1983
2462
  def hook_func(request, page):
1984
2463
  self.assertIsInstance(request, HttpRequest)
@@ -2004,6 +2483,72 @@ class TestPageEdit(WagtailTestUtils, TestCase):
2004
2483
  # page should not be edited
2005
2484
  self.assertEqual(Page.objects.get(id=self.child_page.id).title, "Hello world!")
2006
2485
 
2486
+ def test_before_edit_page_hook_post_with_json_response(self):
2487
+ def non_json_hook_func(request, page):
2488
+ self.assertIsInstance(request, HttpRequest)
2489
+ self.assertEqual(page.id, self.child_page.id)
2490
+
2491
+ return HttpResponse("Overridden!")
2492
+
2493
+ def json_hook_func(request, page):
2494
+ self.assertIsInstance(request, HttpRequest)
2495
+ self.assertEqual(page.id, self.child_page.id)
2496
+
2497
+ return JsonResponse({"status": "purple"})
2498
+
2499
+ with self.register_hook("before_edit_page", non_json_hook_func):
2500
+ post_data = {
2501
+ "title": "I've been edited!",
2502
+ "content": "Some content",
2503
+ "slug": "hello-world-new",
2504
+ }
2505
+ response = self.client.post(
2506
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
2507
+ post_data,
2508
+ headers={"Accept": "application/json"},
2509
+ )
2510
+
2511
+ self.assertEqual(response.status_code, 400)
2512
+ self.assertEqual(
2513
+ response.json(),
2514
+ {
2515
+ "success": False,
2516
+ "error_code": "blocked_by_hook",
2517
+ "error_message": "Request to edit page was blocked by hook.",
2518
+ },
2519
+ )
2520
+
2521
+ # page should not be edited
2522
+ self.assertEqual(
2523
+ Page.objects.get(id=self.child_page.id)
2524
+ .get_latest_revision_as_object()
2525
+ .title,
2526
+ "Hello world!",
2527
+ )
2528
+
2529
+ with self.register_hook("before_edit_page", json_hook_func):
2530
+ post_data = {
2531
+ "title": "I've been edited!",
2532
+ "content": "Some content",
2533
+ "slug": "hello-world-new",
2534
+ }
2535
+ response = self.client.post(
2536
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
2537
+ post_data,
2538
+ headers={"Accept": "application/json"},
2539
+ )
2540
+
2541
+ self.assertEqual(response.status_code, 200)
2542
+ self.assertEqual(response.json(), {"status": "purple"})
2543
+
2544
+ # page should not be edited
2545
+ self.assertEqual(
2546
+ Page.objects.get(id=self.child_page.id)
2547
+ .get_latest_revision_as_object()
2548
+ .title,
2549
+ "Hello world!",
2550
+ )
2551
+
2007
2552
  def test_after_edit_page_hook(self):
2008
2553
  def hook_func(request, page):
2009
2554
  self.assertIsInstance(request, HttpRequest)
@@ -2031,6 +2576,66 @@ class TestPageEdit(WagtailTestUtils, TestCase):
2031
2576
  Page.objects.get(id=self.child_page.id).title, "I've been edited!"
2032
2577
  )
2033
2578
 
2579
+ def test_after_edit_page_hook_with_json_response(self):
2580
+ def non_json_hook_func(request, page):
2581
+ self.assertIsInstance(request, HttpRequest)
2582
+ self.assertEqual(page.id, self.child_page.id)
2583
+
2584
+ return HttpResponse("Overridden!")
2585
+
2586
+ def json_hook_func(request, page):
2587
+ self.assertIsInstance(request, HttpRequest)
2588
+ self.assertEqual(page.id, self.child_page.id)
2589
+
2590
+ return JsonResponse({"status": "purple"})
2591
+
2592
+ with self.register_hook("after_edit_page", non_json_hook_func):
2593
+ post_data = {
2594
+ "title": "I've been edited!",
2595
+ "content": "Some content",
2596
+ "slug": "hello-world-new",
2597
+ }
2598
+ response = self.client.post(
2599
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
2600
+ post_data,
2601
+ headers={"Accept": "application/json"},
2602
+ )
2603
+
2604
+ self.assertEqual(response.status_code, 200)
2605
+ # hook response is ignored, since it's not a JSON response
2606
+ self.assertEqual(response.json()["success"], True)
2607
+
2608
+ # page should be edited
2609
+ self.assertEqual(
2610
+ Page.objects.get(id=self.child_page.id)
2611
+ .get_latest_revision_as_object()
2612
+ .title,
2613
+ "I've been edited!",
2614
+ )
2615
+
2616
+ with self.register_hook("after_edit_page", json_hook_func):
2617
+ post_data = {
2618
+ "title": "I've been edited again!",
2619
+ "content": "Some content",
2620
+ "slug": "hello-world-new",
2621
+ }
2622
+ response = self.client.post(
2623
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)),
2624
+ post_data,
2625
+ headers={"Accept": "application/json"},
2626
+ )
2627
+
2628
+ self.assertEqual(response.status_code, 200)
2629
+ self.assertEqual(response.json(), {"status": "purple"})
2630
+
2631
+ # page should be edited
2632
+ self.assertEqual(
2633
+ Page.objects.get(id=self.child_page.id)
2634
+ .get_latest_revision_as_object()
2635
+ .title,
2636
+ "I've been edited again!",
2637
+ )
2638
+
2034
2639
  def test_after_publish_page(self):
2035
2640
  def hook_func(request, page):
2036
2641
  self.assertIsInstance(request, HttpRequest)
@@ -2103,11 +2708,14 @@ class TestPageEdit(WagtailTestUtils, TestCase):
2103
2708
  self.assertIsNotNone(publish_button)
2104
2709
 
2105
2710
  def test_override_publish_action_menu_item_label(self):
2711
+ class CustomPublishMenuItem(PublishMenuItem):
2712
+ label = "Foobar"
2713
+
2106
2714
  def hook_func(menu_items, request, context):
2107
- for item in menu_items:
2108
- if item.name == "action-publish":
2109
- item.label = "Foobar"
2110
- break
2715
+ menu_items[:] = [
2716
+ CustomPublishMenuItem() if item.name == "action-publish" else item
2717
+ for item in menu_items
2718
+ ]
2111
2719
 
2112
2720
  with self.register_hook("construct_page_action_menu", hook_func):
2113
2721
  response = self.client.get(
@@ -2947,7 +3555,7 @@ class TestParentalM2M(WagtailTestUtils, TestCase):
2947
3555
  self.assertIn(self.men_with_beards_category, updated_page.categories.all())
2948
3556
 
2949
3557
 
2950
- class TestValidationErrorMessages(WagtailTestUtils, TestCase):
3558
+ class TestValidationerror_messages(WagtailTestUtils, TestCase):
2951
3559
  fixtures = ["test.json"]
2952
3560
 
2953
3561
  def setUp(self):
@@ -3013,6 +3621,46 @@ class TestValidationErrorMessages(WagtailTestUtils, TestCase):
3013
3621
  )
3014
3622
  self.assertIn("This field is required", error_message.get_text())
3015
3623
 
3624
+ def test_field_error_with_json_response(self):
3625
+ post_data = {
3626
+ "title": "",
3627
+ "date_from": "2017-12-25",
3628
+ "slug": "christmas",
3629
+ "audience": "public",
3630
+ "location": "The North Pole",
3631
+ "cost": "Free",
3632
+ "carousel_items-TOTAL_FORMS": 0,
3633
+ "carousel_items-INITIAL_FORMS": 0,
3634
+ "carousel_items-MIN_NUM_FORMS": 0,
3635
+ "carousel_items-MAX_NUM_FORMS": 0,
3636
+ "speakers-TOTAL_FORMS": 0,
3637
+ "speakers-INITIAL_FORMS": 0,
3638
+ "speakers-MIN_NUM_FORMS": 0,
3639
+ "speakers-MAX_NUM_FORMS": 0,
3640
+ "related_links-TOTAL_FORMS": 0,
3641
+ "related_links-INITIAL_FORMS": 0,
3642
+ "related_links-MIN_NUM_FORMS": 0,
3643
+ "related_links-MAX_NUM_FORMS": 0,
3644
+ "head_counts-TOTAL_FORMS": 0,
3645
+ "head_counts-INITIAL_FORMS": 0,
3646
+ "head_counts-MIN_NUM_FORMS": 0,
3647
+ "head_counts-MAX_NUM_FORMS": 0,
3648
+ }
3649
+ response = self.client.post(
3650
+ reverse("wagtailadmin_pages:edit", args=(self.christmas_page.id,)),
3651
+ post_data,
3652
+ headers={"Accept": "application/json"},
3653
+ )
3654
+ self.assertEqual(response.status_code, 400)
3655
+ self.assertEqual(
3656
+ response.json(),
3657
+ {
3658
+ "success": False,
3659
+ "error_code": "validation_error",
3660
+ "error_message": "There are validation errors, click save to highlight them.",
3661
+ },
3662
+ )
3663
+
3016
3664
  def test_non_field_error(self):
3017
3665
  """Non-field errors should be shown in the header message"""
3018
3666
  post_data = {
@@ -3231,6 +3879,72 @@ class TestNestedInlinePanel(WagtailTestUtils, TestCase):
3231
3879
  self.assertEqual(awards[0].name, "Beard Of The Century")
3232
3880
  self.assertEqual(awards[1].name, "Bobsleigh Olympic gold medallist")
3233
3881
 
3882
+ def test_post_edit_with_json_response(self):
3883
+ self.christmas_page.unpublish() # so that draft changes are applied to the database record
3884
+
3885
+ post_data = nested_form_data(
3886
+ {
3887
+ "title": "Christmas",
3888
+ "date_from": "2017-12-25",
3889
+ "date_to": "2017-12-25",
3890
+ "slug": "christmas",
3891
+ "audience": "public",
3892
+ "location": "The North Pole",
3893
+ "cost": "Free",
3894
+ "carousel_items": inline_formset([]),
3895
+ "speakers": inline_formset(
3896
+ [
3897
+ {
3898
+ "id": self.speaker.id,
3899
+ "first_name": "Jeff",
3900
+ "last_name": "Christmas",
3901
+ "awards": inline_formset(
3902
+ [
3903
+ {
3904
+ "id": self.speaker.awards.first().id,
3905
+ "name": "Beard Of The Century",
3906
+ "date_awarded": "1997-12-25",
3907
+ },
3908
+ {
3909
+ "name": "Bobsleigh Olympic gold medallist",
3910
+ "date_awarded": "2018-02-01",
3911
+ },
3912
+ ],
3913
+ initial=1,
3914
+ ),
3915
+ },
3916
+ ],
3917
+ initial=1,
3918
+ ),
3919
+ "related_links": inline_formset([]),
3920
+ "head_counts": inline_formset([]),
3921
+ }
3922
+ )
3923
+ response = self.client.post(
3924
+ reverse("wagtailadmin_pages:edit", args=(self.christmas_page.id,)),
3925
+ post_data,
3926
+ headers={"Accept": "application/json"},
3927
+ )
3928
+ self.assertEqual(response.status_code, 200)
3929
+ response_json = response.json()
3930
+ self.assertEqual(response_json["success"], True)
3931
+ self.assertEqual(response_json["pk"], self.christmas_page.id)
3932
+ self.christmas_page.refresh_from_db()
3933
+ self.assertEqual(
3934
+ response_json["revision_id"], self.christmas_page.get_latest_revision().pk
3935
+ )
3936
+
3937
+ new_award = self.christmas_page.speakers.first().awards.get(
3938
+ name="Bobsleigh Olympic gold medallist"
3939
+ )
3940
+ self.assertEqual(
3941
+ response_json["field_updates"],
3942
+ {
3943
+ "speakers-0-awards-INITIAL_FORMS": "2",
3944
+ "speakers-0-awards-1-id": str(new_award.id),
3945
+ },
3946
+ )
3947
+
3234
3948
 
3235
3949
  @override_settings(WAGTAIL_I18N_ENABLED=True)
3236
3950
  class TestLocaleSelector(WagtailTestUtils, TestCase):