wagtail 7.1.1__py3-none-any.whl → 7.2rc1__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 (345) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/actions/copy_page.py +1 -1
  3. wagtail/actions/create_alias.py +1 -1
  4. wagtail/actions/delete_page.py +1 -1
  5. wagtail/actions/publish_page_revision.py +1 -1
  6. wagtail/actions/publish_revision.py +1 -1
  7. wagtail/actions/revert_to_page_revision.py +1 -1
  8. wagtail/actions/unpublish.py +1 -1
  9. wagtail/actions/unpublish_page.py +1 -1
  10. wagtail/admin/auth.py +3 -1
  11. wagtail/admin/checks.py +2 -2
  12. wagtail/admin/filters.py +28 -1
  13. wagtail/admin/forms/collections.py +1 -1
  14. wagtail/admin/forms/comments.py +1 -1
  15. wagtail/admin/forms/models.py +1 -1
  16. wagtail/admin/forms/pages.py +1 -1
  17. wagtail/admin/forms/tags.py +1 -1
  18. wagtail/admin/locale/cs/LC_MESSAGES/django.mo +0 -0
  19. wagtail/admin/locale/cs/LC_MESSAGES/django.po +25 -1
  20. wagtail/admin/locale/en/LC_MESSAGES/django.po +278 -192
  21. wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +29 -15
  22. wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
  23. wagtail/admin/locale/it/LC_MESSAGES/django.po +3 -2
  24. wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
  25. wagtail/admin/locale/nl/LC_MESSAGES/django.po +57 -3
  26. wagtail/admin/locale/nl/LC_MESSAGES/djangojs.mo +0 -0
  27. wagtail/admin/locale/nl/LC_MESSAGES/djangojs.po +8 -2
  28. wagtail/admin/locale/ru/LC_MESSAGES/django.mo +0 -0
  29. wagtail/admin/locale/ru/LC_MESSAGES/django.po +58 -1
  30. wagtail/admin/locale/tr/LC_MESSAGES/django.mo +0 -0
  31. wagtail/admin/locale/tr/LC_MESSAGES/django.po +3 -2
  32. wagtail/admin/static/wagtailadmin/css/core.css +1 -1
  33. wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
  34. wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
  35. wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
  36. wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
  37. wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
  38. wagtail/admin/static/wagtailadmin/js/core.js +1 -1
  39. wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +2 -2
  40. wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
  41. wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
  42. wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
  43. wagtail/admin/static/wagtailadmin/js/icons.js +1 -1
  44. wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
  45. wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
  46. wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
  47. wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
  48. wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
  49. wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
  50. wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
  51. wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
  52. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  53. wagtail/admin/static/wagtailadmin/js/telepath/telepath.js +1 -1
  54. wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
  55. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  56. wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +2 -2
  57. wagtail/admin/static/wagtailadmin/js/vendor/bootstrap-modal.js +1 -1
  58. wagtail/admin/static/wagtailadmin/js/vendor/bootstrap-transition.js +1 -1
  59. wagtail/admin/static/wagtailadmin/js/vendor/jquery-3.6.0.min.js +1 -1
  60. wagtail/admin/static/wagtailadmin/js/vendor/jquery-ui-1.13.2.min.js +1 -1
  61. wagtail/admin/static/wagtailadmin/js/vendor/jquery.datetimepicker.js +1 -1
  62. wagtail/admin/static/wagtailadmin/js/vendor/jquery.fileupload-process.js +1 -1
  63. wagtail/admin/static/wagtailadmin/js/vendor/jquery.fileupload.js +1 -1
  64. wagtail/admin/static/wagtailadmin/js/vendor/jquery.iframe-transport.js +1 -1
  65. wagtail/admin/static/wagtailadmin/js/vendor/tag-it.js +1 -1
  66. wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
  67. wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +1 -1
  68. wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
  69. wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
  70. wagtail/admin/templates/wagtailadmin/account/account.html +2 -0
  71. wagtail/admin/templates/wagtailadmin/base.html +14 -0
  72. wagtail/admin/templates/wagtailadmin/generic/chooser/chooser.html +2 -1
  73. wagtail/admin/templates/wagtailadmin/generic/chooser/creation_form.html +2 -1
  74. wagtail/admin/templates/wagtailadmin/generic/form.html +3 -1
  75. wagtail/admin/templates/wagtailadmin/panels/multi_field_panel_child.html +1 -1
  76. wagtail/admin/templates/wagtailadmin/panels/object_list.html +1 -1
  77. wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +3 -2
  78. wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +1 -1
  79. wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
  80. wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +19 -0
  81. wagtail/admin/templates/wagtailadmin/shared/panel.html +1 -1
  82. wagtail/admin/templates/wagtailadmin/shared/set_privacy.html +15 -0
  83. wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +28 -1
  84. wagtail/admin/templates/wagtailadmin/shared/workflow_history/detail.html +2 -2
  85. wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_header.html → tables/ordering_header.html} +2 -2
  86. wagtail/admin/templates/wagtailadmin/tables/title_cell.html +1 -1
  87. wagtail/admin/templates/wagtailadmin/userbar/base.html +6 -3
  88. wagtail/admin/templates/wagtailadmin/userbar/item_admin.html +2 -2
  89. wagtail/admin/templates/wagtailadmin/userbar/item_page_add.html +2 -2
  90. wagtail/admin/templates/wagtailadmin/userbar/item_page_edit.html +2 -2
  91. wagtail/admin/templates/wagtailadmin/userbar/item_page_explore.html +2 -2
  92. wagtail/admin/templates/wagtailadmin/widgets/{daterange_input.html → range_input.html} +1 -1
  93. wagtail/admin/templates/wagtailadmin/workflows/task_chooser/chooser.html +4 -2
  94. wagtail/admin/templatetags/wagtailadmin_tags.py +56 -22
  95. wagtail/admin/tests/api/test_pages.py +7 -7
  96. wagtail/admin/tests/api/test_renderer_classes.py +16 -0
  97. wagtail/admin/tests/pages/test_create_page.py +34 -2
  98. wagtail/admin/tests/pages/test_edit_page.py +128 -14
  99. wagtail/admin/tests/pages/test_explorer_view.py +34 -7
  100. wagtail/admin/tests/pages/test_reorder_page.py +11 -0
  101. wagtail/admin/tests/test_collections_views.py +12 -0
  102. wagtail/admin/tests/test_edit_handlers.py +3 -3
  103. wagtail/admin/tests/test_filters.py +2 -2
  104. wagtail/admin/tests/test_keyboard_shortcuts.py +52 -2
  105. wagtail/admin/tests/test_menu.py +0 -2
  106. wagtail/admin/tests/test_privacy.py +16 -16
  107. wagtail/admin/tests/test_templatetags.py +137 -0
  108. wagtail/admin/tests/test_userbar.py +75 -35
  109. wagtail/admin/tests/test_views_generic.py +34 -0
  110. wagtail/admin/tests/test_workflows.py +34 -0
  111. wagtail/admin/tests/viewsets/test_model_viewset.py +322 -0
  112. wagtail/admin/ui/tables/orderable.py +73 -0
  113. wagtail/admin/ui/tables/pages.py +3 -13
  114. wagtail/admin/userbar.py +6 -1
  115. wagtail/admin/views/collection_privacy.py +6 -2
  116. wagtail/admin/views/generic/__init__.py +1 -0
  117. wagtail/admin/views/generic/mixins.py +20 -2
  118. wagtail/admin/views/generic/models.py +67 -1
  119. wagtail/admin/views/generic/ordering.py +79 -0
  120. wagtail/admin/views/home.py +3 -3
  121. wagtail/admin/views/page_privacy.py +5 -2
  122. wagtail/admin/views/pages/create.py +1 -1
  123. wagtail/admin/views/pages/edit.py +2 -2
  124. wagtail/admin/views/pages/listing.py +7 -42
  125. wagtail/admin/views/pages/move.py +1 -1
  126. wagtail/admin/views/pages/ordering.py +1 -1
  127. wagtail/admin/viewsets/base.py +1 -1
  128. wagtail/admin/viewsets/model.py +49 -1
  129. wagtail/admin/wagtail_hooks.py +2 -1
  130. wagtail/admin/widgets/slug.py +10 -10
  131. wagtail/api/v2/serializers.py +1 -1
  132. wagtail/api/v2/tests/test_renderer_classes.py +32 -0
  133. wagtail/apps.py +2 -0
  134. wagtail/bin/wagtail.py +1 -1
  135. wagtail/blocks/struct_block.py +2 -1
  136. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +14 -14
  137. wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.mo +0 -0
  138. wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.po +19 -2
  139. wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.mo +0 -0
  140. wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.po +18 -1
  141. wagtail/contrib/frontend_cache/tests.py +4 -2
  142. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +4 -4
  143. wagtail/contrib/redirects/tests/test_tmp_storages.py +20 -0
  144. wagtail/contrib/redirects/tmp_storages.py +1 -1
  145. wagtail/contrib/redirects/views.py +3 -3
  146. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +3 -3
  147. wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.mo +0 -0
  148. wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.po +43 -3
  149. wagtail/contrib/search_promotions/static/wagtailsearchpromotions/js/query-chooser-modal.js +1 -1
  150. wagtail/contrib/search_promotions/views/settings.py +2 -2
  151. wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.mo +0 -0
  152. wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.po +6 -1
  153. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +1 -1
  154. wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.mo +0 -0
  155. wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.po +6 -2
  156. wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.mo +0 -0
  157. wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.po +6 -1
  158. wagtail/contrib/settings/tests/site_specific/test_admin.py +40 -6
  159. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  160. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
  161. wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html +5 -5
  162. wagtail/contrib/table_block/blocks.py +1 -0
  163. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +5 -1
  164. wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
  165. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
  166. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  167. wagtail/coreutils.py +5 -5
  168. wagtail/documents/forms.py +18 -1
  169. wagtail/documents/locale/en/LC_MESSAGES/django.po +10 -10
  170. wagtail/documents/locale/nl/LC_MESSAGES/django.mo +0 -0
  171. wagtail/documents/locale/nl/LC_MESSAGES/django.po +9 -0
  172. wagtail/documents/locale/ru/LC_MESSAGES/django.mo +0 -0
  173. wagtail/documents/locale/ru/LC_MESSAGES/django.po +9 -0
  174. wagtail/documents/models.py +1 -1
  175. wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
  176. wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
  177. wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
  178. wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
  179. wagtail/documents/templates/wagtaildocs/documents/add.html +0 -34
  180. wagtail/documents/tests/test_admin_views.py +132 -26
  181. wagtail/documents/tests/test_collection_privacy.py +18 -4
  182. wagtail/documents/tests/test_form_overrides.py +1 -1
  183. wagtail/documents/tests/test_search.py +21 -8
  184. wagtail/documents/views/documents.py +1 -1
  185. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  186. wagtail/embeds/static/wagtailembeds/js/embed-chooser-modal.js +1 -1
  187. wagtail/images/forms.py +16 -1
  188. wagtail/images/locale/cs/LC_MESSAGES/django.mo +0 -0
  189. wagtail/images/locale/cs/LC_MESSAGES/django.po +12 -1
  190. wagtail/images/locale/en/LC_MESSAGES/django.po +57 -46
  191. wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
  192. wagtail/images/locale/nl/LC_MESSAGES/django.po +37 -14
  193. wagtail/images/locale/ru/LC_MESSAGES/django.mo +0 -0
  194. wagtail/images/locale/ru/LC_MESSAGES/django.po +20 -1
  195. wagtail/images/models.py +1 -1
  196. wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
  197. wagtail/images/static/wagtailimages/js/focal-point-chooser.js +1 -1
  198. wagtail/images/static/wagtailimages/js/image-block.js +1 -1
  199. wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
  200. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  201. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  202. wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
  203. wagtail/images/static/wagtailimages/js/vendor/jquery.Jcrop.min.js +1 -1
  204. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
  205. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-validate.js +1 -1
  206. wagtail/images/static/wagtailimages/js/vendor/load-image.min.js +1 -1
  207. wagtail/images/templates/wagtailimages/chooser/chooser.html +22 -13
  208. wagtail/images/templates/wagtailimages/chooser/image_preview_column_cell.html +10 -0
  209. wagtail/images/templates/wagtailimages/chooser/results.html +24 -20
  210. wagtail/images/templates/wagtailimages/chooser/title_column_cell.html +15 -0
  211. wagtail/images/templates/wagtailimages/images/add.html +0 -34
  212. wagtail/images/templates/wagtailimages/images/index.html +3 -3
  213. wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
  214. wagtail/images/templates/wagtailimages/images/layout_toggle_button.html +8 -7
  215. wagtail/images/templatetags/wagtailimages_tags.py +2 -2
  216. wagtail/images/tests/test_admin_views.py +87 -0
  217. wagtail/images/tests/test_form_overrides.py +1 -1
  218. wagtail/images/tests/test_models.py +48 -9
  219. wagtail/images/views/chooser.py +66 -2
  220. wagtail/locale/en/LC_MESSAGES/django.po +55 -55
  221. wagtail/locale/is_IS/LC_MESSAGES/django.mo +0 -0
  222. wagtail/locale/is_IS/LC_MESSAGES/django.po +3 -3
  223. wagtail/locale/nl/LC_MESSAGES/django.mo +0 -0
  224. wagtail/locale/nl/LC_MESSAGES/django.po +11 -2
  225. wagtail/locale/ru/LC_MESSAGES/django.mo +0 -0
  226. wagtail/locale/ru/LC_MESSAGES/django.po +11 -1
  227. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  228. wagtail/locales/locale/nl/LC_MESSAGES/django.mo +0 -0
  229. wagtail/locales/locale/nl/LC_MESSAGES/django.po +12 -1
  230. wagtail/locales/locale/ru/LC_MESSAGES/django.mo +0 -0
  231. wagtail/locales/locale/ru/LC_MESSAGES/django.po +10 -1
  232. wagtail/locales/views.py +2 -2
  233. wagtail/models/orderable.py +10 -0
  234. wagtail/models/pages.py +9 -11
  235. wagtail/models/sites.py +1 -1
  236. wagtail/models/workflows.py +8 -5
  237. wagtail/project_template/home/tests.py +6 -7
  238. wagtail/project_template/project_name/settings/base.py +9 -9
  239. wagtail/project_template/requirements.txt +1 -1
  240. wagtail/query.py +7 -2
  241. wagtail/rich_text/rewriters.py +1 -1
  242. wagtail/search/apps.py +4 -49
  243. wagtail/search/backends/__init__.py +1 -113
  244. wagtail/search/backends/base.py +1 -547
  245. wagtail/search/backends/database/__init__.py +1 -50
  246. wagtail/search/backends/database/fallback.py +1 -253
  247. wagtail/search/backends/database/mysql/mysql.py +1 -700
  248. wagtail/search/backends/database/mysql/query.py +1 -258
  249. wagtail/search/backends/database/postgres/postgres.py +1 -749
  250. wagtail/search/backends/database/postgres/query.py +1 -83
  251. wagtail/search/backends/database/postgres/weights.py +1 -63
  252. wagtail/search/backends/database/sqlite/query.py +1 -294
  253. wagtail/search/backends/database/sqlite/sqlite.py +1 -719
  254. wagtail/search/backends/database/sqlite/utils.py +1 -35
  255. wagtail/search/backends/deprecation.py +45 -0
  256. wagtail/search/backends/elasticsearch7.py +18 -1260
  257. wagtail/search/backends/elasticsearch8.py +21 -96
  258. wagtail/search/backends/elasticsearch9.py +35 -0
  259. wagtail/search/backends/opensearch2.py +35 -0
  260. wagtail/search/backends/opensearch3.py +35 -0
  261. wagtail/search/index.py +1 -358
  262. wagtail/search/locale/en/LC_MESSAGES/django.po +2 -10
  263. wagtail/search/management/commands/update_index.py +1 -205
  264. wagtail/search/management/commands/wagtail_update_index.py +1 -4
  265. wagtail/search/models.py +32 -158
  266. wagtail/search/query.py +1 -114
  267. wagtail/search/queryset.py +1 -43
  268. wagtail/search/signal_handlers.py +1 -24
  269. wagtail/search/tasks.py +1 -10
  270. wagtail/search/tests/test_elasticsearch.py +22 -0
  271. wagtail/search/utils.py +1 -206
  272. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  273. wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -3
  274. wagtail/snippets/locale/ru/LC_MESSAGES/django.mo +0 -0
  275. wagtail/snippets/locale/ru/LC_MESSAGES/django.po +8 -1
  276. wagtail/snippets/locale/tr/LC_MESSAGES/django.mo +0 -0
  277. wagtail/snippets/locale/tr/LC_MESSAGES/django.po +8 -1
  278. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
  279. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
  280. wagtail/snippets/tests/test_preview.py +5 -6
  281. wagtail/snippets/tests/test_reordering.py +319 -0
  282. wagtail/snippets/tests/test_snippets.py +65 -12
  283. wagtail/snippets/views/snippets.py +16 -0
  284. wagtail/test/numberformat.py +30 -0
  285. wagtail/test/settings.py +35 -12
  286. wagtail/test/testapp/fields.py +12 -0
  287. wagtail/test/testapp/migrations/0056_commentablejsonpage.py +50 -0
  288. wagtail/test/testapp/migrations/0057_featurecompletetoy_sort_order.py +23 -0
  289. wagtail/test/testapp/migrations/0058_customlocktask.py +31 -0
  290. wagtail/test/testapp/models.py +27 -0
  291. wagtail/test/testapp/urls.py +1 -0
  292. wagtail/test/testapp/views.py +18 -2
  293. wagtail/test/utils/page_tests.py +17 -17
  294. wagtail/test/utils/template_tests.py +4 -6
  295. wagtail/test/utils/wagtail_tests.py +1 -2
  296. wagtail/tests/test_blocks.py +15 -0
  297. wagtail/tests/test_page_model.py +15 -0
  298. wagtail/{search/tests → tests}/test_page_search.py +29 -2
  299. wagtail/tests/test_search_fields.py +69 -0
  300. wagtail/tests/test_tests.py +62 -6
  301. wagtail/tests/test_workflow.py +25 -1
  302. wagtail/users/locale/cs/LC_MESSAGES/django.mo +0 -0
  303. wagtail/users/locale/cs/LC_MESSAGES/django.po +3 -0
  304. wagtail/users/locale/en/LC_MESSAGES/django.po +2 -2
  305. wagtail/users/locale/nl/LC_MESSAGES/django.mo +0 -0
  306. wagtail/users/locale/nl/LC_MESSAGES/django.po +6 -3
  307. wagtail/users/locale/ru/LC_MESSAGES/django.mo +0 -0
  308. wagtail/users/locale/ru/LC_MESSAGES/django.po +5 -1
  309. wagtail/users/locale/tr/LC_MESSAGES/django.mo +0 -0
  310. wagtail/users/locale/tr/LC_MESSAGES/django.po +78 -4
  311. wagtail/users/templates/wagtailusers/users/create.html +2 -0
  312. wagtail/users/templates/wagtailusers/users/edit.html +2 -0
  313. wagtail/users/tests/test_admin_views.py +4 -0
  314. wagtail/users/views/users.py +1 -1
  315. {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/METADATA +7 -6
  316. {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/RECORD +322 -328
  317. wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +0 -13
  318. wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +0 -13
  319. wagtail/search/tests/__init__.py +0 -0
  320. wagtail/search/tests/elasticsearch_common_tests.py +0 -251
  321. wagtail/search/tests/test_backends.py +0 -1215
  322. wagtail/search/tests/test_db_backend.py +0 -62
  323. wagtail/search/tests/test_elasticsearch7_backend.py +0 -1452
  324. wagtail/search/tests/test_elasticsearch8_backend.py +0 -15
  325. wagtail/search/tests/test_index_functions.py +0 -256
  326. wagtail/search/tests/test_indexed_class.py +0 -157
  327. wagtail/search/tests/test_mysql_backend.py +0 -192
  328. wagtail/search/tests/test_postgres_backend.py +0 -210
  329. wagtail/search/tests/test_queries.py +0 -332
  330. wagtail/search/tests/test_related_fields.py +0 -102
  331. wagtail/search/tests/test_sqlite_backend.py +0 -52
  332. wagtail/test/search/__init__.py +0 -0
  333. wagtail/test/search/apps.py +0 -9
  334. wagtail/test/search/fixtures/search.json +0 -545
  335. wagtail/test/search/migrations/0001_initial.py +0 -146
  336. wagtail/test/search/migrations/0002_bookunindexed.py +0 -43
  337. wagtail/test/search/migrations/0003_book_summary.py +0 -18
  338. wagtail/test/search/migrations/__init__.py +0 -0
  339. wagtail/test/search/models.py +0 -137
  340. /wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_cell.html → tables/ordering_cell.html} +0 -0
  341. /wagtail/{search/checks.py → checks.py} +0 -0
  342. {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/WHEEL +0 -0
  343. {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/entry_points.txt +0 -0
  344. {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/licenses/LICENSE +0 -0
  345. {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,319 @@
1
+ from django.contrib.admin.utils import quote
2
+ from django.contrib.auth import get_permission_codename
3
+ from django.contrib.auth.models import Permission
4
+ from django.test import TestCase
5
+ from django.urls import reverse
6
+
7
+ from wagtail.test.testapp.models import FullFeaturedSnippet
8
+ from wagtail.test.utils import WagtailTestUtils
9
+
10
+
11
+ class TestIndexViewReordering(WagtailTestUtils, TestCase):
12
+ def setUp(self):
13
+ self.user = self.login()
14
+ # This model extends Orderable, thus it has a sort_order_field on the model
15
+ # and we don't need to set it on the viewset.
16
+ self.obj1 = FullFeaturedSnippet.objects.create(text="Toy 1", sort_order=0)
17
+ self.obj2 = FullFeaturedSnippet.objects.create(text="Toy 2", sort_order=1)
18
+ self.obj3 = FullFeaturedSnippet.objects.create(text="Toy 3", sort_order=2)
19
+
20
+ def get_url_name(self, name):
21
+ return FullFeaturedSnippet.snippet_viewset.get_url_name(name)
22
+
23
+ def test_header_button_rendered(self):
24
+ index_url = reverse(self.get_url_name("list"))
25
+ custom_ordering_url = index_url + "?ordering=sort_order"
26
+ response = self.client.get(index_url)
27
+ self.assertEqual(response.status_code, 200)
28
+ soup = self.get_soup(response.content)
29
+ button = soup.select_one(
30
+ f".w-slim-header .w-dropdown a[href='{custom_ordering_url}']"
31
+ )
32
+ self.assertIsNotNone(button)
33
+ self.assertEqual(button.text.strip(), "Sort item order")
34
+
35
+ # Reordering feature disabled when not sorting by sort_order,
36
+ # the bulk actions column is present instead
37
+ table = soup.select_one("main table")
38
+ self.assertIsNotNone(table)
39
+ self.assertFalse(table.get("data-controller"))
40
+ bulk_actions_all = soup.select_one(
41
+ "main thead th:first-child input[type='checkbox']"
42
+ )
43
+ self.assertIsNotNone(bulk_actions_all)
44
+ self.assertTrue(
45
+ bulk_actions_all.has_attr("data-bulk-action-select-all-checkbox")
46
+ )
47
+
48
+ def test_show_ordering_column(self):
49
+ index_url = reverse(self.get_url_name("list"))
50
+ custom_ordering_url = index_url + "?ordering=sort_order"
51
+ response = self.client.get(custom_ordering_url)
52
+ self.assertEqual(response.status_code, 200)
53
+ soup = self.get_soup(response.content)
54
+
55
+ # The table should have the w-orderable controller
56
+ table = soup.select_one("main table")
57
+ self.assertIsNotNone(table)
58
+ self.assertEqual(table.get("data-controller"), "w-orderable")
59
+ self.assertEqual(
60
+ table.get("data-w-orderable-message-value"),
61
+ "'__LABEL__' has been moved successfully.",
62
+ )
63
+ self.assertEqual(
64
+ table.get("data-w-orderable-url-value"),
65
+ reverse(self.get_url_name("reorder"), args=[999999]),
66
+ )
67
+
68
+ # The bulk actions column should not be present
69
+ bulk_actions_all = table.select_one(
70
+ "thead th:first-child input[type='checkbox']"
71
+ )
72
+ self.assertIsNone(bulk_actions_all)
73
+
74
+ # The ordering column added as the first column
75
+ first_th = table.select_one("thead th:first-child")
76
+ self.assertIsNotNone(first_th)
77
+ self.assertEqual(first_th.text.strip(), "Sort")
78
+
79
+ # All rows have the corresponding attributes for reordering
80
+ rows = table.select("tbody tr")
81
+ self.assertEqual(len(rows), 3)
82
+ expected = [
83
+ {
84
+ "id": f"item_{quote(obj.pk)}",
85
+ "data-w-orderable-item-id": str(quote(obj.pk)),
86
+ "data-w-orderable-item-label": str(obj),
87
+ "data-w-orderable-target": "item",
88
+ }
89
+ for obj in [self.obj1, self.obj2, self.obj3]
90
+ ]
91
+ for row, expected_attrs in zip(rows, expected):
92
+ for attr, value in expected_attrs.items():
93
+ self.assertEqual(row.get(attr), value)
94
+ handle = row.select_one("td button[data-w-orderable-target='handle']")
95
+ self.assertIsNotNone(handle)
96
+
97
+ def test_reordering_disabled_with_insufficient_permission(self):
98
+ self.user.is_superuser = False
99
+ self.user.save()
100
+ admin_permission = Permission.objects.get(
101
+ content_type__app_label="wagtailadmin", codename="access_admin"
102
+ )
103
+ view_permission = Permission.objects.get(
104
+ content_type__app_label=self.obj1._meta.app_label,
105
+ codename=get_permission_codename("view", self.obj1._meta),
106
+ )
107
+ # Even with `change` permission, the reordering feature is not enabled
108
+ # because the model uses DraftStateMixin
109
+ change_permission = Permission.objects.get(
110
+ content_type__app_label=self.obj1._meta.app_label,
111
+ codename=get_permission_codename("change", self.obj1._meta),
112
+ )
113
+ self.user.user_permissions.add(
114
+ admin_permission, view_permission, change_permission
115
+ )
116
+
117
+ index_url = reverse(self.get_url_name("list"))
118
+ custom_ordering_url = index_url + "?ordering=sort_order"
119
+ response = self.client.get(custom_ordering_url)
120
+ self.assertEqual(response.status_code, 200)
121
+ soup = self.get_soup(response.content)
122
+
123
+ # Header button for enabling reordering should not be rendered
124
+ button = soup.select_one(
125
+ f".w-slim-header .w-dropdown a[href='{custom_ordering_url}']"
126
+ )
127
+ self.assertIsNone(button)
128
+
129
+ # Reordering feature not enabled
130
+ table = soup.select_one("main table")
131
+ self.assertIsNotNone(table)
132
+ self.assertFalse(table.get("data-controller"))
133
+ bulk_actions_all = soup.select_one(
134
+ "main thead th:first-child input[type='checkbox']"
135
+ )
136
+ self.assertIsNotNone(bulk_actions_all)
137
+ self.assertTrue(
138
+ bulk_actions_all.has_attr("data-bulk-action-select-all-checkbox")
139
+ )
140
+
141
+ def test_minimal_permission(self):
142
+ self.user.is_superuser = False
143
+ self.user.save()
144
+ admin_permission = Permission.objects.get(
145
+ content_type__app_label="wagtailadmin", codename="access_admin"
146
+ )
147
+ change_permission = Permission.objects.get(
148
+ content_type__app_label=self.obj1._meta.app_label,
149
+ codename=get_permission_codename("change", self.obj1._meta),
150
+ )
151
+ publish_permission = Permission.objects.get(
152
+ content_type__app_label=self.obj1._meta.app_label,
153
+ codename=get_permission_codename("publish", self.obj1._meta),
154
+ )
155
+ self.user.user_permissions.add(
156
+ admin_permission,
157
+ change_permission,
158
+ publish_permission,
159
+ )
160
+
161
+ self.test_header_button_rendered()
162
+ self.test_show_ordering_column()
163
+
164
+
165
+ class TestCreateViewReordering(WagtailTestUtils, TestCase):
166
+ def setUp(self):
167
+ self.user = self.login()
168
+ FullFeaturedSnippet.objects.create(text="Toy 1", sort_order=0)
169
+ FullFeaturedSnippet.objects.create(text="Toy 2", sort_order=1)
170
+ FullFeaturedSnippet.objects.create(text="Toy 3", sort_order=2)
171
+
172
+ def test_create_sets_max_sort_order(self):
173
+ response = self.client.post(
174
+ reverse(FullFeaturedSnippet.snippet_viewset.get_url_name("add")),
175
+ data={"text": "New Toy"},
176
+ )
177
+ new_toy = FullFeaturedSnippet.objects.get(text="New Toy")
178
+ self.assertRedirects(
179
+ response,
180
+ reverse(
181
+ FullFeaturedSnippet.snippet_viewset.get_url_name("edit"),
182
+ args=(quote(new_toy.pk),),
183
+ ),
184
+ )
185
+ new_toy = FullFeaturedSnippet.objects.get(text="New Toy")
186
+ self.assertEqual(new_toy.sort_order, 3)
187
+
188
+
189
+ class TestReorderView(WagtailTestUtils, TestCase):
190
+ def setUp(self):
191
+ self.user = self.login()
192
+ # We don't do any normalization, so the sort_order values may not be
193
+ # consecutive integers (e.g. after an item is deleted), and the update
194
+ # logic may cause the sort_order values to be negative or larger than
195
+ # the number of items in the queryset.
196
+ self.obj1 = FullFeaturedSnippet.objects.create(text="Toy 1", sort_order=0)
197
+ self.obj2 = FullFeaturedSnippet.objects.create(text="Toy 2", sort_order=1)
198
+ self.obj3 = FullFeaturedSnippet.objects.create(text="Toy 3", sort_order=2)
199
+
200
+ def get_url(self, obj):
201
+ return reverse(
202
+ FullFeaturedSnippet.snippet_viewset.get_url_name("reorder"),
203
+ args=(quote(obj.pk),),
204
+ )
205
+
206
+ def assertOrder(self, objs):
207
+ self.assertSequenceEqual(
208
+ [
209
+ (obj, obj.sort_order)
210
+ for obj in FullFeaturedSnippet.objects.order_by("sort_order")
211
+ ],
212
+ objs,
213
+ )
214
+
215
+ def test_get_request_does_not_alter_order(self):
216
+ response = self.client.get(self.get_url(self.obj1))
217
+ self.assertEqual(response.status_code, 405)
218
+
219
+ # Ensure item order does not change
220
+ self.assertOrder([(self.obj1, 0), (self.obj2, 1), (self.obj3, 2)])
221
+
222
+ def test_post_request_without_position_argument_moves_to_the_end(self):
223
+ response = self.client.post(self.get_url(self.obj1))
224
+ self.assertEqual(response.status_code, 200)
225
+
226
+ # The item will be moved to the last position by taking the sort_order
227
+ # of the last item, and the sort_order of the other items updated by -1
228
+ self.assertOrder([(self.obj2, 0), (self.obj3, 1), (self.obj1, 2)])
229
+
230
+ def test_post_request_with_non_integer_position_moves_to_the_end(self):
231
+ response = self.client.post(self.get_url(self.obj1) + "?position=good")
232
+ self.assertEqual(response.status_code, 200)
233
+
234
+ # The item will be moved to the last position by taking the sort_order
235
+ # of the last item, and the sort_order of the other items updated by -1
236
+ self.assertOrder([(self.obj2, 0), (self.obj3, 1), (self.obj1, 2)])
237
+
238
+ def test_move_position_up(self):
239
+ # Move obj3 to the first position
240
+ response = self.client.post(self.get_url(self.obj3) + "?position=0")
241
+ self.assertEqual(response.status_code, 200)
242
+
243
+ # Check if obj3 is now the first item by taking obj1's sort_order and
244
+ # incrementing sort_order of the other items after it (but before obj3's
245
+ # old position) by 1
246
+ self.assertOrder([(self.obj3, 0), (self.obj1, 1), (self.obj2, 2)])
247
+
248
+ def test_move_position_down(self):
249
+ # Move obj1 to the second position
250
+ response = self.client.post(self.get_url(self.obj1) + "?position=1")
251
+ self.assertEqual(response.status_code, 200)
252
+
253
+ # Check if obj1 is now the second item by taking obj2's sort_order
254
+ # and decreasing sort_order of the other items before it by 1
255
+ self.assertOrder([(self.obj2, 0), (self.obj1, 1), (self.obj3, 2)])
256
+
257
+ def test_move_position_to_same_position(self):
258
+ # Move obj1 to position 0 (where it already is)
259
+ response = self.client.post(self.get_url(self.obj1) + "?position=0")
260
+ self.assertEqual(response.status_code, 200)
261
+
262
+ # Ensure item order does not change
263
+ self.assertOrder([(self.obj1, 0), (self.obj2, 1), (self.obj3, 2)])
264
+
265
+ def test_move_position_with_invalid_target_position(self):
266
+ response = self.client.post(self.get_url(self.obj1) + "?position=99")
267
+ self.assertEqual(response.status_code, 200)
268
+
269
+ # The item will be moved to the last position by taking the sort_order
270
+ # of the last item, and the sort_order of the other items updated by -1
271
+ self.assertOrder([(self.obj2, 0), (self.obj3, 1), (self.obj1, 2)])
272
+
273
+ def test_insufficient_permission(self):
274
+ self.user.is_superuser = False
275
+ self.user.save()
276
+ admin_permission = Permission.objects.get(
277
+ content_type__app_label="wagtailadmin", codename="access_admin"
278
+ )
279
+ view_permission = Permission.objects.get(
280
+ content_type__app_label=self.obj1._meta.app_label,
281
+ codename=get_permission_codename("view", self.obj1._meta),
282
+ )
283
+ self.user.user_permissions.add(admin_permission, view_permission)
284
+
285
+ response = self.client.post(self.get_url(self.obj1) + "?position=1")
286
+ self.assertEqual(response.status_code, 302)
287
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
288
+ self.assertOrder([(self.obj1, 0), (self.obj2, 1), (self.obj3, 2)])
289
+
290
+ # `change` permission is not enough if the model uses DraftStateMixin
291
+ change_permission = Permission.objects.get(
292
+ content_type__app_label=self.obj1._meta.app_label,
293
+ codename=get_permission_codename("change", self.obj1._meta),
294
+ )
295
+ self.user.user_permissions.add(admin_permission, change_permission)
296
+
297
+ response = self.client.post(self.get_url(self.obj1) + "?position=1")
298
+ self.assertEqual(response.status_code, 302)
299
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
300
+ self.assertOrder([(self.obj1, 0), (self.obj2, 1), (self.obj3, 2)])
301
+
302
+ def test_minimal_permission(self):
303
+ self.user.is_superuser = False
304
+ self.user.save()
305
+ admin_permission = Permission.objects.get(
306
+ content_type__app_label="wagtailadmin", codename="access_admin"
307
+ )
308
+ publish_permission = Permission.objects.get(
309
+ content_type__app_label=self.obj1._meta.app_label,
310
+ codename=get_permission_codename("publish", self.obj1._meta),
311
+ )
312
+ self.user.user_permissions.add(admin_permission, publish_permission)
313
+
314
+ response = self.client.post(self.get_url(self.obj1) + "?position=1")
315
+ self.assertEqual(response.status_code, 200)
316
+
317
+ # Check if obj1 is now the second item by taking obj2's sort_order
318
+ # and decrementing sort_order of the other items before it by 1
319
+ self.assertOrder([(self.obj2, 0), (self.obj1, 1), (self.obj3, 2)])
@@ -999,12 +999,29 @@ class TestSnippetCreateView(WagtailTestUtils, TestCase):
999
999
 
1000
1000
  def test_create_invalid(self):
1001
1001
  response = self.post(post_data={"foo": "bar"})
1002
- self.assertContains(response, "The advert could not be created due to errors.")
1003
- self.assertContains(response, "error-message", count=1)
1004
- self.assertContains(response, "This field is required", count=1)
1005
1002
 
1006
1003
  soup = self.get_soup(response.content)
1007
1004
 
1005
+ header_messages = soup.css.select(".messages[role='status'] ul > li")
1006
+
1007
+ # there should be one header message that indicates the issue and has a go to error button
1008
+ self.assertEqual(len(header_messages), 1)
1009
+ message = header_messages[0]
1010
+ self.assertIn(
1011
+ "The advert could not be created due to errors.", message.get_text()
1012
+ )
1013
+ buttons = message.find_all("button")
1014
+ self.assertEqual(len(buttons), 1)
1015
+ self.assertEqual(buttons[0].attrs["data-controller"], "w-count w-focus")
1016
+ self.assertIn("Go to the first error", buttons[0].get_text())
1017
+
1018
+ # field specific error should be shown
1019
+ error_messages = soup.css.select(".error-message")
1020
+ self.assertEqual(len(error_messages), 1)
1021
+ error_message = error_messages[0]
1022
+ self.assertEqual(error_message.parent["id"], "panel-child-text-errors")
1023
+ self.assertIn("This field is required", error_message.get_text())
1024
+
1008
1025
  # Should have the unsaved controller set up
1009
1026
  editor_form = soup.select_one("#w-editor-form")
1010
1027
  self.assertIsNotNone(editor_form)
@@ -1773,7 +1790,7 @@ class TestCreateDraftStateSnippet(WagtailTestUtils, TestCase):
1773
1790
  self.assertFormError(
1774
1791
  response.context["form"],
1775
1792
  "expire_at",
1776
- "Expiry date/time must be in the future",
1793
+ "Expiry date/time must be in the future.",
1777
1794
  )
1778
1795
 
1779
1796
  self.assertContains(
@@ -1992,12 +2009,30 @@ class TestSnippetEditView(BaseTestSnippetEditView):
1992
2009
 
1993
2010
  def test_edit_invalid(self):
1994
2011
  response = self.post(post_data={"foo": "bar"})
1995
- self.assertContains(response, "The advert could not be saved due to errors.")
1996
- self.assertContains(response, "error-message", count=1)
1997
- self.assertContains(response, "This field is required", count=1)
1998
-
1999
2012
  soup = self.get_soup(response.content)
2000
2013
 
2014
+ header_messages = soup.css.select(".messages[role='status'] ul > li")
2015
+
2016
+ # the top level message should indicate that the page could not be saved
2017
+ self.assertEqual(len(header_messages), 1)
2018
+ message = header_messages[0]
2019
+ self.assertIn(
2020
+ "The advert could not be saved due to errors.", message.get_text()
2021
+ )
2022
+
2023
+ # the top level message should provide a go to error button
2024
+ buttons = message.find_all("button")
2025
+ self.assertEqual(len(buttons), 1)
2026
+ self.assertEqual(buttons[0].attrs["data-controller"], "w-count w-focus")
2027
+ self.assertIn("Go to the first error", buttons[0].get_text())
2028
+
2029
+ # the error should only appear once: against the field, not in the header message
2030
+ error_messages = soup.css.select(".error-message")
2031
+ self.assertEqual(len(error_messages), 1)
2032
+ error_message = error_messages[0]
2033
+ self.assertEqual(error_message.parent["id"], "panel-child-text-errors")
2034
+ self.assertIn("This field is required", error_message.get_text())
2035
+
2001
2036
  # Should have the unsaved controller set up
2002
2037
  editor_form = soup.select_one("#w-editor-form")
2003
2038
  self.assertIsNotNone(editor_form)
@@ -3086,7 +3121,7 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
3086
3121
  self.assertFormError(
3087
3122
  response.context["form"],
3088
3123
  "expire_at",
3089
- "Expiry date/time must be in the future",
3124
+ "Expiry date/time must be in the future.",
3090
3125
  )
3091
3126
 
3092
3127
  self.assertContains(
@@ -5972,11 +6007,29 @@ class TestSnippetViewWithCustomPrimaryKey(WagtailTestUtils, TestCase):
5972
6007
 
5973
6008
  def test_edit_invalid(self):
5974
6009
  response = self.post(self.snippet_a, post_data={"foo": "bar"})
5975
- self.assertContains(
5976
- response,
6010
+ soup = self.get_soup(response.content)
6011
+ header_messages = soup.css.select(".messages[role='status'] ul > li")
6012
+
6013
+ # the top level message should indicate that the page could not be saved
6014
+ self.assertEqual(len(header_messages), 1)
6015
+ message = header_messages[0]
6016
+ self.assertIn(
5977
6017
  "The standard snippet with custom primary key could not be saved due to errors.",
6018
+ message.get_text(),
5978
6019
  )
5979
- self.assertContains(response, "This field is required.")
6020
+
6021
+ # the top level message should provide a go to error button
6022
+ buttons = message.find_all("button")
6023
+ self.assertEqual(len(buttons), 1)
6024
+ self.assertEqual(buttons[0].attrs["data-controller"], "w-count w-focus")
6025
+ self.assertIn("Go to the first error", buttons[0].get_text())
6026
+
6027
+ # the errors should appear against the fields with issues
6028
+ error_messages = soup.css.select(".error-message")
6029
+ self.assertEqual(len(error_messages), 2)
6030
+ error_message = error_messages[0]
6031
+ self.assertEqual(error_message.parent["id"], "panel-child-snippet_id-errors")
6032
+ self.assertIn("This field is required", error_message.get_text())
5980
6033
 
5981
6034
  def test_edit(self):
5982
6035
  response = self.post(
@@ -444,6 +444,14 @@ class WorkflowHistoryDetailView(
444
444
  permission_required = "change"
445
445
 
446
446
 
447
+ class ReorderView(generic.ReorderView):
448
+ @cached_property
449
+ def permission_required(self):
450
+ if issubclass(self.model, DraftStateMixin):
451
+ return "publish"
452
+ return super().permission_required
453
+
454
+
447
455
  class SnippetViewSet(ModelViewSet):
448
456
  """
449
457
  A viewset that instantiates the admin views for snippets.
@@ -505,6 +513,9 @@ class SnippetViewSet(ModelViewSet):
505
513
  #: The view class to use for the inspect view; must be a subclass of ``wagtail.snippets.views.snippets.InspectView``.
506
514
  inspect_view_class = InspectView
507
515
 
516
+ #: The view class to use for the reorder view; must be a subclass of ``wagtail.snippets.views.snippets.ReorderView``.
517
+ reorder_view_class = ReorderView
518
+
508
519
  #: The view class to use for previewing revisions; must be a subclass of ``wagtail.snippets.views.snippets.PreviewRevisionView``.
509
520
  revisions_view_class = PreviewRevisionView
510
521
 
@@ -1048,6 +1059,11 @@ class SnippetViewSet(ModelViewSet):
1048
1059
  ),
1049
1060
  ]
1050
1061
 
1062
+ if self.reorder_view_enabled:
1063
+ urlpatterns += [
1064
+ path("reorder/<str:pk>/", self.reorder_view, name="reorder")
1065
+ ]
1066
+
1051
1067
  if self.copy_view_enabled:
1052
1068
  urlpatterns += [path("copy/<str:pk>/", self.copy_view, name="copy")]
1053
1069
 
@@ -4,9 +4,11 @@
4
4
  # USE_THOUSAND_SEPARATOR = True incorrectly reformats numbers that are not intended to be
5
5
  # human-readable (such as image dimensions, or IDs within data attributes).
6
6
 
7
+ from contextlib import contextmanager
7
8
  from decimal import Decimal
8
9
 
9
10
  import django.contrib.humanize.templatetags.humanize
11
+ import django.template.base
10
12
  import django.template.defaultfilters
11
13
  import django.templatetags.l10n
12
14
  import django.utils.numberformat
@@ -17,6 +19,34 @@ from django.utils.translation import gettext, ngettext
17
19
 
18
20
  original_numberformat = django.utils.numberformat.format
19
21
  original_intcomma = django.contrib.humanize.templatetags.humanize.intcomma
22
+ OriginalVariableNode = django.template.base.VariableNode
23
+
24
+
25
+ @contextmanager
26
+ def ignore_numberformat(template_names):
27
+ class VariableNode(OriginalVariableNode):
28
+ def render(self, context):
29
+ template_name = context.render_context.template.origin.template_name
30
+ # If the template being rendered is in the list of templates to ignore,
31
+ # restore the original numberformat function so the template can render
32
+ # normally, otherwise reinforce the patch as there might be multiple
33
+ # templates being rendered in the lifespan of the contextmanager.
34
+ if template_name in template_names:
35
+ django.utils.numberformat.format = original_numberformat
36
+ else:
37
+ django.utils.numberformat.format = patched_numberformat
38
+
39
+ return super().render(context)
40
+
41
+ # Within the contextmanager, use a VariableNode class that conditionally
42
+ # enforces the numberformat patch
43
+ django.template.base.VariableNode = VariableNode
44
+
45
+ try:
46
+ yield
47
+ finally:
48
+ # Restore VariableNode to always enforce the numberformat patch
49
+ django.template.base.VariableNode = OriginalVariableNode
20
50
 
21
51
 
22
52
  def patched_numberformat(*args, use_l10n=None, **kwargs):
wagtail/test/settings.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
3
  from django.contrib.messages import constants as message_constants
4
+ from django.core.exceptions import ImproperlyConfigured
4
5
  from django.utils.translation import gettext_lazy as _
5
6
 
6
7
  from wagtail.test.numberformat import patch_number_formats
@@ -151,7 +152,6 @@ INSTALLED_APPS = [
151
152
  "wagtail.test.demosite",
152
153
  "wagtail.test.snippets",
153
154
  "wagtail.test.routablepage",
154
- "wagtail.test.search",
155
155
  "wagtail.test.i18n",
156
156
  "wagtail.test.streamfield_migrations",
157
157
  "wagtail.contrib.simple_translation",
@@ -229,25 +229,48 @@ else:
229
229
 
230
230
  if os.environ.get("DATABASE_ENGINE") == "django.db.backends.postgresql":
231
231
  INSTALLED_APPS.append("django.contrib.postgres")
232
- WAGTAILSEARCH_BACKENDS["postgresql"] = {
233
- "BACKEND": "wagtail.search.backends.database",
234
- "AUTO_UPDATE": False,
235
- "SEARCH_CONFIG": "english",
236
- }
237
232
 
238
- if "ELASTICSEARCH_URL" in os.environ:
239
- if os.environ.get("ELASTICSEARCH_VERSION") == "8":
240
- backend = "wagtail.search.backends.elasticsearch8"
241
- elif os.environ.get("ELASTICSEARCH_VERSION") == "7":
242
- backend = "wagtail.search.backends.elasticsearch7"
233
+ # Tests in wagtail.tests.test_page_search.PageSearchTests will be run against each backend defined
234
+ # in WAGTAILSEARCH_BACKENDS. Define an additional one to test the FTS-enabled backend for the
235
+ # currently active database.
236
+
237
+ WAGTAILSEARCH_BACKENDS["database"] = {
238
+ "BACKEND": "wagtail.search.backends.database",
239
+ "AUTO_UPDATE": False,
240
+ "SEARCH_CONFIG": "english",
241
+ }
243
242
 
244
- WAGTAILSEARCH_BACKENDS["elasticsearch"] = {
243
+ if "ELASTICSEARCH_URL" in os.environ:
244
+ # Define an 'elasticsearch' backend; along with wagtail.tests.test_page_search.PageSearchTests
245
+ # this is also used for the Elasticsearch-specific tests in wagtail.images.tests.test_models and
246
+ # wagtail.documents.tests.test_search. We also want to run these tests under Opensearch; for
247
+ # simplicity we use the backend name 'elasticsearch' in this case too.
248
+ if elasticsearch_version := os.environ.get("ELASTICSEARCH_VERSION"):
249
+ backend = f"wagtail.search.backends.elasticsearch{elasticsearch_version}"
250
+ elif opensearch_version := os.environ.get("OPENSEARCH_VERSION"):
251
+ backend = f"wagtail.search.backends.opensearch{opensearch_version}"
252
+ else:
253
+ raise ImproperlyConfigured(
254
+ "If ELASTICSEARCH_URL is defined, either ELASTICSEARCH_VERSION or OPENSEARCH_VERSION must be defined too"
255
+ )
256
+
257
+ elasticsearch_opts = {
245
258
  "BACKEND": backend,
246
259
  "URLS": [os.environ["ELASTICSEARCH_URL"]],
247
260
  "TIMEOUT": 10,
248
261
  "max_retries": 1,
249
262
  "AUTO_UPDATE": False,
250
263
  "INDEX_SETTINGS": {"settings": {"index": {"number_of_shards": 1}}},
264
+ "OPTIONS": {
265
+ "ca_certs": os.environ.get("ELASTICSEARCH_CA_CERTS"),
266
+ },
267
+ }
268
+ WAGTAILSEARCH_BACKENDS["elasticsearch"] = elasticsearch_opts
269
+
270
+ # RemovedInWagtail80Warning
271
+ WAGTAILSEARCH_BACKENDS["elasticsearch_with_index_option"] = {
272
+ **elasticsearch_opts,
273
+ "INDEX": "wagtailtest", # Deprecated option
251
274
  }
252
275
 
253
276
 
@@ -0,0 +1,12 @@
1
+ from django.db import models
2
+
3
+ from wagtail.coreutils import multigetattr
4
+
5
+
6
+ class CommentableJSONField(models.JSONField):
7
+ def get_block_by_content_path(self, value, path_elements):
8
+ try:
9
+ multigetattr(value, ".".join(path_elements))
10
+ except AttributeError:
11
+ return False
12
+ return True
@@ -0,0 +1,50 @@
1
+ # Generated by Django 5.2.5 on 2025-08-14 19:05
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+ import wagtail.fields
7
+ import wagtail.test.testapp.fields
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ dependencies = [
13
+ ("tests", "0055_previewable_settings"),
14
+ ("wagtailcore", "0095_groupsitepermission"),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name="CommentableJSONPage",
20
+ fields=[
21
+ (
22
+ "page_ptr",
23
+ models.OneToOneField(
24
+ auto_created=True,
25
+ on_delete=django.db.models.deletion.CASCADE,
26
+ parent_link=True,
27
+ primary_key=True,
28
+ serialize=False,
29
+ to="wagtailcore.page",
30
+ ),
31
+ ),
32
+ (
33
+ "commentable_body",
34
+ wagtail.test.testapp.fields.CommentableJSONField(),
35
+ ),
36
+ ("uncommentable_body", models.JSONField()),
37
+ (
38
+ "stream_body",
39
+ wagtail.fields.StreamField(
40
+ [("text", 0)],
41
+ block_lookup={0: ("wagtail.blocks.CharBlock", (), {})},
42
+ ),
43
+ ),
44
+ ],
45
+ options={
46
+ "abstract": False,
47
+ },
48
+ bases=("wagtailcore.page",),
49
+ ),
50
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.1.6 on 2025-03-08 13:10
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("tests", "0056_commentablejsonpage"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="featurecompletetoy",
15
+ name="sort_order",
16
+ field=models.IntegerField(blank=True, null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name="fullfeaturedsnippet",
20
+ name="sort_order",
21
+ field=models.IntegerField(blank=True, editable=False, null=True),
22
+ ),
23
+ ]