wagtail 6.0.1__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 (512) 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/ca/LC_MESSAGES/django.mo +0 -0
  9. wagtail/admin/locale/ca/LC_MESSAGES/django.po +122 -0
  10. wagtail/admin/locale/de/LC_MESSAGES/django.mo +0 -0
  11. wagtail/admin/locale/de/LC_MESSAGES/django.po +5 -5
  12. wagtail/admin/locale/en/LC_MESSAGES/django.po +474 -385
  13. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +3 -3
  14. wagtail/admin/locale/es/LC_MESSAGES/django.mo +0 -0
  15. wagtail/admin/locale/es/LC_MESSAGES/django.po +6 -6
  16. wagtail/admin/locale/fr/LC_MESSAGES/django.mo +0 -0
  17. wagtail/admin/locale/fr/LC_MESSAGES/django.po +70 -3
  18. wagtail/admin/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  19. wagtail/admin/locale/he_IL/LC_MESSAGES/django.po +2 -6
  20. wagtail/admin/locale/he_IL/LC_MESSAGES/djangojs.mo +0 -0
  21. wagtail/admin/locale/he_IL/LC_MESSAGES/djangojs.po +2 -2
  22. wagtail/admin/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
  23. wagtail/admin/locale/hr_HR/LC_MESSAGES/django.po +4 -0
  24. wagtail/admin/locale/hu/LC_MESSAGES/django.mo +0 -0
  25. wagtail/admin/locale/hu/LC_MESSAGES/django.po +142 -2
  26. wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
  27. wagtail/admin/locale/it/LC_MESSAGES/django.po +80 -8
  28. wagtail/admin/locale/it/LC_MESSAGES/djangojs.mo +0 -0
  29. wagtail/admin/locale/it/LC_MESSAGES/djangojs.po +14 -2
  30. wagtail/admin/locale/lv/LC_MESSAGES/django.mo +0 -0
  31. wagtail/admin/locale/lv/LC_MESSAGES/django.po +154 -1
  32. wagtail/admin/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  33. wagtail/admin/locale/pt_PT/LC_MESSAGES/django.po +73 -2
  34. wagtail/admin/locale/ro/LC_MESSAGES/django.mo +0 -0
  35. wagtail/admin/locale/ro/LC_MESSAGES/django.po +3 -3
  36. wagtail/admin/locale/sl/LC_MESSAGES/django.mo +0 -0
  37. wagtail/admin/locale/sl/LC_MESSAGES/django.po +145 -2
  38. wagtail/admin/locale/sv/LC_MESSAGES/django.mo +0 -0
  39. wagtail/admin/locale/sv/LC_MESSAGES/django.po +77 -3
  40. wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
  41. wagtail/admin/locale/zh_Hant/LC_MESSAGES/django.po +17 -1
  42. wagtail/admin/panels/comment_panel.py +1 -1
  43. wagtail/admin/panels/field_panel.py +1 -1
  44. wagtail/admin/rich_text/converters/editor_html.py +3 -1
  45. wagtail/admin/rich_text/editors/draftail/__init__.py +28 -2
  46. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  47. wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
  48. wagtail/admin/static/wagtailadmin/images/favicon.ico +0 -0
  49. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  50. wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
  51. wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
  52. wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
  53. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  54. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  55. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +1 -1
  56. wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
  57. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  58. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +1 -1
  59. wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
  60. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  61. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  62. wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
  63. wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
  64. wagtail/admin/static/wagtailadmin/js/preview-panel.js +1 -1
  65. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  66. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  67. wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
  68. wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
  69. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  70. wagtail/admin/static/wagtailadmin/js/telepath/telepath.js +1 -1
  71. wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
  72. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  73. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  74. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +4 -4
  75. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  76. wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
  77. wagtail/admin/staticfiles.py +1 -0
  78. wagtail/admin/templates/wagtailadmin/admin_base.html +1 -0
  79. wagtail/admin/templates/wagtailadmin/base.html +1 -0
  80. wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +3 -1
  81. wagtail/admin/templates/wagtailadmin/collections/edit.html +0 -1
  82. wagtail/admin/templates/wagtailadmin/collections/index_results.html +10 -0
  83. wagtail/admin/templates/wagtailadmin/generic/base.html +1 -9
  84. wagtail/admin/templates/wagtailadmin/generic/form.html +4 -2
  85. wagtail/admin/templates/wagtailadmin/generic/history/action_cell.html +27 -0
  86. wagtail/admin/templates/wagtailadmin/generic/index_results.html +8 -0
  87. wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html +3 -4
  88. wagtail/admin/templates/wagtailadmin/icons/keyboard.svg +1 -0
  89. wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +3 -1
  90. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -15
  91. wagtail/admin/templates/wagtailadmin/pages/action_menu/save_draft.html +3 -1
  92. wagtail/admin/templates/wagtailadmin/pages/choose_parent.html +17 -0
  93. wagtail/admin/templates/wagtailadmin/pages/explorable_index.html +8 -0
  94. wagtail/admin/templates/wagtailadmin/pages/history.html +1 -61
  95. wagtail/admin/templates/wagtailadmin/pages/index.html +1 -5
  96. wagtail/admin/templates/wagtailadmin/pages/listing/_locked_indicator.html +2 -2
  97. wagtail/admin/templates/wagtailadmin/pages/listing/_page_title_column_header.html +25 -27
  98. wagtail/admin/templates/wagtailadmin/pages/page_listing_header.html +2 -1
  99. wagtail/admin/templates/wagtailadmin/panels/multi_field_panel_child.html +1 -1
  100. wagtail/admin/templates/wagtailadmin/panels/publishing/schedule_publishing_panel.html +3 -1
  101. wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +1 -1
  102. wagtail/admin/templates/wagtailadmin/shared/active_filters.html +2 -1
  103. wagtail/admin/templates/wagtailadmin/shared/breadcrumbs.html +8 -0
  104. wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
  105. wagtail/admin/templates/wagtailadmin/shared/headers/page_edit_header.html +1 -1
  106. wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html +21 -9
  107. wagtail/admin/templates/wagtailadmin/shared/human_readable_date.html +1 -1
  108. wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +29 -0
  109. wagtail/admin/templates/wagtailadmin/shared/side_panel_toggle.html +2 -1
  110. wagtail/admin/templates/wagtailadmin/skeleton.html +2 -1
  111. wagtail/admin/templates/wagtailadmin/tables/related_objects_cell.html +9 -0
  112. wagtail/admin/templates/wagtailadmin/tables/title_cell.html +9 -7
  113. wagtail/admin/templates/wagtailadmin/widgets/draftail_rich_text_area.html +1 -1
  114. wagtail/admin/templates/wagtailadmin/workflows/create.html +6 -23
  115. wagtail/admin/templates/wagtailadmin/workflows/create_task.html +6 -15
  116. wagtail/admin/templates/wagtailadmin/workflows/edit.html +6 -23
  117. wagtail/admin/templates/wagtailadmin/workflows/edit_task.html +6 -13
  118. wagtail/admin/templates/wagtailadmin/workflows/includes/task_usage_cell.html +4 -4
  119. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_tasks_cell.html +18 -0
  120. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_title_cell.html +7 -0
  121. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_used_by_cell.html +25 -0
  122. wagtail/admin/templates/wagtailadmin/workflows/index.html +0 -99
  123. wagtail/admin/templates/wagtailadmin/workflows/index_results.html +10 -0
  124. wagtail/admin/templates/wagtailadmin/workflows/task_index.html +0 -30
  125. wagtail/admin/templates/wagtailadmin/workflows/task_index_results.html +10 -0
  126. wagtail/admin/templates/wagtailadmin/workflows/usage.html +1 -1
  127. wagtail/admin/templatetags/wagtailadmin_tags.py +116 -39
  128. wagtail/admin/tests/api/test_pages.py +26 -10
  129. wagtail/admin/tests/pages/test_create_page.py +10 -4
  130. wagtail/admin/tests/pages/test_custom_listing.py +37 -0
  131. wagtail/admin/tests/pages/test_edit_page.py +6 -6
  132. wagtail/admin/tests/pages/test_explorer_view.py +19 -18
  133. wagtail/admin/tests/pages/test_move_page.py +1 -1
  134. wagtail/admin/tests/pages/test_page_usage.py +50 -2
  135. wagtail/admin/tests/pages/test_parent_page_chooser_view.py +119 -0
  136. wagtail/admin/tests/pages/test_preview.py +18 -4
  137. wagtail/admin/tests/test_account_management.py +20 -1
  138. wagtail/admin/tests/test_audit_log.py +172 -5
  139. wagtail/admin/tests/test_checks.py +92 -0
  140. wagtail/admin/tests/test_collections_views.py +19 -5
  141. wagtail/admin/tests/test_compare.py +6 -6
  142. wagtail/admin/tests/test_dashboard.py +404 -0
  143. wagtail/admin/tests/test_dbwhitelister.py +4 -5
  144. wagtail/admin/tests/test_edit_handlers.py +2 -2
  145. wagtail/admin/tests/test_keyboard_shortcuts.py +84 -0
  146. wagtail/admin/tests/test_page_chooser.py +31 -18
  147. wagtail/admin/tests/test_privacy.py +36 -2
  148. wagtail/admin/tests/test_rich_text.py +168 -23
  149. wagtail/admin/tests/test_templatetags.py +411 -43
  150. wagtail/admin/tests/test_views.py +4 -2
  151. wagtail/admin/tests/test_workflows.py +531 -9
  152. wagtail/admin/tests/tests.py +3 -1
  153. wagtail/admin/tests/ui/test_tables.py +48 -1
  154. wagtail/admin/tests/viewsets/test_model_viewset.py +130 -23
  155. wagtail/admin/ui/side_panels.py +3 -1
  156. wagtail/admin/ui/tables/__init__.py +13 -1
  157. wagtail/admin/ui/tables/pages.py +17 -6
  158. wagtail/admin/urls/__init__.py +8 -3
  159. wagtail/admin/urls/pages.py +5 -0
  160. wagtail/admin/urls/workflows.py +10 -0
  161. wagtail/admin/views/chooser.py +20 -24
  162. wagtail/admin/views/collections.py +17 -1
  163. wagtail/admin/views/generic/base.py +34 -4
  164. wagtail/admin/views/generic/history.py +220 -51
  165. wagtail/admin/views/generic/mixins.py +7 -4
  166. wagtail/admin/views/generic/models.py +54 -47
  167. wagtail/admin/views/generic/multiple_upload.py +17 -8
  168. wagtail/admin/views/generic/usage.py +17 -11
  169. wagtail/admin/views/home.py +15 -12
  170. wagtail/admin/views/mixins.py +30 -0
  171. wagtail/admin/views/pages/choose_parent.py +73 -0
  172. wagtail/admin/views/pages/history.py +54 -66
  173. wagtail/admin/views/pages/listing.py +187 -106
  174. wagtail/admin/views/pages/usage.py +6 -1
  175. wagtail/admin/views/pages/utils.py +70 -1
  176. wagtail/admin/views/workflows.py +150 -21
  177. wagtail/admin/viewsets/model.py +2 -2
  178. wagtail/admin/viewsets/pages.py +77 -0
  179. wagtail/admin/wagtail_hooks.py +40 -2
  180. wagtail/admin/widgets/button.py +10 -10
  181. wagtail/api/v2/filters.py +1 -1
  182. wagtail/api/v2/tests/test_pages.py +1 -1
  183. wagtail/blocks/base.py +18 -9
  184. wagtail/blocks/field_block.py +9 -7
  185. wagtail/blocks/list_block.py +16 -6
  186. wagtail/blocks/static_block.py +3 -0
  187. wagtail/blocks/stream_block.py +58 -23
  188. wagtail/blocks/struct_block.py +15 -9
  189. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +39 -47
  190. wagtail/contrib/forms/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  191. wagtail/contrib/forms/locale/he_IL/LC_MESSAGES/django.po +2 -2
  192. wagtail/contrib/forms/models.py +5 -5
  193. wagtail/contrib/forms/templates/wagtailforms/list_submissions.html +44 -33
  194. wagtail/contrib/forms/templates/wagtailforms/submissions_index.html +2 -63
  195. wagtail/contrib/forms/tests/test_models.py +26 -0
  196. wagtail/contrib/forms/urls.py +6 -0
  197. wagtail/contrib/forms/views.py +52 -49
  198. wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.mo +0 -0
  199. wagtail/contrib/redirects/locale/ca/LC_MESSAGES/django.po +3 -3
  200. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +34 -42
  201. wagtail/contrib/redirects/locale/fr/LC_MESSAGES/django.po +2 -2
  202. wagtail/contrib/redirects/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  203. wagtail/contrib/redirects/locale/he_IL/LC_MESSAGES/django.po +2 -2
  204. wagtail/contrib/redirects/signal_handlers.py +1 -1
  205. wagtail/contrib/redirects/templates/wagtailredirects/index.html +1 -36
  206. wagtail/contrib/redirects/templates/wagtailredirects/index_results.html +18 -0
  207. wagtail/contrib/redirects/templates/wagtailredirects/redirect_target_cell.html +8 -0
  208. wagtail/contrib/redirects/tests/test_import_command.py +1 -1
  209. wagtail/contrib/redirects/tests/test_redirects.py +79 -8
  210. wagtail/contrib/redirects/urls.py +2 -1
  211. wagtail/contrib/redirects/views.py +85 -55
  212. wagtail/contrib/search_promotions/admin_urls.py +2 -1
  213. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +41 -64
  214. wagtail/contrib/search_promotions/locale/fr/LC_MESSAGES/django.po +2 -2
  215. wagtail/contrib/search_promotions/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  216. wagtail/contrib/search_promotions/locale/he_IL/LC_MESSAGES/django.po +2 -2
  217. wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
  218. wagtail/contrib/search_promotions/locale/hr_HR/LC_MESSAGES/django.po +41 -2
  219. wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.mo +0 -0
  220. wagtail/contrib/search_promotions/locale/it/LC_MESSAGES/django.po +9 -3
  221. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index.html +1 -16
  222. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index_results.html +11 -0
  223. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/list.html +0 -51
  224. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/results.html +3 -16
  225. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/search_promotion_column.html +15 -0
  226. wagtail/contrib/search_promotions/tests.py +122 -9
  227. wagtail/contrib/search_promotions/views.py +66 -65
  228. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +3 -3
  229. wagtail/contrib/settings/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  230. wagtail/contrib/settings/locale/he_IL/LC_MESSAGES/django.po +2 -2
  231. wagtail/contrib/settings/locale/tr/LC_MESSAGES/django.mo +0 -0
  232. wagtail/contrib/settings/locale/tr/LC_MESSAGES/django.po +6 -2
  233. wagtail/contrib/settings/registry.py +10 -5
  234. wagtail/contrib/settings/tests/generic/test_admin.py +9 -0
  235. wagtail/contrib/settings/tests/site_specific/test_admin.py +10 -1
  236. wagtail/contrib/settings/tests/site_specific/test_model.py +3 -3
  237. wagtail/contrib/settings/tests/site_specific/test_templates.py +1 -1
  238. wagtail/contrib/settings/views.py +3 -1
  239. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  240. wagtail/contrib/simple_translation/tests/test_wagtail_hooks.py +2 -2
  241. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +7 -7
  242. wagtail/contrib/styleguide/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  243. wagtail/contrib/styleguide/locale/he_IL/LC_MESSAGES/django.po +2 -2
  244. wagtail/contrib/table_block/blocks.py +2 -2
  245. wagtail/contrib/table_block/locale/ca/LC_MESSAGES/django.mo +0 -0
  246. wagtail/contrib/table_block/locale/ca/LC_MESSAGES/django.po +27 -2
  247. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  248. wagtail/contrib/table_block/locale/hu/LC_MESSAGES/django.mo +0 -0
  249. wagtail/contrib/table_block/locale/hu/LC_MESSAGES/django.po +27 -2
  250. wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.mo +0 -0
  251. wagtail/contrib/table_block/locale/it/LC_MESSAGES/django.po +27 -2
  252. wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
  253. wagtail/contrib/table_block/tests.py +6 -0
  254. wagtail/contrib/typed_table_block/locale/ca/LC_MESSAGES/django.mo +0 -0
  255. wagtail/contrib/typed_table_block/locale/ca/LC_MESSAGES/django.po +12 -2
  256. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
  257. wagtail/contrib/typed_table_block/locale/hu/LC_MESSAGES/django.mo +0 -0
  258. wagtail/contrib/typed_table_block/locale/hu/LC_MESSAGES/django.po +12 -2
  259. wagtail/contrib/typed_table_block/locale/it/LC_MESSAGES/django.mo +0 -0
  260. wagtail/contrib/typed_table_block/locale/it/LC_MESSAGES/django.po +12 -2
  261. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  262. wagtail/coreutils.py +3 -2
  263. wagtail/documents/admin_urls.py +2 -2
  264. wagtail/documents/locale/en/LC_MESSAGES/django.po +22 -22
  265. wagtail/documents/locale/fr/LC_MESSAGES/django.po +2 -2
  266. wagtail/documents/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  267. wagtail/documents/locale/he_IL/LC_MESSAGES/django.po +2 -2
  268. wagtail/documents/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
  269. wagtail/documents/locale/hr_HR/LC_MESSAGES/django.po +19 -2
  270. wagtail/documents/locale/hu/LC_MESSAGES/django.mo +0 -0
  271. wagtail/documents/locale/hu/LC_MESSAGES/django.po +16 -2
  272. wagtail/documents/locale/it/LC_MESSAGES/django.mo +0 -0
  273. wagtail/documents/locale/it/LC_MESSAGES/django.po +19 -2
  274. wagtail/documents/migrations/0013_delete_uploadeddocument.py +16 -0
  275. wagtail/documents/models.py +1 -20
  276. wagtail/documents/rich_text/__init__.py +11 -7
  277. wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
  278. wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
  279. wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
  280. wagtail/documents/templates/wagtaildocs/documents/index.html +0 -16
  281. wagtail/documents/tests/test_admin_views.py +155 -23
  282. wagtail/documents/tests/test_collection_privacy.py +55 -1
  283. wagtail/documents/tests/test_rich_text.py +14 -0
  284. wagtail/documents/views/documents.py +25 -22
  285. wagtail/documents/views/multiple.py +6 -7
  286. wagtail/documents/views/serve.py +16 -1
  287. wagtail/documents/wagtail_hooks.py +20 -15
  288. wagtail/embeds/blocks.py +5 -0
  289. wagtail/embeds/locale/en/LC_MESSAGES/django.po +2 -2
  290. wagtail/embeds/locale/fr/LC_MESSAGES/django.po +2 -2
  291. wagtail/embeds/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  292. wagtail/embeds/locale/he_IL/LC_MESSAGES/django.po +2 -2
  293. wagtail/embeds/rich_text/__init__.py +1 -1
  294. wagtail/embeds/tests/test_rich_text.py +14 -0
  295. wagtail/embeds/wagtail_hooks.py +4 -14
  296. wagtail/fields.py +3 -48
  297. wagtail/images/admin_urls.py +2 -2
  298. wagtail/images/check_files/wagtail.jpg +0 -0
  299. wagtail/images/check_files/wagtail.png +0 -0
  300. wagtail/images/fields.py +2 -0
  301. wagtail/images/image_operations.py +1 -1
  302. wagtail/images/locale/ca/LC_MESSAGES/django.mo +0 -0
  303. wagtail/images/locale/ca/LC_MESSAGES/django.po +12 -0
  304. wagtail/images/locale/en/LC_MESSAGES/django.po +33 -45
  305. wagtail/images/locale/fr/LC_MESSAGES/django.po +2 -2
  306. wagtail/images/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  307. wagtail/images/locale/he_IL/LC_MESSAGES/django.po +2 -2
  308. wagtail/images/locale/hu/LC_MESSAGES/django.mo +0 -0
  309. wagtail/images/locale/hu/LC_MESSAGES/django.po +28 -2
  310. wagtail/images/locale/it/LC_MESSAGES/django.mo +0 -0
  311. wagtail/images/locale/it/LC_MESSAGES/django.po +14 -2
  312. wagtail/images/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  313. wagtail/images/locale/pt_PT/LC_MESSAGES/django.po +4 -0
  314. wagtail/images/migrations/0026_delete_uploadedimage.py +16 -0
  315. wagtail/images/models.py +49 -43
  316. wagtail/images/rich_text/__init__.py +18 -8
  317. wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
  318. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  319. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  320. wagtail/images/templates/wagtailimages/images/image_listing_header.html +6 -0
  321. wagtail/images/templates/wagtailimages/images/index.html +11 -51
  322. wagtail/images/tests/test_admin_views.py +119 -62
  323. wagtail/images/tests/test_image_operations.py +10 -0
  324. wagtail/images/tests/test_models.py +35 -33
  325. wagtail/images/tests/test_rich_text.py +14 -0
  326. wagtail/images/tests/utils.py +1 -1
  327. wagtail/images/views/images.py +35 -64
  328. wagtail/images/views/multiple.py +6 -7
  329. wagtail/images/wagtail_hooks.py +4 -14
  330. wagtail/locale/en/LC_MESSAGES/django.po +150 -136
  331. wagtail/locale/es/LC_MESSAGES/django.mo +0 -0
  332. wagtail/locale/es/LC_MESSAGES/django.po +3 -2
  333. wagtail/locale/fr/LC_MESSAGES/django.po +2 -2
  334. wagtail/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  335. wagtail/locale/he_IL/LC_MESSAGES/django.po +2 -2
  336. wagtail/locale/it/LC_MESSAGES/django.mo +0 -0
  337. wagtail/locale/it/LC_MESSAGES/django.po +5 -5
  338. wagtail/locale/sl/LC_MESSAGES/django.mo +0 -0
  339. wagtail/locale/sl/LC_MESSAGES/django.po +27 -2
  340. wagtail/locales/locale/ar/LC_MESSAGES/django.po +1 -1
  341. wagtail/locales/locale/be/LC_MESSAGES/django.po +1 -1
  342. wagtail/locales/locale/bg/LC_MESSAGES/django.po +1 -1
  343. wagtail/locales/locale/ca/LC_MESSAGES/django.po +1 -1
  344. wagtail/locales/locale/cs/LC_MESSAGES/django.po +1 -1
  345. wagtail/locales/locale/cy/LC_MESSAGES/django.po +1 -1
  346. wagtail/locales/locale/da/LC_MESSAGES/django.po +1 -1
  347. wagtail/locales/locale/de/LC_MESSAGES/django.po +1 -1
  348. wagtail/locales/locale/el/LC_MESSAGES/django.po +1 -1
  349. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  350. wagtail/locales/locale/es/LC_MESSAGES/django.po +1 -1
  351. wagtail/locales/locale/et/LC_MESSAGES/django.po +2 -2
  352. wagtail/locales/locale/fa/LC_MESSAGES/django.po +1 -1
  353. wagtail/locales/locale/fi/LC_MESSAGES/django.po +1 -1
  354. wagtail/locales/locale/fr/LC_MESSAGES/django.po +1 -1
  355. wagtail/locales/locale/gl/LC_MESSAGES/django.po +1 -1
  356. wagtail/locales/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  357. wagtail/locales/locale/he_IL/LC_MESSAGES/django.po +3 -3
  358. wagtail/locales/locale/hr_HR/LC_MESSAGES/django.po +1 -1
  359. wagtail/locales/locale/hu/LC_MESSAGES/django.po +1 -1
  360. wagtail/locales/locale/id_ID/LC_MESSAGES/django.po +1 -1
  361. wagtail/locales/locale/is_IS/LC_MESSAGES/django.po +1 -1
  362. wagtail/locales/locale/it/LC_MESSAGES/django.po +1 -1
  363. wagtail/locales/locale/ja/LC_MESSAGES/django.po +1 -1
  364. wagtail/locales/locale/ko/LC_MESSAGES/django.po +1 -1
  365. wagtail/locales/locale/lt/LC_MESSAGES/django.po +1 -1
  366. wagtail/locales/locale/lv/LC_MESSAGES/django.po +1 -1
  367. wagtail/locales/locale/mi/LC_MESSAGES/django.po +1 -1
  368. wagtail/locales/locale/mn/LC_MESSAGES/django.po +1 -1
  369. wagtail/locales/locale/my/LC_MESSAGES/django.po +1 -1
  370. wagtail/locales/locale/nb/LC_MESSAGES/django.po +1 -1
  371. wagtail/locales/locale/nl/LC_MESSAGES/django.po +1 -1
  372. wagtail/locales/locale/pl/LC_MESSAGES/django.po +1 -1
  373. wagtail/locales/locale/pt_BR/LC_MESSAGES/django.po +1 -1
  374. wagtail/locales/locale/pt_PT/LC_MESSAGES/django.po +1 -1
  375. wagtail/locales/locale/ro/LC_MESSAGES/django.po +1 -1
  376. wagtail/locales/locale/ru/LC_MESSAGES/django.po +1 -1
  377. wagtail/locales/locale/sk_SK/LC_MESSAGES/django.po +1 -1
  378. wagtail/locales/locale/sl/LC_MESSAGES/django.po +1 -1
  379. wagtail/locales/locale/sv/LC_MESSAGES/django.po +1 -1
  380. wagtail/locales/locale/tet/LC_MESSAGES/django.po +1 -1
  381. wagtail/locales/locale/th/LC_MESSAGES/django.po +1 -1
  382. wagtail/locales/locale/tr/LC_MESSAGES/django.po +1 -1
  383. wagtail/locales/locale/tr_TR/LC_MESSAGES/django.po +1 -1
  384. wagtail/locales/locale/uk/LC_MESSAGES/django.po +1 -1
  385. wagtail/locales/locale/vi/LC_MESSAGES/django.po +1 -1
  386. wagtail/locales/locale/zh/LC_MESSAGES/django.po +1 -1
  387. wagtail/locales/locale/zh_Hans/LC_MESSAGES/django.po +1 -1
  388. wagtail/locales/locale/zh_Hant/LC_MESSAGES/django.po +1 -1
  389. wagtail/locales/tests.py +18 -3
  390. wagtail/locales/views.py +0 -1
  391. wagtail/management/commands/rebuild_references_index.py +3 -1
  392. wagtail/migrations/0092_alter_collectionviewrestriction_password_and_more.py +33 -0
  393. wagtail/migrations/0093_uploadedfile.py +53 -0
  394. wagtail/models/__init__.py +147 -32
  395. wagtail/models/i18n.py +1 -1
  396. wagtail/models/{collections.py → media.py} +33 -2
  397. wagtail/models/reference_index.py +1 -1
  398. wagtail/models/view_restrictions.py +10 -3
  399. wagtail/project_template/project_name/settings/base.py +6 -0
  400. wagtail/project_template/requirements.txt +1 -1
  401. wagtail/rich_text/__init__.py +25 -8
  402. wagtail/rich_text/pages.py +19 -8
  403. wagtail/rich_text/rewriters.py +140 -68
  404. wagtail/search/backends/database/mysql/mysql.py +3 -3
  405. wagtail/search/backends/database/postgres/postgres.py +3 -3
  406. wagtail/search/backends/database/sqlite/sqlite.py +2 -2
  407. wagtail/search/backends/elasticsearch7.py +4 -0
  408. wagtail/search/locale/en/LC_MESSAGES/django.po +3 -3
  409. wagtail/search/tests/test_postgres_backend.py +50 -0
  410. wagtail/sites/locale/en/LC_MESSAGES/django.po +8 -8
  411. wagtail/sites/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  412. wagtail/sites/locale/he_IL/LC_MESSAGES/django.po +2 -2
  413. wagtail/sites/locale/ro/LC_MESSAGES/django.mo +0 -0
  414. wagtail/sites/locale/ro/LC_MESSAGES/django.po +3 -2
  415. wagtail/sites/tests.py +35 -9
  416. wagtail/sites/views.py +3 -1
  417. wagtail/snippets/locale/de/LC_MESSAGES/django.mo +0 -0
  418. wagtail/snippets/locale/de/LC_MESSAGES/django.po +7 -8
  419. wagtail/snippets/locale/en/LC_MESSAGES/django.po +16 -56
  420. wagtail/snippets/locale/fr/LC_MESSAGES/django.po +2 -2
  421. wagtail/snippets/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  422. wagtail/snippets/locale/he_IL/LC_MESSAGES/django.po +2 -2
  423. wagtail/snippets/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
  424. wagtail/snippets/locale/hr_HR/LC_MESSAGES/django.po +6 -2
  425. wagtail/snippets/locale/lv/LC_MESSAGES/django.mo +0 -0
  426. wagtail/snippets/locale/lv/LC_MESSAGES/django.po +12 -0
  427. wagtail/snippets/locale/zh_Hant/LC_MESSAGES/django.mo +0 -0
  428. wagtail/snippets/locale/zh_Hant/LC_MESSAGES/django.po +4 -0
  429. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
  430. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
  431. wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/publish.html +3 -1
  432. wagtail/snippets/templates/wagtailsnippets/snippets/action_menu/save.html +3 -1
  433. wagtail/snippets/templates/wagtailsnippets/snippets/create.html +2 -3
  434. wagtail/snippets/templates/wagtailsnippets/snippets/edit.html +2 -3
  435. wagtail/snippets/tests/test_preview.py +13 -2
  436. wagtail/snippets/tests/test_snippets.py +41 -16
  437. wagtail/snippets/tests/test_viewset.py +95 -18
  438. wagtail/snippets/tests/test_workflows.py +12 -0
  439. wagtail/snippets/views/snippets.py +1 -40
  440. wagtail/templatetags/wagtailcore_tags.py +1 -1
  441. wagtail/test/demosite/models.py +1 -1
  442. wagtail/test/middleware.py +14 -1
  443. wagtail/test/testapp/fixtures/test.json +20 -0
  444. wagtail/test/testapp/migrations/0001_squashed_0073_revisablechildmodel_secret_text.py +8 -8
  445. wagtail/test/testapp/migrations/0023_snippetchoosermodel_full_featured.py +1 -0
  446. wagtail/test/testapp/migrations/0034_custompermissionmodel.py +44 -0
  447. wagtail/test/testapp/migrations/0035_modelwithcustommanager.py +30 -0
  448. wagtail/test/testapp/migrations/0036_complexdefaultstreampage.py +28 -0
  449. wagtail/test/testapp/models.py +79 -2
  450. wagtail/test/testapp/templates/tests/custom_docs_password_required.html +10 -0
  451. wagtail/test/testapp/templates/tests/custom_page_password_required.html +10 -0
  452. wagtail/test/testapp/views.py +24 -2
  453. wagtail/test/testapp/wagtail_hooks.py +19 -0
  454. wagtail/test/utils/wagtail_tests.py +2 -2
  455. wagtail/tests/test_blocks.py +262 -1
  456. wagtail/tests/test_migrations.py +1 -1
  457. wagtail/tests/test_page_model.py +77 -0
  458. wagtail/tests/test_page_privacy.py +18 -1
  459. wagtail/tests/test_rich_text.py +95 -5
  460. wagtail/tests/test_streamfield.py +43 -0
  461. wagtail/tests/test_utils.py +8 -2
  462. wagtail/tests/test_views.py +52 -1
  463. wagtail/tests/test_whitelist.py +7 -7
  464. wagtail/users/forms.py +3 -1
  465. wagtail/users/locale/en/LC_MESSAGES/django.po +124 -96
  466. wagtail/users/locale/fr/LC_MESSAGES/django.po +2 -2
  467. wagtail/users/locale/he_IL/LC_MESSAGES/django.mo +0 -0
  468. wagtail/users/locale/he_IL/LC_MESSAGES/django.po +2 -2
  469. wagtail/users/locale/hr_HR/LC_MESSAGES/django.mo +0 -0
  470. wagtail/users/locale/hr_HR/LC_MESSAGES/django.po +13 -2
  471. wagtail/users/migrations/0013_userprofile_density.py +23 -0
  472. wagtail/users/models.py +14 -3
  473. wagtail/users/templates/wagtailusers/groups/create.html +1 -7
  474. wagtail/users/templates/wagtailusers/groups/edit.html +1 -13
  475. wagtail/users/templates/wagtailusers/groups/includes/formatted_permissions.html +46 -2
  476. wagtail/users/templates/wagtailusers/groups/includes/group_form_js.html +0 -3
  477. wagtail/users/templates/wagtailusers/users/create.html +1 -14
  478. wagtail/users/templates/wagtailusers/users/edit.html +1 -14
  479. wagtail/users/templates/wagtailusers/users/index.html +2 -5
  480. wagtail/users/templates/wagtailusers/users/index_results.html +3 -13
  481. wagtail/users/templates/wagtailusers/users/user_cell.html +9 -0
  482. wagtail/users/templatetags/wagtailusers_tags.py +107 -20
  483. wagtail/users/tests/test_admin_views.py +669 -90
  484. wagtail/users/views/groups.py +58 -61
  485. wagtail/users/views/users.py +211 -92
  486. wagtail/users/wagtail_hooks.py +6 -38
  487. wagtail/users/widgets.py +3 -5
  488. wagtail/utils/text.py +1 -1
  489. wagtail/views.py +5 -9
  490. wagtail/whitelist.py +1 -1
  491. {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/METADATA +5 -6
  492. {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/RECORD +496 -477
  493. wagtail/admin/static/wagtailadmin/js/page-editor.js +0 -1
  494. wagtail/admin/static/wagtailadmin/js/vendor/mousetrap.min.js +0 -1
  495. wagtail/admin/static/wagtailadmin/js/vendor/urlify.js +0 -1
  496. wagtail/admin/static/wagtailadmin/js/vendor/xregexp.min.js +0 -1
  497. wagtail/admin/templates/wagtailadmin/collections/index.html +0 -34
  498. wagtail/admin/templates/wagtailadmin/pages/revisions/_actions.html +0 -22
  499. wagtail/admin/templates/wagtailadmin/shared/page_breadcrumbs.html +0 -55
  500. wagtail/admin/tests/pages/test_dashboard.py +0 -172
  501. wagtail/contrib/redirects/templates/wagtailredirects/results.html +0 -23
  502. wagtail/documents/templates/wagtaildocs/documents/list.html +0 -2
  503. wagtail/search/tests/test_postgres_stemming.py +0 -40
  504. wagtail/sites/templates/wagtailsites/create.html +0 -7
  505. wagtail/sites/templates/wagtailsites/edit.html +0 -7
  506. wagtail/snippets/templates/wagtailsnippets/snippets/revisions/_actions.html +0 -36
  507. wagtail/users/templates/wagtailusers/users/list.html +0 -62
  508. wagtail/users/urls/users.py +0 -12
  509. {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/LICENSE +0 -0
  510. {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/WHEEL +0 -0
  511. {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/entry_points.txt +0 -0
  512. {wagtail-6.0.1.dist-info → wagtail-6.1rc1.dist-info}/top_level.txt +0 -0
@@ -3,18 +3,28 @@ import unittest.mock
3
3
  from django import forms
4
4
  from django.apps import apps
5
5
  from django.conf import settings
6
+ from django.contrib.admin.utils import quote
6
7
  from django.contrib.auth import get_user_model
7
8
  from django.contrib.auth.models import Group, Permission
9
+ from django.contrib.contenttypes.models import ContentType
8
10
  from django.core.exceptions import ImproperlyConfigured
9
11
  from django.core.files.uploadedfile import SimpleUploadedFile
10
12
  from django.db.models import Q
11
13
  from django.http import HttpRequest, HttpResponse
14
+ from django.template import RequestContext, Template
12
15
  from django.test import TestCase, override_settings
13
16
  from django.urls import reverse
17
+ from django.utils import timezone
18
+ from django.utils.text import capfirst
14
19
 
15
20
  from wagtail import hooks
16
21
  from wagtail.admin.admin_url_finder import AdminURLFinder
22
+ from wagtail.admin.models import Admin
23
+ from wagtail.admin.staticfiles import versioned_static
24
+ from wagtail.admin.widgets.button import ButtonWithDropdown
17
25
  from wagtail.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME
26
+ from wagtail.coreutils import get_dummy_request
27
+ from wagtail.log_actions import log
18
28
  from wagtail.models import (
19
29
  Collection,
20
30
  GroupCollectionPermission,
@@ -29,7 +39,10 @@ from wagtail.users.permission_order import register as register_permission_order
29
39
  from wagtail.users.views.groups import GroupViewSet
30
40
  from wagtail.users.views.users import get_user_creation_form, get_user_edit_form
31
41
  from wagtail.users.wagtail_hooks import get_group_viewset_cls
42
+ from wagtail.users.widgets import UserListingButton
43
+ from wagtail.utils.deprecation import RemovedInWagtail70Warning
32
44
 
45
+ add_user_perm_codename = f"add_{AUTH_USER_MODEL_NAME.lower()}"
33
46
  delete_user_perm_codename = f"delete_{AUTH_USER_MODEL_NAME.lower()}"
34
47
  change_user_perm_codename = f"change_{AUTH_USER_MODEL_NAME.lower()}"
35
48
 
@@ -111,75 +124,23 @@ class TestGroupUsersView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
111
124
  )
112
125
 
113
126
  def test_simple(self):
114
- response = self.get()
115
- self.assertEqual(response.status_code, 200)
116
- self.assertTemplateUsed(response, "wagtailusers/users/index.html")
117
- self.assertContains(response, "testuser")
118
- # response should contain page furniture, including the "Add a user" button
119
- self.assertContains(response, "Add a user")
120
- self.assertBreadcrumbsNotRendered(response.content)
127
+ with self.assertWarnsMessage(
128
+ RemovedInWagtail70Warning,
129
+ "Accessing the list of users in a group via "
130
+ f"/admin/groups/{self.test_group.pk}/users/ is deprecated, use "
131
+ f"/admin/users/?group={self.test_group.pk} instead.",
132
+ ):
133
+ response = self.get()
134
+
135
+ self.assertRedirects(
136
+ response,
137
+ reverse("wagtailusers_users:index") + f"?group={self.test_group.pk}",
138
+ )
121
139
 
122
140
  def test_inexisting_group(self):
123
141
  response = self.get(group_id=9999)
124
142
  self.assertEqual(response.status_code, 404)
125
143
 
126
- def test_search(self):
127
- response = self.get({"q": "Hello"})
128
- self.assertEqual(response.status_code, 200)
129
- self.assertEqual(response.context["query_string"], "Hello")
130
-
131
- def test_search_query_one_field(self):
132
- response = self.get({"q": "first name"})
133
- self.assertEqual(response.status_code, 200)
134
- results = response.context["users"]
135
- self.assertIn(self.test_user, results)
136
-
137
- def test_search_query_multiple_fields(self):
138
- response = self.get({"q": "first name last name"})
139
- self.assertEqual(response.status_code, 200)
140
- results = response.context["users"]
141
- self.assertIn(self.test_user, results)
142
-
143
- def test_pagination(self):
144
- # page numbers in range should be accepted
145
- response = self.get({"p": 1})
146
- self.assertEqual(response.status_code, 200)
147
- # page numbers out of range should return 404
148
- response = self.get({"p": 9999})
149
- self.assertEqual(response.status_code, 404)
150
-
151
-
152
- class TestGroupUsersResultsView(WagtailTestUtils, TestCase):
153
- def setUp(self):
154
- # create a user that should be visible in the listing
155
- self.test_user = self.create_user(
156
- username="testuser",
157
- email="testuser@email.com",
158
- password="password",
159
- first_name="First Name",
160
- last_name="Last Name",
161
- )
162
- self.test_group = Group.objects.create(name="Test Group")
163
- self.test_user.groups.add(self.test_group)
164
- self.login()
165
-
166
- def get(self, params={}, group_id=None):
167
- return self.client.get(
168
- reverse(
169
- "wagtailusers_groups:users_results",
170
- args=(group_id or self.test_group.pk,),
171
- ),
172
- params,
173
- )
174
-
175
- def test_simple(self):
176
- response = self.get()
177
- self.assertEqual(response.status_code, 200)
178
- self.assertTemplateUsed(response, "wagtailusers/users/index_results.html")
179
- self.assertContains(response, "testuser")
180
- # response should contain not page furniture
181
- self.assertNotContains(response, "Add a user")
182
-
183
144
 
184
145
  class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
185
146
  def setUp(self):
@@ -191,7 +152,7 @@ class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
191
152
  first_name="First Name",
192
153
  last_name="Last Name",
193
154
  )
194
- self.login()
155
+ self.user = self.login()
195
156
 
196
157
  def get(self, params={}):
197
158
  return self.client.get(reverse("wagtailusers_users:index"), params)
@@ -203,7 +164,10 @@ class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
203
164
  self.assertContains(response, "testuser")
204
165
  # response should contain page furniture, including the "Add a user" button
205
166
  self.assertContains(response, "Add a user")
206
- self.assertBreadcrumbsNotRendered(response.content)
167
+ self.assertBreadcrumbsItemsRendered(
168
+ [{"url": "", "label": capfirst(User._meta.verbose_name_plural)}],
169
+ response.content,
170
+ )
207
171
 
208
172
  @unittest.skipIf(
209
173
  settings.AUTH_USER_MODEL == "emailuser.EmailUser", "Negative UUID not possible"
@@ -241,21 +205,106 @@ class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
241
205
  response = self.get({"p": 9999})
242
206
  self.assertEqual(response.status_code, 404)
243
207
 
244
- def test_valid_ordering(self):
208
+ def test_ordering(self):
245
209
  # checking that only valid ordering used, in case of `IndexView` the valid
246
- # ordering fields are "name" and "username".
247
- response = self.get({"ordering": "email"})
248
- self.assertNotEqual(response.context_data["ordering"], "email")
249
- # name is default ordering in `IndexView`.
250
- self.assertEqual(response.context_data["ordering"], "name")
251
- response = self.get({"ordering": "username"})
252
- self.assertEqual(response.context_data["ordering"], "username")
210
+ # ordering fields are:
211
+ # - `name`: maps to `User.last_name` and `User.first_name` fields if available
212
+ # - `User.USERNAME_FIELD`: dynamically maps to User.USERNAME_FIELD
213
+ # - `is_superuser`: maps to User.is_superuser (from PermissionsMixin)
214
+ # - `is_active`: maps to User.is_active if available
215
+ # - `last_login`: maps to User.last_login (from AbstractBaseUser)
216
+ cases = {
217
+ "name": ("last_name", "first_name"),
218
+ "-name": ("-last_name", "-first_name"),
219
+ User.USERNAME_FIELD: (User.USERNAME_FIELD,),
220
+ f"-{User.USERNAME_FIELD}": (f"-{User.USERNAME_FIELD}",),
221
+ "is_superuser": ("is_superuser",),
222
+ "-is_superuser": ("-is_superuser",),
223
+ "is_active": ("is_active",),
224
+ "-is_active": ("-is_active",),
225
+ "last_login": ("last_login",),
226
+ "-last_login": ("-last_login",),
227
+ }
228
+ for param, order_by in cases.items():
229
+ with self.subTest(param=param):
230
+ response = self.get({"ordering": param})
231
+ self.assertEqual(
232
+ response.context_data["object_list"].query.order_by,
233
+ order_by,
234
+ )
235
+
236
+ def test_filters(self):
237
+ response = self.get()
238
+ self.assertEqual(response.status_code, 200)
239
+ self.assertCountEqual(
240
+ response.context["object_list"],
241
+ [self.test_user, self.user],
242
+ )
243
+
244
+ response = self.get({"is_superuser": True})
245
+ self.assertEqual(response.status_code, 200)
246
+ self.assertCountEqual(response.context["object_list"], [self.user])
247
+
248
+ response = self.get({"is_superuser": False})
249
+ self.assertEqual(response.status_code, 200)
250
+ self.assertCountEqual(response.context["object_list"], [self.test_user])
251
+
252
+ self.test_user.is_active = False
253
+ self.test_user.save()
254
+
255
+ response = self.get({"is_active": True})
256
+ self.assertEqual(response.status_code, 200)
257
+ self.assertCountEqual(response.context["object_list"], [self.user])
258
+
259
+ response = self.get({"is_active": False})
260
+ self.assertEqual(response.status_code, 200)
261
+ self.assertCountEqual(response.context["object_list"], [self.test_user])
262
+
263
+ now = timezone.now()
264
+ if timezone.is_aware(now):
265
+ today = timezone.localtime(now).date()
266
+ else:
267
+ today = now.date()
268
+ tomorrow = today + timezone.timedelta(days=1)
269
+ yesterday = today - timezone.timedelta(days=1)
270
+
271
+ response = self.get({"last_login_from": str(today)})
272
+ self.assertEqual(response.status_code, 200)
273
+ self.assertCountEqual(response.context["object_list"], [self.user])
274
+
275
+ response = self.get({"last_login_from": str(tomorrow)})
276
+ self.assertEqual(response.status_code, 200)
277
+ self.assertCountEqual(response.context["object_list"], [])
278
+
279
+ response = self.get({"last_login_to": str(today)})
280
+ self.assertEqual(response.status_code, 200)
281
+ self.assertCountEqual(response.context["object_list"], [self.user])
282
+
283
+ response = self.get({"last_login_to": str(yesterday)})
284
+ self.assertEqual(response.status_code, 200)
285
+ self.assertCountEqual(response.context["object_list"], [])
286
+
287
+ musicians = Group.objects.create(name="Musicians")
288
+ songwriters = Group.objects.create(name="Songwriters")
289
+ self.test_user.groups.add(musicians)
290
+ self.user.groups.add(songwriters)
291
+
292
+ response = self.get({"group": musicians.pk})
293
+ self.assertEqual(response.status_code, 200)
294
+ self.assertCountEqual(response.context["object_list"], [self.test_user])
295
+
296
+ response = self.get({"group": [musicians.pk, songwriters.pk]})
297
+ self.assertEqual(response.status_code, 200)
298
+ self.assertCountEqual(
299
+ response.context["object_list"],
300
+ [self.test_user, self.user],
301
+ )
253
302
 
254
303
  def test_num_queries(self):
255
304
  # Warm up
256
305
  self.get()
257
306
 
258
- num_queries = 9
307
+ num_queries = 10
259
308
  with self.assertNumQueries(num_queries):
260
309
  self.get()
261
310
 
@@ -264,8 +313,77 @@ class TestUserIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
264
313
  with self.assertNumQueries(num_queries):
265
314
  self.get()
266
315
 
316
+ def test_default_buttons(self):
317
+ response = self.get()
318
+ soup = self.get_soup(response.content)
319
+ dropdown_buttons = soup.select("li [data-controller='w-dropdown'] a")
320
+ expected_urls = [
321
+ reverse("wagtailusers_users:edit", args=(self.user.pk,)),
322
+ reverse("wagtailusers_users:copy", args=(self.user.pk,)),
323
+ # Should not link to delete page for the current user
324
+ reverse("wagtailusers_users:edit", args=(self.test_user.pk,)),
325
+ reverse("wagtailusers_users:copy", args=(self.test_user.pk,)),
326
+ reverse("wagtailusers_users:delete", args=(self.test_user.pk,)),
327
+ ]
328
+ urls = [button.attrs.get("href") for button in dropdown_buttons]
329
+ self.assertSequenceEqual(urls, expected_urls)
330
+
331
+ def test_buttons_hook(self):
332
+ def hook(user, request_user):
333
+ self.assertEqual(request_user, self.user)
334
+ yield UserListingButton(
335
+ "Show profile",
336
+ f"/goes/to/a/url/{user.pk}",
337
+ priority=30,
338
+ )
339
+ yield ButtonWithDropdown(
340
+ label="Moar pls!",
341
+ buttons=[UserListingButton("Alrighty", "/cheers", priority=10)],
342
+ )
343
+
344
+ with self.register_hook("register_user_listing_buttons", hook):
345
+ response = self.get()
267
346
 
268
- class TestUserIndexResultsView(WagtailTestUtils, TestCase):
347
+ self.assertEqual(response.status_code, 200)
348
+ self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html")
349
+
350
+ soup = self.get_soup(response.content)
351
+ row = soup.select_one(f"tbody tr:has([data-object-id='{self.test_user.pk}'])")
352
+ self.assertIsNotNone(row)
353
+
354
+ profile_url = f"/goes/to/a/url/{self.test_user.pk}"
355
+ actions = row.select_one("td ul.actions")
356
+ top_level_custom_button = actions.select_one(f"li > a[href='{profile_url}']")
357
+ self.assertIsNone(top_level_custom_button)
358
+ custom_button = actions.select_one(
359
+ f"li [data-controller='w-dropdown'] a[href='{profile_url}']"
360
+ )
361
+ self.assertIsNotNone(custom_button)
362
+ self.assertEqual(
363
+ custom_button.text.strip(),
364
+ "Show profile",
365
+ )
366
+
367
+ nested_dropdown = actions.select_one(
368
+ "li [data-controller='w-dropdown'] [data-controller='w-dropdown']"
369
+ )
370
+ self.assertIsNone(nested_dropdown)
371
+ dropdown_buttons = actions.select("li > [data-controller='w-dropdown']")
372
+ # Default "More" button and the custom "Moar pls!" button
373
+ self.assertEqual(len(dropdown_buttons), 2)
374
+ custom_dropdown = None
375
+ for button in dropdown_buttons:
376
+ if "Moar pls!" in button.text.strip():
377
+ custom_dropdown = button
378
+ self.assertIsNotNone(custom_dropdown)
379
+ self.assertEqual(custom_dropdown.select_one("button").text.strip(), "Moar pls!")
380
+ # Should contain the custom button inside the custom dropdown
381
+ custom_button = custom_dropdown.find("a", attrs={"href": "/cheers"})
382
+ self.assertIsNotNone(custom_button)
383
+ self.assertEqual(custom_button.text.strip(), "Alrighty")
384
+
385
+
386
+ class TestUserIndexResultsView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
269
387
  def setUp(self):
270
388
  # create a user that should be visible in the listing
271
389
  self.test_user = self.create_user(
@@ -286,7 +404,7 @@ class TestUserIndexResultsView(WagtailTestUtils, TestCase):
286
404
  self.assertTemplateUsed(response, "wagtailusers/users/index_results.html")
287
405
  self.assertContains(response, "testuser")
288
406
  # response should not contain page furniture
289
- self.assertNotContains(response, "Add a user")
407
+ self.assertBreadcrumbsNotRendered(response.content)
290
408
 
291
409
 
292
410
  class TestUserCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
@@ -307,7 +425,16 @@ class TestUserCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
307
425
  self.assertTemplateUsed(response, "wagtailusers/users/create.html")
308
426
  self.assertContains(response, "Password")
309
427
  self.assertContains(response, "Password confirmation")
310
- self.assertBreadcrumbsNotRendered(response.content)
428
+ self.assertBreadcrumbsItemsRendered(
429
+ [
430
+ {
431
+ "url": "/admin/users/",
432
+ "label": capfirst(User._meta.verbose_name_plural),
433
+ },
434
+ {"url": "", "label": f"New: {capfirst(User._meta.verbose_name)}"},
435
+ ],
436
+ response.content,
437
+ )
311
438
 
312
439
  def test_create(self):
313
440
  response = self.post(
@@ -846,13 +973,44 @@ class TestUserEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
846
973
  self.assertTemplateUsed(response, "wagtailusers/users/edit.html")
847
974
  self.assertContains(response, "Password")
848
975
  self.assertContains(response, "Password confirmation")
849
- self.assertBreadcrumbsNotRendered(response.content)
976
+ self.assertBreadcrumbsItemsRendered(
977
+ [
978
+ {
979
+ "url": "/admin/users/",
980
+ "label": capfirst(User._meta.verbose_name_plural),
981
+ },
982
+ {"url": "", "label": "Original User"},
983
+ ],
984
+ response.content,
985
+ )
986
+
987
+ soup = self.get_soup(response.content)
988
+ header = soup.select_one(".w-slim-header")
989
+ history_url = reverse("wagtailusers_users:history", args=(self.test_user.pk,))
990
+ history_link = header.find("a", attrs={"href": history_url})
991
+ self.assertIsNotNone(history_link)
850
992
 
851
993
  url_finder = AdminURLFinder(self.current_user)
852
- expected_url = "/admin/users/%s/" % self.test_user.pk
994
+ expected_url = f"/admin/users/edit/{self.test_user.pk}/"
853
995
  self.assertEqual(url_finder.get_edit_url(self.test_user), expected_url)
854
996
 
855
- def test_nonexistant_redirect(self):
997
+ def test_legacy_url_redirect(self):
998
+ with self.assertWarnsMessage(
999
+ RemovedInWagtail70Warning,
1000
+ (
1001
+ "UserViewSet's `/<pk>/` edit view URL pattern has been "
1002
+ "deprecated in favour of /edit/<pk>/."
1003
+ ),
1004
+ ):
1005
+ response = self.client.get(f"/admin/users/{self.test_user.pk}/")
1006
+
1007
+ self.assertRedirects(
1008
+ response,
1009
+ f"/admin/users/edit/{self.test_user.pk}/",
1010
+ status_code=301,
1011
+ )
1012
+
1013
+ def test_nonexistent_redirect(self):
856
1014
  invalid_id = (
857
1015
  "99999999-9999-9999-9999-999999999999"
858
1016
  if settings.AUTH_USER_MODEL == "emailuser.EmailUser"
@@ -885,6 +1043,23 @@ class TestUserEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
885
1043
  else:
886
1044
  self.assertContains(response, "User &#x27;testuser&#x27; updated.")
887
1045
 
1046
+ # On next load of the edit view,
1047
+ # should render the status panel with the last updated time
1048
+ response = self.get()
1049
+ self.assertContains(response, "Edited User")
1050
+ soup = self.get_soup(response.content)
1051
+ status_panel = soup.select_one('[data-side-panel="status"]')
1052
+ self.assertIsNotNone(status_panel)
1053
+ last_updated = status_panel.select_one(".w-help-text")
1054
+ self.assertIsNotNone(last_updated)
1055
+ self.assertRegex(
1056
+ last_updated.get_text(strip=True),
1057
+ f"[0-9][0-9]:[0-9][0-9] by {self.current_user.get_username()}",
1058
+ )
1059
+ history_url = reverse("wagtailusers_users:history", args=(self.test_user.pk,))
1060
+ history_link = status_panel.select_one(f'a[href="{history_url}"]')
1061
+ self.assertIsNotNone(history_link)
1062
+
888
1063
  def test_password_optional(self):
889
1064
  """Leaving password fields blank should leave it unchanged"""
890
1065
  response = self.post(
@@ -1248,6 +1423,61 @@ class TestUserEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1248
1423
  self.assertEqual(response.content, b"Overridden!")
1249
1424
 
1250
1425
 
1426
+ class TestUserCopyView(WagtailTestUtils, TestCase):
1427
+ def setUp(self):
1428
+ self.user = self.login()
1429
+
1430
+ @classmethod
1431
+ def setUpTestData(cls):
1432
+ cls.test_user = cls.create_user(
1433
+ username="testuser",
1434
+ email="testuser@email.com",
1435
+ first_name="Original",
1436
+ last_name="User",
1437
+ password="password",
1438
+ )
1439
+ cls.url = reverse("wagtailusers_users:copy", args=[quote(cls.test_user.pk)])
1440
+
1441
+ def test_without_permission(self):
1442
+ self.user.is_superuser = False
1443
+ self.user.save()
1444
+ admin_permission = Permission.objects.get(
1445
+ content_type__app_label="wagtailadmin", codename="access_admin"
1446
+ )
1447
+ self.user.user_permissions.add(admin_permission)
1448
+
1449
+ response = self.client.get(self.url)
1450
+ self.assertEqual(response.status_code, 302)
1451
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
1452
+
1453
+ def test_with_minimal_permission(self):
1454
+ self.user.is_superuser = False
1455
+ self.user.save()
1456
+ self.user.user_permissions.add(
1457
+ Permission.objects.get(
1458
+ content_type__app_label="wagtailadmin", codename="access_admin"
1459
+ ),
1460
+ Permission.objects.get(
1461
+ content_type__app_label=AUTH_USER_APP_LABEL,
1462
+ codename=add_user_perm_codename,
1463
+ ),
1464
+ )
1465
+
1466
+ # Form should be prefilled
1467
+ response = self.client.get(self.url)
1468
+ self.assertEqual(response.status_code, 200)
1469
+ soup = self.get_soup(response.content)
1470
+ first_name = soup.select_one('input[name="first_name"]')
1471
+ self.assertEqual(first_name.attrs.get("value"), "Original")
1472
+ last_name = soup.select_one('input[name="last_name"]')
1473
+ self.assertEqual(last_name.attrs.get("value"), "User")
1474
+ # Password fields should be empty
1475
+ password1 = soup.select_one('input[name="password1"]')
1476
+ password2 = soup.select_one('input[name="password2"]')
1477
+ self.assertIsNone(password1.attrs.get("value"))
1478
+ self.assertIsNone(password2.attrs.get("value"))
1479
+
1480
+
1251
1481
  class TestUserProfileCreation(WagtailTestUtils, TestCase):
1252
1482
  def setUp(self):
1253
1483
  # Create a user
@@ -1326,6 +1556,32 @@ class TestUserEditViewForNonSuperuser(WagtailTestUtils, TestCase):
1326
1556
  self.assertIs(user.is_superuser, False)
1327
1557
 
1328
1558
 
1559
+ class TestUserHistoryView(WagtailTestUtils, TestCase):
1560
+ # More thorough tests are in test_model_viewset
1561
+
1562
+ @classmethod
1563
+ def setUpTestData(cls):
1564
+ cls.test_user = cls.create_user(
1565
+ username="testuser",
1566
+ email="testuser@email.com",
1567
+ first_name="Original",
1568
+ last_name="User",
1569
+ password="password",
1570
+ )
1571
+ cls.url = reverse("wagtailusers_users:history", args=(cls.test_user.pk,))
1572
+
1573
+ def setUp(self):
1574
+ self.user = self.login()
1575
+
1576
+ def test_simple(self):
1577
+ log(self.test_user, "wagtail.create", user=self.user)
1578
+ log(self.test_user, "wagtail.edit", user=self.user)
1579
+ response = self.client.get(self.url)
1580
+ self.assertTemplateUsed("wagtailadmin/generic/listing.html")
1581
+ self.assertContains(response, "Created")
1582
+ self.assertContains(response, "Edited")
1583
+
1584
+
1329
1585
  class TestGroupIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1330
1586
  def setUp(self):
1331
1587
  self.login()
@@ -1340,7 +1596,9 @@ class TestGroupIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1340
1596
  self.assertTemplateUsed(response, "wagtailadmin/generic/index.html")
1341
1597
  # response should contain page furniture, including the "Add a group" button
1342
1598
  self.assertContains(response, "Add a group")
1343
- self.assertBreadcrumbsNotRendered(response.content)
1599
+ self.assertBreadcrumbsItemsRendered(
1600
+ [{"url": "", "label": "Groups"}], response.content
1601
+ )
1344
1602
 
1345
1603
  def test_search(self):
1346
1604
  response = self.get({"q": "Hello"})
@@ -1413,7 +1671,24 @@ class TestGroupCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1413
1671
  response = self.get()
1414
1672
  self.assertEqual(response.status_code, 200)
1415
1673
  self.assertTemplateUsed(response, "wagtailusers/groups/create.html")
1416
- self.assertBreadcrumbsNotRendered(response.content)
1674
+ self.assertBreadcrumbsItemsRendered(
1675
+ [
1676
+ {"url": "/admin/groups/", "label": "Groups"},
1677
+ {"url": "", "label": "New: Group"},
1678
+ ],
1679
+ response.content,
1680
+ )
1681
+ # Should contain the JS from the form and the template include
1682
+ page_chooser_js = versioned_static("wagtailadmin/js/page-chooser.js")
1683
+ group_form_js = versioned_static("wagtailusers/js/group-form.js")
1684
+ self.assertContains(response, page_chooser_js)
1685
+ self.assertContains(response, group_form_js)
1686
+
1687
+ def test_num_queries(self):
1688
+ # Warm up the cache
1689
+ self.get()
1690
+ with self.assertNumQueries(20):
1691
+ self.get()
1417
1692
 
1418
1693
  def test_create_group(self):
1419
1694
  response = self.post({"name": "test group"})
@@ -1527,6 +1802,13 @@ class TestGroupCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1527
1802
  | Q(codename__startswith="publish")
1528
1803
  ).delete()
1529
1804
 
1805
+ # A custom permission that happens to also start with "change"
1806
+ Permission.objects.filter(
1807
+ codename="change_text",
1808
+ content_type__app_label="tests",
1809
+ content_type__model="custompermissionmodel",
1810
+ ).delete()
1811
+
1530
1812
  response = self.get()
1531
1813
 
1532
1814
  self.assertInHTML("Custom permissions", response.content.decode(), count=0)
@@ -1567,6 +1849,102 @@ class TestGroupCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1567
1849
  # Should not show inputs for publish permissions on models without DraftStateMixin
1568
1850
  self.assertNotInHTML("Can publish advert", html)
1569
1851
 
1852
+ def test_strip_model_name_from_custom_permissions(self):
1853
+ """
1854
+ https://github.com/wagtail/wagtail/issues/10982
1855
+ Ensure model name or verbose name is stripped from permissions' labels
1856
+ for consistency with built-in permissions.
1857
+ """
1858
+ response = self.get()
1859
+
1860
+ self.assertContains(response, "Can bulk update")
1861
+ self.assertContains(response, "Can start trouble")
1862
+ self.assertContains(response, "Cause chaos for")
1863
+ self.assertContains(response, "Change text")
1864
+ self.assertContains(response, "Manage")
1865
+ self.assertNotContains(response, "Can bulk_update")
1866
+ self.assertNotContains(response, "Can bulk update ADVANCED permission model")
1867
+ self.assertNotContains(response, "Cause chaos for advanced permission model")
1868
+ self.assertNotContains(response, "Manage custom permission model")
1869
+
1870
+ def test_permission_with_same_action(self):
1871
+ """
1872
+ https://github.com/wagtail/wagtail/issues/11650
1873
+ Ensure that permissions with the same action (part before the first _ in
1874
+ the codename) are not hidden.
1875
+ """
1876
+ response = self.get()
1877
+ soup = self.get_soup(response.content)
1878
+ main_change_permission = Permission.objects.get(
1879
+ codename="change_custompermissionmodel",
1880
+ content_type__app_label="tests",
1881
+ content_type__model="custompermissionmodel",
1882
+ )
1883
+ custom_change_permission = Permission.objects.get(
1884
+ codename="change_text",
1885
+ content_type__app_label="tests",
1886
+ content_type__model="custompermissionmodel",
1887
+ )
1888
+
1889
+ # Main change permission is in the dedicated column, so it's directly
1890
+ # inside a <td>, not inside a <fieldset>"
1891
+ self.assertIsNotNone(
1892
+ soup.select_one(f'td > input[value="{main_change_permission.pk}"]')
1893
+ )
1894
+ self.assertIsNone(
1895
+ soup.select_one(f'td > fieldset input[value="{main_change_permission.pk}"]')
1896
+ )
1897
+
1898
+ # Custom "change_text" permission is in the custom permissions column,
1899
+ # so it's inside a <fieldset> and not directly inside a <td>
1900
+ self.assertIsNone(
1901
+ soup.select_one(f'td > input[value="{custom_change_permission.pk}"]')
1902
+ )
1903
+ self.assertIsNotNone(
1904
+ soup.select_one(
1905
+ f'td > fieldset input[value="{custom_change_permission.pk}"]'
1906
+ )
1907
+ )
1908
+
1909
+ def test_custom_other_permissions_with_wagtail_admin_content_type(self):
1910
+ """
1911
+ https://github.com/wagtail/wagtail/issues/8086
1912
+ Allow custom permissions using Wagtail's Admin content type to be
1913
+ displayed in the "Other permissions" section.
1914
+ """
1915
+ admin_ct = ContentType.objects.get_for_model(Admin)
1916
+ custom_permission = Permission.objects.create(
1917
+ codename="roadmap_sync",
1918
+ name="Can sync roadmap items from GitHub",
1919
+ content_type=admin_ct,
1920
+ )
1921
+
1922
+ with self.register_hook(
1923
+ "register_permissions",
1924
+ lambda: Permission.objects.filter(
1925
+ codename="roadmap_sync", content_type=admin_ct
1926
+ ),
1927
+ ):
1928
+ response = self.get()
1929
+
1930
+ soup = self.get_soup(response.content)
1931
+
1932
+ other_permissions = soup.select_one("#other-permissions-section")
1933
+ self.assertIsNotNone(other_permissions)
1934
+
1935
+ custom_checkbox = other_permissions.select_one(
1936
+ f'input[value="{custom_permission.pk}"]'
1937
+ )
1938
+ self.assertIsNotNone(custom_checkbox)
1939
+
1940
+ custom_label = other_permissions.select_one(
1941
+ f'label[for="{custom_checkbox.attrs.get("id")}"]'
1942
+ )
1943
+ self.assertIsNotNone(custom_label)
1944
+ self.assertEqual(
1945
+ custom_label.get_text(strip=True), "Can sync roadmap items from GitHub"
1946
+ )
1947
+
1570
1948
 
1571
1949
  class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1572
1950
  def setUp(self):
@@ -1655,13 +2033,39 @@ class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1655
2033
  response = self.get()
1656
2034
  self.assertEqual(response.status_code, 200)
1657
2035
  self.assertTemplateUsed(response, "wagtailusers/groups/edit.html")
1658
- self.assertBreadcrumbsNotRendered(response.content)
2036
+ self.assertBreadcrumbsItemsRendered(
2037
+ [
2038
+ {
2039
+ "url": "/admin/groups/",
2040
+ "label": "Groups",
2041
+ },
2042
+ {"url": "", "label": str(self.test_group)},
2043
+ ],
2044
+ response.content,
2045
+ )
2046
+ # Should contain the JS from the form and the template include
2047
+ page_chooser_js = versioned_static("wagtailadmin/js/page-chooser.js")
2048
+ group_form_js = versioned_static("wagtailusers/js/group-form.js")
2049
+ self.assertContains(response, page_chooser_js)
2050
+ self.assertContains(response, group_form_js)
2051
+
2052
+ soup = self.get_soup(response.content)
2053
+ header = soup.select_one(".w-slim-header")
2054
+ history_url = reverse("wagtailusers_groups:history", args=(self.test_group.pk,))
2055
+ history_link = header.find("a", attrs={"href": history_url})
2056
+ self.assertIsNotNone(history_link)
1659
2057
 
1660
2058
  url_finder = AdminURLFinder(self.user)
1661
2059
  expected_url = "/admin/groups/edit/%d/" % self.test_group.id
1662
2060
  self.assertEqual(url_finder.get_edit_url(self.test_group), expected_url)
1663
2061
 
1664
- def test_nonexistant_group_redirect(self):
2062
+ def test_num_queries(self):
2063
+ # Warm up the cache
2064
+ self.get()
2065
+ with self.assertNumQueries(32):
2066
+ self.get()
2067
+
2068
+ def test_nonexistent_group_redirect(self):
1665
2069
  self.assertEqual(self.get(group_id=100000).status_code, 404)
1666
2070
 
1667
2071
  def test_group_edit(self):
@@ -1674,6 +2078,23 @@ class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1674
2078
  group = Group.objects.get(pk=self.test_group.pk)
1675
2079
  self.assertEqual(group.name, "test group edited")
1676
2080
 
2081
+ # On next load of the edit view,
2082
+ # should render the status panel with the last updated time
2083
+ response = self.get()
2084
+ self.assertContains(response, "test group edited")
2085
+ soup = self.get_soup(response.content)
2086
+ status_panel = soup.select_one('[data-side-panel="status"]')
2087
+ self.assertIsNotNone(status_panel)
2088
+ last_updated = status_panel.select_one(".w-help-text")
2089
+ self.assertIsNotNone(last_updated)
2090
+ self.assertRegex(
2091
+ last_updated.get_text(strip=True),
2092
+ f"[0-9][0-9]:[0-9][0-9] by {self.user.get_username()}",
2093
+ )
2094
+ history_url = reverse("wagtailusers_groups:history", args=(self.test_group.pk,))
2095
+ history_link = status_panel.select_one(f'a[href="{history_url}"]')
2096
+ self.assertIsNotNone(history_link)
2097
+
1677
2098
  def test_group_edit_validation_error(self):
1678
2099
  # Leave "name" field blank. This should give a validation error
1679
2100
  response = self.post({"name": ""})
@@ -1971,12 +2392,21 @@ class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
1971
2392
 
1972
2393
  response = self.get()
1973
2394
 
1974
- self.assertTagInHTML(
1975
- '<input type="checkbox" name="permissions" value="%s" checked>'
1976
- % custom_permission.id,
1977
- response.content.decode(),
2395
+ soup = self.get_soup(response.content)
2396
+ checkbox = soup.find_all(
2397
+ "input",
2398
+ attrs={
2399
+ "name": "permissions",
2400
+ "checked": True,
2401
+ "value": custom_permission.id,
2402
+ "data-action": "w-bulk#toggle",
2403
+ "data-w-bulk-group-param": "custom",
2404
+ "data-w-bulk-target": "item",
2405
+ },
1978
2406
  )
1979
2407
 
2408
+ self.assertEqual(len(checkbox), 1)
2409
+
1980
2410
  def test_show_publish_permissions(self):
1981
2411
  response = self.get()
1982
2412
  html = response.content.decode()
@@ -2029,7 +2459,13 @@ class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
2029
2459
  perm.content_type.model,
2030
2460
  )
2031
2461
  for perm_set in object_perms
2032
- for perm in [next(v for v in flatten(perm_set) if "perm" in v)["perm"]]
2462
+ for perm in [
2463
+ next(
2464
+ v
2465
+ for v in flatten(perm_set)
2466
+ if isinstance(v, dict) and "perm" in v
2467
+ )["perm"]
2468
+ ]
2033
2469
  ]
2034
2470
 
2035
2471
  # Set order on two objects, should appear first and second
@@ -2072,6 +2508,52 @@ class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
2072
2508
  msg="Default object permission order is incorrect",
2073
2509
  )
2074
2510
 
2511
+ def test_data_attributes_for_bulk_selection(self):
2512
+ response = self.get()
2513
+ soup = self.get_soup(response.content)
2514
+
2515
+ table = soup.find("table", "listing")
2516
+ self.assertIn(table["data-controller"], "w-bulk")
2517
+
2518
+ # confirm there is a single select all checkbox for all items
2519
+ toggle_all = table.select('tfoot th input[data-w-bulk-target="all"]')
2520
+ self.assertEqual(len(toggle_all), 1)
2521
+ self.assertEqual(toggle_all[0]["data-action"], "w-bulk#toggleAll")
2522
+
2523
+ # confirm there is one 'add' select all checkbox
2524
+ toggle_all_add = table.select(
2525
+ 'tfoot td input[data-w-bulk-target="all"][data-w-bulk-group-param="add"]'
2526
+ )
2527
+ self.assertEqual(len(toggle_all_add), 1)
2528
+ self.assertEqual(toggle_all_add[0]["data-action"], "w-bulk#toggleAll")
2529
+
2530
+ # confirm that the individual object permissions have the correct attributes
2531
+ toggle_add_items = table.select(
2532
+ 'tbody td input[data-w-bulk-target="item"][data-w-bulk-group-param="add"]'
2533
+ )
2534
+ self.assertGreaterEqual(len(toggle_add_items), 30)
2535
+ self.assertEqual(toggle_add_items[0]["data-action"], "w-bulk#toggle")
2536
+
2537
+
2538
+ class TestGroupHistoryView(WagtailTestUtils, TestCase):
2539
+ # More thorough tests are in test_model_viewset
2540
+
2541
+ @classmethod
2542
+ def setUpTestData(cls):
2543
+ cls.test_group = Group.objects.create(name="test group")
2544
+ cls.url = reverse("wagtailusers_groups:history", args=(cls.test_group.pk,))
2545
+
2546
+ def setUp(self):
2547
+ self.user = self.login()
2548
+
2549
+ def test_simple(self):
2550
+ log(self.test_group, "wagtail.create", user=self.user)
2551
+ log(self.test_group, "wagtail.edit", user=self.user)
2552
+ response = self.client.get(self.url)
2553
+ self.assertTemplateUsed("wagtailadmin/generic/listing.html")
2554
+ self.assertContains(response, "Created")
2555
+ self.assertContains(response, "Edited")
2556
+
2075
2557
 
2076
2558
  class TestGroupViewSet(TestCase):
2077
2559
  def setUp(self):
@@ -2368,3 +2850,100 @@ class TestAuthorisationDeleteView(WagtailTestUtils, TestCase):
2368
2850
  self.assertRedirects(response, reverse("wagtailusers_users:index"))
2369
2851
  user = get_user_model().objects.filter(email="test_user@email.com")
2370
2852
  self.assertFalse(user.exists())
2853
+
2854
+
2855
+ class TestTemplateTags(WagtailTestUtils, TestCase):
2856
+ @classmethod
2857
+ def setUpTestData(cls):
2858
+ cls.user = cls.create_superuser("admin")
2859
+ cls.request = get_dummy_request()
2860
+ cls.request.user = cls.user
2861
+ cls.test_user = cls.create_user(
2862
+ username="testuser",
2863
+ email="testuser@email.com",
2864
+ password="password",
2865
+ )
2866
+
2867
+ def test_user_listing_buttons(self):
2868
+ template = """
2869
+ {% load wagtailusers_tags %}
2870
+ {% for user in users %}
2871
+ <ul class="actions">
2872
+ {% user_listing_buttons user %}
2873
+ </ul>
2874
+ {% endfor %}
2875
+ """
2876
+
2877
+ def hook(user, request_user):
2878
+ self.assertEqual(user, self.test_user)
2879
+ self.assertEqual(request_user, self.user)
2880
+ yield UserListingButton(
2881
+ "Show profile",
2882
+ f"/goes/to/a/url/{user.pk}",
2883
+ priority=30,
2884
+ )
2885
+
2886
+ with self.register_hook("register_user_listing_buttons", hook):
2887
+ with self.assertWarnsMessage(
2888
+ RemovedInWagtail70Warning,
2889
+ "`user_listing_buttons` template tag is deprecated.",
2890
+ ):
2891
+ html = Template(template).render(
2892
+ RequestContext(self.request, {"users": [self.test_user]})
2893
+ )
2894
+
2895
+ soup = self.get_soup(html)
2896
+
2897
+ profile_url = f"/goes/to/a/url/{self.test_user.pk}"
2898
+ top_level_custom_button = soup.select_one(f"li > a[href='{profile_url}']")
2899
+ self.assertIsNotNone(top_level_custom_button)
2900
+ self.assertEqual(
2901
+ top_level_custom_button.text.strip(),
2902
+ "Show profile",
2903
+ )
2904
+
2905
+ def test_user_listing_buttons_with_deprecated_hook(self):
2906
+ template = """
2907
+ {% load wagtailusers_tags %}
2908
+ {% for user in users %}
2909
+ <ul class="actions">
2910
+ {% user_listing_buttons user %}
2911
+ </ul>
2912
+ {% endfor %}
2913
+ """
2914
+
2915
+ def deprecated_hook(context, user):
2916
+ self.assertEqual(user, self.test_user)
2917
+ self.assertEqual(context.request.user, self.user)
2918
+ yield UserListingButton(
2919
+ "Show profile",
2920
+ f"/goes/to/a/url/{user.pk}",
2921
+ priority=30,
2922
+ )
2923
+
2924
+ with self.register_hook("register_user_listing_buttons", deprecated_hook):
2925
+ with self.assertWarns(RemovedInWagtail70Warning) as warning_manager:
2926
+ html = Template(template).render(
2927
+ RequestContext(self.request, {"users": [self.test_user]})
2928
+ )
2929
+
2930
+ self.assertEqual(
2931
+ [str(w.message) for w in warning_manager.warnings],
2932
+ [
2933
+ # Deprecation of the template tag
2934
+ "`user_listing_buttons` template tag is deprecated.",
2935
+ # Deprecation of the hook signature
2936
+ "`register_user_listing_buttons` hook functions should accept a "
2937
+ "`request_user` argument instead of `context` - "
2938
+ "wagtail.users.tests.test_admin_views.deprecated_hook needs to be updated",
2939
+ ],
2940
+ )
2941
+
2942
+ soup = self.get_soup(html)
2943
+ profile_url = f"/goes/to/a/url/{self.test_user.pk}"
2944
+ top_level_custom_button = soup.select_one(f"li > a[href='{profile_url}']")
2945
+ self.assertIsNotNone(top_level_custom_button)
2946
+ self.assertEqual(
2947
+ top_level_custom_button.text.strip(),
2948
+ "Show profile",
2949
+ )