wagtail 7.1.2__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 (332) 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/panels/multi_field_panel_child.html +1 -1
  75. wagtail/admin/templates/wagtailadmin/panels/object_list.html +1 -1
  76. wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +3 -2
  77. wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +1 -1
  78. wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
  79. wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +19 -0
  80. wagtail/admin/templates/wagtailadmin/shared/panel.html +1 -1
  81. wagtail/admin/templates/wagtailadmin/shared/set_privacy.html +15 -0
  82. wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +28 -1
  83. wagtail/admin/templates/wagtailadmin/shared/workflow_history/detail.html +2 -2
  84. wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_header.html → tables/ordering_header.html} +2 -2
  85. wagtail/admin/templates/wagtailadmin/tables/title_cell.html +1 -1
  86. wagtail/admin/templates/wagtailadmin/widgets/{daterange_input.html → range_input.html} +1 -1
  87. wagtail/admin/templates/wagtailadmin/workflows/task_chooser/chooser.html +4 -2
  88. wagtail/admin/templatetags/wagtailadmin_tags.py +41 -22
  89. wagtail/admin/tests/api/test_pages.py +7 -7
  90. wagtail/admin/tests/api/test_renderer_classes.py +16 -0
  91. wagtail/admin/tests/pages/test_create_page.py +34 -2
  92. wagtail/admin/tests/pages/test_edit_page.py +128 -14
  93. wagtail/admin/tests/pages/test_explorer_view.py +34 -7
  94. wagtail/admin/tests/pages/test_reorder_page.py +11 -0
  95. wagtail/admin/tests/test_collections_views.py +12 -0
  96. wagtail/admin/tests/test_edit_handlers.py +3 -3
  97. wagtail/admin/tests/test_filters.py +2 -2
  98. wagtail/admin/tests/test_keyboard_shortcuts.py +52 -2
  99. wagtail/admin/tests/test_menu.py +0 -2
  100. wagtail/admin/tests/test_privacy.py +16 -16
  101. wagtail/admin/tests/test_templatetags.py +137 -0
  102. wagtail/admin/tests/test_workflows.py +34 -0
  103. wagtail/admin/tests/viewsets/test_model_viewset.py +322 -0
  104. wagtail/admin/ui/tables/orderable.py +73 -0
  105. wagtail/admin/ui/tables/pages.py +3 -13
  106. wagtail/admin/views/collection_privacy.py +6 -2
  107. wagtail/admin/views/generic/__init__.py +1 -0
  108. wagtail/admin/views/generic/mixins.py +20 -2
  109. wagtail/admin/views/generic/models.py +67 -1
  110. wagtail/admin/views/generic/ordering.py +79 -0
  111. wagtail/admin/views/home.py +3 -3
  112. wagtail/admin/views/page_privacy.py +5 -2
  113. wagtail/admin/views/pages/create.py +1 -1
  114. wagtail/admin/views/pages/edit.py +2 -2
  115. wagtail/admin/views/pages/listing.py +7 -42
  116. wagtail/admin/views/pages/move.py +1 -1
  117. wagtail/admin/views/pages/ordering.py +1 -1
  118. wagtail/admin/viewsets/base.py +1 -1
  119. wagtail/admin/viewsets/model.py +49 -1
  120. wagtail/admin/wagtail_hooks.py +2 -1
  121. wagtail/admin/widgets/slug.py +10 -10
  122. wagtail/api/v2/serializers.py +1 -1
  123. wagtail/api/v2/tests/test_renderer_classes.py +32 -0
  124. wagtail/apps.py +2 -0
  125. wagtail/bin/wagtail.py +1 -1
  126. wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +14 -14
  127. wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.mo +0 -0
  128. wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.po +19 -2
  129. wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.mo +0 -0
  130. wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.po +18 -1
  131. wagtail/contrib/frontend_cache/tests.py +4 -2
  132. wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +4 -4
  133. wagtail/contrib/redirects/tests/test_tmp_storages.py +20 -0
  134. wagtail/contrib/redirects/tmp_storages.py +1 -1
  135. wagtail/contrib/redirects/views.py +3 -3
  136. wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +3 -3
  137. wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.mo +0 -0
  138. wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.po +43 -3
  139. wagtail/contrib/search_promotions/static/wagtailsearchpromotions/js/query-chooser-modal.js +1 -1
  140. wagtail/contrib/search_promotions/views/settings.py +2 -2
  141. wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.mo +0 -0
  142. wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.po +6 -1
  143. wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +1 -1
  144. wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.mo +0 -0
  145. wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.po +6 -2
  146. wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.mo +0 -0
  147. wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.po +6 -1
  148. wagtail/contrib/settings/tests/site_specific/test_admin.py +40 -6
  149. wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
  150. wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
  151. wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html +5 -5
  152. wagtail/contrib/table_block/blocks.py +1 -0
  153. wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +5 -1
  154. wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
  155. wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
  156. wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
  157. wagtail/coreutils.py +5 -5
  158. wagtail/documents/forms.py +18 -1
  159. wagtail/documents/locale/en/LC_MESSAGES/django.po +10 -10
  160. wagtail/documents/locale/nl/LC_MESSAGES/django.mo +0 -0
  161. wagtail/documents/locale/nl/LC_MESSAGES/django.po +9 -0
  162. wagtail/documents/locale/ru/LC_MESSAGES/django.mo +0 -0
  163. wagtail/documents/locale/ru/LC_MESSAGES/django.po +9 -0
  164. wagtail/documents/models.py +1 -1
  165. wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
  166. wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
  167. wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
  168. wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
  169. wagtail/documents/templates/wagtaildocs/documents/add.html +0 -34
  170. wagtail/documents/tests/test_admin_views.py +132 -26
  171. wagtail/documents/tests/test_collection_privacy.py +18 -4
  172. wagtail/documents/tests/test_form_overrides.py +1 -1
  173. wagtail/documents/tests/test_search.py +21 -8
  174. wagtail/documents/views/documents.py +1 -1
  175. wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
  176. wagtail/embeds/static/wagtailembeds/js/embed-chooser-modal.js +1 -1
  177. wagtail/images/forms.py +16 -1
  178. wagtail/images/locale/cs/LC_MESSAGES/django.mo +0 -0
  179. wagtail/images/locale/cs/LC_MESSAGES/django.po +12 -1
  180. wagtail/images/locale/en/LC_MESSAGES/django.po +57 -46
  181. wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
  182. wagtail/images/locale/nl/LC_MESSAGES/django.po +37 -14
  183. wagtail/images/locale/ru/LC_MESSAGES/django.mo +0 -0
  184. wagtail/images/locale/ru/LC_MESSAGES/django.po +20 -1
  185. wagtail/images/models.py +1 -1
  186. wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
  187. wagtail/images/static/wagtailimages/js/focal-point-chooser.js +1 -1
  188. wagtail/images/static/wagtailimages/js/image-block.js +1 -1
  189. wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
  190. wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
  191. wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
  192. wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
  193. wagtail/images/static/wagtailimages/js/vendor/jquery.Jcrop.min.js +1 -1
  194. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
  195. wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-validate.js +1 -1
  196. wagtail/images/static/wagtailimages/js/vendor/load-image.min.js +1 -1
  197. wagtail/images/templates/wagtailimages/chooser/chooser.html +22 -13
  198. wagtail/images/templates/wagtailimages/chooser/image_preview_column_cell.html +10 -0
  199. wagtail/images/templates/wagtailimages/chooser/results.html +24 -20
  200. wagtail/images/templates/wagtailimages/chooser/title_column_cell.html +15 -0
  201. wagtail/images/templates/wagtailimages/images/add.html +0 -34
  202. wagtail/images/templates/wagtailimages/images/index.html +3 -3
  203. wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
  204. wagtail/images/templates/wagtailimages/images/layout_toggle_button.html +8 -7
  205. wagtail/images/templatetags/wagtailimages_tags.py +2 -2
  206. wagtail/images/tests/test_admin_views.py +87 -0
  207. wagtail/images/tests/test_form_overrides.py +1 -1
  208. wagtail/images/tests/test_models.py +48 -9
  209. wagtail/images/views/chooser.py +66 -2
  210. wagtail/locale/en/LC_MESSAGES/django.po +55 -55
  211. wagtail/locale/is_IS/LC_MESSAGES/django.mo +0 -0
  212. wagtail/locale/is_IS/LC_MESSAGES/django.po +3 -3
  213. wagtail/locale/nl/LC_MESSAGES/django.mo +0 -0
  214. wagtail/locale/nl/LC_MESSAGES/django.po +11 -2
  215. wagtail/locale/ru/LC_MESSAGES/django.mo +0 -0
  216. wagtail/locale/ru/LC_MESSAGES/django.po +11 -1
  217. wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
  218. wagtail/locales/locale/nl/LC_MESSAGES/django.mo +0 -0
  219. wagtail/locales/locale/nl/LC_MESSAGES/django.po +12 -1
  220. wagtail/locales/locale/ru/LC_MESSAGES/django.mo +0 -0
  221. wagtail/locales/locale/ru/LC_MESSAGES/django.po +10 -1
  222. wagtail/locales/views.py +2 -2
  223. wagtail/models/orderable.py +10 -0
  224. wagtail/models/pages.py +9 -11
  225. wagtail/models/sites.py +1 -1
  226. wagtail/models/workflows.py +8 -5
  227. wagtail/project_template/home/tests.py +6 -7
  228. wagtail/project_template/project_name/settings/base.py +9 -9
  229. wagtail/project_template/requirements.txt +1 -1
  230. wagtail/query.py +7 -2
  231. wagtail/rich_text/rewriters.py +1 -1
  232. wagtail/search/apps.py +4 -49
  233. wagtail/search/backends/__init__.py +1 -113
  234. wagtail/search/backends/base.py +1 -547
  235. wagtail/search/backends/database/__init__.py +1 -50
  236. wagtail/search/backends/database/fallback.py +1 -253
  237. wagtail/search/backends/database/mysql/mysql.py +1 -700
  238. wagtail/search/backends/database/mysql/query.py +1 -258
  239. wagtail/search/backends/database/postgres/postgres.py +1 -749
  240. wagtail/search/backends/database/postgres/query.py +1 -83
  241. wagtail/search/backends/database/postgres/weights.py +1 -63
  242. wagtail/search/backends/database/sqlite/query.py +1 -294
  243. wagtail/search/backends/database/sqlite/sqlite.py +1 -719
  244. wagtail/search/backends/database/sqlite/utils.py +1 -35
  245. wagtail/search/backends/deprecation.py +45 -0
  246. wagtail/search/backends/elasticsearch7.py +18 -1260
  247. wagtail/search/backends/elasticsearch8.py +21 -96
  248. wagtail/search/backends/elasticsearch9.py +35 -0
  249. wagtail/search/backends/opensearch2.py +35 -0
  250. wagtail/search/backends/opensearch3.py +35 -0
  251. wagtail/search/index.py +1 -358
  252. wagtail/search/locale/en/LC_MESSAGES/django.po +2 -10
  253. wagtail/search/management/commands/update_index.py +1 -205
  254. wagtail/search/management/commands/wagtail_update_index.py +1 -4
  255. wagtail/search/models.py +32 -158
  256. wagtail/search/query.py +1 -114
  257. wagtail/search/queryset.py +1 -43
  258. wagtail/search/signal_handlers.py +1 -24
  259. wagtail/search/tasks.py +1 -10
  260. wagtail/search/tests/test_elasticsearch.py +22 -0
  261. wagtail/search/utils.py +1 -206
  262. wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
  263. wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -3
  264. wagtail/snippets/locale/ru/LC_MESSAGES/django.mo +0 -0
  265. wagtail/snippets/locale/ru/LC_MESSAGES/django.po +8 -1
  266. wagtail/snippets/locale/tr/LC_MESSAGES/django.mo +0 -0
  267. wagtail/snippets/locale/tr/LC_MESSAGES/django.po +8 -1
  268. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
  269. wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
  270. wagtail/snippets/tests/test_reordering.py +319 -0
  271. wagtail/snippets/tests/test_snippets.py +65 -12
  272. wagtail/snippets/views/snippets.py +16 -0
  273. wagtail/test/numberformat.py +30 -0
  274. wagtail/test/settings.py +35 -12
  275. wagtail/test/testapp/fields.py +12 -0
  276. wagtail/test/testapp/migrations/0056_commentablejsonpage.py +50 -0
  277. wagtail/test/testapp/migrations/0057_featurecompletetoy_sort_order.py +23 -0
  278. wagtail/test/testapp/migrations/0058_customlocktask.py +31 -0
  279. wagtail/test/testapp/models.py +27 -0
  280. wagtail/test/testapp/views.py +3 -1
  281. wagtail/test/utils/page_tests.py +17 -17
  282. wagtail/test/utils/template_tests.py +4 -6
  283. wagtail/test/utils/wagtail_tests.py +1 -2
  284. wagtail/tests/test_page_model.py +15 -0
  285. wagtail/{search/tests → tests}/test_page_search.py +29 -2
  286. wagtail/tests/test_search_fields.py +69 -0
  287. wagtail/tests/test_tests.py +62 -6
  288. wagtail/tests/test_workflow.py +25 -1
  289. wagtail/users/locale/cs/LC_MESSAGES/django.mo +0 -0
  290. wagtail/users/locale/cs/LC_MESSAGES/django.po +3 -0
  291. wagtail/users/locale/en/LC_MESSAGES/django.po +2 -2
  292. wagtail/users/locale/nl/LC_MESSAGES/django.mo +0 -0
  293. wagtail/users/locale/nl/LC_MESSAGES/django.po +6 -3
  294. wagtail/users/locale/ru/LC_MESSAGES/django.mo +0 -0
  295. wagtail/users/locale/ru/LC_MESSAGES/django.po +5 -1
  296. wagtail/users/locale/tr/LC_MESSAGES/django.mo +0 -0
  297. wagtail/users/locale/tr/LC_MESSAGES/django.po +78 -4
  298. wagtail/users/templates/wagtailusers/users/create.html +2 -0
  299. wagtail/users/templates/wagtailusers/users/edit.html +2 -0
  300. wagtail/users/tests/test_admin_views.py +4 -0
  301. wagtail/users/views/users.py +1 -1
  302. {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/METADATA +7 -6
  303. {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/RECORD +309 -315
  304. wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +0 -13
  305. wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +0 -13
  306. wagtail/search/tests/__init__.py +0 -0
  307. wagtail/search/tests/elasticsearch_common_tests.py +0 -251
  308. wagtail/search/tests/test_backends.py +0 -1215
  309. wagtail/search/tests/test_db_backend.py +0 -62
  310. wagtail/search/tests/test_elasticsearch7_backend.py +0 -1452
  311. wagtail/search/tests/test_elasticsearch8_backend.py +0 -15
  312. wagtail/search/tests/test_index_functions.py +0 -256
  313. wagtail/search/tests/test_indexed_class.py +0 -157
  314. wagtail/search/tests/test_mysql_backend.py +0 -192
  315. wagtail/search/tests/test_postgres_backend.py +0 -210
  316. wagtail/search/tests/test_queries.py +0 -332
  317. wagtail/search/tests/test_related_fields.py +0 -102
  318. wagtail/search/tests/test_sqlite_backend.py +0 -52
  319. wagtail/test/search/__init__.py +0 -0
  320. wagtail/test/search/apps.py +0 -9
  321. wagtail/test/search/fixtures/search.json +0 -545
  322. wagtail/test/search/migrations/0001_initial.py +0 -146
  323. wagtail/test/search/migrations/0002_bookunindexed.py +0 -43
  324. wagtail/test/search/migrations/0003_book_summary.py +0 -18
  325. wagtail/test/search/migrations/__init__.py +0 -0
  326. wagtail/test/search/models.py +0 -137
  327. /wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_cell.html → tables/ordering_cell.html} +0 -0
  328. /wagtail/{search/checks.py → checks.py} +0 -0
  329. {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/WHEEL +0 -0
  330. {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/entry_points.txt +0 -0
  331. {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/licenses/LICENSE +0 -0
  332. {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/top_level.txt +0 -0
wagtail/search/utils.py CHANGED
@@ -1,206 +1 @@
1
- import operator
2
- import re
3
- from functools import partial
4
-
5
- from django.apps import apps
6
- from django.db import connections
7
- from django.http import QueryDict
8
-
9
- from wagtail.search.index import RelatedFields, SearchField
10
-
11
- from .query import MATCH_NONE, Phrase, PlainText
12
-
13
- NOT_SET = object()
14
-
15
-
16
- def balanced_reduce(operator, seq, initializer=NOT_SET):
17
- """
18
- Has the same result as Python's reduce function, but performs the calculations in a different order.
19
-
20
- This is important when the operator is constructing data structures such as search query classes.
21
- This method will make the resulting data structures flatter, so operations that need to traverse
22
- them don't end up crashing with recursion errors.
23
-
24
- For example:
25
-
26
- Python's builtin reduce() function will do the following calculation:
27
-
28
- reduce(add, [1, 2, 3, 4, 5, 6, 7, 8])
29
- (1 + (2 + (3 + (4 + (5 + (6 + (7 + 8)))))))
30
-
31
- When using this with query classes, it would create a large data structure with a depth of 7
32
- Whereas balanced_reduce will execute this like so:
33
-
34
- balanced_reduce(add, [1, 2, 3, 4, 5, 6, 7, 8])
35
- ((1 + 2) + (3 + 4)) + ((5 + 6) + (7 + 8))
36
-
37
- Which only has a depth of 2
38
- """
39
- # Casting all iterables to list makes the implementation simpler
40
- if not isinstance(seq, list):
41
- seq = list(seq)
42
-
43
- # Note, it needs to be possible to use None as an initial value
44
- if initializer is not NOT_SET:
45
- if len(seq) == 0:
46
- return initializer
47
- else:
48
- return operator(initializer, balanced_reduce(operator, seq))
49
-
50
- if len(seq) == 0:
51
- raise TypeError("reduce() of empty sequence with no initial value")
52
- elif len(seq) == 1:
53
- return seq[0]
54
- else:
55
- break_point = len(seq) // 2
56
- first_set = balanced_reduce(operator, seq[:break_point])
57
- second_set = balanced_reduce(operator, seq[break_point:])
58
- return operator(first_set, second_set)
59
-
60
-
61
- # Reduce any iterable to a single value using a logical OR e.g. (a | b | ...)
62
- OR = partial(balanced_reduce, operator.or_)
63
- # Reduce any iterable to a single value using a logical AND e.g. (a & b & ...)
64
- AND = partial(balanced_reduce, operator.and_)
65
- # Reduce any iterable to a single value using an addition
66
- ADD = partial(balanced_reduce, operator.add)
67
- # Reduce any iterable to a single value using a multiplication
68
- MUL = partial(balanced_reduce, operator.mul)
69
-
70
- MAX_QUERY_STRING_LENGTH = 255
71
-
72
- filters_regexp = re.compile(r'\b(\w+):(\w+|"[^"]+"|\'[^\']+\')')
73
-
74
-
75
- def normalise_query_string(query_string):
76
- # Truncate query string
77
- query_string = query_string[:MAX_QUERY_STRING_LENGTH]
78
- # Convert query_string to lowercase
79
- query_string = query_string.lower()
80
-
81
- # Remove leading, trailing and multiple spaces
82
- query_string = re.sub(" +", " ", query_string).strip()
83
-
84
- return query_string
85
-
86
-
87
- def separate_filters_from_query(query_string):
88
- filters = QueryDict(mutable=True)
89
- for match_object in filters_regexp.finditer(query_string):
90
- key, value = match_object.groups()
91
- filters.update({key: value.strip("\"'")})
92
-
93
- query_string = filters_regexp.sub("", query_string).strip()
94
-
95
- return filters, query_string
96
-
97
-
98
- def parse_query_string(query_string, operator=None, zero_terms=MATCH_NONE):
99
- """
100
- This takes a query string typed in by a user and extracts the following:
101
-
102
- - Quoted terms (for phrase search)
103
- - Filters
104
-
105
- For example, the following query:
106
-
107
- `hello "this is a phrase" live:true` would be parsed into:
108
-
109
- filters: {'live': 'true'}
110
- tokens: And([PlainText('hello'), Phrase('this is a phrase')])
111
- """
112
- filters, query_string = separate_filters_from_query(query_string)
113
-
114
- is_phrase = False
115
- tokens = []
116
- if '"' in query_string:
117
- parts = query_string.split('"')
118
- else:
119
- parts = query_string.split("'")
120
-
121
- for part in parts:
122
- part = part.strip()
123
-
124
- if part:
125
- if is_phrase:
126
- tokens.append(Phrase(part))
127
- else:
128
- tokens.append(
129
- PlainText(part, operator=operator or PlainText.DEFAULT_OPERATOR)
130
- )
131
-
132
- is_phrase = not is_phrase
133
-
134
- if tokens:
135
- if operator == "or":
136
- search_query = OR(tokens)
137
- else:
138
- search_query = AND(tokens)
139
- else:
140
- search_query = zero_terms
141
-
142
- return filters, search_query
143
-
144
-
145
- def get_descendant_models(model):
146
- """
147
- Returns all descendants of a model, including the model itself.
148
- """
149
- descendant_models = {
150
- other_model
151
- for other_model in apps.get_models()
152
- if issubclass(other_model, model)
153
- }
154
- descendant_models.add(model)
155
- return descendant_models
156
-
157
-
158
- def get_content_type_pk(model):
159
- # We import it locally because this file is loaded before apps are ready.
160
- from django.contrib.contenttypes.models import ContentType
161
-
162
- return ContentType.objects.get_for_model(model).pk
163
-
164
-
165
- def get_ancestors_content_types_pks(model):
166
- """
167
- Returns content types ids for the ancestors of this model, excluding it.
168
- """
169
- from django.contrib.contenttypes.models import ContentType
170
-
171
- return [
172
- ct.pk
173
- for ct in ContentType.objects.get_for_models(
174
- *model._meta.get_parent_list()
175
- ).values()
176
- ]
177
-
178
-
179
- def get_descendants_content_types_pks(model):
180
- """
181
- Returns content types ids for the descendants of this model, including it.
182
- """
183
- from django.contrib.contenttypes.models import ContentType
184
-
185
- return [
186
- ct.pk
187
- for ct in ContentType.objects.get_for_models(
188
- *get_descendant_models(model)
189
- ).values()
190
- ]
191
-
192
-
193
- def get_search_fields(search_fields):
194
- for search_field in search_fields:
195
- if isinstance(search_field, SearchField):
196
- yield search_field
197
- elif isinstance(search_field, RelatedFields):
198
- yield from get_search_fields(search_field.fields)
199
-
200
-
201
- def get_postgresql_connections():
202
- return [
203
- connection
204
- for connection in connections.all()
205
- if connection.vendor == "postgresql"
206
- ]
1
+ from wagtailmodelsearch.utils import * # noqa: F403
@@ -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: 2025-07-24 16:20+0200\n"
11
+ "POT-Creation-Date: 2025-10-23 16:45+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"
@@ -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: 2025-07-24 16:20+0200\n"
11
+ "POT-Creation-Date: 2025-10-23 16:45+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"
@@ -178,7 +178,7 @@ msgstr ""
178
178
  msgid "Choose"
179
179
  msgstr ""
180
180
 
181
- #: views/snippets.py:87 views/snippets.py:864 wagtail_hooks.py:38
181
+ #: views/snippets.py:87 views/snippets.py:875 wagtail_hooks.py:38
182
182
  msgid "Snippets"
183
183
  msgstr ""
184
184
 
@@ -195,7 +195,7 @@ msgstr ""
195
195
  msgid "More options for '%(title)s'"
196
196
  msgstr ""
197
197
 
198
- #: views/snippets.py:856
198
+ #: views/snippets.py:867
199
199
  msgid "Home"
200
200
  msgstr ""
201
201
 
@@ -12,6 +12,7 @@
12
12
  # sergeybe <sergeybe@gmail.com>, 2020
13
13
  # sergeybe <sergeybe@gmail.com>, 2020
14
14
  # Tatsiana Tsygan <art.tatsiana@gmail.com>, 2018
15
+ # Twixxik, 2025
15
16
  # Vassily <zuber.kg@gmail.com>, 2017
16
17
  # Vassily <zuber.kg@gmail.com>, 2017
17
18
  # Виктор Виктор <spam.vitek@gmail.com>, 2020-2021
@@ -22,7 +23,7 @@ msgstr ""
22
23
  "Report-Msgid-Bugs-To: \n"
23
24
  "POT-Creation-Date: 2025-07-24 16:20+0200\n"
24
25
  "PO-Revision-Date: 2014-02-19 19:01+0000\n"
25
- "Last-Translator: Влад <integration.into.society@gmail.com>, 2022\n"
26
+ "Last-Translator: Twixxik, 2025\n"
26
27
  "Language-Team: Russian (http://app.transifex.com/torchbox/wagtail/language/"
27
28
  "ru/)\n"
28
29
  "MIME-Version: 1.0\n"
@@ -132,9 +133,15 @@ msgstr ""
132
133
  "href=\"%(wagtailsnippets_create_snippet_url)s\" target=\"_blank\" "
133
134
  "rel=\"noopener noreferrer\">создать сейчас</a>?,"
134
135
 
136
+ msgid "Scheduling…"
137
+ msgstr "Планирование…"
138
+
135
139
  msgid "Publishing…"
136
140
  msgstr "Публикуется..."
137
141
 
142
+ msgid "Schedule to publish"
143
+ msgstr "Запланировать публикацию"
144
+
138
145
  msgid "Publish this version"
139
146
  msgstr "Опубликовать эту версию"
140
147
 
@@ -6,6 +6,7 @@
6
6
  # Basitlik İyidir, 2020
7
7
  # Basitlik İyidir, 2020
8
8
  # Cihad GÜNDOĞDU <cihadgundogdu@gmail.com>, 2016,2025
9
+ # Ertuğrul Keremoğlu, 2025
9
10
  # Py Data Geek <pydatageek@gmail.com>, 2019
10
11
  # Py Data Geek <pydatageek@gmail.com>, 2019
11
12
  # Ragıp Ünal <ragip@ragipunal.com>, 2016
@@ -15,7 +16,7 @@ msgstr ""
15
16
  "Report-Msgid-Bugs-To: \n"
16
17
  "POT-Creation-Date: 2025-07-24 16:20+0200\n"
17
18
  "PO-Revision-Date: 2014-02-19 19:01+0000\n"
18
- "Last-Translator: Ragıp Ünal <ragip@ragipunal.com>, 2016\n"
19
+ "Last-Translator: Ertuğrul Keremoğlu, 2025\n"
19
20
  "Language-Team: Turkish (http://app.transifex.com/torchbox/wagtail/language/"
20
21
  "tr/)\n"
21
22
  "MIME-Version: 1.0\n"
@@ -70,6 +71,12 @@ msgstr "Seçilen blokları sil"
70
71
  msgid "%(model_name)s '%(object)s' deleted."
71
72
  msgstr "%(model_name)s '%(object)s' silindi."
72
73
 
74
+ #, python-format
75
+ msgid "%(count)d %(model_name)s deleted."
76
+ msgid_plural "%(count)d %(model_name)s deleted."
77
+ msgstr[0] "%(count)d %(model_name)s silindi."
78
+ msgstr[1] "%(count)d %(model_name)s silindi."
79
+
73
80
  #, python-format
74
81
  msgid "Delete %(snippet_type_name)s"
75
82
  msgstr "%(snippet_type_name)s sil"
@@ -1 +1 @@
1
- (()=>{"use strict";var e,r={8039:(e,r,t)=>{var o=t(2614),a=t(9465);class n extends o.ZZ{}class i extends a.y{titleStateKey="string";chooserModalClass=n}class s extends a._{widgetClass=i;chooserModalClass=n}window.telepath.register("wagtail.snippets.widgets.SnippetChooser",s)},1669:e=>{e.exports=jQuery}},t={};function o(e){var a=t[e];if(void 0!==a)return a.exports;var n=t[e]={exports:{}};return r[e](n,n.exports,o),n.exports}o.m=r,e=[],o.O=(r,t,a,n)=>{if(!t){var i=1/0;for(d=0;d<e.length;d++){for(var[t,a,n]=e[d],s=!0,l=0;l<t.length;l++)(!1&n||i>=n)&&Object.keys(o.O).every((e=>o.O[e](t[l])))?t.splice(l--,1):(s=!1,n<i&&(i=n));if(s){e.splice(d--,1);var u=a();void 0!==u&&(r=u)}}return r}n=n||0;for(var d=e.length;d>0&&e[d-1][2]>n;d--)e[d]=e[d-1];e[d]=[t,a,n]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.j=474,(()=>{var e={474:0};o.O.j=r=>0===e[r];var r=(r,t)=>{var a,n,[i,s,l]=t,u=0;if(i.some((r=>0!==e[r]))){for(a in s)o.o(s,a)&&(o.m[a]=s[a]);if(l)var d=l(o)}for(r&&r(t);u<i.length;u++)n=i[u],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(d)},t=globalThis.webpackChunkwagtail=globalThis.webpackChunkwagtail||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})();var a=o.O(void 0,[321],(()=>o(8039)));a=o.O(a)})();
1
+ (()=>{"use strict";var e,r={1669:e=>{e.exports=jQuery},8039:(e,r,t)=>{var o=t(2614),a=t(9465);class n extends o.ZZ{}class i extends a.y{titleStateKey="string";chooserModalClass=n}class s extends a._{widgetClass=i;chooserModalClass=n}window.telepath.register("wagtail.snippets.widgets.SnippetChooser",s)}},t={};function o(e){var a=t[e];if(void 0!==a)return a.exports;var n=t[e]={exports:{}};return r[e](n,n.exports,o),n.exports}o.m=r,e=[],o.O=(r,t,a,n)=>{if(!t){var i=1/0;for(d=0;d<e.length;d++){for(var[t,a,n]=e[d],s=!0,l=0;l<t.length;l++)(!1&n||i>=n)&&Object.keys(o.O).every(e=>o.O[e](t[l]))?t.splice(l--,1):(s=!1,n<i&&(i=n));if(s){e.splice(d--,1);var u=a();void 0!==u&&(r=u)}}return r}n=n||0;for(var d=e.length;d>0&&e[d-1][2]>n;d--)e[d]=e[d-1];e[d]=[t,a,n]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.j=474,(()=>{var e={474:0};o.O.j=r=>0===e[r];var r=(r,t)=>{var a,n,[i,s,l]=t,u=0;if(i.some(r=>0!==e[r])){for(a in s)o.o(s,a)&&(o.m[a]=s[a]);if(l)var d=l(o)}for(r&&r(t);u<i.length;u++)n=i[u],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(d)},t=globalThis.webpackChunkwagtail=globalThis.webpackChunkwagtail||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})();var a=o.O(void 0,[321],()=>o(8039));a=o.O(a)})();
@@ -1 +1 @@
1
- (()=>{"use strict";var e,r={9987:(e,r,t)=>{var o=t(2614),n=t(9465);class a extends o.ZZ{}class i extends n.y{titleStateKey="string";chooserModalClass=a}window.SnippetChooser=i},1669:e=>{e.exports=jQuery}},t={};function o(e){var n=t[e];if(void 0!==n)return n.exports;var a=t[e]={exports:{}};return r[e](a,a.exports,o),a.exports}o.m=r,e=[],o.O=(r,t,n,a)=>{if(!t){var i=1/0;for(f=0;f<e.length;f++){for(var[t,n,a]=e[f],l=!0,s=0;s<t.length;s++)(!1&a||i>=a)&&Object.keys(o.O).every((e=>o.O[e](t[s])))?t.splice(s--,1):(l=!1,a<i&&(i=a));if(l){e.splice(f--,1);var u=n();void 0!==u&&(r=u)}}return r}a=a||0;for(var f=e.length;f>0&&e[f-1][2]>a;f--)e[f]=e[f-1];e[f]=[t,n,a]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.j=686,(()=>{var e={686:0};o.O.j=r=>0===e[r];var r=(r,t)=>{var n,a,[i,l,s]=t,u=0;if(i.some((r=>0!==e[r]))){for(n in l)o.o(l,n)&&(o.m[n]=l[n]);if(s)var f=s(o)}for(r&&r(t);u<i.length;u++)a=i[u],o.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return o.O(f)},t=globalThis.webpackChunkwagtail=globalThis.webpackChunkwagtail||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})();var n=o.O(void 0,[321],(()=>o(9987)));n=o.O(n)})();
1
+ (()=>{"use strict";var e,r={1669:e=>{e.exports=jQuery},9987:(e,r,t)=>{var o=t(2614),n=t(9465);class a extends o.ZZ{}class i extends n.y{titleStateKey="string";chooserModalClass=a}window.SnippetChooser=i}},t={};function o(e){var n=t[e];if(void 0!==n)return n.exports;var a=t[e]={exports:{}};return r[e](a,a.exports,o),a.exports}o.m=r,e=[],o.O=(r,t,n,a)=>{if(!t){var i=1/0;for(f=0;f<e.length;f++){for(var[t,n,a]=e[f],l=!0,s=0;s<t.length;s++)(!1&a||i>=a)&&Object.keys(o.O).every(e=>o.O[e](t[s]))?t.splice(s--,1):(l=!1,a<i&&(i=a));if(l){e.splice(f--,1);var u=n();void 0!==u&&(r=u)}}return r}a=a||0;for(var f=e.length;f>0&&e[f-1][2]>a;f--)e[f]=e[f-1];e[f]=[t,n,a]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.j=686,(()=>{var e={686:0};o.O.j=r=>0===e[r];var r=(r,t)=>{var n,a,[i,l,s]=t,u=0;if(i.some(r=>0!==e[r])){for(n in l)o.o(l,n)&&(o.m[n]=l[n]);if(s)var f=s(o)}for(r&&r(t);u<i.length;u++)a=i[u],o.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return o.O(f)},t=globalThis.webpackChunkwagtail=globalThis.webpackChunkwagtail||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})();var n=o.O(void 0,[321],()=>o(9987));n=o.O(n)})();
@@ -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)])