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
@@ -1,749 +1 @@
1
- import warnings
2
- from collections import OrderedDict
3
- from functools import reduce
4
-
5
- from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
6
- from django.db import (
7
- NotSupportedError,
8
- connections,
9
- router,
10
- transaction,
11
- )
12
- from django.db.models import Avg, Count, F, Manager, Q, TextField, Value
13
- from django.db.models.constants import LOOKUP_SEP
14
- from django.db.models.functions import Cast, Length
15
- from django.db.models.sql.subqueries import InsertQuery
16
- from django.utils.encoding import force_str
17
- from django.utils.functional import cached_property
18
-
19
- from ....index import AutocompleteField, RelatedFields, SearchField, get_indexed_models
20
- from ....models import IndexEntry
21
- from ....query import And, Boost, MatchAll, Not, Or, Phrase, PlainText
22
- from ....utils import (
23
- ADD,
24
- MUL,
25
- OR,
26
- get_content_type_pk,
27
- get_descendants_content_types_pks,
28
- )
29
- from ...base import (
30
- BaseSearchBackend,
31
- BaseSearchQueryCompiler,
32
- BaseSearchResults,
33
- FilterFieldError,
34
- )
35
- from .query import Lexeme
36
- from .weights import get_sql_weights, get_weight
37
-
38
- EMPTY_VECTOR = SearchVector(Value("", output_field=TextField()))
39
-
40
-
41
- class ObjectIndexer:
42
- """
43
- Responsible for extracting data from an object to be inserted into the index.
44
- """
45
-
46
- def __init__(self, obj, backend):
47
- self.obj = obj
48
- self.search_fields = obj.get_search_fields()
49
- self.config = backend.config
50
- self.autocomplete_config = backend.autocomplete_config
51
-
52
- def prepare_value(self, value):
53
- if isinstance(value, str):
54
- return value
55
-
56
- elif isinstance(value, list):
57
- return ", ".join(self.prepare_value(item) for item in value)
58
-
59
- elif isinstance(value, dict):
60
- return ", ".join(self.prepare_value(item) for item in value.values())
61
-
62
- return force_str(value)
63
-
64
- def prepare_field(self, obj, field):
65
- if isinstance(field, SearchField):
66
- yield (
67
- field,
68
- get_weight(field.boost),
69
- self.prepare_value(field.get_value(obj)),
70
- )
71
-
72
- elif isinstance(field, AutocompleteField):
73
- # AutocompleteField does not define a boost parameter, so use a base weight of 'D'
74
- yield (field, "D", self.prepare_value(field.get_value(obj)))
75
-
76
- elif isinstance(field, RelatedFields):
77
- sub_obj = field.get_value(obj)
78
- if sub_obj is None:
79
- return
80
-
81
- if isinstance(sub_obj, Manager):
82
- sub_objs = sub_obj.all()
83
-
84
- else:
85
- if callable(sub_obj):
86
- sub_obj = sub_obj()
87
-
88
- sub_objs = [sub_obj]
89
-
90
- for sub_obj in sub_objs:
91
- for sub_field in field.fields:
92
- yield from self.prepare_field(sub_obj, sub_field)
93
-
94
- def as_vector(self, texts, for_autocomplete=False):
95
- """
96
- Converts an array of strings into a SearchVector that can be indexed.
97
- """
98
- texts = [(text.strip(), weight) for text, weight in texts]
99
- texts = [(text, weight) for text, weight in texts if text]
100
-
101
- if not texts:
102
- return EMPTY_VECTOR
103
-
104
- search_config = self.autocomplete_config if for_autocomplete else self.config
105
-
106
- return ADD(
107
- [
108
- SearchVector(
109
- Value(text, output_field=TextField()),
110
- weight=weight,
111
- config=search_config,
112
- )
113
- for text, weight in texts
114
- ]
115
- )
116
-
117
- @cached_property
118
- def id(self):
119
- """
120
- Returns the value to use as the ID of the record in the index
121
- """
122
- return force_str(self.obj.pk)
123
-
124
- @cached_property
125
- def title(self):
126
- """
127
- Returns all values to index as "title". This is the value of all SearchFields that have the field_name 'title'
128
- """
129
- texts = []
130
- for field in self.search_fields:
131
- for current_field, boost, value in self.prepare_field(self.obj, field):
132
- if (
133
- isinstance(current_field, SearchField)
134
- and current_field.field_name == "title"
135
- ):
136
- texts.append((value, boost))
137
-
138
- return self.as_vector(texts)
139
-
140
- @cached_property
141
- def body(self):
142
- """
143
- Returns all values to index as "body". This is the value of all SearchFields excluding the title
144
- """
145
- texts = []
146
- for field in self.search_fields:
147
- for current_field, boost, value in self.prepare_field(self.obj, field):
148
- if (
149
- isinstance(current_field, SearchField)
150
- and not current_field.field_name == "title"
151
- ):
152
- texts.append((value, boost))
153
-
154
- return self.as_vector(texts)
155
-
156
- @cached_property
157
- def autocomplete(self):
158
- """
159
- Returns all values to index as "autocomplete". This is the value of all AutocompleteFields
160
- """
161
- texts = []
162
- for field in self.search_fields:
163
- for current_field, boost, value in self.prepare_field(self.obj, field):
164
- if isinstance(current_field, AutocompleteField):
165
- texts.append((value, boost))
166
-
167
- return self.as_vector(texts, for_autocomplete=True)
168
-
169
-
170
- class Index:
171
- def __init__(self, backend):
172
- self.backend = backend
173
- self.name = self.backend.index_name
174
-
175
- self.read_connection = connections[router.db_for_read(IndexEntry)]
176
- self.write_connection = connections[router.db_for_write(IndexEntry)]
177
-
178
- if (
179
- self.read_connection.vendor != "postgresql"
180
- or self.write_connection.vendor != "postgresql"
181
- ):
182
- raise NotSupportedError(
183
- "You must select a PostgreSQL database to use PostgreSQL search."
184
- )
185
-
186
- self.entries = IndexEntry._default_manager.all()
187
-
188
- def add_model(self, model):
189
- pass
190
-
191
- def refresh(self):
192
- pass
193
-
194
- def _refresh_title_norms(self, full=False):
195
- """
196
- Refreshes the value of the title_norm field.
197
-
198
- This needs to be set to 'lavg/ld' where:
199
- - lavg is the average length of titles in all documents (also in terms)
200
- - ld is the length of the title field in this document (in terms)
201
- """
202
-
203
- lavg = (
204
- self.entries.annotate(title_length=Length("title"))
205
- .filter(title_length__gt=0)
206
- .aggregate(Avg("title_length"))["title_length__avg"]
207
- )
208
-
209
- if full:
210
- # Update the whole table
211
- # This is the most accurate option but requires a full table rewrite
212
- # so we can't do it too often as it could lead to locking issues.
213
- entries = self.entries
214
-
215
- else:
216
- # Only update entries where title_norm is 1.0
217
- # This is the default value set on new entries.
218
- # It's possible that other entries could have this exact value but there shouldn't be too many of those
219
- entries = self.entries.filter(title_norm=1.0)
220
-
221
- entries.annotate(title_length=Length("title")).filter(
222
- title_length__gt=0
223
- ).update(title_norm=lavg / F("title_length"))
224
-
225
- def delete_stale_model_entries(self, model):
226
- existing_pks = model._default_manager.annotate(
227
- object_id=Cast("pk", TextField())
228
- ).values("object_id")
229
- content_types_pks = get_descendants_content_types_pks(model)
230
- stale_entries = self.entries.filter(
231
- content_type_id__in=content_types_pks
232
- ).exclude(object_id__in=existing_pks)
233
- stale_entries.delete()
234
-
235
- def delete_stale_entries(self):
236
- for model in get_indexed_models():
237
- # We don’t need to delete stale entries for non-root models,
238
- # since we already delete them by deleting roots.
239
- if not model._meta.parents:
240
- self.delete_stale_model_entries(model)
241
-
242
- def add_item(self, obj):
243
- self.add_items(obj._meta.model, [obj])
244
-
245
- def add_items(self, model, objs):
246
- search_fields = model.get_search_fields()
247
- if not search_fields:
248
- return
249
-
250
- indexers = [ObjectIndexer(obj, self.backend) for obj in objs]
251
-
252
- # TODO: Delete unindexed objects while dealing with proxy models.
253
- if not indexers:
254
- return
255
-
256
- content_type_pk = get_content_type_pk(model)
257
- compiler = InsertQuery(IndexEntry).get_compiler(
258
- connection=self.write_connection
259
- )
260
- title_sql = []
261
- autocomplete_sql = []
262
- body_sql = []
263
- data_params = []
264
-
265
- for indexer in indexers:
266
- data_params.extend((content_type_pk, indexer.id))
267
-
268
- # Compile title value
269
- value = compiler.prepare_value(
270
- IndexEntry._meta.get_field("title"), indexer.title
271
- )
272
- sql, params = value.as_sql(compiler, self.write_connection)
273
- title_sql.append(sql)
274
- data_params.extend(params)
275
-
276
- # Compile autocomplete value
277
- value = compiler.prepare_value(
278
- IndexEntry._meta.get_field("autocomplete"), indexer.autocomplete
279
- )
280
- sql, params = value.as_sql(compiler, self.write_connection)
281
- autocomplete_sql.append(sql)
282
- data_params.extend(params)
283
-
284
- # Compile body value
285
- value = compiler.prepare_value(
286
- IndexEntry._meta.get_field("body"), indexer.body
287
- )
288
- sql, params = value.as_sql(compiler, self.write_connection)
289
- body_sql.append(sql)
290
- data_params.extend(params)
291
-
292
- data_sql = ", ".join(
293
- [
294
- f"(%s, %s, {a}, {b}, {c}, 1.0)"
295
- for a, b, c in zip(title_sql, autocomplete_sql, body_sql)
296
- ]
297
- )
298
-
299
- with self.write_connection.cursor() as cursor:
300
- cursor.execute(
301
- """
302
- INSERT INTO %s (content_type_id, object_id, title, autocomplete, body, title_norm)
303
- (VALUES %s)
304
- ON CONFLICT (content_type_id, object_id)
305
- DO UPDATE SET title = EXCLUDED.title,
306
- title_norm = 1.0,
307
- autocomplete = EXCLUDED.autocomplete,
308
- body = EXCLUDED.body
309
- """
310
- % (IndexEntry._meta.db_table, data_sql),
311
- data_params,
312
- )
313
-
314
- self._refresh_title_norms()
315
-
316
- def delete_item(self, item):
317
- item.index_entries.all()._raw_delete(using=self.write_connection.alias)
318
-
319
- def __str__(self):
320
- return self.name
321
-
322
-
323
- class PostgresSearchQueryCompiler(BaseSearchQueryCompiler):
324
- DEFAULT_OPERATOR = "and"
325
- LAST_TERM_IS_PREFIX = False
326
- TARGET_SEARCH_FIELD_TYPE = SearchField
327
- HANDLES_ORDER_BY_EXPRESSIONS = True
328
-
329
- def __init__(self, *args, **kwargs):
330
- super().__init__(*args, **kwargs)
331
-
332
- local_search_fields = self.get_search_fields_for_model()
333
-
334
- # Due to a Django bug, arrays are not automatically converted
335
- # when we use WEIGHTS_VALUES.
336
- self.sql_weights = get_sql_weights()
337
-
338
- if self.fields is None:
339
- # search over the fields defined on the current model
340
- self.search_fields = local_search_fields
341
- else:
342
- # build a search_fields set from the passed definition,
343
- # which may involve traversing relations
344
- self.search_fields = {
345
- field_lookup: self.get_search_field(
346
- field_lookup, fields=local_search_fields
347
- )
348
- for field_lookup in self.fields
349
- }
350
-
351
- def get_config(self, backend):
352
- return backend.config
353
-
354
- def get_search_fields_for_model(self):
355
- return self.queryset.model.get_searchable_search_fields()
356
-
357
- def get_search_field(self, field_lookup, fields=None):
358
- if fields is None:
359
- fields = self.search_fields
360
-
361
- if LOOKUP_SEP in field_lookup:
362
- field_lookup, sub_field_name = field_lookup.split(LOOKUP_SEP, 1)
363
- else:
364
- sub_field_name = None
365
-
366
- for field in fields:
367
- if (
368
- isinstance(field, self.TARGET_SEARCH_FIELD_TYPE)
369
- and field.field_name == field_lookup
370
- ):
371
- return field
372
-
373
- # Note: Searching on a specific related field using
374
- # `.search(fields=…)` is not yet supported by Wagtail.
375
- # This method anticipates by already implementing it.
376
- if isinstance(field, RelatedFields) and field.field_name == field_lookup:
377
- return self.get_search_field(sub_field_name, field.fields)
378
-
379
- def build_tsquery_content(self, query, config=None, invert=False):
380
- if isinstance(query, PlainText):
381
- terms = query.query_string.split()
382
- if not terms:
383
- return None
384
-
385
- last_term = terms.pop()
386
-
387
- lexemes = Lexeme(last_term, invert=invert, prefix=self.LAST_TERM_IS_PREFIX)
388
- for term in terms:
389
- new_lexeme = Lexeme(term, invert=invert)
390
-
391
- if query.operator == "and":
392
- lexemes &= new_lexeme
393
- else:
394
- lexemes |= new_lexeme
395
-
396
- return SearchQuery(lexemes, search_type="raw", config=config)
397
-
398
- elif isinstance(query, Phrase):
399
- return SearchQuery(query.query_string, search_type="phrase", config=config)
400
-
401
- elif isinstance(query, Boost):
402
- # Not supported
403
- msg = "The Boost query is not supported by the PostgreSQL search backend."
404
- warnings.warn(msg, RuntimeWarning)
405
-
406
- return self.build_tsquery_content(
407
- query.subquery, config=config, invert=invert
408
- )
409
-
410
- elif isinstance(query, Not):
411
- return self.build_tsquery_content(
412
- query.subquery, config=config, invert=not invert
413
- )
414
-
415
- elif isinstance(query, (And, Or)):
416
- # If this part of the query is inverted, we swap the operator and
417
- # pass down the inversion state to the child queries.
418
- # This works thanks to De Morgan's law.
419
- #
420
- # For example, the following query:
421
- #
422
- # Not(And(Term("A"), Term("B")))
423
- #
424
- # Is equivalent to:
425
- #
426
- # Or(Not(Term("A")), Not(Term("B")))
427
- #
428
- # It's simpler to code it this way as we only need to store the
429
- # invert status of the terms rather than all the operators.
430
-
431
- subquery_lexemes = [
432
- self.build_tsquery_content(subquery, config=config, invert=invert)
433
- for subquery in query.subqueries
434
- ]
435
-
436
- is_and = isinstance(query, And)
437
-
438
- if invert:
439
- is_and = not is_and
440
-
441
- if is_and:
442
- return reduce(lambda a, b: a & b, subquery_lexemes)
443
- else:
444
- return reduce(lambda a, b: a | b, subquery_lexemes)
445
-
446
- raise NotImplementedError(
447
- "`%s` is not supported by the PostgreSQL search backend."
448
- % query.__class__.__name__
449
- )
450
-
451
- def build_tsquery(self, query, config=None):
452
- return self.build_tsquery_content(query, config=config)
453
-
454
- def build_tsrank(self, vector, query, config=None, boost=1.0):
455
- if isinstance(query, (Phrase, PlainText, Not)):
456
- rank_expression = SearchRank(
457
- vector,
458
- self.build_tsquery(query, config=config),
459
- weights=self.sql_weights,
460
- )
461
-
462
- if boost != 1.0:
463
- rank_expression *= boost
464
-
465
- return rank_expression
466
-
467
- elif isinstance(query, Boost):
468
- boost *= query.boost
469
- return self.build_tsrank(vector, query.subquery, config=config, boost=boost)
470
-
471
- elif isinstance(query, And):
472
- return (
473
- MUL(
474
- 1 + self.build_tsrank(vector, subquery, config=config, boost=boost)
475
- for subquery in query.subqueries
476
- )
477
- - 1
478
- )
479
-
480
- elif isinstance(query, Or):
481
- return ADD(
482
- self.build_tsrank(vector, subquery, config=config, boost=boost)
483
- for subquery in query.subqueries
484
- ) / (len(query.subqueries) or 1)
485
-
486
- raise NotImplementedError(
487
- "`%s` is not supported by the PostgreSQL search backend."
488
- % query.__class__.__name__
489
- )
490
-
491
- def get_index_vectors(self, search_query):
492
- return [
493
- (F("index_entries__title"), F("index_entries__title_norm")),
494
- (F("index_entries__body"), 1.0),
495
- ]
496
-
497
- def get_fields_vectors(self, search_query):
498
- return [
499
- (
500
- SearchVector(
501
- field_lookup,
502
- config=search_query.config,
503
- ),
504
- search_field.boost,
505
- )
506
- for field_lookup, search_field in self.search_fields.items()
507
- ]
508
-
509
- def get_search_vectors(self, search_query):
510
- if self.fields is None:
511
- return self.get_index_vectors(search_query)
512
-
513
- else:
514
- return self.get_fields_vectors(search_query)
515
-
516
- def _build_rank_expression(self, vectors, config):
517
- rank_expressions = [
518
- self.build_tsrank(vector, self.query, config=config) * boost
519
- for vector, boost in vectors
520
- ]
521
-
522
- rank_expression = rank_expressions[0]
523
- for other_rank_expression in rank_expressions[1:]:
524
- rank_expression += other_rank_expression
525
-
526
- return rank_expression
527
-
528
- def search(self, config, start, stop, score_field=None):
529
- # TODO: Handle MatchAll nested inside other search query classes.
530
- if isinstance(self.query, MatchAll):
531
- return self.queryset[start:stop]
532
-
533
- elif isinstance(self.query, Not) and isinstance(self.query.subquery, MatchAll):
534
- return self.queryset.none()
535
-
536
- search_query = self.build_tsquery(self.query, config=config)
537
- vectors = self.get_search_vectors(search_query)
538
- rank_expression = self._build_rank_expression(vectors, config)
539
-
540
- combined_vector = vectors[0][0]
541
- for vector, boost in vectors[1:]:
542
- combined_vector = combined_vector._combine(vector, "||", False)
543
-
544
- queryset = self.queryset.annotate(_vector_=combined_vector).filter(
545
- _vector_=search_query
546
- )
547
-
548
- if self.order_by_relevance:
549
- queryset = queryset.order_by(rank_expression.desc(), "-pk")
550
-
551
- elif not queryset.query.order_by:
552
- # Adds a default ordering to avoid issue #3729.
553
- queryset = queryset.order_by("-pk")
554
- rank_expression = F("pk")
555
-
556
- if score_field is not None:
557
- queryset = queryset.annotate(**{score_field: rank_expression})
558
-
559
- return queryset[start:stop]
560
-
561
- def _process_lookup(self, field, lookup, value):
562
- lhs = field.get_attname(self.queryset.model) + "__" + lookup
563
- return Q(**{lhs: value})
564
-
565
- def _process_match_none(self):
566
- return Q(pk__in=[])
567
-
568
- def _connect_filters(self, filters, connector, negated):
569
- if connector == "AND":
570
- q = Q(*filters)
571
-
572
- elif connector == "OR":
573
- q = OR([Q(fil) for fil in filters])
574
-
575
- else:
576
- return
577
-
578
- if negated:
579
- q = ~q
580
-
581
- return q
582
-
583
-
584
- class PostgresAutocompleteQueryCompiler(PostgresSearchQueryCompiler):
585
- LAST_TERM_IS_PREFIX = True
586
- TARGET_SEARCH_FIELD_TYPE = AutocompleteField
587
-
588
- def get_config(self, backend):
589
- return backend.autocomplete_config
590
-
591
- def get_search_fields_for_model(self):
592
- return self.queryset.model.get_autocomplete_search_fields()
593
-
594
- def get_index_vectors(self, search_query):
595
- return [(F("index_entries__autocomplete"), 1.0)]
596
-
597
- def get_fields_vectors(self, search_query):
598
- return [
599
- (
600
- SearchVector(
601
- field_lookup,
602
- config=search_query.config,
603
- weight="D",
604
- ),
605
- 1.0,
606
- )
607
- for field_lookup, search_field in self.search_fields.items()
608
- ]
609
-
610
-
611
- class PostgresSearchResults(BaseSearchResults):
612
- def get_queryset(self, for_count=False):
613
- if for_count:
614
- start = None
615
- stop = None
616
- else:
617
- start = self.start
618
- stop = self.stop
619
-
620
- return self.query_compiler.search(
621
- self.query_compiler.get_config(self.backend),
622
- start,
623
- stop,
624
- score_field=self._score_field,
625
- )
626
-
627
- def _do_search(self):
628
- return list(self.get_queryset())
629
-
630
- def _do_count(self):
631
- return self.get_queryset(for_count=True).count()
632
-
633
- supports_facet = True
634
-
635
- def facet(self, field_name):
636
- # Get field
637
- field = self.query_compiler._get_filterable_field(field_name)
638
- if field is None:
639
- raise FilterFieldError(
640
- 'Cannot facet search results with field "'
641
- + field_name
642
- + "\". Please add index.FilterField('"
643
- + field_name
644
- + "') to "
645
- + self.query_compiler.queryset.model.__name__
646
- + ".search_fields.",
647
- field_name=field_name,
648
- )
649
-
650
- query = self.query_compiler.search(
651
- self.query_compiler.get_config(self.backend), None, None
652
- )
653
- results = (
654
- query.values(field_name).annotate(count=Count("pk")).order_by("-count")
655
- )
656
-
657
- return OrderedDict(
658
- [(result[field_name], result["count"]) for result in results]
659
- )
660
-
661
-
662
- class PostgresSearchRebuilder:
663
- def __init__(self, index):
664
- self.index = index
665
-
666
- def start(self):
667
- self.index.delete_stale_entries()
668
- return self.index
669
-
670
- def finish(self):
671
- self.index._refresh_title_norms(full=True)
672
-
673
-
674
- class PostgresSearchAtomicRebuilder(PostgresSearchRebuilder):
675
- def __init__(self, index):
676
- super().__init__(index)
677
- self.transaction = transaction.atomic(using=index.write_connection.alias)
678
- self.transaction_opened = False
679
-
680
- def start(self):
681
- self.transaction.__enter__()
682
- self.transaction_opened = True
683
- return super().start()
684
-
685
- def finish(self):
686
- self.index._refresh_title_norms(full=True)
687
-
688
- self.transaction.__exit__(None, None, None)
689
- self.transaction_opened = False
690
-
691
- def __del__(self):
692
- # TODO: Implement a cleaner way to close the connection on failure.
693
- if self.transaction_opened:
694
- self.transaction.needs_rollback = True
695
- self.finish()
696
-
697
-
698
- class PostgresSearchBackend(BaseSearchBackend):
699
- query_compiler_class = PostgresSearchQueryCompiler
700
- autocomplete_query_compiler_class = PostgresAutocompleteQueryCompiler
701
- results_class = PostgresSearchResults
702
- rebuilder_class = PostgresSearchRebuilder
703
- atomic_rebuilder_class = PostgresSearchAtomicRebuilder
704
-
705
- def __init__(self, params):
706
- super().__init__(params)
707
- self.index_name = params.get("INDEX", "default")
708
- self.config = params.get("SEARCH_CONFIG")
709
-
710
- # Use 'simple' config for autocomplete to disable stemming
711
- # A good description for why this is important can be found at:
712
- # https://www.postgresql.org/docs/9.1/datatype-textsearch.html#DATATYPE-TSQUERY
713
- self.autocomplete_config = params.get("AUTOCOMPLETE_SEARCH_CONFIG", "simple")
714
-
715
- if params.get("ATOMIC_REBUILD", False):
716
- self.rebuilder_class = self.atomic_rebuilder_class
717
-
718
- def get_index_for_model(self, model):
719
- return Index(self)
720
-
721
- def get_index_for_object(self, obj):
722
- return self.get_index_for_model(obj._meta.model)
723
-
724
- def reset_index(self):
725
- for connection in [
726
- connection
727
- for connection in connections.all()
728
- if connection.vendor == "postgresql"
729
- ]:
730
- IndexEntry._default_manager.all()._raw_delete(using=connection.alias)
731
-
732
- def add_type(self, model):
733
- pass # Not needed.
734
-
735
- def refresh_index(self):
736
- pass # Not needed.
737
-
738
- def add(self, obj):
739
- self.get_index_for_object(obj).add_item(obj)
740
-
741
- def add_bulk(self, model, obj_list):
742
- if obj_list:
743
- self.get_index_for_object(obj_list[0]).add_items(model, obj_list)
744
-
745
- def delete(self, obj):
746
- self.get_index_for_object(obj).delete_item(obj)
747
-
748
-
749
- SearchBackend = PostgresSearchBackend
1
+ from wagtailmodelsearch.backends.database.postgres.postgres import * # noqa: F403