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
@@ -8,7 +8,7 @@ msgid ""
8
8
  msgstr ""
9
9
  "Project-Id-Version: PACKAGE VERSION\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
- "POT-Creation-Date: 2024-01-24 13:51+0000\n"
11
+ "POT-Creation-Date: 2024-04-18 17:28+0100\n"
12
12
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
13
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
14
  "Language-Team: LANGUAGE <LL@li.org>\n"
wagtail/locales/tests.py CHANGED
@@ -20,7 +20,10 @@ class TestLocaleIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
20
20
  response = self.get()
21
21
  self.assertEqual(response.status_code, 200)
22
22
  self.assertTemplateUsed(response, "wagtailadmin/generic/index.html")
23
- self.assertBreadcrumbsNotRendered(response.content)
23
+ self.assertBreadcrumbsItemsRendered(
24
+ [{"url": "", "label": "Locales"}],
25
+ response.content,
26
+ )
24
27
 
25
28
 
26
29
  class TestLocaleCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
@@ -43,7 +46,13 @@ class TestLocaleCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
43
46
  response = self.get()
44
47
  self.assertEqual(response.status_code, 200)
45
48
  self.assertTemplateUsed(response, "wagtaillocales/create.html")
46
- self.assertBreadcrumbsNotRendered(response.content)
49
+ self.assertBreadcrumbsItemsRendered(
50
+ [
51
+ {"label": "Locales", "url": "/admin/locales/"},
52
+ {"label": "New: Locale", "url": ""},
53
+ ],
54
+ response.content,
55
+ )
47
56
 
48
57
  self.assertEqual(
49
58
  response.context["form"].fields["language_code"].choices, [("fr", "French")]
@@ -116,7 +125,13 @@ class TestLocaleEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
116
125
  response = self.get()
117
126
  self.assertEqual(response.status_code, 200)
118
127
  self.assertTemplateUsed(response, "wagtaillocales/edit.html")
119
- self.assertBreadcrumbsNotRendered(response.content)
128
+ self.assertBreadcrumbsItemsRendered(
129
+ [
130
+ {"url": "/admin/locales/", "label": "Locales"},
131
+ {"url": "", "label": str(self.english)},
132
+ ],
133
+ response.content,
134
+ )
120
135
 
121
136
  self.assertEqual(
122
137
  response.context["form"].fields["language_code"].choices,
wagtail/locales/views.py CHANGED
@@ -98,7 +98,6 @@ class LocaleViewSet(ModelViewSet):
98
98
  model = Locale
99
99
  permission_policy = locale_permission_policy
100
100
  add_to_reference_index = False
101
- _show_breadcrumbs = False
102
101
 
103
102
  index_view_class = IndexView
104
103
  add_view_class = CreateView
@@ -38,7 +38,9 @@ class Command(BaseCommand):
38
38
 
39
39
  with transaction.atomic():
40
40
  with disable_reference_index_auto_update():
41
- ReferenceIndex.objects.all().delete()
41
+ # Use `_raw_delete` to avoid loading instances into memory
42
+ all_references = ReferenceIndex.objects.all()
43
+ all_references._raw_delete(using=all_references.db)
42
44
 
43
45
  for model in apps.get_models():
44
46
  if not ReferenceIndex.is_indexed(model):
@@ -0,0 +1,33 @@
1
+ # Generated by Django 4.2.7 on 2024-02-01 21:21
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('wagtailcore', '0091_remove_revision_submitted_for_moderation'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='collectionviewrestriction',
15
+ name='password',
16
+ field=models.CharField(blank=True, help_text='Shared passwords should not be used to protect sensitive content. Anyone who has this password will be able to view the content.', max_length=255, verbose_name='shared password'),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='collectionviewrestriction',
20
+ name='restriction_type',
21
+ field=models.CharField(choices=[('none', 'Public'), ('password', 'Private, accessible with a shared password'), ('login', 'Private, accessible to any logged-in users'), ('groups', 'Private, accessible to users in specific groups')], max_length=20),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='pageviewrestriction',
25
+ name='password',
26
+ field=models.CharField(blank=True, help_text='Shared passwords should not be used to protect sensitive content. Anyone who has this password will be able to view the content.', max_length=255, verbose_name='shared password'),
27
+ ),
28
+ migrations.AlterField(
29
+ model_name='pageviewrestriction',
30
+ name='restriction_type',
31
+ field=models.CharField(choices=[('none', 'Public'), ('password', 'Private, accessible with a shared password'), ('login', 'Private, accessible to any logged-in users'), ('groups', 'Private, accessible to users in specific groups')], max_length=20),
32
+ ),
33
+ ]
@@ -0,0 +1,53 @@
1
+ # Generated by Django 4.2.7 on 2024-02-12 21:04
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+ ("contenttypes", "0002_remove_content_type_name"),
13
+ ("wagtailcore", "0092_alter_collectionviewrestriction_password_and_more"),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="UploadedFile",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.AutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ ("file", models.FileField(max_length=200, upload_to="wagtail_uploads")),
30
+ (
31
+ "for_content_type",
32
+ models.ForeignKey(
33
+ null=True,
34
+ on_delete=django.db.models.deletion.CASCADE,
35
+ related_name="uploads",
36
+ to="contenttypes.contenttype",
37
+ verbose_name="for content type",
38
+ ),
39
+ ),
40
+ (
41
+ "uploaded_by_user",
42
+ models.ForeignKey(
43
+ blank=True,
44
+ editable=False,
45
+ null=True,
46
+ on_delete=django.db.models.deletion.SET_NULL,
47
+ to=settings.AUTH_USER_MODEL,
48
+ verbose_name="uploaded by user",
49
+ ),
50
+ ),
51
+ ],
52
+ ),
53
+ ]
@@ -9,12 +9,16 @@ should implement low-level generic functionality which is then imported by highe
9
9
  as Page.
10
10
  """
11
11
 
12
+ from __future__ import annotations
13
+
12
14
  import functools
13
15
  import logging
14
16
  import posixpath
15
17
  import uuid
16
18
  from io import StringIO
19
+ from typing import TYPE_CHECKING
17
20
  from urllib.parse import urlparse
21
+ from warnings import warn
18
22
 
19
23
  from django import forms
20
24
  from django.conf import settings
@@ -92,6 +96,7 @@ from wagtail.signals import (
92
96
  workflow_submitted,
93
97
  )
94
98
  from wagtail.url_routing import RouteResult
99
+ from wagtail.utils.deprecation import RemovedInWagtail70Warning
95
100
  from wagtail.utils.timestamps import ensure_utc
96
101
 
97
102
  from .audit_log import ( # noqa: F401
@@ -100,16 +105,6 @@ from .audit_log import ( # noqa: F401
100
105
  LogEntryQuerySet,
101
106
  ModelLogEntry,
102
107
  )
103
- from .collections import ( # noqa: F401
104
- BaseCollectionManager,
105
- Collection,
106
- CollectionManager,
107
- CollectionMember,
108
- CollectionViewRestriction,
109
- GroupCollectionPermission,
110
- GroupCollectionPermissionManager,
111
- get_root_collection_id,
112
- )
113
108
  from .copying import _copy, _copy_m2m_relations, _extract_field_data # noqa: F401
114
109
  from .i18n import ( # noqa: F401
115
110
  BootstrapTranslatableMixin,
@@ -120,11 +115,25 @@ from .i18n import ( # noqa: F401
120
115
  bootstrap_translatable_model,
121
116
  get_translatable_models,
122
117
  )
118
+ from .media import ( # noqa: F401
119
+ BaseCollectionManager,
120
+ Collection,
121
+ CollectionManager,
122
+ CollectionMember,
123
+ CollectionViewRestriction,
124
+ GroupCollectionPermission,
125
+ GroupCollectionPermissionManager,
126
+ UploadedFile,
127
+ get_root_collection_id,
128
+ )
123
129
  from .reference_index import ReferenceIndex # noqa: F401
124
130
  from .sites import Site, SiteManager, SiteRootPath # noqa: F401
125
131
  from .specific import SpecificMixin
126
132
  from .view_restrictions import BaseViewRestriction
127
133
 
134
+ if TYPE_CHECKING:
135
+ from django.http import HttpRequest
136
+
128
137
  logger = logging.getLogger("wagtail")
129
138
 
130
139
  PAGE_TEMPLATE_VAR = "page"
@@ -1266,7 +1275,6 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
1266
1275
  exclude_fields_in_copy = []
1267
1276
  default_exclude_fields_in_copy = [
1268
1277
  "id",
1269
- "path",
1270
1278
  "depth",
1271
1279
  "numchild",
1272
1280
  "url_path",
@@ -1283,6 +1291,43 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
1283
1291
  promote_panels = []
1284
1292
  settings_panels = []
1285
1293
 
1294
+ @staticmethod
1295
+ def route_for_request(request: "HttpRequest", path: str) -> RouteResult | None:
1296
+ """
1297
+ Find the page route for the given HTTP request object, and URL path. The route
1298
+ result (`page`, `args`, and `kwargs`) will be cached via
1299
+ `request._wagtail_route_for_request`.
1300
+ """
1301
+ if not hasattr(request, "_wagtail_route_for_request"):
1302
+ try:
1303
+ # we need a valid Site object for this request in order to proceed
1304
+ if site := Site.find_for_request(request):
1305
+ path_components = [
1306
+ component for component in path.split("/") if component
1307
+ ]
1308
+ request._wagtail_route_for_request = (
1309
+ site.root_page.localized.specific.route(
1310
+ request, path_components
1311
+ )
1312
+ )
1313
+ else:
1314
+ request._wagtail_route_for_request = None
1315
+ except Http404:
1316
+ # .route() can raise Http404
1317
+ request._wagtail_route_for_request = None
1318
+
1319
+ return request._wagtail_route_for_request
1320
+
1321
+ @staticmethod
1322
+ def find_for_request(request: "HttpRequest", path: str) -> "Page" | None:
1323
+ """
1324
+ Find the page for the given HTTP request object, and URL path. The full
1325
+ page route will be cached via `request._wagtail_route_for_request`
1326
+ """
1327
+ result = Page.route_for_request(request, path)
1328
+ if result is not None:
1329
+ return result[0]
1330
+
1286
1331
  def __init__(self, *args, **kwargs):
1287
1332
  super().__init__(*args, **kwargs)
1288
1333
  if not self.id:
@@ -1625,6 +1670,11 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
1625
1670
 
1626
1671
  try:
1627
1672
  subpage = self.get_children().get(slug=child_slug)
1673
+ # Cache the parent page on the subpage to avoid another db query
1674
+ # Treebeard's get_parent will use the `_cached_parent_obj` attribute if it exists
1675
+ # And update = False
1676
+ setattr(subpage, "_cached_parent_obj", self)
1677
+
1628
1678
  except Page.DoesNotExist:
1629
1679
  raise Http404
1630
1680
 
@@ -2549,9 +2599,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
2549
2599
 
2550
2600
  return PageViewRestriction.objects.filter(page_id__in=page_ids_to_check)
2551
2601
 
2552
- password_required_template = getattr(
2553
- settings, "PASSWORD_REQUIRED_TEMPLATE", "wagtailcore/password_required.html"
2554
- )
2602
+ password_required_template = None
2555
2603
 
2556
2604
  def serve_password_required_response(self, request, form, action_url):
2557
2605
  """
@@ -2561,10 +2609,32 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
2561
2609
  (and zero or more hidden fields that also need to be output on the template)
2562
2610
  action_url = URL that this form should be POSTed to
2563
2611
  """
2612
+
2613
+ password_required_template = self.password_required_template
2614
+
2615
+ if not password_required_template:
2616
+ password_required_template = getattr(
2617
+ settings,
2618
+ "WAGTAIL_PASSWORD_REQUIRED_TEMPLATE",
2619
+ "wagtailcore/password_required.html",
2620
+ )
2621
+
2622
+ if hasattr(settings, "PASSWORD_REQUIRED_TEMPLATE"):
2623
+ warn(
2624
+ "The `PASSWORD_REQUIRED_TEMPLATE` setting is deprecated - use `WAGTAIL_PASSWORD_REQUIRED_TEMPLATE` instead.",
2625
+ category=RemovedInWagtail70Warning,
2626
+ )
2627
+
2628
+ password_required_template = getattr(
2629
+ settings,
2630
+ "PASSWORD_REQUIRED_TEMPLATE",
2631
+ password_required_template,
2632
+ )
2633
+
2564
2634
  context = self.get_context(request)
2565
2635
  context["form"] = form
2566
2636
  context["action_url"] = action_url
2567
- return TemplateResponse(request, self.password_required_template, context)
2637
+ return TemplateResponse(request, password_required_template, context)
2568
2638
 
2569
2639
  def with_content_json(self, content):
2570
2640
  """
@@ -2727,7 +2797,31 @@ class RevisionQuerySet(models.QuerySet):
2727
2797
  )
2728
2798
 
2729
2799
 
2730
- RevisionsManager = models.Manager.from_queryset(RevisionQuerySet)
2800
+ class RevisionsManager(models.Manager.from_queryset(RevisionQuerySet)):
2801
+ def previous_revision_id_subquery(self, revision_fk_name="revision"):
2802
+ """
2803
+ Returns a Subquery that can be used to annotate a queryset with the ID
2804
+ of the previous revision, based on the revision_fk_name field. Useful
2805
+ to avoid N+1 queries when generating comparison links between revisions.
2806
+
2807
+ The logic is similar to Revision.get_previous().pk.
2808
+ """
2809
+ fk = revision_fk_name
2810
+ return Subquery(
2811
+ Revision.objects.filter(
2812
+ base_content_type_id=OuterRef(f"{fk}__base_content_type_id"),
2813
+ object_id=OuterRef(f"{fk}__object_id"),
2814
+ )
2815
+ .filter(
2816
+ Q(
2817
+ created_at=OuterRef(f"{fk}__created_at"),
2818
+ pk__lt=OuterRef(f"{fk}__pk"),
2819
+ )
2820
+ | Q(created_at__lt=OuterRef(f"{fk}__created_at"))
2821
+ )
2822
+ .order_by("-created_at", "-pk")
2823
+ .values_list("pk", flat=True)[:1]
2824
+ )
2731
2825
 
2732
2826
 
2733
2827
  class PageRevisionsManager(RevisionsManager):
@@ -3520,7 +3614,7 @@ class WorkflowManager(models.Manager):
3520
3614
  return self.filter(active=True)
3521
3615
 
3522
3616
 
3523
- class Workflow(ClusterableModel):
3617
+ class AbstractWorkflow(ClusterableModel):
3524
3618
  name = models.CharField(max_length=255, verbose_name=_("name"))
3525
3619
  active = models.BooleanField(
3526
3620
  verbose_name=_("active"),
@@ -3609,9 +3703,14 @@ class Workflow(ClusterableModel):
3609
3703
  class Meta:
3610
3704
  verbose_name = _("workflow")
3611
3705
  verbose_name_plural = _("workflows")
3706
+ abstract = True
3612
3707
 
3613
3708
 
3614
- class GroupApprovalTask(Task):
3709
+ class Workflow(AbstractWorkflow):
3710
+ pass
3711
+
3712
+
3713
+ class AbstractGroupApprovalTask(Task):
3615
3714
  groups = models.ManyToManyField(
3616
3715
  Group,
3617
3716
  verbose_name=_("groups"),
@@ -3643,24 +3742,37 @@ class GroupApprovalTask(Task):
3643
3742
 
3644
3743
  return super().start(workflow_state, user=user)
3645
3744
 
3745
+ def _user_in_groups(self, user):
3746
+ # Cache the check whether "this user is in any of this
3747
+ # GroupApprovalTask's groups" on the user object, in case we do it
3748
+ # against the same user and task multiple times in a request.
3749
+ # Use a dict to map the task id to the check result, in case we also
3750
+ # check against different GroupApprovalTasks for the same user.
3751
+ cache_attr = "_group_approval_task_checks"
3752
+ if not (checks_cache := getattr(user, cache_attr, {})):
3753
+ setattr(user, cache_attr, checks_cache)
3754
+
3755
+ if self.pk not in checks_cache:
3756
+ checks_cache[self.pk] = self.groups.filter(
3757
+ id__in=user.groups.all()
3758
+ ).exists()
3759
+
3760
+ return checks_cache[self.pk]
3761
+
3646
3762
  def user_can_access_editor(self, obj, user):
3647
- return (
3648
- self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser
3649
- )
3763
+ return user.is_superuser or self._user_in_groups(user)
3650
3764
 
3651
3765
  def locked_for_user(self, obj, user):
3652
- return not (
3653
- self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser
3654
- )
3766
+ return not (user.is_superuser or self._user_in_groups(user))
3655
3767
 
3656
3768
  def user_can_lock(self, obj, user):
3657
- return self.groups.filter(id__in=user.groups.all()).exists()
3769
+ return self._user_in_groups(user)
3658
3770
 
3659
3771
  def user_can_unlock(self, obj, user):
3660
3772
  return False
3661
3773
 
3662
3774
  def get_actions(self, obj, user):
3663
- if self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser:
3775
+ if user.is_superuser or self._user_in_groups(user):
3664
3776
  return [
3665
3777
  ("reject", _("Request changes"), True),
3666
3778
  ("approve", _("Approve"), False),
@@ -3670,10 +3782,8 @@ class GroupApprovalTask(Task):
3670
3782
  return []
3671
3783
 
3672
3784
  def get_task_states_user_can_moderate(self, user, **kwargs):
3673
- if self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser:
3674
- return TaskState.objects.filter(
3675
- status=TaskState.STATUS_IN_PROGRESS, task=self.task_ptr
3676
- )
3785
+ if user.is_superuser or self._user_in_groups(user):
3786
+ return self.task_states.filter(status=TaskState.STATUS_IN_PROGRESS)
3677
3787
  else:
3678
3788
  return TaskState.objects.none()
3679
3789
 
@@ -3682,10 +3792,15 @@ class GroupApprovalTask(Task):
3682
3792
  return _("Members of the chosen Wagtail Groups can approve this task")
3683
3793
 
3684
3794
  class Meta:
3795
+ abstract = True
3685
3796
  verbose_name = _("Group approval task")
3686
3797
  verbose_name_plural = _("Group approval tasks")
3687
3798
 
3688
3799
 
3800
+ class GroupApprovalTask(AbstractGroupApprovalTask):
3801
+ pass
3802
+
3803
+
3689
3804
  class WorkflowStateQuerySet(models.QuerySet):
3690
3805
  def active(self):
3691
3806
  """
@@ -4118,10 +4233,10 @@ class WorkflowState(models.Model):
4118
4233
 
4119
4234
  class BaseTaskStateManager(models.Manager):
4120
4235
  def reviewable_by(self, user):
4121
- tasks = Task.objects.filter(active=True)
4236
+ tasks = Task.objects.filter(active=True).specific()
4122
4237
  states = TaskState.objects.none()
4123
4238
  for task in tasks:
4124
- states = states | task.specific.get_task_states_user_can_moderate(user=user)
4239
+ states = states | task.get_task_states_user_can_moderate(user=user)
4125
4240
  return states
4126
4241
 
4127
4242
 
wagtail/models/i18n.py CHANGED
@@ -378,7 +378,7 @@ def bootstrap_translatable_model(model, locale):
378
378
  This function populates the "translation_key", and "locale" fields on model instances that were created
379
379
  before wagtail-localize was added to the site.
380
380
 
381
- This can be called from a data migration, or instead you could use the "boostrap_translatable_models"
381
+ This can be called from a data migration, or instead you could use the "bootstrap_translatable_models"
382
382
  management command.
383
383
  """
384
384
  for instance in (
@@ -1,4 +1,6 @@
1
+ from django.conf import settings
1
2
  from django.contrib.auth.models import Group, Permission
3
+ from django.contrib.contenttypes.models import ContentType
2
4
  from django.db import models
3
5
  from django.utils.html import format_html
4
6
  from django.utils.safestring import mark_safe
@@ -12,13 +14,16 @@ from .view_restrictions import BaseViewRestriction
12
14
 
13
15
 
14
16
  class CollectionQuerySet(TreeQuerySet):
17
+ def get_min_depth(self):
18
+ return self.aggregate(models.Min("depth"))["depth__min"] or 2
19
+
15
20
  def get_indented_choices(self):
16
21
  """
17
22
  Return a list of (id, label) tuples for use as a list of choices in a collection chooser
18
23
  dropdown, where the label is formatted with get_indented_name to provide a tree layout.
19
24
  The indent level is chosen to place the minimum-depth collection at indent 0.
20
25
  """
21
- min_depth = self.aggregate(models.Min("depth"))["depth__min"] or 2
26
+ min_depth = self.get_min_depth()
22
27
  return [
23
28
  (collection.pk, collection.get_indented_name(min_depth, html=True))
24
29
  for collection in self
@@ -90,7 +95,7 @@ class Collection(MP_Node):
90
95
  Renders this Collection's name as a formatted string that displays its hierarchical depth via indentation.
91
96
  If indentation_start_depth is supplied, the Collection's depth is rendered relative to that depth.
92
97
  indentation_start_depth defaults to 2, the depth of the first non-Root Collection.
93
- Pass html=True to get a HTML representation, instead of the default plain-text.
98
+ Pass html=True to get an HTML representation, instead of the default plain-text.
94
99
 
95
100
  Example text output: " ↳ Pies"
96
101
  Example HTML output: "&nbsp;&nbsp;&nbsp;&nbsp;&#x21b3 Pies"
@@ -188,3 +193,29 @@ class GroupCollectionPermission(models.Model):
188
193
  unique_together = ("group", "collection", "permission")
189
194
  verbose_name = _("group collection permission")
190
195
  verbose_name_plural = _("group collection permissions")
196
+
197
+
198
+ class UploadedFile(models.Model):
199
+ """
200
+ Temporary storage for media fields uploaded through the multiple image/document uploader.
201
+ When validation rules (e.g. required metadata fields) prevent creating an Image/Document object from the file alone.
202
+ In this case, the file is stored against this model, to be turned into an Image/Document object once the full form
203
+ has been filled in.
204
+ """
205
+
206
+ for_content_type = models.ForeignKey(
207
+ ContentType,
208
+ verbose_name=_("for content type"),
209
+ related_name="uploads",
210
+ on_delete=models.CASCADE,
211
+ null=True,
212
+ )
213
+ file = models.FileField(upload_to="wagtail_uploads", max_length=200)
214
+ uploaded_by_user = models.ForeignKey(
215
+ settings.AUTH_USER_MODEL,
216
+ verbose_name=_("uploaded by user"),
217
+ null=True,
218
+ blank=True,
219
+ editable=False,
220
+ on_delete=models.SET_NULL,
221
+ )
@@ -160,7 +160,7 @@ class ReferenceIndex(models.Model):
160
160
  # linked by a ParentalKey.
161
161
  tracked_models = set()
162
162
 
163
- # Ths set of models that can appear as the 'from' object in the reference index.
163
+ # The set of models that can appear as the 'from' object in the reference index.
164
164
  # This only includes those registered with `register_model`, and NOT child models linked
165
165
  # by ParentalKey (object references on those are recorded under the parent).
166
166
  indexed_models = set()
@@ -19,13 +19,20 @@ class BaseViewRestriction(models.Model):
19
19
 
20
20
  RESTRICTION_CHOICES = (
21
21
  (NONE, _("Public")),
22
- (LOGIN, _("Private, accessible to logged-in users")),
23
- (PASSWORD, _("Private, accessible with the following password")),
22
+ (PASSWORD, _("Private, accessible with a shared password")),
23
+ (LOGIN, _("Private, accessible to any logged-in users")),
24
24
  (GROUPS, _("Private, accessible to users in specific groups")),
25
25
  )
26
26
 
27
27
  restriction_type = models.CharField(max_length=20, choices=RESTRICTION_CHOICES)
28
- password = models.CharField(verbose_name=_("password"), max_length=255, blank=True)
28
+ password = models.CharField(
29
+ verbose_name=_("shared password"),
30
+ max_length=255,
31
+ blank=True,
32
+ help_text=_(
33
+ "Shared passwords should not be used to protect sensitive content. Anyone who has this password will be able to view the content."
34
+ ),
35
+ )
29
36
  groups = models.ManyToManyField(Group, verbose_name=_("groups"), blank=True)
30
37
 
31
38
  def accept_request(self, request):
@@ -172,3 +172,9 @@ WAGTAILSEARCH_BACKENDS = {
172
172
  # Base URL to use when referring to full URLs within the Wagtail admin backend -
173
173
  # e.g. in notification emails. Don't include '/admin' or a trailing slash
174
174
  WAGTAILADMIN_BASE_URL = "http://example.com"
175
+
176
+ # Allowed file extensions for documents in the document library.
177
+ # This can be omitted to allow all files, but note that this may present a security risk
178
+ # if untrusted users are allowed to upload files -
179
+ # see https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files
180
+ WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'key', 'odt', 'pdf', 'pptx', 'rtf', 'txt', 'xlsx', 'zip']
@@ -1,2 +1,2 @@
1
1
  Django>=4.2,<5.1
2
- wagtail>=6.0,<6.1
2
+ wagtail==6.1rc1
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  from functools import lru_cache
3
3
  from html import unescape
4
+ from typing import List
4
5
 
5
6
  from django.core.validators import MaxLengthValidator
6
7
  from django.db.models import Model
@@ -26,23 +27,23 @@ def get_rewriter():
26
27
  return MultiRuleRewriter(
27
28
  [
28
29
  LinkRewriter(
29
- {
30
- linktype: handler.expand_db_attributes
30
+ bulk_rules={
31
+ linktype: handler.expand_db_attributes_many
31
32
  for linktype, handler in link_rules.items()
32
33
  },
33
- {
34
+ reference_extractors={
34
35
  linktype: handler.extract_references
35
36
  for linktype, handler in link_rules.items()
36
37
  },
37
38
  ),
38
39
  EmbedRewriter(
39
- {
40
- embedtype: handler.expand_db_attributes
40
+ bulk_rules={
41
+ embedtype: handler.expand_db_attributes_many
41
42
  for embedtype, handler in embed_rules.items()
42
43
  },
43
- {
44
- linktype: handler.extract_references
45
- for linktype, handler in embed_rules.items()
44
+ reference_extractors={
45
+ embedtype: handler.extract_references
46
+ for embedtype, handler in embed_rules.items()
46
47
  },
47
48
  ),
48
49
  ]
@@ -130,6 +131,14 @@ class EntityHandler:
130
131
  model = cls.get_model()
131
132
  return model._default_manager.get(id=attrs["id"])
132
133
 
134
+ @classmethod
135
+ def get_many(cls, attrs_list: List[dict]) -> List[Model]:
136
+ model = cls.get_model()
137
+ instance_ids = [attrs.get("id") for attrs in attrs_list]
138
+ instances_by_id = model._default_manager.in_bulk(instance_ids)
139
+ instances_by_str_id = {str(k): v for k, v in instances_by_id.items()}
140
+ return [instances_by_str_id.get(str(id_)) for id_ in instance_ids]
141
+
133
142
  @staticmethod
134
143
  def expand_db_attributes(attrs: dict) -> str:
135
144
  """
@@ -138,6 +147,14 @@ class EntityHandler:
138
147
  """
139
148
  raise NotImplementedError
140
149
 
150
+ @classmethod
151
+ def expand_db_attributes_many(cls, attrs_list: List[dict]) -> List[str]:
152
+ """
153
+ Given a list of attribute dicts from a list of entity tags stored in
154
+ the database, return the real HTML representation of each one.
155
+ """
156
+ return list(map(cls.expand_db_attributes, attrs_list))
157
+
141
158
  @classmethod
142
159
  def extract_references(cls, attrs):
143
160
  """