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
@@ -16,6 +16,7 @@ from django_filters.filters import (
16
16
  DateFromToRangeFilter,
17
17
  ModelChoiceFilter,
18
18
  ModelMultipleChoiceFilter,
19
+ MultipleChoiceFilter,
19
20
  )
20
21
 
21
22
  from wagtail.admin import messages
@@ -93,7 +94,6 @@ class WagtailAdminTemplateMixin(TemplateResponseMixin, ContextMixin):
93
94
  if self._show_breadcrumbs:
94
95
  context["breadcrumbs_items"] = self.get_breadcrumbs_items()
95
96
  context["header_buttons"] = self.get_header_buttons()
96
- context["header_more_buttons"] = self.get_header_more_buttons()
97
97
  return context
98
98
 
99
99
  def get_template_names(self):
@@ -120,13 +120,16 @@ class BaseObjectMixin:
120
120
  def get_pk(self):
121
121
  return unquote(str(self.kwargs[self.pk_url_kwarg]))
122
122
 
123
+ def get_base_object_queryset(self):
124
+ return self.model._default_manager.all()
125
+
123
126
  def get_object(self):
124
127
  if not self.model:
125
128
  raise ImproperlyConfigured(
126
129
  "Subclasses of wagtail.admin.views.generic.base.BaseObjectMixin must provide a "
127
130
  "model attribute or a get_object method"
128
131
  )
129
- return get_object_or_404(self.model, pk=self.pk)
132
+ return get_object_or_404(self.get_base_object_queryset(), pk=self.pk)
130
133
 
131
134
 
132
135
  class BaseOperationView(BaseObjectMixin, View):
@@ -201,7 +204,7 @@ class BaseListingView(WagtailAdminTemplateMixin, BaseListView):
201
204
  @cached_property
202
205
  def filters(self):
203
206
  if self.filterset_class:
204
- filterset = self.filterset_class(self.request.GET, request=self.request)
207
+ filterset = self.filterset_class(**self.get_filterset_kwargs())
205
208
  # Don't use the filterset if it has no fields
206
209
  if filterset.form.fields:
207
210
  return filterset
@@ -213,6 +216,12 @@ class BaseListingView(WagtailAdminTemplateMixin, BaseListView):
213
216
  self.filters and self.filters.is_valid() and self.filters.form.has_changed()
214
217
  )
215
218
 
219
+ def get_filterset_kwargs(self):
220
+ return {
221
+ "data": self.request.GET,
222
+ "request": self.request,
223
+ }
224
+
216
225
  def filter_queryset(self, queryset):
217
226
  if self.filters and self.filters.is_valid():
218
227
  queryset = self.filters.filter_queryset(queryset)
@@ -262,6 +271,9 @@ class BaseListingView(WagtailAdminTemplateMixin, BaseListView):
262
271
  except KeyError:
263
272
  continue # invalid filter value
264
273
 
274
+ if value == bound_field.initial:
275
+ continue # filter value is the same as the default
276
+
265
277
  if isinstance(filter_def, ModelMultipleChoiceFilter):
266
278
  field = filter_def.field
267
279
  for item in value:
@@ -275,6 +287,17 @@ class BaseListingView(WagtailAdminTemplateMixin, BaseListView):
275
287
  ),
276
288
  )
277
289
  )
290
+ elif isinstance(filter_def, MultipleChoiceFilter):
291
+ choices = {str(id): label for id, label in filter_def.field.choices}
292
+ for item in value:
293
+ filters.append(
294
+ ActiveFilter(
295
+ bound_field.auto_id,
296
+ filter_def.label,
297
+ choices.get(str(item), str(item)),
298
+ self.get_url_without_filter_param_value(field_name, item),
299
+ )
300
+ )
278
301
  elif isinstance(filter_def, ModelChoiceFilter):
279
302
  field = filter_def.field
280
303
  filters.append(
@@ -288,13 +311,17 @@ class BaseListingView(WagtailAdminTemplateMixin, BaseListView):
288
311
  elif isinstance(filter_def, DateFromToRangeFilter):
289
312
  start_date_display = date_format(value.start) if value.start else ""
290
313
  end_date_display = date_format(value.stop) if value.stop else ""
314
+ widget = filter_def.field.widget
291
315
  filters.append(
292
316
  ActiveFilter(
293
317
  bound_field.auto_id,
294
318
  filter_def.label,
295
319
  "%s - %s" % (start_date_display, end_date_display),
296
320
  self.get_url_without_filter_param(
297
- [f"{field_name}_before", f"{field_name}_after"]
321
+ [
322
+ widget.suffixed(field_name, suffix)
323
+ for suffix in widget.suffixes
324
+ ]
298
325
  ),
299
326
  )
300
327
  )
@@ -1,8 +1,9 @@
1
1
  from datetime import timedelta
2
2
 
3
3
  import django_filters
4
- from django.contrib.admin.utils import quote, unquote
4
+ from django.contrib.admin.utils import quote
5
5
  from django.core.paginator import Paginator
6
+ from django.forms import CheckboxSelectMultiple
6
7
  from django.shortcuts import get_object_or_404
7
8
  from django.urls import reverse
8
9
  from django.utils.functional import cached_property
@@ -10,8 +11,13 @@ from django.utils.text import capfirst
10
11
  from django.utils.translation import gettext, gettext_lazy
11
12
  from django.views.generic import TemplateView
12
13
 
13
- from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
14
+ from wagtail.admin.filters import (
15
+ DateRangePickerWidget,
16
+ MultipleUserFilter,
17
+ WagtailFilterSet,
18
+ )
14
19
  from wagtail.admin.ui.tables import Column, DateColumn, InlineActionsTable, UserColumn
20
+ from wagtail.admin.utils import get_latest_str
15
21
  from wagtail.admin.views.generic.base import (
16
22
  BaseListingView,
17
23
  BaseObjectMixin,
@@ -23,90 +29,238 @@ from wagtail.log_actions import registry as log_registry
23
29
  from wagtail.models import (
24
30
  BaseLogEntry,
25
31
  DraftStateMixin,
26
- ModelLogEntry,
32
+ PreviewableMixin,
27
33
  Revision,
34
+ RevisionMixin,
28
35
  TaskState,
29
36
  WorkflowState,
30
37
  )
31
38
 
32
39
 
33
- def get_actions_for_filter():
40
+ def get_actions_for_filter(queryset):
34
41
  # Only return those actions used by model log entries.
35
- actions = set(ModelLogEntry.objects.all().get_actions())
42
+ actions = set(queryset.get_actions())
36
43
  return [action for action in log_registry.get_choices() if action[0] in actions]
37
44
 
38
45
 
39
- class HistoryReportFilterSet(WagtailFilterSet):
40
- action = django_filters.ChoiceFilter(
46
+ class HistoryFilterSet(WagtailFilterSet):
47
+ action = django_filters.MultipleChoiceFilter(
41
48
  label=gettext_lazy("Action"),
49
+ widget=CheckboxSelectMultiple,
42
50
  # choices are set dynamically in __init__()
43
51
  )
44
- user = django_filters.ModelChoiceFilter(
52
+ user = MultipleUserFilter(
45
53
  label=gettext_lazy("User"),
46
- field_name="user",
47
- queryset=lambda request: ModelLogEntry.objects.all().get_users(),
54
+ widget=CheckboxSelectMultiple,
55
+ # queryset is set dynamically in __init__()
48
56
  )
49
57
  timestamp = django_filters.DateFromToRangeFilter(
50
58
  label=gettext_lazy("Date"), widget=DateRangePickerWidget
51
59
  )
52
60
 
53
- class Meta:
54
- model = ModelLogEntry
55
- fields = ["action", "user", "timestamp"]
56
-
57
61
  def __init__(self, *args, **kwargs):
58
62
  super().__init__(*args, **kwargs)
59
- self.filters["action"].extra["choices"] = get_actions_for_filter()
63
+ actions = get_actions_for_filter(self.queryset)
64
+ if not actions:
65
+ del self.filters["action"]
66
+ else:
67
+ self.filters["action"].extra["choices"] = actions
68
+
69
+ users = self.queryset.get_users()
70
+ if not users.exists():
71
+ del self.filters["user"]
72
+ else:
73
+ self.filters["user"].extra["queryset"] = users
74
+
75
+
76
+ class ActionColumn(Column):
77
+ def __init__(self, *args, object, url_names, user_can_unschedule, **kwargs):
78
+ super().__init__(*args, **kwargs)
79
+ self.object = object
80
+ self.url_names = url_names
81
+ self.user_can_unschedule = user_can_unschedule
82
+ self.revision_enabled = isinstance(object, RevisionMixin)
83
+ self.draftstate_enabled = isinstance(object, DraftStateMixin)
84
+
85
+ @cached_property
86
+ def cell_template_name(self):
87
+ if self.revision_enabled:
88
+ return "wagtailadmin/generic/history/action_cell.html"
89
+ return super().cell_template_name
90
+
91
+ def get_status(self, instance, parent_context):
92
+ if self.draftstate_enabled:
93
+ if (
94
+ instance.action == "wagtail.publish"
95
+ and instance.revision_id == self.object.live_revision_id
96
+ ):
97
+ return gettext("Live version")
98
+ elif (
99
+ instance.content_changed
100
+ and instance.revision_id == self.object.latest_revision_id
101
+ ):
102
+ return gettext("Current draft")
103
+ return None
104
+
105
+ def get_actions(self, instance, parent_context):
106
+ actions = []
107
+
108
+ # Do not show the revision actions if the log entry:
109
+ # - has no revision attached
110
+ # - has no content changes
111
+ # - is a "publish" action
112
+ # (because we want to show the options on the "edit" action instead)
113
+ if (
114
+ not self.revision_enabled
115
+ or not instance.revision_id
116
+ or not instance.content_changed
117
+ or instance.action == "wagtail.publish"
118
+ ):
119
+ return actions
120
+
121
+ if (
122
+ isinstance(self.object, PreviewableMixin)
123
+ and self.object.is_previewable()
124
+ and (url_name := self.url_names.get("revisions_view"))
125
+ ):
126
+ url = reverse(url_name, args=(quote(self.object.pk), instance.revision_id))
127
+ action = {"url": url, "label": gettext("Preview")}
128
+ actions.append(action)
129
+
130
+ if instance.revision_id == self.object.latest_revision_id:
131
+ if url_name := self.url_names.get("edit"):
132
+ url = reverse(url_name, args=(quote(self.object.pk),))
133
+ action = {"url": url, "label": gettext("Edit")}
134
+ actions.append(action)
135
+ elif url_name := self.url_names.get("revisions_revert"):
136
+ url = reverse(url_name, args=(quote(self.object.pk), instance.revision_id))
137
+ action = {"url": url, "label": gettext("Review this version")}
138
+ actions.append(action)
139
+
140
+ if url_name := self.url_names.get("revisions_compare"):
141
+ if instance.previous_revision_id:
142
+ url = reverse(
143
+ url_name,
144
+ args=(
145
+ quote(self.object.pk),
146
+ instance.previous_revision_id,
147
+ instance.revision_id,
148
+ ),
149
+ )
150
+ action = {"url": url, "label": gettext("Compare with previous version")}
151
+ actions.append(action)
152
+ if instance.revision_id != self.object.latest_revision_id:
153
+ url = reverse(
154
+ url_name,
155
+ args=(quote(self.object.pk), instance.revision_id, "latest"),
156
+ )
157
+ action = {"url": url, "label": gettext("Compare with current version")}
158
+ actions.append(action)
159
+
160
+ if (
161
+ (url_name := self.url_names.get("revisions_unschedule"))
162
+ and instance.revision.approved_go_live_at
163
+ and self.user_can_unschedule
164
+ ):
165
+ url = reverse(url_name, args=(quote(self.object.pk), instance.revision_id))
166
+ action = {"url": url, "label": gettext("Cancel scheduled publish")}
167
+ actions.append(action)
168
+
169
+ return actions
170
+
171
+ def get_cell_context_data(self, instance, parent_context):
172
+ context = super().get_cell_context_data(instance, parent_context)
173
+ context["status"] = self.get_status(instance, parent_context)
174
+ context["actions"] = self.get_actions(instance, parent_context)
175
+ return context
60
176
 
61
177
 
62
- class HistoryView(PermissionCheckedMixin, BaseListingView):
178
+ class LogEntryUserColumn(UserColumn):
179
+ def __init__(self, name, **kwargs):
180
+ # Instead of accepting a blank_display_name arg, we'll make use of the
181
+ # BaseLogEntry.user_display_name property which also handles the display
182
+ # name for a deleted user (as the BaseLogEntry still stores the ID).
183
+ super().__init__(name, blank_display_name=None, **kwargs)
184
+
185
+ def get_cell_context_data(self, instance, parent_context):
186
+ context = super().get_cell_context_data(instance, parent_context)
187
+ if not context["display_name"]:
188
+ context["display_name"] = instance.user_display_name
189
+ return context
190
+
191
+
192
+ class HistoryView(PermissionCheckedMixin, BaseObjectMixin, BaseListingView):
63
193
  any_permission_required = ["add", "change", "delete"]
64
194
  page_title = gettext_lazy("History")
65
195
  results_template_name = "wagtailadmin/generic/history_results.html"
66
- history_url_name = None
67
- history_results_url_name = None
68
196
  header_icon = "history"
69
197
  is_searchable = False
70
198
  paginate_by = 20
71
- filterset_class = HistoryReportFilterSet
199
+ filterset_class = HistoryFilterSet
72
200
  table_class = InlineActionsTable
73
- columns = [
74
- Column("message", label=gettext_lazy("Action")),
75
- UserColumn("user", blank_display_name="system"),
76
- DateColumn("timestamp", label=gettext_lazy("Date")),
77
- ]
201
+ history_url_name = None
202
+ history_results_url_name = None
78
203
  edit_url_name = None
204
+ revisions_view_url_name = None
205
+ revisions_revert_url_name = None
206
+ revisions_compare_url_name = None
207
+ revisions_unschedule_url_name = None
79
208
 
80
- def setup(self, request, *args, pk, **kwargs):
81
- self.pk = pk
82
- self.object = self.get_object()
83
- super().setup(request, *args, **kwargs)
209
+ @cached_property
210
+ def columns(self):
211
+ return [
212
+ ActionColumn(
213
+ "message",
214
+ label=gettext_lazy("Action"),
215
+ object=self.object,
216
+ url_names={
217
+ "edit": self.edit_url_name,
218
+ "revisions_view": self.revisions_view_url_name,
219
+ "revisions_revert": self.revisions_revert_url_name,
220
+ "revisions_compare": self.revisions_compare_url_name,
221
+ "revisions_unschedule": self.revisions_unschedule_url_name,
222
+ },
223
+ user_can_unschedule=self.user_can_unschedule(),
224
+ ),
225
+ LogEntryUserColumn("user", width="25%"),
226
+ DateColumn("timestamp", label=gettext_lazy("Date"), width="15%"),
227
+ ]
84
228
 
85
- def get_object(self):
86
- object = get_object_or_404(self.model, pk=unquote(self.pk))
87
- if isinstance(object, DraftStateMixin):
88
- return object.get_latest_revision_as_object()
89
- return object
229
+ def get_base_object_queryset(self):
230
+ queryset = super().get_base_object_queryset()
231
+ if issubclass(queryset.model, RevisionMixin):
232
+ return queryset.select_related("latest_revision")
233
+ return queryset
90
234
 
91
235
  def get_page_subtitle(self):
92
- return str(self.object)
236
+ return get_latest_str(self.object)
93
237
 
94
238
  def get_breadcrumbs_items(self):
95
- return self.breadcrumbs_items + [
96
- {
97
- "url": reverse(self.index_url_name),
98
- "label": capfirst(self.model._meta.verbose_name_plural),
99
- },
100
- {
101
- "url": self.get_edit_url(self.object),
102
- "label": str(self.object),
103
- },
239
+ items = []
240
+ if self.index_url_name:
241
+ items.append(
242
+ {
243
+ "url": reverse(self.index_url_name),
244
+ "label": capfirst(self.model._meta.verbose_name_plural),
245
+ }
246
+ )
247
+ edit_url = self.get_edit_url(self.object)
248
+ obj_name = self.get_page_subtitle()
249
+ if edit_url:
250
+ items.append(
251
+ {
252
+ "url": edit_url,
253
+ "label": obj_name,
254
+ }
255
+ )
256
+ items.append(
104
257
  {
105
258
  "url": "",
106
259
  "label": gettext("History"),
107
- "sublabel": self.get_page_subtitle(),
108
- },
109
- ]
260
+ "sublabel": obj_name,
261
+ }
262
+ )
263
+ return self.breadcrumbs_items + items
110
264
 
111
265
  @cached_property
112
266
  def header_buttons(self):
@@ -136,19 +290,34 @@ class HistoryView(PermissionCheckedMixin, BaseListingView):
136
290
  def get_index_results_url(self):
137
291
  return self.get_history_results_url(self.object)
138
292
 
293
+ def user_can_unschedule(self):
294
+ return self.user_has_permission("publish")
295
+
139
296
  def get_context_data(self, *args, object_list=None, **kwargs):
140
297
  context = super().get_context_data(*args, object_list=object_list, **kwargs)
141
298
  context["object"] = self.object
142
299
  context["model_opts"] = BaseLogEntry._meta
143
300
  return context
144
301
 
145
- def get_queryset(self):
146
- queryset = log_registry.get_logs_for_instance(self.object).select_related(
147
- "revision", "user", "user__wagtail_userprofile"
148
- )
149
- queryset = self.filter_queryset(queryset)
302
+ def get_base_queryset(self):
303
+ queryset = log_registry.get_logs_for_instance(self.object)
304
+ return self._annotate_queryset(queryset)
305
+
306
+ def _annotate_queryset(self, queryset):
307
+ queryset = queryset.select_related("user", "user__wagtail_userprofile")
308
+ if isinstance(self.object, RevisionMixin):
309
+ queryset = queryset.select_related("revision").annotate(
310
+ previous_revision_id=Revision.objects.previous_revision_id_subquery(),
311
+ )
150
312
  return queryset
151
313
 
314
+ def get_filterset_kwargs(self):
315
+ # Pass custom queryset so the FilterSet can use it when initialising the
316
+ # filters, instead of using the default model.objects.all() queryset.
317
+ kwargs = super().get_filterset_kwargs()
318
+ kwargs["queryset"] = self.get_base_queryset()
319
+ return kwargs
320
+
152
321
 
153
322
  class WorkflowHistoryView(BaseObjectMixin, WagtailAdminTemplateMixin, TemplateView):
154
323
  template_name = "wagtailadmin/shared/workflow_history/index.html"
@@ -101,10 +101,13 @@ class BeforeAfterHookMixin(HookResponseMixin):
101
101
 
102
102
 
103
103
  class LocaleMixin:
104
- def setup(self, request, *args, **kwargs):
105
- super().setup(request, *args, **kwargs)
106
- self.locale = self.get_locale()
107
- self.translations = self.get_translations() if self.locale else []
104
+ @cached_property
105
+ def locale(self):
106
+ return self.get_locale()
107
+
108
+ @cached_property
109
+ def translations(self):
110
+ return self.get_translations() if self.locale else []
108
111
 
109
112
  def get_locale(self):
110
113
  if not getattr(self, "model", None):
@@ -9,6 +9,7 @@ from django.core.exceptions import (
9
9
  )
10
10
  from django.db import models, transaction
11
11
  from django.db.models import Q
12
+ from django.db.models.constants import LOOKUP_SEP
12
13
  from django.db.models.functions import Cast
13
14
  from django.http import Http404, HttpResponseRedirect
14
15
  from django.shortcuts import get_object_or_404, redirect
@@ -41,7 +42,6 @@ from wagtail.admin.ui.tables import (
41
42
  from wagtail.admin.utils import get_latest_str, get_valid_next_url_from_request
42
43
  from wagtail.admin.views.mixins import SpreadsheetExportMixin
43
44
  from wagtail.admin.widgets.button import (
44
- Button,
45
45
  ButtonWithDropdown,
46
46
  HeaderButton,
47
47
  ListingButton,
@@ -233,7 +233,7 @@ class IndexView(
233
233
  query |= Q(**{field + "__icontains": self.search_query})
234
234
  return queryset.filter(query)
235
235
 
236
- def _get_title_column(self, field_name, column_class=TitleColumn, **kwargs):
236
+ def _get_title_column_class(self, column_class):
237
237
  if not issubclass(column_class, ButtonsColumnMixin):
238
238
 
239
239
  def get_buttons(column, instance, *args, **kwargs):
@@ -244,6 +244,10 @@ class IndexView(
244
244
  (ButtonsColumnMixin, column_class),
245
245
  {"get_buttons": get_buttons},
246
246
  )
247
+ return column_class
248
+
249
+ def _get_title_column(self, field_name, column_class=TitleColumn, **kwargs):
250
+ column_class = self._get_title_column_class(column_class)
247
251
  if not self.model:
248
252
  return column_class(
249
253
  "name",
@@ -256,7 +260,32 @@ class IndexView(
256
260
  )
257
261
 
258
262
  def _get_custom_column(self, field_name, column_class=Column, **kwargs):
259
- label, attr = label_for_field(field_name, self.model, return_attr=True)
263
+ lookups = (
264
+ [field_name]
265
+ if hasattr(self.model, field_name)
266
+ else field_name.split(LOOKUP_SEP)
267
+ )
268
+ *relations, field = lookups
269
+ model_class = self.model
270
+
271
+ # Iterate over the relation list to try to get the last model
272
+ # where the field exists
273
+ foreign_field_name = ""
274
+ for model in relations:
275
+ foreign_field = model_class._meta.get_field(model)
276
+ foreign_field_name = foreign_field.verbose_name
277
+ model_class = foreign_field.related_model
278
+
279
+ label, attr = label_for_field(field, model_class, return_attr=True)
280
+
281
+ # For some languages, it may be more appropriate to put the field label
282
+ # before the related model name
283
+ if foreign_field_name:
284
+ label = _("%(related_model_name)s %(field_label)s") % {
285
+ "related_model_name": foreign_field_name,
286
+ "field_label": label,
287
+ }
288
+
260
289
  sort_key = getattr(attr, "admin_order_field", None)
261
290
 
262
291
  # attr is None if the field is an actual database field,
@@ -264,8 +293,12 @@ class IndexView(
264
293
  if attr is None:
265
294
  sort_key = field_name
266
295
 
296
+ accessor = field_name
297
+ # Build the dotted relation if needed, for use in multigetattr
298
+ if relations:
299
+ accessor = ".".join(lookups)
267
300
  return column_class(
268
- field_name,
301
+ accessor,
269
302
  label=capfirst(label),
270
303
  sort_key=sort_key,
271
304
  **kwargs,
@@ -338,29 +371,6 @@ class IndexView(
338
371
  )
339
372
  return buttons
340
373
 
341
- @cached_property
342
- def header_more_buttons(self):
343
- buttons = []
344
- if self.list_export:
345
- buttons.append(
346
- Button(
347
- _("Download XLSX"),
348
- url=self.xlsx_export_url,
349
- icon_name="download",
350
- priority=90,
351
- )
352
- )
353
- buttons.append(
354
- Button(
355
- _("Download CSV"),
356
- url=self.csv_export_url,
357
- icon_name="download",
358
- priority=100,
359
- )
360
- )
361
-
362
- return buttons
363
-
364
374
  def get_list_more_buttons(self, instance):
365
375
  buttons = []
366
376
  edit_url = self.get_edit_url(instance)
@@ -597,14 +607,16 @@ class CreateView(
597
607
  return context
598
608
 
599
609
  def get_side_panels(self):
600
- side_panels = [
601
- StatusSidePanel(
602
- self.form.instance,
603
- self.request,
604
- locale=self.locale,
605
- translations=self.translations,
610
+ side_panels = []
611
+ if self.locale:
612
+ side_panels.append(
613
+ StatusSidePanel(
614
+ self.form.instance,
615
+ self.request,
616
+ locale=self.locale,
617
+ translations=self.translations,
618
+ )
606
619
  )
607
- ]
608
620
  return MediaContainer(side_panels)
609
621
 
610
622
  def get_translations(self):
@@ -659,7 +671,7 @@ class CreateView(
659
671
 
660
672
  class CopyView(CreateView):
661
673
  def get_object(self, queryset=None):
662
- return get_object_or_404(self.model, pk=self.kwargs["pk"])
674
+ return get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"])))
663
675
 
664
676
  def get_form_kwargs(self):
665
677
  return {**super().get_form_kwargs(), "instance": self.get_object()}
@@ -710,7 +722,7 @@ class EditView(
710
722
  return super().get_object(queryset)
711
723
 
712
724
  def get_page_subtitle(self):
713
- return str(self.object)
725
+ return get_latest_str(self.object)
714
726
 
715
727
  def get_breadcrumbs_items(self):
716
728
  if not self.model:
@@ -723,7 +735,7 @@ class EditView(
723
735
  "label": capfirst(self.model._meta.verbose_name_plural),
724
736
  }
725
737
  )
726
- items.append({"url": "", "label": get_latest_str(self.object)})
738
+ items.append({"url": "", "label": self.get_page_subtitle()})
727
739
  return self.breadcrumbs_items + items
728
740
 
729
741
  def get_side_panels(self):
@@ -745,7 +757,11 @@ class EditView(
745
757
  return MediaContainer(side_panels)
746
758
 
747
759
  def get_last_updated_info(self):
748
- return log_registry.get_logs_for_instance(self.object).first()
760
+ return (
761
+ log_registry.get_logs_for_instance(self.object)
762
+ .select_related("user")
763
+ .first()
764
+ )
749
765
 
750
766
  def get_edit_url(self):
751
767
  if not self.edit_url_name: