wagtail 6.1.3__py3-none-any.whl → 6.2rc1__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 (257) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/copy_for_translation.py +15 -1
  3. wagtail/admin/checks.py +20 -30
  4. wagtail/admin/forms/pages.py +10 -0
  5. wagtail/admin/icons.py +43 -0
  6. wagtail/admin/locale/en/LC_MESSAGES/django.po +405 -295
  7. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +21 -3
  8. wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
  9. wagtail/admin/locale/sl/LC_MESSAGES/django.po +30 -0
  10. wagtail/admin/menu.py +2 -2
  11. wagtail/admin/migrations/0004_editingsession.py +57 -0
  12. wagtail/admin/migrations/0005_editingsession_is_editing.py +18 -0
  13. wagtail/admin/models.py +36 -3
  14. wagtail/admin/rich_text/editors/draftail/__init__.py +2 -20
  15. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  16. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  17. wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
  18. wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
  19. wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
  20. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  21. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  22. wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
  23. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  24. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +1 -1
  25. wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
  26. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  27. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  28. wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
  29. wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
  30. wagtail/admin/static/wagtailadmin/js/preview-panel.js +2 -1
  31. wagtail/admin/static/wagtailadmin/js/preview-panel.js.LICENSE.txt +11 -0
  32. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  33. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  34. wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
  38. wagtail/admin/static/wagtailadmin/js/userbar.js +2 -1
  39. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +11 -0
  40. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +0 -12
  42. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
  44. wagtail/admin/templates/wagtailadmin/collection_privacy/ancestor_privacy.html +2 -6
  45. wagtail/admin/templates/wagtailadmin/generic/index_results.html +1 -17
  46. wagtail/admin/templates/wagtailadmin/generic/listing_results.html +20 -1
  47. wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html +2 -11
  48. wagtail/admin/templates/wagtailadmin/page_privacy/ancestor_privacy.html +2 -6
  49. wagtail/admin/templates/wagtailadmin/page_privacy/no_privacy.html +2 -0
  50. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -1
  51. wagtail/admin/templates/wagtailadmin/pages/action_menu/menu.html +1 -1
  52. wagtail/admin/templates/wagtailadmin/reports/aging_pages_results.html +54 -0
  53. wagtail/admin/templates/wagtailadmin/reports/base_page_report.html +1 -17
  54. wagtail/admin/templates/wagtailadmin/reports/base_page_report_results.html +10 -0
  55. wagtail/admin/templates/wagtailadmin/reports/base_report.html +1 -40
  56. wagtail/admin/templates/wagtailadmin/reports/base_report_results.html +1 -0
  57. wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_report.html +21 -27
  58. wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_types_usage.html +48 -54
  59. wagtail/admin/templates/wagtailadmin/reports/{locked_pages.html → locked_pages_results.html} +3 -3
  60. wagtail/admin/templates/wagtailadmin/reports/page_types_usage_results.html +10 -0
  61. wagtail/admin/templates/wagtailadmin/reports/site_history_results.html +53 -0
  62. wagtail/admin/templates/wagtailadmin/reports/workflow_results.html +74 -0
  63. wagtail/admin/templates/wagtailadmin/reports/workflow_tasks_results.html +56 -0
  64. wagtail/admin/templates/wagtailadmin/shared/_workflow_init.html +8 -44
  65. wagtail/admin/templates/wagtailadmin/shared/avatar.html +11 -1
  66. wagtail/admin/templates/wagtailadmin/shared/dialog/dialog.html +5 -4
  67. wagtail/admin/templates/wagtailadmin/shared/dropdown/dropdown_button.html +2 -1
  68. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/list.html +132 -0
  69. wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html +44 -0
  70. wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +7 -1
  71. wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html +1 -1
  72. wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +32 -16
  73. wagtail/admin/templates/wagtailadmin/skeleton.html +1 -1
  74. wagtail/admin/templates/wagtailadmin/userbar/item_accessibility.html +9 -11
  75. wagtail/admin/templatetags/wagtailadmin_tags.py +13 -2
  76. wagtail/admin/tests/formats/en/__init__.py +0 -0
  77. wagtail/admin/tests/formats/en/formats.py +1 -0
  78. wagtail/admin/tests/pages/test_create_page.py +47 -0
  79. wagtail/admin/tests/pages/test_edit_page.py +10 -8
  80. wagtail/admin/tests/pages/test_parent_page_chooser_view.py +45 -1
  81. wagtail/admin/tests/test_checks.py +53 -3
  82. wagtail/admin/tests/test_collections_views.py +62 -1
  83. wagtail/admin/tests/test_edit_handlers.py +37 -0
  84. wagtail/admin/tests/test_editing_sessions.py +1336 -0
  85. wagtail/admin/tests/test_icon_sprite.py +12 -21
  86. wagtail/admin/tests/test_page_chooser.py +309 -7
  87. wagtail/admin/tests/test_privacy.py +82 -0
  88. wagtail/admin/tests/test_reports_views.py +464 -70
  89. wagtail/admin/tests/test_userbar.py +93 -6
  90. wagtail/admin/tests/test_workflows.py +223 -33
  91. wagtail/admin/tests/viewsets/test_model_viewset.py +151 -2
  92. wagtail/admin/ui/editing_sessions.py +57 -0
  93. wagtail/admin/urls/__init__.py +9 -15
  94. wagtail/admin/urls/editing_sessions.py +17 -0
  95. wagtail/admin/urls/reports.py +33 -1
  96. wagtail/admin/userbar.py +77 -20
  97. wagtail/admin/views/chooser.py +49 -22
  98. wagtail/admin/views/collections.py +0 -11
  99. wagtail/admin/views/editing_sessions.py +193 -0
  100. wagtail/admin/views/generic/__init__.py +1 -0
  101. wagtail/admin/views/generic/history.py +9 -3
  102. wagtail/admin/views/generic/mixins.py +44 -3
  103. wagtail/admin/views/generic/models.py +46 -72
  104. wagtail/admin/views/generic/permissions.py +20 -10
  105. wagtail/admin/views/home.py +2 -31
  106. wagtail/admin/views/page_privacy.py +20 -5
  107. wagtail/admin/views/pages/choose_parent.py +62 -0
  108. wagtail/admin/views/pages/edit.py +28 -0
  109. wagtail/admin/views/reports/aging_pages.py +6 -10
  110. wagtail/admin/views/reports/audit_logging.py +13 -42
  111. wagtail/admin/views/reports/base.py +31 -4
  112. wagtail/admin/views/reports/locked_pages.py +5 -8
  113. wagtail/admin/views/reports/page_types_usage.py +6 -10
  114. wagtail/admin/views/reports/workflows.py +36 -12
  115. wagtail/admin/viewsets/base.py +8 -3
  116. wagtail/admin/viewsets/chooser.py +1 -1
  117. wagtail/admin/viewsets/model.py +26 -1
  118. wagtail/admin/wagtail_hooks.py +2 -1
  119. wagtail/api/v2/filters.py +6 -0
  120. wagtail/api/v2/tests/test_documents.py +1 -1
  121. wagtail/api/v2/tests/test_images.py +1 -1
  122. wagtail/api/v2/tests/test_pages.py +11 -1
  123. wagtail/api/v2/utils.py +2 -2
  124. wagtail/blocks/base.py +35 -12
  125. wagtail/blocks/definition_lookup.py +85 -0
  126. wagtail/blocks/list_block.py +12 -0
  127. wagtail/blocks/migrations/migrate_operation.py +2 -0
  128. wagtail/blocks/stream_block.py +19 -0
  129. wagtail/blocks/struct_block.py +19 -0
  130. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +1 -1
  131. wagtail/contrib/frontend_cache/backends/__init__.py +5 -0
  132. wagtail/contrib/frontend_cache/backends/azure.py +179 -0
  133. wagtail/contrib/frontend_cache/backends/base.py +28 -0
  134. wagtail/contrib/frontend_cache/backends/cloudflare.py +114 -0
  135. wagtail/contrib/frontend_cache/backends/cloudfront.py +99 -0
  136. wagtail/contrib/frontend_cache/backends/http.py +64 -0
  137. wagtail/contrib/frontend_cache/tests.py +59 -17
  138. wagtail/contrib/frontend_cache/utils.py +26 -8
  139. wagtail/contrib/redirects/filters.py +15 -1
  140. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +37 -72
  141. wagtail/contrib/redirects/models.py +6 -5
  142. wagtail/contrib/redirects/templates/wagtailredirects/edit.html +1 -38
  143. wagtail/contrib/redirects/tests/test_redirects.py +141 -1
  144. wagtail/contrib/redirects/urls.py +1 -2
  145. wagtail/contrib/redirects/views.py +39 -80
  146. wagtail/contrib/routable_page/models.py +6 -4
  147. wagtail/contrib/routable_page/tests.py +11 -0
  148. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +1 -1
  149. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +4 -4
  150. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +5 -1
  151. wagtail/contrib/simple_translation/models.py +2 -1
  152. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
  153. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  154. wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
  155. wagtail/contrib/typed_table_block/blocks.py +19 -0
  156. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  157. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  158. wagtail/contrib/typed_table_block/tests.py +38 -0
  159. wagtail/coreutils.py +1 -1
  160. wagtail/documents/__init__.py +1 -1
  161. wagtail/documents/locale/en/LC_MESSAGES/django.po +8 -8
  162. wagtail/documents/locale/sl/LC_MESSAGES/django.mo +0 -0
  163. wagtail/documents/locale/sl/LC_MESSAGES/django.po +20 -0
  164. wagtail/documents/models.py +5 -1
  165. wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
  166. wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
  167. wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
  168. wagtail/documents/tests/test_models.py +5 -1
  169. wagtail/embeds/apps.py +2 -0
  170. wagtail/embeds/embeds.py +12 -14
  171. wagtail/embeds/finders/__init__.py +2 -0
  172. wagtail/embeds/finders/facebook.py +17 -33
  173. wagtail/embeds/finders/instagram.py +19 -16
  174. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  175. wagtail/embeds/signal_handlers.py +13 -0
  176. wagtail/embeds/tests/test_embeds.py +7 -7
  177. wagtail/fields.py +58 -14
  178. wagtail/images/__init__.py +1 -1
  179. wagtail/images/locale/en/LC_MESSAGES/django.po +34 -34
  180. wagtail/images/locale/sl/LC_MESSAGES/django.mo +0 -0
  181. wagtail/images/locale/sl/LC_MESSAGES/django.po +20 -0
  182. wagtail/images/models.py +2 -0
  183. wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
  184. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  185. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  186. wagtail/images/templates/wagtailimages/images/edit.html +4 -4
  187. wagtail/images/tests/test_admin_views.py +26 -2
  188. wagtail/images/views/chooser.py +6 -1
  189. wagtail/locale/en/LC_MESSAGES/django.po +84 -80
  190. wagtail/locales/locale/en/LC_MESSAGES/django.po +2 -2
  191. wagtail/locales/tests.py +16 -0
  192. wagtail/locales/wagtail_hooks.py +0 -9
  193. wagtail/migrations/0094_alter_page_locale.py +19 -0
  194. wagtail/models/__init__.py +11 -5
  195. wagtail/models/i18n.py +6 -1
  196. wagtail/project_template/requirements.txt +1 -1
  197. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  198. wagtail/signals.py +4 -0
  199. wagtail/sites/locale/en/LC_MESSAGES/django.po +2 -2
  200. wagtail/sites/tests.py +15 -0
  201. wagtail/sites/wagtail_hooks.py +0 -9
  202. wagtail/snippets/locale/en/LC_MESSAGES/django.po +9 -9
  203. wagtail/snippets/permissions.py +5 -3
  204. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
  205. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
  206. wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/menu.html +1 -1
  207. wagtail/snippets/tests/test_snippets.py +78 -12
  208. wagtail/snippets/tests/test_viewset.py +22 -0
  209. wagtail/snippets/views/snippets.py +19 -14
  210. wagtail/snippets/wagtail_hooks.py +2 -10
  211. wagtail/templatetags/wagtailcore_tags.py +3 -0
  212. wagtail/test/dummy_external_storage.py +1 -1
  213. wagtail/test/i18n/migrations/0003_alter_clusterabletestmodel_locale_and_more.py +40 -0
  214. wagtail/test/routablepage/models.py +4 -0
  215. wagtail/test/snippets/migrations/0012_alter_translatablesnippet_locale.py +20 -0
  216. wagtail/test/testapp/migrations/0038_sociallink.py +52 -0
  217. wagtail/test/testapp/migrations/0039_alter_eventcategory_locale_and_more.py +45 -0
  218. wagtail/test/testapp/models.py +24 -0
  219. wagtail/test/testapp/views.py +1 -0
  220. wagtail/test/testapp/wagtail_hooks.py +9 -0
  221. wagtail/test/urls_multilang.py +6 -1
  222. wagtail/test/urls_multilang_non_root.py +11 -0
  223. wagtail/tests/streamfield_migrations/test_migrations.py +53 -12
  224. wagtail/tests/test_audit_log.py +72 -2
  225. wagtail/tests/test_blocks.py +103 -0
  226. wagtail/tests/test_signals.py +49 -2
  227. wagtail/tests/test_streamfield.py +153 -0
  228. wagtail/tests/test_utils.py +14 -0
  229. wagtail/tests/tests.py +5 -0
  230. wagtail/users/apps.py +1 -0
  231. wagtail/users/forms.py +7 -0
  232. wagtail/users/locale/en/LC_MESSAGES/django.po +55 -50
  233. wagtail/users/models.py +1 -0
  234. wagtail/users/templates/wagtailusers/groups/includes/formatted_permissions.html +3 -2
  235. wagtail/users/templatetags/wagtailusers_tags.py +9 -0
  236. wagtail/users/tests/__init__.py +7 -1
  237. wagtail/users/tests/test_admin_views.py +117 -32
  238. wagtail/users/views/groups.py +4 -0
  239. wagtail/users/views/users.py +58 -14
  240. wagtail/users/wagtail_hooks.py +7 -123
  241. wagtail/utils/utils.py +1 -0
  242. wagtail/utils/version.py +5 -2
  243. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/METADATA +3 -3
  244. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/RECORD +248 -222
  245. wagtail/admin/templates/wagtailadmin/reports/aging_pages.html +0 -58
  246. wagtail/admin/templates/wagtailadmin/reports/page_types_usage.html +0 -18
  247. wagtail/admin/templates/wagtailadmin/reports/site_history.html +0 -57
  248. wagtail/admin/templates/wagtailadmin/reports/workflow.html +0 -81
  249. wagtail/admin/templates/wagtailadmin/reports/workflow_tasks.html +0 -63
  250. wagtail/contrib/frontend_cache/backends.py +0 -400
  251. wagtail/contrib/redirects/templates/wagtailredirects/list.html +0 -43
  252. wagtail/contrib/redirects/templates/wagtailredirects/reports/redirects_report.html +0 -14
  253. wagtail/contrib/redirects/tests/test_reports_view.py +0 -82
  254. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/LICENSE +0 -0
  255. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/WHEEL +0 -0
  256. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/entry_points.txt +0 -0
  257. {wagtail-6.1.3.dist-info → wagtail-6.2rc1.dist-info}/top_level.txt +0 -0
@@ -208,22 +208,32 @@ class TestAccessibilityCheckerConfig(WagtailTestUtils, TestCase):
208
208
  config = self.get_config()
209
209
  self.assertIsInstance(config.get("messages"), dict)
210
210
  self.assertEqual(
211
- config["messages"]["empty-heading"],
212
- "Empty heading found. Use meaningful text for screen reader users.",
211
+ config["messages"]["empty-heading"]["error_name"],
212
+ "Empty heading found",
213
+ )
214
+ self.assertEqual(
215
+ config["messages"]["empty-heading"]["help_text"],
216
+ "Use meaningful text for screen reader users",
213
217
  )
214
218
 
215
219
  def test_custom_message(self):
216
220
  class CustomMessageAccessibilityItem(AccessibilityItem):
217
221
  # Override via class attribute
218
222
  axe_messages = {
219
- "empty-heading": "Headings should not be empty!",
223
+ "empty-heading": {
224
+ "error_name": "Headings should not be empty!",
225
+ "help_text": "Use meaningful text!",
226
+ },
220
227
  }
221
228
 
222
229
  # Override via method
223
230
  def get_axe_messages(self, request):
224
231
  return {
225
232
  **super().get_axe_messages(request),
226
- "color-contrast-enhanced": "Increase colour contrast!",
233
+ "color-contrast-enhanced": {
234
+ "error_name": "Insufficient colour contrast!",
235
+ "help_text": "Ensure contrast ratio of at least 4.5:1",
236
+ },
227
237
  }
228
238
 
229
239
  with hooks.register_temporarily(
@@ -234,8 +244,14 @@ class TestAccessibilityCheckerConfig(WagtailTestUtils, TestCase):
234
244
  self.assertEqual(
235
245
  config["messages"],
236
246
  {
237
- "empty-heading": "Headings should not be empty!",
238
- "color-contrast-enhanced": "Increase colour contrast!",
247
+ "empty-heading": {
248
+ "error_name": "Headings should not be empty!",
249
+ "help_text": "Use meaningful text!",
250
+ },
251
+ "color-contrast-enhanced": {
252
+ "error_name": "Insufficient colour contrast!",
253
+ "help_text": "Ensure contrast ratio of at least 4.5:1",
254
+ },
239
255
  },
240
256
  )
241
257
 
@@ -341,6 +357,77 @@ class TestAccessibilityCheckerConfig(WagtailTestUtils, TestCase):
341
357
  },
342
358
  )
343
359
 
360
+ def test_custom_rules_and_checks(self):
361
+ class CustomRulesAndChecksAccessibilityItem(AccessibilityItem):
362
+ # Override via class attribute
363
+ axe_custom_checks = [
364
+ {
365
+ "id": "check-image-alt-text",
366
+ "options": {"pattern": "\\.[a-z]{1,4}$|_"},
367
+ },
368
+ ]
369
+
370
+ # Add via method
371
+ def get_axe_custom_rules(self, request):
372
+ return super().get_axe_custom_rules(request) + [
373
+ {
374
+ "id": "link-text-quality",
375
+ "impact": "serious",
376
+ "selector": "a",
377
+ "tags": ["best-practice"],
378
+ "any": ["check-link-text"],
379
+ "enabled": True,
380
+ }
381
+ ]
382
+
383
+ def get_axe_custom_checks(self, request):
384
+ return super().get_axe_custom_checks(request) + [
385
+ {
386
+ "id": "check-link-text",
387
+ "options": {"pattern": "learn more$"},
388
+ }
389
+ ]
390
+
391
+ with hooks.register_temporarily(
392
+ "construct_wagtail_userbar",
393
+ self.get_hook(CustomRulesAndChecksAccessibilityItem),
394
+ ):
395
+ self.maxDiff = None
396
+ config = self.get_config()
397
+ self.assertEqual(
398
+ config["spec"],
399
+ {
400
+ "rules": [
401
+ {
402
+ "id": "alt-text-quality",
403
+ "impact": "serious",
404
+ "selector": "img[alt]",
405
+ "tags": ["best-practice"],
406
+ "any": ["check-image-alt-text"],
407
+ "enabled": True,
408
+ },
409
+ {
410
+ "id": "link-text-quality",
411
+ "impact": "serious",
412
+ "selector": "a",
413
+ "tags": ["best-practice"],
414
+ "any": ["check-link-text"],
415
+ "enabled": True,
416
+ },
417
+ ],
418
+ "checks": [
419
+ {
420
+ "id": "check-image-alt-text",
421
+ "options": {"pattern": "\\.[a-z]{1,4}$|_"},
422
+ },
423
+ {
424
+ "id": "check-link-text",
425
+ "options": {"pattern": "learn more$"},
426
+ },
427
+ ],
428
+ },
429
+ )
430
+
344
431
 
345
432
  class TestUserbarInPageServe(WagtailTestUtils, TestCase):
346
433
  def setUp(self):
@@ -20,6 +20,7 @@ from wagtail.admin.mail import (
20
20
  WorkflowStateApprovalEmailNotifier,
21
21
  WorkflowStateRejectionEmailNotifier,
22
22
  )
23
+ from wagtail.admin.staticfiles import versioned_static
23
24
  from wagtail.admin.utils import (
24
25
  get_admin_base_url,
25
26
  get_latest_str,
@@ -293,11 +294,13 @@ class TestWorkflowsIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase)
293
294
 
294
295
 
295
296
  class TestWorkflowPermissions(WagtailTestUtils, TestCase):
297
+ url_name = "wagtailadmin_reports:workflow"
298
+
296
299
  def setUp(self):
297
300
  self.user = self.login()
298
301
 
299
302
  def get(self, params={}):
300
- return self.client.get(reverse("wagtailadmin_reports:workflow"), params)
303
+ return self.client.get(reverse(self.url_name), params)
301
304
 
302
305
  def test_simple(self):
303
306
  response = self.get()
@@ -340,6 +343,10 @@ class TestWorkflowPermissions(WagtailTestUtils, TestCase):
340
343
  self.assertEqual(response.status_code, 200)
341
344
 
342
345
 
346
+ class TestWorkflowTaskPermissions(TestWorkflowPermissions):
347
+ url_name = "wagtailadmin_reports:workflow_tasks"
348
+
349
+
343
350
  class TestWorkflowsCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
344
351
  def setUp(self):
345
352
  delete_existing_workflows()
@@ -1493,7 +1500,7 @@ class TestEditTaskView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1493
1500
  self.assertEqual(moderator_url_finder.get_edit_url(self.task), expected_url)
1494
1501
 
1495
1502
 
1496
- class BasePageWorkflowTests(WagtailTestUtils, TestCase):
1503
+ class BasePageWorkflowTests(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1497
1504
  model_name = "page"
1498
1505
 
1499
1506
  def setUp(self):
@@ -2502,11 +2509,53 @@ class TestApproveRejectPageWorkflow(BasePageWorkflowTests):
2502
2509
  def test_workflow_dashboard_panel(self):
2503
2510
  response = self.client.get(reverse("wagtailadmin_home"))
2504
2511
  self.assertContains(response, "Awaiting your review")
2505
- # check that ActivateWorkflowActionsForDashboard is present and passes a valid csrf token
2506
- self.assertRegex(
2507
- response.content.decode("utf-8"),
2508
- r"ActivateWorkflowActionsForDashboard\(\'\w+\'\)",
2512
+ soup = self.get_soup(response.content)
2513
+ # check that the workflow-action script is present with the correct data-activate attribute
2514
+ workflow_action_js = versioned_static("wagtailadmin/js/workflow-action.js")
2515
+ scripts = soup.select(f"script[src='{workflow_action_js}']")
2516
+ self.assertEqual(len(scripts), 1)
2517
+ script = scripts[0]
2518
+ self.assertIsNotNone(script)
2519
+ self.assertEqual(script.get("data-activate"), "dashboard")
2520
+ # Should no longer contain inline JS for activating the workflow actions
2521
+ self.assertNotContains(response, "ActivateWorkflowActionsForDashboard")
2522
+
2523
+ def test_workflow_action_script_included(self):
2524
+ response = self.client.get(self.get_url("edit"))
2525
+ self.assertEqual(response.status_code, 200)
2526
+ soup = self.get_soup(response.content)
2527
+ # check that the workflow-action script is present with the correct
2528
+ # data-activate and data-confirm-cancellation-url attributes
2529
+ workflow_action_js = versioned_static("wagtailadmin/js/workflow-action.js")
2530
+ scripts = soup.select(f"script[src='{workflow_action_js}']")
2531
+ self.assertEqual(len(scripts), 1)
2532
+ script = scripts[0]
2533
+ self.assertIsNotNone(script)
2534
+ self.assertEqual(script.get("data-activate"), "editor")
2535
+ self.assertEqual(
2536
+ script.get("data-confirm-cancellation-url"),
2537
+ self.get_url("confirm_workflow_cancellation"),
2509
2538
  )
2539
+ # Should no longer contain inline JS for activating the workflow actions
2540
+ self.assertNotContains(response, "ActivateWorkflowActionsForEditView")
2541
+
2542
+ @override_settings(WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH=False)
2543
+ def test_workflow_action_script_included_without_cancel_confirmation(self):
2544
+ response = self.client.get(self.get_url("edit"))
2545
+ self.assertEqual(response.status_code, 200)
2546
+ soup = self.get_soup(response.content)
2547
+ # check that the workflow-action script is present with the correct data-activate attribute
2548
+ workflow_action_js = versioned_static("wagtailadmin/js/workflow-action.js")
2549
+ scripts = soup.select(f"script[src='{workflow_action_js}']")
2550
+ self.assertEqual(len(scripts), 1)
2551
+ script = scripts[0]
2552
+ self.assertIsNotNone(script)
2553
+ self.assertEqual(script.get("data-activate"), "editor")
2554
+ # data-confirm-cancellation-url attribute should not be present as
2555
+ # WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH is set to False
2556
+ self.assertIsNone(script.get("data-confirm-cancellation-url"))
2557
+ # Should no longer contain inline JS for activating the workflow actions
2558
+ self.assertNotContains(response, "ActivateWorkflowActionsForEditView")
2510
2559
 
2511
2560
  def test_workflow_action_get(self):
2512
2561
  """
@@ -2892,6 +2941,11 @@ class TestApproveRejectSnippetWorkflowNotLockable(TestApproveRejectSnippetWorkfl
2892
2941
  @freeze_time("2020-03-31 12:00:00")
2893
2942
  class TestPageWorkflowReport(BasePageWorkflowTests):
2894
2943
  export_formats = ["xlsx", "csv"]
2944
+ workflow_url_name = "wagtailadmin_reports:workflow"
2945
+ workflow_tasks_url_name = "wagtailadmin_reports:workflow_tasks"
2946
+ header_buttons_parent_selector = "#w-slim-header-buttons"
2947
+ drilldown_selector = ".w-drilldown"
2948
+ extra_params = ""
2895
2949
 
2896
2950
  def setUp(self):
2897
2951
  super().setUp()
@@ -2901,6 +2955,15 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
2901
2955
  self.post("submit", follow=True)
2902
2956
  self.login(user=self.moderator)
2903
2957
 
2958
+ def assertBreadcrumbs(self, breadcrumbs, html):
2959
+ self.assertBreadcrumbsItemsRendered(breadcrumbs, html)
2960
+
2961
+ def assertPageTitle(self, soup, title):
2962
+ self.assertEqual(soup.select_one("title").text.strip(), title)
2963
+
2964
+ def get(self, url, params=None):
2965
+ return self.client.get(url, params)
2966
+
2904
2967
  def setup_workflow_and_tasks(self):
2905
2968
  self.workflow = Workflow.objects.create(name="test_workflow")
2906
2969
  self.task_1 = GroupApprovalTask.objects.create(name="test_task_1")
@@ -2921,46 +2984,136 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
2921
2984
  return response.getvalue().decode()
2922
2985
 
2923
2986
  def test_workflow_report(self):
2924
- response = self.client.get(reverse("wagtailadmin_reports:workflow"))
2987
+ response = self.get(reverse(self.workflow_url_name))
2925
2988
  self.assertEqual(response.status_code, 200)
2926
2989
  self.assertContains(response, "Hello world!")
2927
2990
  self.assertContains(response, "test_workflow")
2928
2991
  self.assertContains(response, "Sebastian Mitter")
2929
2992
  self.assertContains(response, "March 31, 2020")
2993
+ self.assertBreadcrumbs(
2994
+ [{"url": "", "label": "Workflows"}],
2995
+ response.content,
2996
+ )
2997
+ soup = self.get_soup(response.content)
2998
+ by_task_link = soup.select_one(
2999
+ f"{self.header_buttons_parent_selector} .w-header-button"
3000
+ )
3001
+ self.assertIsNotNone(by_task_link)
3002
+ self.assertEqual(
3003
+ by_task_link.get("href"),
3004
+ reverse("wagtailadmin_reports:workflow_tasks"),
3005
+ )
3006
+ self.assertEqual(list(by_task_link.children)[-1].strip(), "By task")
3007
+ self.assertIsNone(soup.select_one(".w-active-filters"))
3008
+ self.assertPageTitle(soup, "Workflows - Wagtail")
2930
3009
 
2931
- response = self.client.get(reverse("wagtailadmin_reports:workflow_tasks"))
3010
+ response = self.get(reverse(self.workflow_tasks_url_name))
2932
3011
  self.assertEqual(response.status_code, 200)
2933
3012
  self.assertContains(response, "Hello world!")
3013
+ self.assertBreadcrumbs(
3014
+ [{"url": "", "label": "Workflow tasks"}],
3015
+ response.content,
3016
+ )
3017
+ soup = self.get_soup(response.content)
3018
+ by_task_link = soup.select_one(
3019
+ f"{self.header_buttons_parent_selector} .w-header-button"
3020
+ )
3021
+ self.assertIsNotNone(by_task_link)
3022
+ self.assertEqual(
3023
+ by_task_link.get("href"),
3024
+ reverse("wagtailadmin_reports:workflow"),
3025
+ )
3026
+ self.assertEqual(list(by_task_link.children)[-1].strip(), "By workflow")
3027
+ self.assertIsNone(soup.select_one(".w-active-filters"))
3028
+ self.assertPageTitle(soup, "Workflow tasks - Wagtail")
2934
3029
 
2935
3030
  def test_workflow_report_filtered(self):
2936
3031
  # the moderator can review the task, so the workflow state should show up even when reports are filtered by reviewable
2937
- response = self.client.get(
2938
- reverse("wagtailadmin_reports:workflow"), {"reviewable": "true"}
2939
- )
3032
+ response = self.get(reverse(self.workflow_url_name), {"reviewable": "true"})
2940
3033
  self.assertEqual(response.status_code, 200)
2941
3034
  self.assertContains(response, "Hello world!")
2942
3035
  self.assertContains(response, "test_workflow")
2943
3036
  self.assertContains(response, "Sebastian Mitter")
2944
3037
  self.assertContains(response, "March 31, 2020")
2945
3038
 
2946
- response = self.client.get(
2947
- reverse("wagtailadmin_reports:workflow_tasks"), {"reviewable": "true"}
3039
+ # Should render the export buttons inside the header "more" dropdown
3040
+ # with the filtered URL
3041
+ soup = self.get_soup(response.content)
3042
+ links = soup.select(f"{self.header_buttons_parent_selector} .w-dropdown a")
3043
+ unfiltered_url = reverse(self.workflow_url_name)
3044
+ filtered_url = f"{unfiltered_url}?reviewable=true{self.extra_params}"
3045
+ self.assertEqual(len(links), 2)
3046
+ self.assertEqual(
3047
+ [link.get("href") for link in links],
3048
+ [f"{filtered_url}&export=xlsx", f"{filtered_url}&export=csv"],
3049
+ )
3050
+
3051
+ # Should render the active filter pill
3052
+ active_filter = soup.select_one(".w-active-filters .w-pill__content")
3053
+ clear_button = soup.select_one(".w-active-filters .w-pill__remove")
3054
+ self.assertIsNotNone(active_filter)
3055
+ self.assertIsNotNone(clear_button)
3056
+ self.assertNotIn("reviewable", clear_button.attrs.get("data-w-swap-src-value"))
3057
+ self.assertEqual(clear_button.attrs.get("data-w-swap-reflect-value"), "true")
3058
+
3059
+ # Should render the filter inside the drilldown component
3060
+ inputs = soup.select(
3061
+ f"{self.drilldown_selector} input[name='reviewable'][type='radio']"
3062
+ )
3063
+ self.assertEqual(len(inputs), 2)
3064
+ self.assertEqual(inputs[0].get("value"), "")
3065
+ self.assertIsNone(inputs[0].get("checked"))
3066
+ self.assertEqual(inputs[1].get("value"), "true")
3067
+ self.assertEqual(inputs[1].get("checked"), "")
3068
+
3069
+ response = self.get(
3070
+ reverse(self.workflow_tasks_url_name),
3071
+ {"reviewable": "true"},
2948
3072
  )
2949
3073
  self.assertEqual(response.status_code, 200)
2950
3074
  self.assertContains(response, "Hello world!")
2951
3075
 
3076
+ # Should render the export buttons inside the header "more" dropdown
3077
+ # with the filtered URL
3078
+ soup = self.get_soup(response.content)
3079
+ links = soup.select(f"{self.header_buttons_parent_selector} .w-dropdown a")
3080
+ unfiltered_url = reverse(self.workflow_tasks_url_name)
3081
+ filtered_url = f"{unfiltered_url}?reviewable=true{self.extra_params}"
3082
+ self.assertEqual(len(links), 2)
3083
+ self.assertEqual(
3084
+ [link.get("href") for link in links],
3085
+ [f"{filtered_url}&export=xlsx", f"{filtered_url}&export=csv"],
3086
+ )
3087
+
3088
+ # Should render the active filter pill
3089
+ active_filter = soup.select_one(".w-active-filters .w-pill__content")
3090
+ clear_button = soup.select_one(".w-active-filters .w-pill__remove")
3091
+ self.assertIsNotNone(active_filter)
3092
+ self.assertIsNotNone(clear_button)
3093
+ self.assertNotIn("reviewable", clear_button.attrs.get("data-w-swap-src-value"))
3094
+ self.assertEqual(clear_button.attrs.get("data-w-swap-reflect-value"), "true")
3095
+
3096
+ # Should render the filter inside the drilldown component
3097
+ inputs = soup.select(
3098
+ f"{self.drilldown_selector} input[name='reviewable'][type='radio']"
3099
+ )
3100
+ self.assertEqual(len(inputs), 2)
3101
+ self.assertEqual(inputs[0].get("value"), "")
3102
+ self.assertIsNone(inputs[0].get("checked"))
3103
+ self.assertEqual(inputs[1].get("value"), "true")
3104
+ self.assertEqual(inputs[1].get("checked"), "")
3105
+
2952
3106
  # the submitter cannot review the task, so the workflow state shouldn't show up when reports are filtered by reviewable
2953
3107
  self.login(self.submitter)
2954
- response = self.client.get(
2955
- reverse("wagtailadmin_reports:workflow"), {"reviewable": "true"}
2956
- )
3108
+ response = self.get(reverse(self.workflow_url_name), {"reviewable": "true"})
2957
3109
  self.assertEqual(response.status_code, 200)
2958
3110
  self.assertNotContains(response, "Hello world!")
2959
3111
  self.assertNotContains(response, "Sebastian Mitter")
2960
3112
  self.assertNotContains(response, "March 31, 2020")
2961
3113
 
2962
- response = self.client.get(
2963
- reverse("wagtailadmin_reports:workflow_tasks"), {"reviewable": "true"}
3114
+ response = self.get(
3115
+ reverse(self.workflow_tasks_url_name),
3116
+ {"reviewable": "true"},
2964
3117
  )
2965
3118
  self.assertEqual(response.status_code, 200)
2966
3119
  self.assertNotContains(response, "Hello world!")
@@ -2968,8 +3121,8 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
2968
3121
  def test_workflow_report_export(self):
2969
3122
  for export_format in self.export_formats:
2970
3123
  with self.subTest(export_format=export_format):
2971
- response = self.client.get(
2972
- reverse("wagtailadmin_reports:workflow"),
3124
+ response = self.get(
3125
+ reverse(self.workflow_url_name),
2973
3126
  {"export": export_format},
2974
3127
  )
2975
3128
  content = self.get_file_content(response, export_format)
@@ -2979,8 +3132,8 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
2979
3132
  self.assertIn("submitter", content)
2980
3133
  self.assertIn("2020-03-31", content)
2981
3134
 
2982
- response = self.client.get(
2983
- reverse("wagtailadmin_reports:workflow_tasks"),
3135
+ response = self.get(
3136
+ reverse(self.workflow_tasks_url_name),
2984
3137
  {"export": export_format},
2985
3138
  )
2986
3139
  content = self.get_file_content(response, export_format)
@@ -2992,8 +3145,8 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
2992
3145
  with self.subTest(export_format=export_format):
2993
3146
  # the moderator can review the task, so the workflow state should show up even when reports are filtered by reviewable
2994
3147
  self.login(self.moderator)
2995
- response = self.client.get(
2996
- reverse("wagtailadmin_reports:workflow"),
3148
+ response = self.get(
3149
+ reverse(self.workflow_url_name),
2997
3150
  {"reviewable": "true", "export": export_format},
2998
3151
  )
2999
3152
  content = self.get_file_content(response, export_format)
@@ -3003,8 +3156,8 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
3003
3156
  self.assertIn("submitter", content)
3004
3157
  self.assertIn("2020-03-31", content)
3005
3158
 
3006
- response = self.client.get(
3007
- reverse("wagtailadmin_reports:workflow_tasks"),
3159
+ response = self.get(
3160
+ reverse(self.workflow_tasks_url_name),
3008
3161
  {"reviewable": "true", "export": export_format},
3009
3162
  )
3010
3163
  content = self.get_file_content(response, export_format)
@@ -3013,8 +3166,8 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
3013
3166
 
3014
3167
  # the submitter cannot review the task, so the workflow state shouldn't show up when reports are filtered by reviewable
3015
3168
  self.login(self.submitter)
3016
- response = self.client.get(
3017
- reverse("wagtailadmin_reports:workflow"),
3169
+ response = self.get(
3170
+ reverse(self.workflow_url_name),
3018
3171
  {"reviewable": "true", "export": export_format},
3019
3172
  )
3020
3173
  content = self.get_file_content(response, export_format)
@@ -3023,8 +3176,8 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
3023
3176
  self.assertNotIn("submitter", content)
3024
3177
  self.assertNotIn("2020-03-31", content)
3025
3178
 
3026
- response = self.client.get(
3027
- reverse("wagtailadmin_reports:workflow_tasks"),
3179
+ response = self.get(
3180
+ reverse(self.workflow_tasks_url_name),
3028
3181
  {"reviewable": "true", "export": export_format},
3029
3182
  )
3030
3183
  content = self.get_file_content(response, export_format)
@@ -3033,7 +3186,7 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
3033
3186
 
3034
3187
  def test_workflow_report_deleted(self):
3035
3188
  self.object.delete()
3036
- response = self.client.get(reverse("wagtailadmin_reports:workflow"))
3189
+ response = self.get(reverse(self.workflow_url_name))
3037
3190
  self.assertEqual(response.status_code, 200)
3038
3191
  self.assertNotContains(response, "Hello world!")
3039
3192
  # test_workflow is only rendered in the filter, not the results
@@ -3041,15 +3194,46 @@ class TestPageWorkflowReport(BasePageWorkflowTests):
3041
3194
  self.assertNotContains(response, "Sebastian Mitter")
3042
3195
  self.assertNotContains(response, "March 31, 2020")
3043
3196
 
3044
- response = self.client.get(reverse("wagtailadmin_reports:workflow_tasks"))
3197
+ response = self.get(reverse(self.workflow_tasks_url_name))
3045
3198
  self.assertEqual(response.status_code, 200)
3046
3199
  self.assertNotContains(response, "Hello world!")
3047
3200
 
3048
3201
 
3202
+ class TestPageWorkflowReportResults(TestPageWorkflowReport):
3203
+ workflow_url_name = "wagtailadmin_reports:workflow_results"
3204
+ workflow_tasks_url_name = "wagtailadmin_reports:workflow_tasks_results"
3205
+ header_buttons_parent_selector = (
3206
+ '[data-controller="w-teleport"]'
3207
+ '[data-w-teleport-target-value="#w-slim-header-buttons"]'
3208
+ )
3209
+ drilldown_selector = (
3210
+ '[data-controller="w-teleport"]'
3211
+ '[data-w-teleport-target-value="#filters-drilldown"]'
3212
+ )
3213
+ extra_params = "&_w_filter_fragment=true"
3214
+
3215
+ def assertBreadcrumbs(self, breadcrumbs, html):
3216
+ self.assertBreadcrumbsNotRendered(html)
3217
+
3218
+ def assertPageTitle(self, soup, title):
3219
+ self.assertIsNone(soup.select_one("title"))
3220
+
3221
+ def get(self, url, params=None):
3222
+ params = params or {}
3223
+ params["_w_filter_fragment"] = "true"
3224
+ return super().get(url, params)
3225
+
3226
+
3049
3227
  class TestSnippetWorkflowReport(TestPageWorkflowReport, BaseSnippetWorkflowTests):
3050
3228
  pass
3051
3229
 
3052
3230
 
3231
+ class TestSnippetWorkflowReportResults(
3232
+ TestPageWorkflowReportResults, BaseSnippetWorkflowTests
3233
+ ):
3234
+ pass
3235
+
3236
+
3053
3237
  class TestNonLockableSnippetWorkflowReport(
3054
3238
  TestPageWorkflowReport, BaseSnippetWorkflowTests
3055
3239
  ):
@@ -3060,6 +3244,12 @@ class TestNonLockableSnippetWorkflowReport(
3060
3244
  model = ModeratedModel
3061
3245
 
3062
3246
 
3247
+ class TestNonLockableSnippetWorkflowReportResults(
3248
+ TestPageWorkflowReportResults, BaseSnippetWorkflowTests
3249
+ ):
3250
+ model = ModeratedModel
3251
+
3252
+
3063
3253
  class TestPageNotificationPreferences(BasePageWorkflowTests):
3064
3254
  def setUp(self):
3065
3255
  super().setUp()
@@ -3325,7 +3515,7 @@ class TestSnippetNotificationPreferencesHTML(TestSnippetNotificationPreferences)
3325
3515
  pass
3326
3516
 
3327
3517
 
3328
- class TestDisableViews(AdminTemplateTestUtils, BasePageWorkflowTests):
3518
+ class TestDisableViews(BasePageWorkflowTests):
3329
3519
  def test_disable_workflow(self):
3330
3520
  """Test that deactivating a workflow sets it to inactive and cancels in progress states"""
3331
3521
  self.login(self.submitter)