wagtail 6.0.2__py3-none-any.whl → 6.1rc1__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 (355) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/admin/checks.py +51 -0
  3. wagtail/admin/compare.py +1 -1
  4. wagtail/admin/filters.py +70 -1
  5. wagtail/admin/forms/account.py +1 -1
  6. wagtail/admin/forms/collections.py +15 -0
  7. wagtail/admin/forms/pages.py +49 -0
  8. wagtail/admin/locale/de/LC_MESSAGES/django.mo +0 -0
  9. wagtail/admin/locale/de/LC_MESSAGES/django.po +5 -5
  10. wagtail/admin/locale/en/LC_MESSAGES/django.po +474 -385
  11. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +3 -3
  12. wagtail/admin/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  13. wagtail/admin/locale/pt_PT/LC_MESSAGES/django.po +73 -2
  14. wagtail/admin/locale/ro/LC_MESSAGES/django.mo +0 -0
  15. wagtail/admin/locale/ro/LC_MESSAGES/django.po +3 -3
  16. wagtail/admin/panels/comment_panel.py +1 -1
  17. wagtail/admin/panels/field_panel.py +1 -1
  18. wagtail/admin/rich_text/converters/editor_html.py +3 -1
  19. wagtail/admin/rich_text/editors/draftail/__init__.py +28 -2
  20. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  21. wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
  22. wagtail/admin/static/wagtailadmin/images/favicon.ico +0 -0
  23. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  24. wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
  25. wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
  26. wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
  27. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  28. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  29. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -1
  30. wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
  31. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  32. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +1 -1
  33. wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
  34. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
  38. wagtail/admin/static/wagtailadmin/js/preview-panel.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  40. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
  42. wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  44. wagtail/admin/static/wagtailadmin/js/telepath/telepath.js +1 -1
  45. wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
  46. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  47. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  48. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +4 -4
  49. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  50. wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
  51. wagtail/admin/staticfiles.py +1 -0
  52. wagtail/admin/templates/wagtailadmin/base.html +1 -0
  53. wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +3 -1
  54. wagtail/admin/templates/wagtailadmin/collections/index_results.html +10 -0
  55. wagtail/admin/templates/wagtailadmin/generic/base.html +1 -9
  56. wagtail/admin/templates/wagtailadmin/generic/form.html +3 -2
  57. wagtail/admin/templates/wagtailadmin/generic/history/action_cell.html +27 -0
  58. wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html +3 -3
  59. wagtail/admin/templates/wagtailadmin/icons/keyboard.svg +1 -0
  60. wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +3 -1
  61. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -14
  62. wagtail/admin/templates/wagtailadmin/pages/action_menu/save_draft.html +3 -1
  63. wagtail/admin/templates/wagtailadmin/pages/choose_parent.html +17 -0
  64. wagtail/admin/templates/wagtailadmin/pages/explorable_index.html +8 -0
  65. wagtail/admin/templates/wagtailadmin/pages/history.html +1 -61
  66. wagtail/admin/templates/wagtailadmin/pages/index.html +1 -3
  67. wagtail/admin/templates/wagtailadmin/pages/listing/_locked_indicator.html +2 -2
  68. wagtail/admin/templates/wagtailadmin/pages/listing/_page_title_column_header.html +25 -27
  69. wagtail/admin/templates/wagtailadmin/pages/page_listing_header.html +2 -1
  70. wagtail/admin/templates/wagtailadmin/panels/multi_field_panel_child.html +1 -1
  71. wagtail/admin/templates/wagtailadmin/panels/publishing/schedule_publishing_panel.html +3 -1
  72. wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +1 -1
  73. wagtail/admin/templates/wagtailadmin/shared/active_filters.html +2 -1
  74. wagtail/admin/templates/wagtailadmin/shared/breadcrumbs.html +8 -0
  75. wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
  76. wagtail/admin/templates/wagtailadmin/shared/headers/page_edit_header.html +1 -1
  77. wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +21 -9
  78. wagtail/admin/templates/wagtailadmin/shared/human_readable_date.html +1 -1
  79. wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +29 -0
  80. wagtail/admin/templates/wagtailadmin/shared/side_panel_toggle.html +2 -1
  81. wagtail/admin/templates/wagtailadmin/skeleton.html +2 -1
  82. wagtail/admin/templates/wagtailadmin/tables/related_objects_cell.html +9 -0
  83. wagtail/admin/templates/wagtailadmin/tables/title_cell.html +9 -7
  84. wagtail/admin/templates/wagtailadmin/widgets/draftail_rich_text_area.html +1 -1
  85. wagtail/admin/templates/wagtailadmin/workflows/create.html +6 -23
  86. wagtail/admin/templates/wagtailadmin/workflows/create_task.html +6 -15
  87. wagtail/admin/templates/wagtailadmin/workflows/edit.html +6 -23
  88. wagtail/admin/templates/wagtailadmin/workflows/edit_task.html +6 -13
  89. wagtail/admin/templates/wagtailadmin/workflows/includes/task_usage_cell.html +4 -4
  90. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_tasks_cell.html +18 -0
  91. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_title_cell.html +7 -0
  92. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_used_by_cell.html +25 -0
  93. wagtail/admin/templates/wagtailadmin/workflows/index.html +0 -99
  94. wagtail/admin/templates/wagtailadmin/workflows/index_results.html +10 -0
  95. wagtail/admin/templates/wagtailadmin/workflows/task_index.html +0 -30
  96. wagtail/admin/templates/wagtailadmin/workflows/task_index_results.html +10 -0
  97. wagtail/admin/templates/wagtailadmin/workflows/usage.html +1 -1
  98. wagtail/admin/templatetags/wagtailadmin_tags.py +116 -39
  99. wagtail/admin/tests/pages/test_create_page.py +10 -4
  100. wagtail/admin/tests/pages/test_custom_listing.py +37 -0
  101. wagtail/admin/tests/pages/test_edit_page.py +6 -6
  102. wagtail/admin/tests/pages/test_explorer_view.py +19 -18
  103. wagtail/admin/tests/pages/test_move_page.py +1 -1
  104. wagtail/admin/tests/pages/test_page_usage.py +50 -2
  105. wagtail/admin/tests/pages/test_parent_page_chooser_view.py +119 -0
  106. wagtail/admin/tests/pages/test_preview.py +18 -4
  107. wagtail/admin/tests/test_account_management.py +20 -1
  108. wagtail/admin/tests/test_audit_log.py +172 -5
  109. wagtail/admin/tests/test_checks.py +92 -0
  110. wagtail/admin/tests/test_collections_views.py +19 -5
  111. wagtail/admin/tests/test_compare.py +6 -6
  112. wagtail/admin/tests/test_dashboard.py +404 -0
  113. wagtail/admin/tests/test_dbwhitelister.py +4 -5
  114. wagtail/admin/tests/test_edit_handlers.py +2 -2
  115. wagtail/admin/tests/test_keyboard_shortcuts.py +84 -0
  116. wagtail/admin/tests/test_page_chooser.py +31 -18
  117. wagtail/admin/tests/test_privacy.py +36 -2
  118. wagtail/admin/tests/test_rich_text.py +168 -23
  119. wagtail/admin/tests/test_templatetags.py +411 -43
  120. wagtail/admin/tests/test_views.py +4 -2
  121. wagtail/admin/tests/test_workflows.py +531 -9
  122. wagtail/admin/tests/tests.py +3 -1
  123. wagtail/admin/tests/ui/test_tables.py +48 -1
  124. wagtail/admin/tests/viewsets/test_model_viewset.py +126 -29
  125. wagtail/admin/ui/side_panels.py +3 -1
  126. wagtail/admin/ui/tables/__init__.py +13 -1
  127. wagtail/admin/ui/tables/pages.py +17 -6
  128. wagtail/admin/urls/__init__.py +8 -3
  129. wagtail/admin/urls/pages.py +5 -0
  130. wagtail/admin/urls/workflows.py +10 -0
  131. wagtail/admin/views/chooser.py +20 -24
  132. wagtail/admin/views/collections.py +17 -1
  133. wagtail/admin/views/generic/base.py +31 -4
  134. wagtail/admin/views/generic/history.py +220 -51
  135. wagtail/admin/views/generic/mixins.py +7 -4
  136. wagtail/admin/views/generic/models.py +54 -38
  137. wagtail/admin/views/generic/multiple_upload.py +17 -8
  138. wagtail/admin/views/generic/usage.py +17 -11
  139. wagtail/admin/views/home.py +15 -12
  140. wagtail/admin/views/mixins.py +30 -0
  141. wagtail/admin/views/pages/choose_parent.py +73 -0
  142. wagtail/admin/views/pages/history.py +54 -66
  143. wagtail/admin/views/pages/listing.py +187 -106
  144. wagtail/admin/views/pages/usage.py +6 -1
  145. wagtail/admin/views/pages/utils.py +70 -1
  146. wagtail/admin/views/workflows.py +150 -21
  147. wagtail/admin/viewsets/model.py +2 -2
  148. wagtail/admin/viewsets/pages.py +77 -0
  149. wagtail/admin/wagtail_hooks.py +40 -2
  150. wagtail/admin/widgets/button.py +10 -9
  151. wagtail/api/v2/filters.py +1 -1
  152. wagtail/api/v2/tests/test_pages.py +1 -1
  153. wagtail/blocks/base.py +18 -9
  154. wagtail/blocks/field_block.py +9 -7
  155. wagtail/blocks/list_block.py +16 -6
  156. wagtail/blocks/static_block.py +3 -0
  157. wagtail/blocks/stream_block.py +58 -23
  158. wagtail/blocks/struct_block.py +15 -9
  159. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +39 -47
  160. wagtail/contrib/forms/models.py +5 -5
  161. wagtail/contrib/forms/templates/wagtailforms/list_submissions.html +44 -33
  162. wagtail/contrib/forms/templates/wagtailforms/submissions_index.html +2 -63
  163. wagtail/contrib/forms/tests/test_models.py +26 -0
  164. wagtail/contrib/forms/urls.py +6 -0
  165. wagtail/contrib/forms/views.py +52 -49
  166. wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.mo +0 -0
  167. wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.po +3 -3
  168. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +34 -42
  169. wagtail/contrib/redirects/signal_handlers.py +1 -1
  170. wagtail/contrib/redirects/templates/wagtailredirects/index.html +1 -36
  171. wagtail/contrib/redirects/templates/wagtailredirects/index_results.html +18 -0
  172. wagtail/contrib/redirects/templates/wagtailredirects/redirect_target_cell.html +8 -0
  173. wagtail/contrib/redirects/tests/test_import_command.py +1 -1
  174. wagtail/contrib/redirects/tests/test_redirects.py +79 -8
  175. wagtail/contrib/redirects/urls.py +2 -1
  176. wagtail/contrib/redirects/views.py +85 -55
  177. wagtail/contrib/search_promotions/admin_urls.py +2 -1
  178. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +41 -64
  179. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index.html +1 -16
  180. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index_results.html +11 -0
  181. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/list.html +0 -51
  182. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/results.html +3 -16
  183. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/search_promotion_column.html +15 -0
  184. wagtail/contrib/search_promotions/tests.py +122 -9
  185. wagtail/contrib/search_promotions/views.py +66 -65
  186. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +3 -3
  187. wagtail/contrib/settings/registry.py +10 -5
  188. wagtail/contrib/settings/tests/generic/test_admin.py +9 -0
  189. wagtail/contrib/settings/tests/site_specific/test_admin.py +10 -1
  190. wagtail/contrib/settings/tests/site_specific/test_model.py +3 -3
  191. wagtail/contrib/settings/tests/site_specific/test_templates.py +1 -1
  192. wagtail/contrib/settings/views.py +3 -1
  193. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  194. wagtail/contrib/simple_translation/tests/test_wagtail_hooks.py +2 -2
  195. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
  196. wagtail/contrib/table_block/blocks.py +1 -1
  197. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  198. wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
  199. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
  200. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  201. wagtail/coreutils.py +3 -2
  202. wagtail/documents/admin_urls.py +2 -2
  203. wagtail/documents/locale/en/LC_MESSAGES/django.po +22 -22
  204. wagtail/documents/migrations/0013_delete_uploadeddocument.py +16 -0
  205. wagtail/documents/models.py +1 -20
  206. wagtail/documents/rich_text/__init__.py +11 -7
  207. wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
  208. wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
  209. wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
  210. wagtail/documents/templates/wagtaildocs/documents/index.html +0 -16
  211. wagtail/documents/tests/test_admin_views.py +155 -23
  212. wagtail/documents/tests/test_collection_privacy.py +55 -1
  213. wagtail/documents/tests/test_rich_text.py +14 -0
  214. wagtail/documents/views/documents.py +25 -22
  215. wagtail/documents/views/multiple.py +6 -7
  216. wagtail/documents/views/serve.py +16 -1
  217. wagtail/documents/wagtail_hooks.py +20 -15
  218. wagtail/embeds/blocks.py +5 -0
  219. wagtail/embeds/locale/en/LC_MESSAGES/django.po +2 -2
  220. wagtail/embeds/rich_text/__init__.py +1 -1
  221. wagtail/embeds/tests/test_rich_text.py +14 -0
  222. wagtail/embeds/wagtail_hooks.py +4 -14
  223. wagtail/fields.py +3 -48
  224. wagtail/images/admin_urls.py +2 -2
  225. wagtail/images/check_files/wagtail.jpg +0 -0
  226. wagtail/images/check_files/wagtail.png +0 -0
  227. wagtail/images/fields.py +2 -0
  228. wagtail/images/image_operations.py +1 -1
  229. wagtail/images/locale/en/LC_MESSAGES/django.po +33 -45
  230. wagtail/images/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  231. wagtail/images/locale/pt_PT/LC_MESSAGES/django.po +4 -0
  232. wagtail/images/migrations/0026_delete_uploadedimage.py +16 -0
  233. wagtail/images/models.py +49 -43
  234. wagtail/images/rich_text/__init__.py +18 -8
  235. wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
  236. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  237. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  238. wagtail/images/templates/wagtailimages/images/image_listing_header.html +6 -0
  239. wagtail/images/templates/wagtailimages/images/index.html +11 -51
  240. wagtail/images/tests/test_admin_views.py +119 -62
  241. wagtail/images/tests/test_image_operations.py +10 -0
  242. wagtail/images/tests/test_models.py +35 -33
  243. wagtail/images/tests/test_rich_text.py +14 -0
  244. wagtail/images/tests/utils.py +1 -1
  245. wagtail/images/views/images.py +35 -64
  246. wagtail/images/views/multiple.py +6 -7
  247. wagtail/images/wagtail_hooks.py +4 -14
  248. wagtail/locale/en/LC_MESSAGES/django.po +150 -136
  249. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  250. wagtail/locales/tests.py +18 -3
  251. wagtail/locales/views.py +0 -1
  252. wagtail/management/commands/rebuild_references_index.py +3 -1
  253. wagtail/migrations/0092_alter_collectionviewrestriction_password_and_more.py +33 -0
  254. wagtail/migrations/0093_uploadedfile.py +53 -0
  255. wagtail/models/__init__.py +147 -32
  256. wagtail/models/i18n.py +1 -1
  257. wagtail/models/{collections.py → media.py} +33 -2
  258. wagtail/models/reference_index.py +1 -1
  259. wagtail/models/view_restrictions.py +10 -3
  260. wagtail/project_template/project_name/settings/base.py +6 -0
  261. wagtail/project_template/requirements.txt +1 -1
  262. wagtail/rich_text/__init__.py +25 -8
  263. wagtail/rich_text/pages.py +19 -8
  264. wagtail/rich_text/rewriters.py +140 -68
  265. wagtail/search/backends/database/mysql/mysql.py +3 -3
  266. wagtail/search/backends/database/postgres/postgres.py +3 -3
  267. wagtail/search/backends/database/sqlite/sqlite.py +2 -2
  268. wagtail/search/backends/elasticsearch7.py +4 -0
  269. wagtail/search/locale/en/LC_MESSAGES/django.po +3 -3
  270. wagtail/search/tests/test_postgres_backend.py +50 -0
  271. wagtail/sites/locale/en/LC_MESSAGES/django.po +8 -8
  272. wagtail/sites/tests.py +35 -9
  273. wagtail/sites/views.py +3 -1
  274. wagtail/snippets/locale/de/LC_MESSAGES/django.mo +0 -0
  275. wagtail/snippets/locale/de/LC_MESSAGES/django.po +5 -6
  276. wagtail/snippets/locale/en/LC_MESSAGES/django.po +16 -56
  277. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
  278. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
  279. wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/publish.html +3 -1
  280. wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/save.html +3 -1
  281. wagtail/snippets/templates/wagtailsnippets/snippets/create.html +1 -1
  282. wagtail/snippets/templates/wagtailsnippets/snippets/edit.html +1 -1
  283. wagtail/snippets/tests/test_preview.py +13 -2
  284. wagtail/snippets/tests/test_snippets.py +41 -16
  285. wagtail/snippets/tests/test_viewset.py +63 -18
  286. wagtail/snippets/tests/test_workflows.py +12 -0
  287. wagtail/snippets/views/snippets.py +1 -40
  288. wagtail/templatetags/wagtailcore_tags.py +1 -1
  289. wagtail/test/demosite/models.py +1 -1
  290. wagtail/test/middleware.py +14 -1
  291. wagtail/test/testapp/fixtures/test.json +20 -0
  292. wagtail/test/testapp/migrations/0001_squashed_0073_revisablechildmodel_secret_text.py +8 -8
  293. wagtail/test/testapp/migrations/0023_snippetchoosermodel_full_featured.py +1 -0
  294. wagtail/test/testapp/migrations/0034_custompermissionmodel.py +44 -0
  295. wagtail/test/testapp/migrations/0035_modelwithcustommanager.py +30 -0
  296. wagtail/test/testapp/migrations/0036_complexdefaultstreampage.py +28 -0
  297. wagtail/test/testapp/models.py +79 -2
  298. wagtail/test/testapp/templates/tests/custom_docs_password_required.html +10 -0
  299. wagtail/test/testapp/templates/tests/custom_page_password_required.html +10 -0
  300. wagtail/test/testapp/views.py +24 -2
  301. wagtail/test/testapp/wagtail_hooks.py +19 -0
  302. wagtail/test/utils/wagtail_tests.py +2 -2
  303. wagtail/tests/test_blocks.py +262 -1
  304. wagtail/tests/test_migrations.py +1 -1
  305. wagtail/tests/test_page_model.py +77 -0
  306. wagtail/tests/test_page_privacy.py +18 -1
  307. wagtail/tests/test_rich_text.py +95 -5
  308. wagtail/tests/test_streamfield.py +43 -0
  309. wagtail/tests/test_utils.py +8 -2
  310. wagtail/tests/test_views.py +52 -1
  311. wagtail/tests/test_whitelist.py +7 -7
  312. wagtail/users/forms.py +3 -1
  313. wagtail/users/locale/en/LC_MESSAGES/django.po +124 -96
  314. wagtail/users/migrations/0013_userprofile_density.py +23 -0
  315. wagtail/users/models.py +14 -3
  316. wagtail/users/templates/wagtailusers/groups/create.html +1 -7
  317. wagtail/users/templates/wagtailusers/groups/edit.html +1 -13
  318. wagtail/users/templates/wagtailusers/groups/includes/formatted_permissions.html +46 -2
  319. wagtail/users/templates/wagtailusers/groups/includes/group_form_js.html +0 -2
  320. wagtail/users/templates/wagtailusers/users/create.html +1 -9
  321. wagtail/users/templates/wagtailusers/users/edit.html +1 -9
  322. wagtail/users/templates/wagtailusers/users/index.html +2 -5
  323. wagtail/users/templates/wagtailusers/users/index_results.html +3 -13
  324. wagtail/users/templates/wagtailusers/users/user_cell.html +9 -0
  325. wagtail/users/templatetags/wagtailusers_tags.py +107 -20
  326. wagtail/users/tests/test_admin_views.py +669 -90
  327. wagtail/users/views/groups.py +58 -61
  328. wagtail/users/views/users.py +211 -92
  329. wagtail/users/wagtail_hooks.py +6 -38
  330. wagtail/users/widgets.py +3 -5
  331. wagtail/utils/text.py +1 -1
  332. wagtail/views.py +5 -9
  333. wagtail/whitelist.py +1 -1
  334. {wagtail-6.0.2.dist-info → wagtail-6.1rc1.dist-info}/METADATA +4 -5
  335. {wagtail-6.0.2.dist-info → wagtail-6.1rc1.dist-info}/RECORD +339 -320
  336. wagtail/admin/static/wagtailadmin/js/page-editor.js +0 -1
  337. wagtail/admin/static/wagtailadmin/js/vendor/mousetrap.min.js +0 -1
  338. wagtail/admin/static/wagtailadmin/js/vendor/urlify.js +0 -1
  339. wagtail/admin/static/wagtailadmin/js/vendor/xregexp.min.js +0 -1
  340. wagtail/admin/templates/wagtailadmin/collections/index.html +0 -34
  341. wagtail/admin/templates/wagtailadmin/pages/revisions/_actions.html +0 -22
  342. wagtail/admin/templates/wagtailadmin/shared/page_breadcrumbs.html +0 -55
  343. wagtail/admin/tests/pages/test_dashboard.py +0 -172
  344. wagtail/contrib/redirects/templates/wagtailredirects/results.html +0 -23
  345. wagtail/documents/templates/wagtaildocs/documents/list.html +0 -2
  346. wagtail/search/tests/test_postgres_stemming.py +0 -40
  347. wagtail/sites/templates/wagtailsites/create.html +0 -6
  348. wagtail/sites/templates/wagtailsites/edit.html +0 -6
  349. wagtail/snippets/templates/wagtailsnippets/snippets/revisions/_actions.html +0 -36
  350. wagtail/users/templates/wagtailusers/users/list.html +0 -62
  351. wagtail/users/urls/users.py +0 -12
  352. {wagtail-6.0.2.dist-info → wagtail-6.1rc1.dist-info}/LICENSE +0 -0
  353. {wagtail-6.0.2.dist-info → wagtail-6.1rc1.dist-info}/WHEEL +0 -0
  354. {wagtail-6.0.2.dist-info → wagtail-6.1rc1.dist-info}/entry_points.txt +0 -0
  355. {wagtail-6.0.2.dist-info → wagtail-6.1rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,404 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.auth.models import Group, Permission
3
+ from django.contrib.contenttypes.models import ContentType
4
+ from django.test import TestCase
5
+ from django.urls import reverse
6
+ from django.utils import timezone
7
+ from freezegun import freeze_time
8
+
9
+ from wagtail.admin.views.home import (
10
+ LockedPagesPanel,
11
+ RecentEditsPanel,
12
+ UserObjectsInWorkflowModerationPanel,
13
+ WorkflowObjectsToModeratePanel,
14
+ )
15
+ from wagtail.coreutils import get_dummy_request
16
+ from wagtail.models import GroupPagePermission, Page, Workflow, WorkflowContentType
17
+ from wagtail.test.testapp.models import FullFeaturedSnippet, SimplePage
18
+ from wagtail.test.utils import WagtailTestUtils
19
+
20
+
21
+ class TestRecentEditsPanel(WagtailTestUtils, TestCase):
22
+ def setUp(self):
23
+ # Find root page
24
+ self.root_page = Page.objects.get(id=2)
25
+
26
+ # Add child page
27
+ child_page = SimplePage(
28
+ title="Hello world!",
29
+ slug="hello-world",
30
+ content="Some content here",
31
+ )
32
+ self.root_page.add_child(instance=child_page)
33
+ self.revision = child_page.save_revision()
34
+ self.revision.publish()
35
+ self.child_page = SimplePage.objects.get(id=child_page.id)
36
+
37
+ self.user_alice = self.create_superuser(username="alice", password="password")
38
+ self.create_superuser(username="bob", password="password")
39
+
40
+ def change_something(self, title):
41
+ post_data = {"title": title, "content": "Some content", "slug": "hello-world"}
42
+ response = self.client.post(
43
+ reverse("wagtailadmin_pages:edit", args=(self.child_page.id,)), post_data
44
+ )
45
+
46
+ # Should be redirected to edit page
47
+ self.assertRedirects(
48
+ response, reverse("wagtailadmin_pages:edit", args=(self.child_page.id,))
49
+ )
50
+
51
+ # The page should have "has_unpublished_changes" flag set
52
+ child_page_new = SimplePage.objects.get(id=self.child_page.id)
53
+ self.assertTrue(child_page_new.has_unpublished_changes)
54
+
55
+ def go_to_dashboard_response(self):
56
+ response = self.client.get(reverse("wagtailadmin_home"))
57
+ self.assertEqual(response.status_code, 200)
58
+ return response
59
+
60
+ def test_your_recent_edits(self):
61
+ # Login as Bob
62
+ self.login(username="bob", password="password")
63
+
64
+ # Bob hasn't edited anything yet
65
+ response = self.client.get(reverse("wagtailadmin_home"))
66
+ self.assertNotIn("Your most recent edits", response.content.decode("utf-8"))
67
+
68
+ # Login as Alice
69
+ self.client.logout()
70
+ self.login(username="alice", password="password")
71
+
72
+ # Alice changes something
73
+ self.change_something("Alice's edit")
74
+
75
+ # Edit should show up on dashboard
76
+ response = self.go_to_dashboard_response()
77
+ self.assertIn("Your most recent edits", response.content.decode("utf-8"))
78
+
79
+ # Bob changes something
80
+ self.login(username="bob", password="password")
81
+ self.change_something("Bob's edit")
82
+
83
+ # Edit shows up on Bobs dashboard
84
+ response = self.go_to_dashboard_response()
85
+ self.assertIn("Your most recent edits", response.content.decode("utf-8"))
86
+
87
+ # Login as Alice again
88
+ self.client.logout()
89
+ self.login(username="alice", password="password")
90
+
91
+ # Alice's dashboard should still list that first edit
92
+ response = self.go_to_dashboard_response()
93
+ self.assertIn("Your most recent edits", response.content.decode("utf-8"))
94
+
95
+ def test_missing_page_record(self):
96
+ # Ensure that the panel still renders when one of the page IDs returned from querying
97
+ # PageLogEntry has no corresponding Page object. This can happen if a page is deleted,
98
+ # because PageLogEntry records are kept on deletion.
99
+
100
+ self.login(username="alice", password="password")
101
+ self.change_something("Alice's edit")
102
+ self.child_page.delete()
103
+ response = self.client.get(reverse("wagtailadmin_home"))
104
+ self.assertEqual(response.status_code, 200)
105
+
106
+ def test_panel(self):
107
+ """Test if the panel actually returns expected pages"""
108
+ self.login(username="bob", password="password")
109
+ # change a page
110
+
111
+ edit_timestamp = timezone.now()
112
+ with freeze_time(edit_timestamp):
113
+ self.change_something("Bob's edit")
114
+
115
+ # set a user to 'mock' a request
116
+ self.client.user = get_user_model().objects.get(email="bob@example.com")
117
+ # get the panel to get the last edits
118
+ panel = RecentEditsPanel()
119
+ ctx = panel.get_context_data({"request": self.client})
120
+
121
+ page = Page.objects.get(pk=self.child_page.id).specific
122
+
123
+ # check the timestamp matches the edit
124
+ self.assertEqual(ctx["last_edits"][0][0], edit_timestamp)
125
+ # check if the page in this list is the specific page
126
+ self.assertEqual(ctx["last_edits"][0][1], page)
127
+
128
+ def test_copying_does_not_count_as_an_edit(self):
129
+ self.login(username="bob", password="password")
130
+ # change a page
131
+ self.change_something("Bob was ere")
132
+
133
+ # copy the page
134
+ post_data = {
135
+ "new_title": "Goodbye world!",
136
+ "new_slug": "goodbye-world",
137
+ "new_parent_page": str(self.root_page.id),
138
+ "copy_subpages": False,
139
+ "alias": False,
140
+ }
141
+ self.client.post(
142
+ reverse("wagtailadmin_pages:copy", args=(self.child_page.id,)), post_data
143
+ )
144
+ # check that page has been copied
145
+ self.assertTrue(Page.objects.get(title="Goodbye world!"))
146
+
147
+ response = self.client.get(reverse("wagtailadmin_home"))
148
+ self.assertEqual(response.status_code, 200)
149
+ self.assertContains(response, "Your most recent edits")
150
+ self.assertContains(response, "Bob was ere")
151
+ self.assertNotContains(response, "Goodbye world!")
152
+
153
+
154
+ class TestRecentEditsQueryCount(WagtailTestUtils, TestCase):
155
+ fixtures = ["test.json"]
156
+
157
+ def setUp(self):
158
+ self.bob = self.create_superuser(username="bob", password="password")
159
+ self.dummy_request = get_dummy_request()
160
+ self.dummy_request.user = self.bob
161
+ workflow = Workflow.objects.first()
162
+ workflow_pages = {5, 6}
163
+ locked_pages = {6, 9}
164
+ scheduled_pages = {9, 12}
165
+ # make a bunch of page edits (all to EventPages, so that calls to specific() don't add
166
+ # an unpredictable number of queries)
167
+ pages_to_edit = list(
168
+ Page.objects.filter(id__in=[4, 5, 6, 9, 12, 13]).order_by("pk").specific()
169
+ )
170
+ for page in pages_to_edit:
171
+ revision = page.save_revision(user=self.bob, log_action=True)
172
+ if page.pk in workflow_pages:
173
+ workflow.start(page, self.bob)
174
+ if page.pk in locked_pages:
175
+ page.locked = True
176
+ page.locked_by = self.bob
177
+ page.locked_at = timezone.now()
178
+ page.save()
179
+ if page.pk in scheduled_pages:
180
+ revision.approved_go_live_at = timezone.now()
181
+ revision.save()
182
+
183
+ def test_panel_query_count(self):
184
+ panel = RecentEditsPanel()
185
+ parent_context = {"request": self.dummy_request}
186
+ # Warm up the cache
187
+ html = panel.render_html(parent_context)
188
+
189
+ with self.assertNumQueries(5):
190
+ # Rendering RecentEditsPanel should not generate N+1 queries -
191
+ # i.e. any number less than 6 would be reasonable here
192
+ html = panel.render_html(parent_context)
193
+ # check that the panel is still actually returning results
194
+ self.assertIn("Ameristralia Day", html)
195
+ soup = self.get_soup(html)
196
+ self.assertEqual(len(soup.select('svg use[href="#icon-lock"]')), 2)
197
+ expected_statuses = [
198
+ "live + draft",
199
+ "live + scheduled",
200
+ "live + scheduled",
201
+ "in moderation",
202
+ "in moderation",
203
+ ]
204
+ statuses = [
205
+ "".join(e.find_all(string=True, recursive=False)).strip()
206
+ for e in soup.select(".w-status")
207
+ ]
208
+ self.assertEqual(statuses, expected_statuses)
209
+
210
+
211
+ class TestLockedPagesQueryCount(WagtailTestUtils, TestCase):
212
+ fixtures = ["test.json"]
213
+
214
+ def setUp(self):
215
+ self.bob = self.create_superuser(username="bob", password="password")
216
+ self.dummy_request = get_dummy_request()
217
+ self.dummy_request.user = self.bob
218
+
219
+ pages = Page.objects.filter(pk__in=[9, 12, 13]).order_by("pk")
220
+ for i, page in enumerate(pages):
221
+ page.locked = True
222
+ page.locked_by = self.bob
223
+ page.locked_at = timezone.now() + timezone.timedelta(hours=i)
224
+ page.save()
225
+
226
+ def test_panel_query_count(self):
227
+ panel = LockedPagesPanel()
228
+ parent_context = {"request": self.dummy_request, "csrf_token": "dummy"}
229
+ # Warm up the cache
230
+ html = panel.render_html(parent_context)
231
+
232
+ with self.assertNumQueries(1):
233
+ html = panel.render_html(parent_context)
234
+ soup = self.get_soup(html)
235
+ # Should be sorted descending by locked_at
236
+ expected_titles = [
237
+ "Saint Patrick (single event)",
238
+ "Steal underpants",
239
+ "Ameristralia Day",
240
+ ]
241
+ titles = [e.get_text(strip=True) for e in soup.select(".title-wrapper a")]
242
+ self.assertEqual(titles, expected_titles)
243
+
244
+
245
+ class UserObjectsInWorkflowModerationQueryCount(WagtailTestUtils, TestCase):
246
+ fixtures = ["test.json"]
247
+
248
+ def setUp(self):
249
+ self.superuser = self.create_superuser(username="admin", password="password")
250
+ self.bob = self.create_user(username="bob", password="password")
251
+ self.someone_else = self.create_user(
252
+ username="someoneelse", password="password"
253
+ )
254
+ editors = Group.objects.get(name="Editors")
255
+ editors.user_set.add(self.bob, self.someone_else)
256
+
257
+ workflow = Workflow.objects.first()
258
+ WorkflowContentType.objects.create(
259
+ workflow=workflow,
260
+ content_type=ContentType.objects.get_for_model(FullFeaturedSnippet),
261
+ )
262
+ GroupPagePermission.objects.create(
263
+ group=editors, page=Page.get_first_root_node(), permission_type="change"
264
+ )
265
+ editors.permissions.add(
266
+ Permission.objects.get(codename="change_fullfeaturedsnippet")
267
+ )
268
+
269
+ # Pages owned by bob, but workflow started by someone else
270
+ Page.objects.filter(id__in=[9, 12]).update(owner=self.bob)
271
+ for page in Page.objects.filter(id__in=[9, 12]).specific():
272
+ page.save_revision()
273
+ workflow.start(page, self.someone_else)
274
+ # Lock it to test the lock indicator
275
+ page.locked = True
276
+ page.locked_by = self.superuser
277
+ page.locked_at = timezone.now()
278
+ page.save()
279
+
280
+ # Page workflow started by bob
281
+ for page in Page.objects.filter(id__in=[4, 13]).specific():
282
+ page.save_revision()
283
+ workflow.start(page, self.bob)
284
+
285
+ # Snippet workflow started by bob
286
+ for i in range(1, 3):
287
+ obj = FullFeaturedSnippet.objects.create(text=f"Some obj {i}")
288
+ obj.save_revision()
289
+ workflow.start(obj, self.bob)
290
+
291
+ self.dummy_request = get_dummy_request()
292
+ self.dummy_request.user = self.bob
293
+
294
+ def test_panel_query_count(self):
295
+ panel = UserObjectsInWorkflowModerationPanel()
296
+ parent_context = {"request": self.dummy_request}
297
+ # Warm up the cache
298
+ html = panel.render_html(parent_context)
299
+
300
+ with self.assertNumQueries(4):
301
+ html = panel.render_html(parent_context)
302
+
303
+ soup = self.get_soup(html)
304
+ self.assertEqual(len(soup.select('svg use[href="#icon-lock"]')), 2)
305
+ expected_titles = [
306
+ "Some obj 2",
307
+ "Some obj 1",
308
+ "Saint Patrick (single event)",
309
+ "Christmas",
310
+ "Steal underpants",
311
+ "Ameristralia Day",
312
+ ]
313
+ titles = [e.get_text(strip=True) for e in soup.select(".title-wrapper a")]
314
+ self.assertEqual(titles, expected_titles)
315
+
316
+
317
+ class WorkflowObjectsToModerateQueryCount(WagtailTestUtils, TestCase):
318
+ fixtures = ["test.json"]
319
+
320
+ def setUp(self):
321
+ self.superuser = self.create_superuser(username="admin", password="password")
322
+ self.bob = self.create_user(username="bob", password="password")
323
+ self.moderator = self.create_user(username="moderator", password="password")
324
+
325
+ editors = Group.objects.get(name="Editors")
326
+ moderators = Group.objects.get(name="Moderators")
327
+
328
+ editors.user_set.add(self.bob)
329
+ moderators.user_set.add(self.moderator)
330
+
331
+ root = Page.get_first_root_node()
332
+ GroupPagePermission.objects.create(
333
+ group=editors, page=root, permission_type="change"
334
+ )
335
+ GroupPagePermission.objects.create(
336
+ group=moderators, page=root, permission_type="change"
337
+ )
338
+ GroupPagePermission.objects.create(
339
+ group=moderators, page=root, permission_type="publish"
340
+ )
341
+
342
+ editors.permissions.add(
343
+ Permission.objects.get(codename="change_fullfeaturedsnippet")
344
+ )
345
+ moderators.permissions.add(
346
+ *Permission.objects.filter(
347
+ codename__in=[
348
+ "change_fullfeaturedsnippet",
349
+ "publish_fullfeaturedsnippet",
350
+ ]
351
+ ),
352
+ )
353
+
354
+ workflow = Workflow.objects.first()
355
+ WorkflowContentType.objects.create(
356
+ workflow=workflow,
357
+ content_type=ContentType.objects.get_for_model(FullFeaturedSnippet),
358
+ )
359
+
360
+ # Pages workflow started by bob and locked by moderator
361
+ for page in Page.objects.filter(id__in=[9, 12]).specific():
362
+ page.save_revision()
363
+ workflow.start(page, self.bob)
364
+ # Lock it to test the lock indicator
365
+ page.locked = True
366
+ page.locked_by = self.moderator
367
+ page.locked_at = timezone.now()
368
+ page.save()
369
+
370
+ # Page workflow started by bob
371
+ for page in Page.objects.filter(id__in=[4, 13]).specific():
372
+ page.save_revision()
373
+ workflow.start(page, self.bob)
374
+
375
+ # Snippet workflow started by bob
376
+ for i in range(1, 3):
377
+ obj = FullFeaturedSnippet.objects.create(text=f"Some obj {i}")
378
+ obj.save_revision()
379
+ workflow.start(obj, self.bob)
380
+
381
+ self.dummy_request = get_dummy_request()
382
+ self.dummy_request.user = self.moderator
383
+
384
+ def test_panel_query_count(self):
385
+ panel = WorkflowObjectsToModeratePanel()
386
+ parent_context = {"request": self.dummy_request, "csrf_token": "dummy"}
387
+ # Warm up the cache
388
+ html = panel.render_html(parent_context)
389
+
390
+ with self.assertNumQueries(13):
391
+ html = panel.render_html(parent_context)
392
+
393
+ soup = self.get_soup(html)
394
+ self.assertEqual(len(soup.select('svg use[href="#icon-lock"]')), 2)
395
+ expected_titles = [
396
+ "Some obj 2",
397
+ "Some obj 1",
398
+ "Saint Patrick (single event)",
399
+ "Christmas",
400
+ "Steal underpants",
401
+ "Ameristralia Day",
402
+ ]
403
+ titles = [e.get_text(strip=True) for e in soup.select(".title-wrapper a")]
404
+ self.assertEqual(titles, expected_titles)
@@ -9,7 +9,7 @@ class TestDbWhitelisterMethods(WagtailTestUtils, TestCase):
9
9
  self.whitelister = EditorHTMLConverter().whitelister
10
10
 
11
11
  def test_clean_tag_node_div(self):
12
- soup = self.get_soup("<div>foo</div>", "html5lib")
12
+ soup = self.get_soup("<div>foo</div>")
13
13
  tag = soup.div
14
14
  self.assertEqual(tag.name, "div")
15
15
  self.whitelister.clean_tag_node(soup, tag)
@@ -18,7 +18,6 @@ class TestDbWhitelisterMethods(WagtailTestUtils, TestCase):
18
18
  def test_clean_tag_node_with_data_embedtype(self):
19
19
  soup = self.get_soup(
20
20
  '<p><a data-embedtype="image" data-id=1 data-format="left" data-alt="bar" irrelevant="baz">foo</a></p>',
21
- "html5lib",
22
21
  )
23
22
  tag = soup.p
24
23
  self.whitelister.clean_tag_node(soup, tag)
@@ -29,14 +28,13 @@ class TestDbWhitelisterMethods(WagtailTestUtils, TestCase):
29
28
  def test_clean_tag_node_with_data_linktype(self):
30
29
  soup = self.get_soup(
31
30
  '<a data-linktype="document" data-id="1" irrelevant="baz">foo</a>',
32
- "html5lib",
33
31
  )
34
32
  tag = soup.a
35
33
  self.whitelister.clean_tag_node(soup, tag)
36
34
  self.assertEqual(str(tag), '<a id="1" linktype="document">foo</a>')
37
35
 
38
36
  def test_clean_tag_node(self):
39
- soup = self.get_soup('<a irrelevant="baz">foo</a>', "html5lib")
37
+ soup = self.get_soup('<a irrelevant="baz">foo</a>')
40
38
  tag = soup.a
41
39
  self.whitelister.clean_tag_node(soup, tag)
42
40
  self.assertEqual(str(tag), "<a>foo</a>")
@@ -52,7 +50,8 @@ class TestDbWhitelister(WagtailTestUtils, TestCase):
52
50
  (necessary because we can't guarantee the order that attributes are output in)
53
51
  """
54
52
  self.assertEqual(
55
- self.get_soup(str1, "html5lib"), self.get_soup(str2, "html5lib")
53
+ self.get_soup(str1),
54
+ self.get_soup(str2),
56
55
  )
57
56
 
58
57
  def test_page_link_is_rewritten(self):
@@ -189,7 +189,7 @@ class TestGetFormForModel(TestCase):
189
189
  self.assertIn("speakers", form.formsets)
190
190
  self.assertNotIn("related_links", form.formsets)
191
191
 
192
- def test_get_form_for_model_with_widget_overides_by_class(self):
192
+ def test_get_form_for_model_with_widget_overrides_by_class(self):
193
193
  EventPageForm = get_form_for_model(
194
194
  EventPage,
195
195
  form_class=WagtailAdminPageForm,
@@ -201,7 +201,7 @@ class TestGetFormForModel(TestCase):
201
201
  self.assertEqual(type(form.fields["date_from"]), forms.DateField)
202
202
  self.assertEqual(type(form.fields["date_from"].widget), forms.PasswordInput)
203
203
 
204
- def test_get_form_for_model_with_widget_overides_by_instance(self):
204
+ def test_get_form_for_model_with_widget_overrides_by_instance(self):
205
205
  EventPageForm = get_form_for_model(
206
206
  EventPage,
207
207
  form_class=WagtailAdminPageForm,
@@ -0,0 +1,84 @@
1
+ import json
2
+ import re
3
+
4
+ from django.test import TestCase
5
+ from django.test.client import Client
6
+ from django.urls import reverse
7
+
8
+ from wagtail.test.utils import WagtailTestUtils
9
+
10
+
11
+ class TestKeyboardShortcutsDialog(WagtailTestUtils, TestCase):
12
+ def setUp(self):
13
+ self.login()
14
+
15
+ def test_keyboard_shortcuts_trigger_in_sidebar(self):
16
+ response = self.client.get(reverse("wagtailadmin_home"))
17
+ self.assertEqual(response.status_code, 200)
18
+
19
+ sidebar_data = (
20
+ self.get_soup(response.content)
21
+ .select_one("#wagtail-sidebar-props")
22
+ .contents[0]
23
+ )
24
+
25
+ self.assertIn(
26
+ json.dumps(
27
+ {
28
+ "role": "button",
29
+ "data-a11y-dialog-show": "keyboard-shortcuts-dialog",
30
+ "data-action": "w-action#noop:prevent:stop",
31
+ "data-controller": "w-action",
32
+ }
33
+ ),
34
+ sidebar_data,
35
+ )
36
+
37
+ def test_keyboard_shortcuts_dialog(self):
38
+ response = self.client.get(reverse("wagtailadmin_home"))
39
+
40
+ self.assertEqual(response.status_code, 200)
41
+ self.assertTemplateUsed(
42
+ response, "wagtailadmin/shared/keyboard_shortcuts_dialog.html"
43
+ )
44
+
45
+ soup = self.get_soup(response.content)
46
+
47
+ # Check that the keyboard shortcuts dialog is present
48
+ shortcuts_dialog = soup.select_one("#keyboard-shortcuts-dialog")
49
+ self.assertIsNotNone(shortcuts_dialog)
50
+
51
+ # Check that the keyboard shortcuts dialog has basic accessible content
52
+ self.assertIn(
53
+ "All keyboard shortcuts", shortcuts_dialog.find("caption").prettify()
54
+ )
55
+ self.assertIn("Keyboard shortcut", shortcuts_dialog.find("thead").prettify())
56
+
57
+
58
+ class TestMacKeyboardShortcutsDialog(WagtailTestUtils, TestCase):
59
+ def setUp(self):
60
+ # Creates a client with a Mac user agent
61
+ self.client = Client(
62
+ headers={
63
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
64
+ }
65
+ )
66
+ self.login()
67
+
68
+ def test_mac_useragent_and_behavior(self):
69
+ response = self.client.get(reverse("wagtailadmin_home"))
70
+
71
+ # Check that the user agent is a Mac
72
+ user_agent = response.context["request"].headers.get("User-Agent", "")
73
+ is_mac = re.search(r"Mac|iPod|iPhone|iPad", user_agent)
74
+
75
+ # Add assertions based on expected Mac behavior
76
+ self.assertTrue(is_mac)
77
+
78
+ # Check that the keyboard shortcuts dialog has Mac-specific content
79
+ soup = self.get_soup(response.content)
80
+ shortcuts_dialog = soup.select_one("#keyboard-shortcuts-dialog")
81
+ all_shortcuts = shortcuts_dialog.select("kbd")
82
+ for shortcut in all_shortcuts:
83
+ # All shortcuts should have the ⌘ symbol
84
+ self.assertIn("⌘", shortcut.prettify())
@@ -4,6 +4,7 @@ import urllib.parse as urlparse
4
4
  from django.contrib.auth import get_user_model
5
5
  from django.test import TestCase, TransactionTestCase, override_settings
6
6
  from django.urls import reverse
7
+ from django.utils.html import escape
7
8
  from django.utils.http import urlencode
8
9
 
9
10
  from wagtail.admin.views.chooser import can_choose_page
@@ -1253,7 +1254,7 @@ class TestPageChooserLocaleSelector(WagtailTestUtils, TestCase):
1253
1254
  self.child_page_fr.save()
1254
1255
 
1255
1256
  switch_to_french_url = self.get_choose_page_url(
1256
- self.fr_locale, parent_page_id=self.child_page_fr.pk
1257
+ parent_page_id=self.child_page_fr.pk
1257
1258
  )
1258
1259
  self.LOCALE_SELECTOR_HTML_FR = (
1259
1260
  f'<a href="{switch_to_french_url}" data-locale-selector-link>'
@@ -1266,20 +1267,12 @@ class TestPageChooserLocaleSelector(WagtailTestUtils, TestCase):
1266
1267
  reverse("wagtailadmin_choose_page_child", args=[parent_page_id])
1267
1268
  )
1268
1269
 
1269
- def get_choose_page_url(self, locale=None, parent_page_id=None, html=True):
1270
+ def get_choose_page_url(self, parent_page_id=None, params=""):
1270
1271
  if parent_page_id is not None:
1271
1272
  url = reverse("wagtailadmin_choose_page_child", args=[parent_page_id])
1272
1273
  else:
1273
1274
  url = reverse("wagtailadmin_choose_page")
1274
-
1275
- suffix = ""
1276
- if parent_page_id is None:
1277
- # the locale param should only be appended at the root level
1278
- if locale is None:
1279
- locale = self.fr_locale
1280
- separator = "&amp;" if html else "&"
1281
- suffix = f"{separator}locale={locale.language_code}"
1282
- return f"{url}?page_type=wagtailcore.page{suffix}"
1275
+ return f"{url}?{params}"
1283
1276
 
1284
1277
  def test_locale_selector_present_in_root_view(self):
1285
1278
  response = self.client.get(reverse("wagtailadmin_choose_page"))
@@ -1287,7 +1280,7 @@ class TestPageChooserLocaleSelector(WagtailTestUtils, TestCase):
1287
1280
 
1288
1281
  self.assertRegex(html, self.LOCALE_SELECTOR_HTML)
1289
1282
 
1290
- switch_to_french_url = self.get_choose_page_url(locale=self.fr_locale)
1283
+ switch_to_french_url = self.get_choose_page_url(params="locale=fr")
1291
1284
  fr_selector = f'<a href="{switch_to_french_url}" data-locale-selector-link>'
1292
1285
  self.assertIn(fr_selector, html)
1293
1286
 
@@ -1306,9 +1299,7 @@ class TestPageChooserLocaleSelector(WagtailTestUtils, TestCase):
1306
1299
  self.assertNotIn("data-locale-selector", html)
1307
1300
 
1308
1301
  def test_locale_selector_with_active_locale(self):
1309
- switch_to_french_url = self.get_choose_page_url(
1310
- locale=self.fr_locale, html=False
1311
- )
1302
+ switch_to_french_url = self.get_choose_page_url(params="locale=fr")
1312
1303
  response = self.client.get(switch_to_french_url)
1313
1304
  html = response.json().get("html")
1314
1305
 
@@ -1319,9 +1310,7 @@ class TestPageChooserLocaleSelector(WagtailTestUtils, TestCase):
1319
1310
  html,
1320
1311
  r"data-locale-selector[^<]+<button[^<]+<svg[^<]+<use[^<]+<\/use[^<]+<\/svg[^<]+French",
1321
1312
  )
1322
- switch_to_english_url = self.get_choose_page_url(
1323
- locale=Locale.objects.get(language_code="en")
1324
- )
1313
+ switch_to_english_url = self.get_choose_page_url(params="locale=en")
1325
1314
  self.assertIn(
1326
1315
  f'<a href="{switch_to_english_url}" data-locale-selector-link>',
1327
1316
  html,
@@ -1332,3 +1321,27 @@ class TestPageChooserLocaleSelector(WagtailTestUtils, TestCase):
1332
1321
  response = self.get(self.child_page.pk)
1333
1322
  html = response.json().get("html")
1334
1323
  self.assertNotIn("data-locale-selector", html)
1324
+
1325
+ def test_query_params_preserved(self):
1326
+ choose_url = reverse(
1327
+ "wagtailadmin_choose_page_child", args=[self.child_page.pk]
1328
+ )
1329
+ params = "can_choose_root=false&user_perms=copy_to&match_subclass=true"
1330
+ response = self.client.get(f"{choose_url}?{params}&p=1")
1331
+ html = response.json().get("html")
1332
+ self.assertIn("data-locale-selector", html)
1333
+
1334
+ switch_to_french_url = self.get_choose_page_url(
1335
+ parent_page_id=self.child_page_fr.pk, params=params
1336
+ )
1337
+ self.assertIn(escape(switch_to_french_url), html)
1338
+
1339
+ def test_query_params_preserved_in_root_view(self):
1340
+ choose_url = reverse("wagtailadmin_choose_page")
1341
+ params = "can_choose_root=false&user_perms=copy_to&match_subclass=true"
1342
+ response = self.client.get(f"{choose_url}?{params}&p=1")
1343
+ html = response.json().get("html")
1344
+ self.assertIn("data-locale-selector", html)
1345
+
1346
+ switch_to_french_url = self.get_choose_page_url(params=params + "&locale=fr")
1347
+ self.assertIn(escape(switch_to_french_url), html)