wagtail 6.3.2__py3-none-any.whl → 6.4rc1__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 (296) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/publish_revision.py +4 -5
  3. wagtail/admin/auth.py +0 -2
  4. wagtail/admin/checks.py +1 -1
  5. wagtail/admin/filters.py +3 -1
  6. wagtail/admin/forms/account.py +21 -11
  7. wagtail/admin/forms/collections.py +2 -9
  8. wagtail/admin/forms/formsets.py +32 -0
  9. wagtail/admin/forms/pages.py +5 -1
  10. wagtail/admin/forms/workflows.py +2 -13
  11. wagtail/admin/locale/ar/LC_MESSAGES/django.mo +0 -0
  12. wagtail/admin/locale/ar/LC_MESSAGES/django.po +68 -1
  13. wagtail/admin/locale/ar/LC_MESSAGES/djangojs.mo +0 -0
  14. wagtail/admin/locale/ar/LC_MESSAGES/djangojs.po +5 -1
  15. wagtail/admin/locale/en/LC_MESSAGES/django.po +312 -356
  16. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +21 -16
  17. wagtail/admin/menu.py +0 -13
  18. wagtail/admin/panels/base.py +2 -2
  19. wagtail/admin/panels/group.py +4 -1
  20. wagtail/admin/panels/inline_panel.py +5 -2
  21. wagtail/admin/panels/model_utils.py +36 -0
  22. wagtail/admin/panels/page_utils.py +2 -40
  23. wagtail/admin/panels/signal_handlers.py +0 -2
  24. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  25. wagtail/admin/static/wagtailadmin/css/panels/draftail.css +1 -1
  26. wagtail/admin/static/wagtailadmin/css/panels/streamfield.css +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 -8
  30. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  31. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  32. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  33. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  34. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +1 -1
  38. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +7 -0
  40. wagtail/admin/templates/wagtailadmin/404.html +4 -0
  41. wagtail/admin/templates/wagtailadmin/chooser/browse.html +2 -1
  42. wagtail/admin/templates/wagtailadmin/chooser/tables/parent_page_cell.html +1 -1
  43. wagtail/admin/templates/wagtailadmin/collections/_privacy_switch.html +8 -1
  44. wagtail/admin/templates/wagtailadmin/generic/confirm_delete.html +15 -9
  45. wagtail/admin/templates/wagtailadmin/generic/confirm_unpublish.html +21 -25
  46. wagtail/admin/templates/wagtailadmin/generic/form.html +1 -1
  47. wagtail/admin/templates/wagtailadmin/generic/preview_error.html +3 -0
  48. wagtail/admin/templates/wagtailadmin/generic/revisions/compare.html +63 -76
  49. wagtail/admin/templates/wagtailadmin/pages/_editor_js.html +0 -2
  50. wagtail/admin/templates/wagtailadmin/pages/edit.html +1 -5
  51. wagtail/admin/templates/wagtailadmin/panels/inline_panel_child.html +1 -0
  52. wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_form.html +1 -1
  53. wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_formset.html +6 -22
  54. wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +2 -2
  55. wagtail/admin/templates/wagtailadmin/shared/header.html +2 -2
  56. wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html +32 -39
  57. wagtail/admin/templates/wagtailadmin/shared/revisions/confirm_unschedule.html +13 -17
  58. wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/privacy.html +15 -3
  59. wagtail/admin/templates/wagtailadmin/shared/side_panels/preview.html +1 -1
  60. wagtail/admin/templates/wagtailadmin/skeleton.html +4 -2
  61. wagtail/admin/templates/wagtailadmin/workflows/create.html +1 -1
  62. wagtail/admin/templates/wagtailadmin/workflows/edit.html +1 -1
  63. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_pages_form.html +1 -1
  64. wagtail/admin/templates/wagtailadmin/workflows/includes/workflow_pages_formset.html +6 -23
  65. wagtail/admin/templatetags/wagtailadmin_tags.py +12 -0
  66. wagtail/admin/templatetags/wagtailuserbar.py +2 -3
  67. wagtail/admin/tests/pages/test_create_page.py +110 -1
  68. wagtail/admin/tests/pages/test_edit_page.py +3 -2
  69. wagtail/admin/tests/pages/test_explorer_view.py +18 -0
  70. wagtail/admin/tests/pages/test_page_usage.py +24 -20
  71. wagtail/admin/tests/pages/test_preview.py +69 -1
  72. wagtail/admin/tests/pages/test_revisions.py +40 -6
  73. wagtail/admin/tests/test_account_management.py +39 -1
  74. wagtail/admin/tests/test_audit_log.py +4 -2
  75. wagtail/admin/tests/test_block_preview.py +224 -0
  76. wagtail/admin/tests/test_edit_handlers.py +23 -6
  77. wagtail/admin/tests/test_page_chooser.py +50 -3
  78. wagtail/admin/tests/test_privacy.py +49 -26
  79. wagtail/admin/tests/test_site_summary.py +15 -10
  80. wagtail/admin/tests/test_templatetags.py +19 -0
  81. wagtail/admin/tests/test_userbar.py +82 -1
  82. wagtail/admin/tests/test_views_generic.py +27 -12
  83. wagtail/admin/tests/test_workflows.py +69 -0
  84. wagtail/admin/tests/tests.py +23 -4
  85. wagtail/admin/tests/ui/test_sidebar.py +1 -1
  86. wagtail/admin/tests/viewsets/test_model_viewset.py +15 -13
  87. wagtail/admin/ui/side_panels.py +7 -4
  88. wagtail/admin/urls/__init__.py +6 -0
  89. wagtail/admin/urls/pages.py +1 -1
  90. wagtail/admin/userbar.py +21 -1
  91. wagtail/admin/views/account.py +5 -0
  92. wagtail/admin/views/chooser.py +5 -1
  93. wagtail/admin/views/collections.py +0 -2
  94. wagtail/admin/views/generic/base.py +20 -10
  95. wagtail/admin/views/generic/history.py +0 -1
  96. wagtail/admin/views/generic/models.py +79 -21
  97. wagtail/admin/views/generic/preview.py +50 -1
  98. wagtail/admin/views/mixins.py +4 -2
  99. wagtail/admin/views/pages/bulk_actions/delete.py +11 -23
  100. wagtail/admin/views/pages/bulk_actions/page_bulk_action.py +17 -0
  101. wagtail/admin/views/pages/bulk_actions/publish.py +11 -31
  102. wagtail/admin/views/pages/bulk_actions/unpublish.py +11 -31
  103. wagtail/admin/views/pages/create.py +1 -0
  104. wagtail/admin/views/pages/edit.py +38 -30
  105. wagtail/admin/views/pages/revisions.py +43 -114
  106. wagtail/admin/views/pages/utils.py +0 -1
  107. wagtail/admin/views/tags.py +6 -2
  108. wagtail/admin/views/workflows.py +8 -6
  109. wagtail/admin/viewsets/model.py +0 -4
  110. wagtail/admin/viewsets/pages.py +0 -1
  111. wagtail/admin/widgets/tags.py +1 -0
  112. wagtail/api/v2/tests/test_documents.py +4 -2
  113. wagtail/api/v2/tests/test_images.py +4 -2
  114. wagtail/api/v2/tests/test_pages.py +8 -4
  115. wagtail/blocks/base.py +59 -1
  116. wagtail/blocks/field_block.py +6 -0
  117. wagtail/blocks/list_block.py +4 -0
  118. wagtail/blocks/static_block.py +3 -0
  119. wagtail/blocks/stream_block.py +5 -1
  120. wagtail/blocks/struct_block.py +6 -0
  121. wagtail/compat.py +16 -0
  122. wagtail/contrib/forms/forms.py +27 -7
  123. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +2 -2
  124. wagtail/contrib/forms/tests/test_models.py +7 -5
  125. wagtail/contrib/forms/tests/test_views.py +75 -0
  126. wagtail/contrib/frontend_cache/tasks.py +83 -0
  127. wagtail/contrib/frontend_cache/tests.py +47 -32
  128. wagtail/contrib/frontend_cache/utils.py +2 -70
  129. wagtail/contrib/redirects/base_formats.py +2 -2
  130. wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.mo +0 -0
  131. wagtail/contrib/redirects/locale/ar/LC_MESSAGES/django.po +3 -0
  132. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +24 -37
  133. wagtail/contrib/redirects/templates/wagtailredirects/add.html +1 -24
  134. wagtail/contrib/redirects/templates/wagtailredirects/confirm_delete.html +3 -13
  135. wagtail/contrib/redirects/tests/test_redirects.py +122 -110
  136. wagtail/contrib/redirects/tests/test_signal_handlers.py +75 -69
  137. wagtail/contrib/redirects/urls.py +2 -2
  138. wagtail/contrib/redirects/views.py +35 -73
  139. wagtail/contrib/search_promotions/admin_urls.py +10 -3
  140. wagtail/contrib/search_promotions/forms.py +55 -26
  141. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +44 -54
  142. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/add.html +21 -31
  143. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/confirm_delete.html +3 -12
  144. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/edit.html +11 -34
  145. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotion_form.html +1 -0
  146. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js +2 -1
  147. wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/index.html +0 -1
  148. wagtail/contrib/search_promotions/tests.py +814 -13
  149. wagtail/contrib/search_promotions/views/__init__.py +1 -0
  150. wagtail/contrib/search_promotions/views/reports.py +56 -0
  151. wagtail/contrib/search_promotions/views/settings.py +258 -0
  152. wagtail/contrib/search_promotions/wagtail_hooks.py +12 -1
  153. wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.mo +0 -0
  154. wagtail/contrib/settings/locale/ar/LC_MESSAGES/django.po +6 -1
  155. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +3 -3
  156. wagtail/contrib/settings/templates/wagtailsettings/edit.html +1 -5
  157. wagtail/contrib/settings/tests/generic/test_admin.py +2 -5
  158. wagtail/contrib/settings/tests/generic/test_register.py +1 -1
  159. wagtail/contrib/settings/tests/site_specific/test_admin.py +2 -5
  160. wagtail/contrib/settings/tests/site_specific/test_register.py +1 -1
  161. wagtail/contrib/settings/views.py +9 -23
  162. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  163. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
  164. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +1 -1
  165. wagtail/contrib/table_block/tests.py +4 -1
  166. wagtail/contrib/typed_table_block/blocks.py +3 -0
  167. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +10 -10
  168. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  169. wagtail/contrib/typed_table_block/tests.py +33 -0
  170. wagtail/documents/locale/en/LC_MESSAGES/django.po +26 -26
  171. wagtail/documents/migrations/0011_add_choose_permissions.py +1 -0
  172. wagtail/documents/models.py +1 -0
  173. wagtail/documents/signal_handlers.py +6 -2
  174. wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
  175. wagtail/documents/templates/wagtaildocs/documents/edit.html +1 -3
  176. wagtail/documents/templates/wagtaildocs/multiple/add.html +7 -1
  177. wagtail/documents/tests/test_admin_views.py +74 -33
  178. wagtail/documents/tests/test_views.py +21 -12
  179. wagtail/documents/views/chooser.py +1 -0
  180. wagtail/documents/views/documents.py +1 -2
  181. wagtail/documents/views/multiple.py +0 -1
  182. wagtail/documents/views/serve.py +9 -2
  183. wagtail/documents/wagtail_hooks.py +6 -1
  184. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  185. wagtail/embeds/oembed_providers.py +0 -64
  186. wagtail/fields.py +3 -0
  187. wagtail/images/apps.py +2 -1
  188. wagtail/images/blocks.py +6 -2
  189. wagtail/images/forms.py +40 -3
  190. wagtail/images/locale/ar/LC_MESSAGES/django.mo +0 -0
  191. wagtail/images/locale/ar/LC_MESSAGES/django.po +4 -0
  192. wagtail/images/locale/en/LC_MESSAGES/django.po +49 -49
  193. wagtail/images/migrations/0023_add_choose_permissions.py +1 -0
  194. wagtail/images/rich_text/contentstate.py +1 -0
  195. wagtail/images/rich_text/editor_html.py +1 -0
  196. wagtail/images/signal_handlers.py +17 -10
  197. wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
  198. wagtail/images/static/wagtailimages/js/image-block.js +1 -1
  199. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  200. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  201. wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
  202. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
  203. wagtail/images/tasks.py +18 -0
  204. wagtail/images/templates/wagtailimages/images/edit.html +1 -3
  205. wagtail/images/templates/wagtailimages/images/url_generator.html +1 -1
  206. wagtail/images/templates/wagtailimages/multiple/add.html +7 -2
  207. wagtail/images/templates/wagtailimages/widgets/image_chooser.html +1 -1
  208. wagtail/images/tests/test_admin_views.py +53 -29
  209. wagtail/images/tests/test_blocks.py +3 -2
  210. wagtail/images/tests/test_models.py +12 -10
  211. wagtail/images/tests/tests.py +10 -0
  212. wagtail/images/views/chooser.py +1 -0
  213. wagtail/images/views/images.py +1 -3
  214. wagtail/images/views/multiple.py +0 -1
  215. wagtail/images/views/serve.py +18 -2
  216. wagtail/images/widgets.py +3 -0
  217. wagtail/locale/en/LC_MESSAGES/django.po +228 -216
  218. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  219. wagtail/management/commands/publish_scheduled.py +1 -1
  220. wagtail/migrations/0087_alter_grouppagepermission_unique_together_and_more.py +16 -8
  221. wagtail/models/__init__.py +300 -119
  222. wagtail/models/i18n.py +2 -2
  223. wagtail/models/panels.py +37 -0
  224. wagtail/models/sites.py +7 -6
  225. wagtail/permission_policies/pages.py +2 -2
  226. wagtail/project_template/project_name/settings/base.py +4 -0
  227. wagtail/project_template/requirements.txt +1 -1
  228. wagtail/query.py +145 -0
  229. wagtail/search/backends/database/mysql/mysql.py +25 -17
  230. wagtail/search/backends/database/postgres/postgres.py +44 -83
  231. wagtail/search/backends/database/sqlite/sqlite.py +25 -17
  232. wagtail/search/backends/elasticsearch7.py +4 -0
  233. wagtail/search/locale/en/LC_MESSAGES/django.po +1 -1
  234. wagtail/search/query.py +8 -2
  235. wagtail/search/signal_handlers.py +6 -9
  236. wagtail/search/tasks.py +10 -0
  237. wagtail/search/tests/test_elasticsearch7_backend.py +21 -0
  238. wagtail/search/tests/test_index_functions.py +10 -6
  239. wagtail/search/tests/test_postgres_backend.py +0 -14
  240. wagtail/signal_handlers.py +5 -20
  241. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  242. wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -13
  243. wagtail/snippets/tests/test_preview.py +5 -0
  244. wagtail/snippets/tests/test_snippets.py +100 -45
  245. wagtail/snippets/tests/test_usage.py +29 -24
  246. wagtail/snippets/tests/test_viewset.py +1 -1
  247. wagtail/snippets/views/snippets.py +0 -12
  248. wagtail/tasks.py +41 -0
  249. wagtail/templates/wagtailcore/shared/block_preview.html +29 -0
  250. wagtail/test/earlypage/__init__.py +0 -0
  251. wagtail/test/earlypage/migrations/0001_initial.py +37 -0
  252. wagtail/test/earlypage/migrations/__init__.py +0 -0
  253. wagtail/test/earlypage/models.py +14 -0
  254. wagtail/test/settings.py +3 -0
  255. wagtail/test/testapp/fixtures/test.json +7 -0
  256. wagtail/test/testapp/fixtures/test_specific.json +6 -3
  257. wagtail/test/testapp/models.py +58 -44
  258. wagtail/test/testapp/templates/tests/custom_block_preview.html +16 -0
  259. wagtail/test/testapp/templates/tests/static_block_preview.html +5 -0
  260. wagtail/test/testapp/wagtail_hooks.py +9 -0
  261. wagtail/tests/test_blocks.py +189 -2
  262. wagtail/tests/test_hooks.py +166 -1
  263. wagtail/tests/test_management_commands.py +54 -13
  264. wagtail/tests/test_page_allowed_http_methods.py +32 -0
  265. wagtail/tests/test_page_model.py +68 -0
  266. wagtail/tests/test_page_privacy.py +10 -0
  267. wagtail/tests/test_page_queryset.py +79 -0
  268. wagtail/tests/test_reference_index.py +84 -75
  269. wagtail/tests/test_streamfield.py +30 -0
  270. wagtail/tests/test_utils.py +61 -0
  271. wagtail/users/forms.py +2 -9
  272. wagtail/users/locale/en/LC_MESSAGES/django.po +17 -17
  273. wagtail/users/templates/wagtailusers/groups/create.html +0 -5
  274. wagtail/users/templates/wagtailusers/groups/includes/page_permissions_form.html +1 -1
  275. wagtail/users/templates/wagtailusers/groups/includes/page_permissions_formset.html +6 -6
  276. wagtail/users/tests/test_admin_views.py +96 -4
  277. wagtail/users/tests/test_utils.py +76 -0
  278. wagtail/users/utils.py +43 -11
  279. wagtail/utils/setup.py +2 -2
  280. wagtail/utils/templates.py +26 -0
  281. wagtail/utils/widgets.py +1 -0
  282. wagtail/views.py +9 -1
  283. wagtail/wagtail_hooks.py +67 -29
  284. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/METADATA +2 -2
  285. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/RECORD +289 -276
  286. wagtail/admin/static/wagtailadmin/js/expanding-formset.js +0 -1
  287. wagtail/admin/static/wagtailadmin/js/vendor/rangy-core.js +0 -1
  288. wagtail/admin/static/wagtailadmin/js/vendor/uuidv4.min.js +0 -1
  289. wagtail/contrib/search_promotions/views.py +0 -323
  290. wagtail/images/static/wagtailimages/js/vendor/canvas-to-blob.min.js +0 -1
  291. wagtail/users/static/wagtailusers/js/group-form.js +0 -1
  292. wagtail/users/templates/wagtailusers/groups/includes/group_form_js.html +0 -3
  293. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/LICENSE +0 -0
  294. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/WHEEL +0 -0
  295. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/entry_points.txt +0 -0
  296. {wagtail-6.3.2.dist-info → wagtail-6.4rc1.dist-info}/top_level.txt +0 -0
wagtail/models/i18n.py CHANGED
@@ -56,7 +56,7 @@ class Locale(models.Model):
56
56
  @classmethod
57
57
  def get_default(cls):
58
58
  """
59
- Returns the default Locale based on the site's LANGUAGE_CODE setting
59
+ Returns the default Locale based on the site's ``LANGUAGE_CODE`` setting.
60
60
  """
61
61
  return cls.objects.get_for_language(settings.LANGUAGE_CODE)
62
62
 
@@ -309,7 +309,7 @@ class TranslatableMixin(models.Model):
309
309
  """
310
310
  Finds the translation in the specified locale.
311
311
 
312
- If there is no translation in that locale, this returns None.
312
+ If there is no translation in that locale, this returns ``None``.
313
313
  """
314
314
  try:
315
315
  return self.get_translation(locale)
@@ -0,0 +1,37 @@
1
+ # Placeholder for panel types defined in wagtail.admin.panels.
2
+ # These are needed because we wish to define properties such as `content_panels` on core models
3
+ # such as Page, but importing from wagtail.admin would create a circular import. We therefore use a
4
+ # placeholder object, and swap it out for the real panel class inside
5
+ # `wagtail.admin.panels.model_utils.expand_panel_list` at the same time as converting strings to
6
+ # FieldPanel instances.
7
+
8
+ from django.conf import settings
9
+ from django.utils.functional import cached_property
10
+ from django.utils.module_loading import import_string
11
+
12
+
13
+ class PanelPlaceholder:
14
+ def __init__(self, path, args, kwargs):
15
+ self.path = path
16
+ self.args = args
17
+ self.kwargs = kwargs
18
+
19
+ @cached_property
20
+ def panel_class(self):
21
+ return import_string(self.path)
22
+
23
+ def construct(self):
24
+ return self.panel_class(*self.args, **self.kwargs)
25
+
26
+
27
+ class CommentPanelPlaceholder(PanelPlaceholder):
28
+ def __init__(self):
29
+ super().__init__(
30
+ "wagtail.admin.panels.CommentPanel",
31
+ [],
32
+ {},
33
+ )
34
+
35
+ def construct(self):
36
+ if getattr(settings, "WAGTAILADMIN_COMMENTS_ENABLED", True):
37
+ return super().construct()
wagtail/models/sites.py CHANGED
@@ -160,7 +160,8 @@ class Site(models.Model):
160
160
 
161
161
  @staticmethod
162
162
  def _find_for_request(request):
163
- hostname = split_domain_port(request.get_host())[0]
163
+ # Use `_get_raw_host` to avoid ALLOWED_HOSTS checks
164
+ hostname = split_domain_port(request._get_raw_host())[0]
164
165
  port = request.get_port()
165
166
  site = None
166
167
  try:
@@ -206,15 +207,15 @@ class Site(models.Model):
206
207
  def get_site_root_paths():
207
208
  """
208
209
  Return a list of `SiteRootPath` instances, most specific path
209
- first - used to translate url_paths into actual URLs with hostnames
210
+ first - used to translate url_paths into actual URLs with hostnames.
210
211
 
211
212
  Each root path is an instance of the `SiteRootPath` named tuple,
212
213
  and have the following attributes:
213
214
 
214
- - `site_id` - The ID of the Site record
215
- - `root_path` - The internal URL path of the site's home page (for example '/home/')
216
- - `root_url` - The scheme/domain name of the site (for example 'https://www.example.com/')
217
- - `language_code` - The language code of the site (for example 'en')
215
+ - ``site_id`` - The ID of the Site record
216
+ - ``root_path`` - The internal URL path of the site's home page (for example '/home/')
217
+ - ``root_url`` - The scheme/domain name of the site (for example 'https://www.example.com/')
218
+ - ``language_code`` - The language code of the site (for example 'en')
218
219
  """
219
220
  result = cache.get(
220
221
  SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION
@@ -166,7 +166,7 @@ class PagePermissionPolicy(OwnershipPermissionPolicy):
166
166
  return Page.objects.filter(depth=1)
167
167
  else:
168
168
  codenames = self._get_permission_codenames(
169
- {"add", "change", "publish", "lock"}
169
+ {"add", "change", "publish", "lock", "unlock", "bulk_delete"}
170
170
  )
171
171
  return [
172
172
  perm.page
@@ -195,7 +195,7 @@ class PagePermissionPolicy(OwnershipPermissionPolicy):
195
195
  return base_queryset
196
196
 
197
197
  explorable_pages = self.instances_user_has_any_permission_for(
198
- user, {"add", "change", "publish", "lock"}
198
+ user, {"add", "change", "publish", "lock", "unlock", "bulk_delete"}
199
199
  )
200
200
 
201
201
  # For all pages with specific permissions, add their ancestors as
@@ -156,6 +156,10 @@ STORAGES = {
156
156
  },
157
157
  }
158
158
 
159
+ # Django sets a maximum of 1000 fields per form by default, but particularly complex page models
160
+ # can exceed this limit within Wagtail's page editor.
161
+ DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
162
+
159
163
 
160
164
  # Wagtail settings
161
165
 
@@ -1,2 +1,2 @@
1
1
  Django>=5.1,<5.2
2
- wagtail>=6.3,<6.4
2
+ wagtail==6.4rc1
wagtail/query.py CHANGED
@@ -147,11 +147,17 @@ class SpecificQuerySetMixin:
147
147
  super().__init__(*args, **kwargs)
148
148
  # set by PageQuerySet.defer_streamfields()
149
149
  self._defer_streamfields = False
150
+ self._specific_select_related_fields = ()
151
+ self._specific_prefetch_related_lookups = ()
150
152
 
151
153
  def _clone(self):
152
154
  """Ensure clones inherit custom attribute values."""
153
155
  clone = super()._clone()
154
156
  clone._defer_streamfields = self._defer_streamfields
157
+ clone._specific_select_related_fields = self._specific_select_related_fields
158
+ clone._specific_prefetch_related_lookups = (
159
+ self._specific_prefetch_related_lookups
160
+ )
155
161
  return clone
156
162
 
157
163
  def specific(self, defer=False):
@@ -179,6 +185,137 @@ class SpecificQuerySetMixin:
179
185
  (SpecificIterable, DeferredSpecificIterable),
180
186
  )
181
187
 
188
+ def select_related(self, *fields, for_specific_subqueries: bool = False):
189
+ """
190
+ Overrides Django's native :meth:`~django.db.models.query.QuerySet.select_related`
191
+ to allow related objects to be fetched by the subqueries made when a specific
192
+ queryset is evaluated.
193
+
194
+ When ``for_specific_subqueries`` is ``False`` (the default), the method functions
195
+ exactly like the original method. However, when ``True``, ``fields`` are
196
+ **required**, and must match names of ForeignKey fields on all specific models
197
+ that might be included in the result (which can include fields inherited from
198
+ concrete parents). Unlike when ``for_specific_subqueries`` is ``False``, no
199
+ validation is applied to ``fields`` when the method is called. Rather, that when
200
+ the method is called. Instead, that validation is applied for each individual
201
+ subquery when the queryset is evaluated. This difference in behaviour should be
202
+ taken into account when experimenting with ``for_specific_subqueries=True`` .
203
+
204
+ As with Django's native implementation, you chain multiple applications of
205
+ ``select_related()`` with ``for_specific_subqueries=True`` to progressively add
206
+ to the list of fields to be fetched. For example:
207
+
208
+ .. code-block:: python
209
+
210
+ # Fetch 'author' when retrieving specific page data
211
+ queryset = Page.objects.specific().select_related("author", for_specific_subqueries=True)
212
+
213
+ # We're rendering cards with images, so fetch the listing image too
214
+ queryset = queryset.select_related("listing_image", for_specific_subqueries=True)
215
+
216
+ # Fetch some key taxonomy data too
217
+ queryset = queryset.select_related("topic", "target_audience", for_specific_subqueries=True)
218
+
219
+ As with Django's native implementation, ``None`` can be supplied in place of
220
+ ``fields`` to negate a previous application of ``select_related()``. By default,
221
+ this will only work for cases where ``select_related()`` was called without
222
+ ``for_specific_subqueries``, or with ``for_specific_subqueries=False``. However,
223
+ you can use ``for_specific_subqueries=True`` to negate subquery-specific
224
+ applications too. For example:
225
+
226
+ .. code-block:: python
227
+
228
+ # Fetch 'author' and 'listing_image' when retrieving specific page data
229
+ queryset = Page.objects.specific().select_related(
230
+ "author",
231
+ "listing_image",
232
+ for_specific_subqueries=True
233
+ )
234
+
235
+ # I've changed my mind. Do not fetch any additional data
236
+ queryset = queryset.select_related(None, for_specific_subqueries=True)
237
+ """
238
+
239
+ if not for_specific_subqueries:
240
+ return super().select_related(*fields)
241
+ if not fields:
242
+ raise ValueError(
243
+ "'fields' must be specified when calling select_related() with for_specific_subqueries=True"
244
+ )
245
+ clone = self._chain()
246
+ if fields == (None,):
247
+ clone._specific_select_related_fields = ()
248
+ else:
249
+ clone._specific_select_related_fields = (
250
+ self._specific_select_related_fields + fields
251
+ )
252
+ return clone
253
+
254
+ def prefetch_related(self, *lookups, for_specific_subqueries: bool = False):
255
+ """
256
+ Overrides Django's native :meth:`~django.db.models.query.QuerySet.prefetch_related`
257
+ implementation to allow related objects to be fetched alongside the subqueries made
258
+ when a specific queryset is evaluated.
259
+
260
+ When ``for_specific_subqueries`` is ``False`` (the default), the method functions
261
+ exactly like the original method. However, when ``True``, ``lookups`` are
262
+ **required**, and must match names of related fields on all specific models that
263
+ might be included in the result (which can include relationships inherited from
264
+ concrete parents). Unlike when ``for_specific_subqueries`` is ``False``, no
265
+ validation is applied to ``lookups`` when the method is called. Instead, that
266
+ validation is applied for each individual subquery when the queryset is
267
+ evaluated. This difference in behaviour should be taken into account when
268
+ experimenting with ``for_specific_subqueries=True``.
269
+
270
+ As with Django's native implementation, you chain multiple applications of
271
+ ``prefetch_related()`` with ``for_specific_subqueries=True`` to progressively
272
+ add to the list of lookups to be made. For example:
273
+
274
+ .. code-block:: python
275
+
276
+ # Fetch 'contributors' when retrieving specific page data
277
+ queryset = Page.objects.specific().prefetch_related("contributors", for_specific_subqueries=True)
278
+
279
+ # We're rendering cards with images, so prefetch listing image renditions too
280
+ queryset = queryset.prefetch_related("listing_image__renditions", for_specific_subqueries=True)
281
+
282
+ # Fetch some key taxonomy data also
283
+ queryset = queryset.prefetch_related("tags", for_specific_subqueries=True)
284
+
285
+ As with Django's native implementation, ``None`` can be supplied in place of
286
+ ``lookups`` to negate a previous application of ``prefetch_related()``. By default,
287
+ this will only work for cases where ``prefetch_related()`` was called without
288
+ ``for_specific_subqueries``, or with ``for_specific_subqueries=False``. However,
289
+ you can use ``for_specific_subqueries=True`` to negate subquery-specific
290
+ applications too. For example:
291
+
292
+ .. code-block:: python
293
+
294
+ # Fetch 'contributors' and 'listing_image' renditions when retrieving specific page data
295
+ queryset = Page.objects.specific().prefetch_related(
296
+ "contributors",
297
+ "listing_image__renditions",
298
+ for_specific_subqueries=True
299
+ )
300
+
301
+ # I've changed my mind. Do not make any additional queries
302
+ queryset = queryset.prefetch_related(None, for_specific_subqueries=True)
303
+ """
304
+ if not for_specific_subqueries:
305
+ return super().prefetch_related(*lookups)
306
+ if not lookups:
307
+ raise ValueError(
308
+ "'lookups' must be provided when calling prefetch_related() with for_specific_subqueries=True"
309
+ )
310
+ clone = self._chain()
311
+ if lookups == (None,):
312
+ clone._specific_prefetch_related_lookups = ()
313
+ else:
314
+ clone._specific_prefetch_related_lookups = (
315
+ self._specific_prefetch_related_lookups + lookups
316
+ )
317
+ return clone
318
+
182
319
 
183
320
  class PageQuerySet(SearchableQuerySetMixin, SpecificQuerySetMixin, TreeQuerySet):
184
321
  def live_q(self):
@@ -561,6 +698,14 @@ class SpecificIterable(ModelIterable):
561
698
  model = content_types[content_type].model_class() or qs.model
562
699
  items = model.objects.filter(pk__in=pks)
563
700
 
701
+ if qs._specific_select_related_fields:
702
+ items = items.select_related(*qs._specific_select_related_fields)
703
+
704
+ if qs._specific_prefetch_related_lookups:
705
+ items = items.prefetch_related(
706
+ *qs._specific_prefetch_related_lookups
707
+ )
708
+
564
709
  if qs._defer_streamfields and hasattr(items, "defer_streamfields"):
565
710
  items = items.defer_streamfields()
566
711
 
@@ -1,7 +1,12 @@
1
1
  import warnings
2
2
  from collections import OrderedDict
3
3
 
4
- from django.db import DEFAULT_DB_ALIAS, NotSupportedError, connections, transaction
4
+ from django.db import (
5
+ NotSupportedError,
6
+ connections,
7
+ router,
8
+ transaction,
9
+ )
5
10
  from django.db.models import Case, When
6
11
  from django.db.models.aggregates import Avg, Count
7
12
  from django.db.models.constants import LOOKUP_SEP
@@ -151,17 +156,22 @@ class ObjectIndexer:
151
156
 
152
157
 
153
158
  class Index:
154
- def __init__(self, backend, db_alias=None):
159
+ def __init__(self, backend):
155
160
  self.backend = backend
156
161
  self.name = self.backend.index_name
157
- self.db_alias = DEFAULT_DB_ALIAS if db_alias is None else db_alias
158
- self.connection = connections[self.db_alias]
159
- if self.connection.vendor != "mysql":
162
+
163
+ self.read_connection = connections[router.db_for_read(IndexEntry)]
164
+ self.write_connection = connections[router.db_for_write(IndexEntry)]
165
+
166
+ if (
167
+ self.read_connection.vendor != "mysql"
168
+ or self.write_connection.vendor != "mysql"
169
+ ):
160
170
  raise NotSupportedError(
161
- "You must select a MySQL database " "to use MySQL search."
171
+ "You must select a MySQL database to use MySQL search."
162
172
  )
163
173
 
164
- self.entries = IndexEntry._default_manager.using(self.db_alias)
174
+ self.entries = IndexEntry._default_manager.all()
165
175
 
166
176
  def add_model(self, model):
167
177
  pass
@@ -201,11 +211,9 @@ class Index:
201
211
  ).update(title_norm=lavg / F("title_length"))
202
212
 
203
213
  def delete_stale_model_entries(self, model):
204
- existing_pks = (
205
- model._default_manager.using(self.db_alias)
206
- .annotate(object_id=Cast("pk", TextField()))
207
- .values("object_id")
208
- )
214
+ existing_pks = model._default_manager.annotate(
215
+ object_id=Cast("pk", TextField())
216
+ ).values("object_id")
209
217
  content_types_pks = get_descendants_content_types_pks(model)
210
218
  stale_entries = self.entries.filter(
211
219
  content_type_id__in=content_types_pks
@@ -276,7 +284,7 @@ class Index:
276
284
  update_method(content_type_pk, indexers)
277
285
 
278
286
  def delete_item(self, item):
279
- item.index_entries.all()._raw_delete(using=self.db_alias)
287
+ item.index_entries.all()._raw_delete(using=self.write_connection.alias)
280
288
 
281
289
  def __str__(self):
282
290
  return self.name
@@ -610,7 +618,7 @@ class MySQLSearchRebuilder:
610
618
  class MySQLSearchAtomicRebuilder(MySQLSearchRebuilder):
611
619
  def __init__(self, index):
612
620
  super().__init__(index)
613
- self.transaction = transaction.atomic(using=index.db_alias)
621
+ self.transaction = transaction.atomic(using=index.write_connection.alias)
614
622
  self.transaction_opened = False
615
623
 
616
624
  def start(self):
@@ -650,11 +658,11 @@ class MySQLSearchBackend(BaseSearchBackend):
650
658
  if params.get("ATOMIC_REBUILD", False):
651
659
  self.rebuilder_class = self.atomic_rebuilder_class
652
660
 
653
- def get_index_for_model(self, model, db_alias=None):
654
- return Index(self, db_alias)
661
+ def get_index_for_model(self, model):
662
+ return Index(self)
655
663
 
656
664
  def get_index_for_object(self, obj):
657
- return self.get_index_for_model(obj._meta.model, obj._state.db)
665
+ return self.get_index_for_model(obj._meta.model)
658
666
 
659
667
  def reset_index(self):
660
668
  for connection in [
@@ -3,7 +3,12 @@ from collections import OrderedDict
3
3
  from functools import reduce
4
4
 
5
5
  from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
6
- from django.db import DEFAULT_DB_ALIAS, NotSupportedError, connections, transaction
6
+ from django.db import (
7
+ NotSupportedError,
8
+ connections,
9
+ router,
10
+ transaction,
11
+ )
7
12
  from django.db.models import Avg, Count, F, Manager, Q, TextField, Value
8
13
  from django.db.models.constants import LOOKUP_SEP
9
14
  from django.db.models.functions import Cast, Length
@@ -163,20 +168,22 @@ class ObjectIndexer:
163
168
 
164
169
 
165
170
  class Index:
166
- def __init__(self, backend, db_alias=None):
171
+ def __init__(self, backend):
167
172
  self.backend = backend
168
173
  self.name = self.backend.index_name
169
- self.db_alias = DEFAULT_DB_ALIAS if db_alias is None else db_alias
170
- self.connection = connections[self.db_alias]
171
- if self.connection.vendor != "postgresql":
174
+
175
+ self.read_connection = connections[router.db_for_read(IndexEntry)]
176
+ self.write_connection = connections[router.db_for_write(IndexEntry)]
177
+
178
+ if (
179
+ self.read_connection.vendor != "postgresql"
180
+ or self.write_connection.vendor != "postgresql"
181
+ ):
172
182
  raise NotSupportedError(
173
- "You must select a PostgreSQL database " "to use PostgreSQL search."
183
+ "You must select a PostgreSQL database to use PostgreSQL search."
174
184
  )
175
185
 
176
- # Whether to allow adding items via the faster upsert method available in Postgres >=9.5
177
- self._enable_upsert = self.connection.pg_version >= 90500
178
-
179
- self.entries = IndexEntry._default_manager.using(self.db_alias)
186
+ self.entries = IndexEntry._default_manager.all()
180
187
 
181
188
  def add_model(self, model):
182
189
  pass
@@ -216,11 +223,9 @@ class Index:
216
223
  ).update(title_norm=lavg / F("title_length"))
217
224
 
218
225
  def delete_stale_model_entries(self, model):
219
- existing_pks = (
220
- model._default_manager.using(self.db_alias)
221
- .annotate(object_id=Cast("pk", TextField()))
222
- .values("object_id")
223
- )
226
+ existing_pks = model._default_manager.annotate(
227
+ object_id=Cast("pk", TextField())
228
+ ).values("object_id")
224
229
  content_types_pks = get_descendants_content_types_pks(model)
225
230
  stale_entries = self.entries.filter(
226
231
  content_type_id__in=content_types_pks
@@ -237,8 +242,21 @@ class Index:
237
242
  def add_item(self, obj):
238
243
  self.add_items(obj._meta.model, [obj])
239
244
 
240
- def add_items_upsert(self, content_type_pk, indexers):
241
- compiler = InsertQuery(IndexEntry).get_compiler(connection=self.connection)
245
+ def add_items(self, model, objs):
246
+ search_fields = model.get_search_fields()
247
+ if not search_fields:
248
+ return
249
+
250
+ indexers = [ObjectIndexer(obj, self.backend) for obj in objs]
251
+
252
+ # TODO: Delete unindexed objects while dealing with proxy models.
253
+ if not indexers:
254
+ return
255
+
256
+ content_type_pk = get_content_type_pk(model)
257
+ compiler = InsertQuery(IndexEntry).get_compiler(
258
+ connection=self.write_connection
259
+ )
242
260
  title_sql = []
243
261
  autocomplete_sql = []
244
262
  body_sql = []
@@ -251,7 +269,7 @@ class Index:
251
269
  value = compiler.prepare_value(
252
270
  IndexEntry._meta.get_field("title"), indexer.title
253
271
  )
254
- sql, params = value.as_sql(compiler, self.connection)
272
+ sql, params = value.as_sql(compiler, self.write_connection)
255
273
  title_sql.append(sql)
256
274
  data_params.extend(params)
257
275
 
@@ -259,7 +277,7 @@ class Index:
259
277
  value = compiler.prepare_value(
260
278
  IndexEntry._meta.get_field("autocomplete"), indexer.autocomplete
261
279
  )
262
- sql, params = value.as_sql(compiler, self.connection)
280
+ sql, params = value.as_sql(compiler, self.write_connection)
263
281
  autocomplete_sql.append(sql)
264
282
  data_params.extend(params)
265
283
 
@@ -267,7 +285,7 @@ class Index:
267
285
  value = compiler.prepare_value(
268
286
  IndexEntry._meta.get_field("body"), indexer.body
269
287
  )
270
- sql, params = value.as_sql(compiler, self.connection)
288
+ sql, params = value.as_sql(compiler, self.write_connection)
271
289
  body_sql.append(sql)
272
290
  data_params.extend(params)
273
291
 
@@ -278,7 +296,7 @@ class Index:
278
296
  ]
279
297
  )
280
298
 
281
- with self.connection.cursor() as cursor:
299
+ with self.write_connection.cursor() as cursor:
282
300
  cursor.execute(
283
301
  """
284
302
  INSERT INTO %s (content_type_id, object_id, title, autocomplete, body, title_norm)
@@ -295,65 +313,8 @@ class Index:
295
313
 
296
314
  self._refresh_title_norms()
297
315
 
298
- def add_items_update_then_create(self, content_type_pk, indexers):
299
- ids_and_data = {}
300
- for indexer in indexers:
301
- ids_and_data[indexer.id] = (
302
- indexer.title,
303
- indexer.autocomplete,
304
- indexer.body,
305
- )
306
-
307
- index_entries_for_ct = self.entries.filter(content_type_id=content_type_pk)
308
- indexed_ids = frozenset(
309
- index_entries_for_ct.filter(object_id__in=ids_and_data.keys()).values_list(
310
- "object_id", flat=True
311
- )
312
- )
313
- for indexed_id in indexed_ids:
314
- title, autocomplete, body = ids_and_data[indexed_id]
315
- index_entries_for_ct.filter(object_id=indexed_id).update(
316
- title=title, autocomplete=autocomplete, body=body
317
- )
318
-
319
- to_be_created = []
320
- for object_id in ids_and_data.keys():
321
- if object_id not in indexed_ids:
322
- title, autocomplete, body = ids_and_data[object_id]
323
- to_be_created.append(
324
- IndexEntry(
325
- content_type_id=content_type_pk,
326
- object_id=object_id,
327
- title=title,
328
- autocomplete=autocomplete,
329
- body=body,
330
- )
331
- )
332
-
333
- self.entries.bulk_create(to_be_created)
334
-
335
- self._refresh_title_norms()
336
-
337
- def add_items(self, model, objs):
338
- search_fields = model.get_search_fields()
339
- if not search_fields:
340
- return
341
-
342
- indexers = [ObjectIndexer(obj, self.backend) for obj in objs]
343
-
344
- # TODO: Delete unindexed objects while dealing with proxy models.
345
- if indexers:
346
- content_type_pk = get_content_type_pk(model)
347
-
348
- update_method = (
349
- self.add_items_upsert
350
- if self._enable_upsert
351
- else self.add_items_update_then_create
352
- )
353
- update_method(content_type_pk, indexers)
354
-
355
316
  def delete_item(self, item):
356
- item.index_entries.all()._raw_delete(using=self.db_alias)
317
+ item.index_entries.all()._raw_delete(using=self.write_connection.alias)
357
318
 
358
319
  def __str__(self):
359
320
  return self.name
@@ -709,7 +670,7 @@ class PostgresSearchRebuilder:
709
670
  class PostgresSearchAtomicRebuilder(PostgresSearchRebuilder):
710
671
  def __init__(self, index):
711
672
  super().__init__(index)
712
- self.transaction = transaction.atomic(using=index.db_alias)
673
+ self.transaction = transaction.atomic(using=index.write_connection.alias)
713
674
  self.transaction_opened = False
714
675
 
715
676
  def start(self):
@@ -750,11 +711,11 @@ class PostgresSearchBackend(BaseSearchBackend):
750
711
  if params.get("ATOMIC_REBUILD", False):
751
712
  self.rebuilder_class = self.atomic_rebuilder_class
752
713
 
753
- def get_index_for_model(self, model, db_alias=None):
754
- return Index(self, db_alias)
714
+ def get_index_for_model(self, model):
715
+ return Index(self)
755
716
 
756
717
  def get_index_for_object(self, obj):
757
- return self.get_index_for_model(obj._meta.model, obj._state.db)
718
+ return self.get_index_for_model(obj._meta.model)
758
719
 
759
720
  def reset_index(self):
760
721
  for connection in [