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.
- wagtail/__init__.py +1 -1
- wagtail/actions/copy_page.py +1 -1
- wagtail/actions/create_alias.py +1 -1
- wagtail/actions/delete_page.py +1 -1
- wagtail/actions/publish_page_revision.py +1 -1
- wagtail/actions/publish_revision.py +1 -1
- wagtail/actions/revert_to_page_revision.py +1 -1
- wagtail/actions/unpublish.py +1 -1
- wagtail/actions/unpublish_page.py +1 -1
- wagtail/admin/auth.py +3 -1
- wagtail/admin/checks.py +2 -2
- wagtail/admin/filters.py +28 -1
- wagtail/admin/forms/collections.py +1 -1
- wagtail/admin/forms/comments.py +1 -1
- wagtail/admin/forms/models.py +1 -1
- wagtail/admin/forms/pages.py +1 -1
- wagtail/admin/forms/tags.py +1 -1
- wagtail/admin/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/cs/LC_MESSAGES/django.po +25 -1
- wagtail/admin/locale/en/LC_MESSAGES/django.po +278 -192
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +29 -15
- wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/django.po +3 -2
- wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/django.po +57 -3
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.po +8 -2
- wagtail/admin/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ru/LC_MESSAGES/django.po +58 -1
- wagtail/admin/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/tr/LC_MESSAGES/django.po +3 -2
- wagtail/admin/static/wagtailadmin/css/core.css +1 -1
- wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
- wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +2 -2
- wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
- wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
- wagtail/admin/static/wagtailadmin/js/icons.js +1 -1
- wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
- wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +2 -2
- wagtail/admin/static/wagtailadmin/js/vendor/bootstrap-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/bootstrap-transition.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery-3.6.0.min.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery-ui-1.13.2.min.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.datetimepicker.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.fileupload-process.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.fileupload.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.iframe-transport.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/tag-it.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +1 -1
- wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
- wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
- wagtail/admin/templates/wagtailadmin/account/account.html +2 -0
- wagtail/admin/templates/wagtailadmin/base.html +14 -0
- wagtail/admin/templates/wagtailadmin/generic/chooser/chooser.html +2 -1
- wagtail/admin/templates/wagtailadmin/generic/chooser/creation_form.html +2 -1
- wagtail/admin/templates/wagtailadmin/generic/form.html +3 -1
- wagtail/admin/templates/wagtailadmin/panels/multi_field_panel_child.html +1 -1
- wagtail/admin/templates/wagtailadmin/panels/object_list.html +1 -1
- wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +3 -2
- wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +19 -0
- wagtail/admin/templates/wagtailadmin/shared/panel.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/set_privacy.html +15 -0
- wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +28 -1
- wagtail/admin/templates/wagtailadmin/shared/workflow_history/detail.html +2 -2
- wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_header.html → tables/ordering_header.html} +2 -2
- wagtail/admin/templates/wagtailadmin/tables/title_cell.html +1 -1
- wagtail/admin/templates/wagtailadmin/userbar/base.html +6 -3
- wagtail/admin/templates/wagtailadmin/userbar/item_admin.html +2 -2
- wagtail/admin/templates/wagtailadmin/userbar/item_page_add.html +2 -2
- wagtail/admin/templates/wagtailadmin/userbar/item_page_edit.html +2 -2
- wagtail/admin/templates/wagtailadmin/userbar/item_page_explore.html +2 -2
- wagtail/admin/templates/wagtailadmin/widgets/{daterange_input.html → range_input.html} +1 -1
- wagtail/admin/templates/wagtailadmin/workflows/task_chooser/chooser.html +4 -2
- wagtail/admin/templatetags/wagtailadmin_tags.py +56 -22
- wagtail/admin/tests/api/test_pages.py +7 -7
- wagtail/admin/tests/api/test_renderer_classes.py +16 -0
- wagtail/admin/tests/pages/test_create_page.py +34 -2
- wagtail/admin/tests/pages/test_edit_page.py +128 -14
- wagtail/admin/tests/pages/test_explorer_view.py +34 -7
- wagtail/admin/tests/pages/test_reorder_page.py +11 -0
- wagtail/admin/tests/test_collections_views.py +12 -0
- wagtail/admin/tests/test_edit_handlers.py +3 -3
- wagtail/admin/tests/test_filters.py +2 -2
- wagtail/admin/tests/test_keyboard_shortcuts.py +52 -2
- wagtail/admin/tests/test_menu.py +0 -2
- wagtail/admin/tests/test_privacy.py +16 -16
- wagtail/admin/tests/test_templatetags.py +137 -0
- wagtail/admin/tests/test_userbar.py +75 -35
- wagtail/admin/tests/test_views_generic.py +34 -0
- wagtail/admin/tests/test_workflows.py +34 -0
- wagtail/admin/tests/viewsets/test_model_viewset.py +322 -0
- wagtail/admin/ui/tables/orderable.py +73 -0
- wagtail/admin/ui/tables/pages.py +3 -13
- wagtail/admin/userbar.py +6 -1
- wagtail/admin/views/collection_privacy.py +6 -2
- wagtail/admin/views/generic/__init__.py +1 -0
- wagtail/admin/views/generic/mixins.py +20 -2
- wagtail/admin/views/generic/models.py +67 -1
- wagtail/admin/views/generic/ordering.py +79 -0
- wagtail/admin/views/home.py +3 -3
- wagtail/admin/views/page_privacy.py +5 -2
- wagtail/admin/views/pages/create.py +1 -1
- wagtail/admin/views/pages/edit.py +2 -2
- wagtail/admin/views/pages/listing.py +7 -42
- wagtail/admin/views/pages/move.py +1 -1
- wagtail/admin/views/pages/ordering.py +1 -1
- wagtail/admin/viewsets/base.py +1 -1
- wagtail/admin/viewsets/model.py +49 -1
- wagtail/admin/wagtail_hooks.py +2 -1
- wagtail/admin/widgets/slug.py +10 -10
- wagtail/api/v2/serializers.py +1 -1
- wagtail/api/v2/tests/test_renderer_classes.py +32 -0
- wagtail/apps.py +2 -0
- wagtail/bin/wagtail.py +1 -1
- wagtail/blocks/struct_block.py +2 -1
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +14 -14
- wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.po +19 -2
- wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.po +18 -1
- wagtail/contrib/frontend_cache/tests.py +4 -2
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +4 -4
- wagtail/contrib/redirects/tests/test_tmp_storages.py +20 -0
- wagtail/contrib/redirects/tmp_storages.py +1 -1
- wagtail/contrib/redirects/views.py +3 -3
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +3 -3
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.po +43 -3
- wagtail/contrib/search_promotions/static/wagtailsearchpromotions/js/query-chooser-modal.js +1 -1
- wagtail/contrib/search_promotions/views/settings.py +2 -2
- wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.po +6 -1
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.po +6 -2
- wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.po +6 -1
- wagtail/contrib/settings/tests/site_specific/test_admin.py +40 -6
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html +5 -5
- wagtail/contrib/table_block/blocks.py +1 -0
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +5 -1
- wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
- wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
- wagtail/coreutils.py +5 -5
- wagtail/documents/forms.py +18 -1
- wagtail/documents/locale/en/LC_MESSAGES/django.po +10 -10
- wagtail/documents/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/nl/LC_MESSAGES/django.po +9 -0
- wagtail/documents/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ru/LC_MESSAGES/django.po +9 -0
- wagtail/documents/models.py +1 -1
- wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
- wagtail/documents/templates/wagtaildocs/documents/add.html +0 -34
- wagtail/documents/tests/test_admin_views.py +132 -26
- wagtail/documents/tests/test_collection_privacy.py +18 -4
- wagtail/documents/tests/test_form_overrides.py +1 -1
- wagtail/documents/tests/test_search.py +21 -8
- wagtail/documents/views/documents.py +1 -1
- wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/embeds/static/wagtailembeds/js/embed-chooser-modal.js +1 -1
- wagtail/images/forms.py +16 -1
- wagtail/images/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/cs/LC_MESSAGES/django.po +12 -1
- wagtail/images/locale/en/LC_MESSAGES/django.po +57 -46
- wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/nl/LC_MESSAGES/django.po +37 -14
- wagtail/images/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ru/LC_MESSAGES/django.po +20 -1
- wagtail/images/models.py +1 -1
- wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
- wagtail/images/static/wagtailimages/js/focal-point-chooser.js +1 -1
- wagtail/images/static/wagtailimages/js/image-block.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
- wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/jquery.Jcrop.min.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-validate.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/load-image.min.js +1 -1
- wagtail/images/templates/wagtailimages/chooser/chooser.html +22 -13
- wagtail/images/templates/wagtailimages/chooser/image_preview_column_cell.html +10 -0
- wagtail/images/templates/wagtailimages/chooser/results.html +24 -20
- wagtail/images/templates/wagtailimages/chooser/title_column_cell.html +15 -0
- wagtail/images/templates/wagtailimages/images/add.html +0 -34
- wagtail/images/templates/wagtailimages/images/index.html +3 -3
- wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
- wagtail/images/templates/wagtailimages/images/layout_toggle_button.html +8 -7
- wagtail/images/templatetags/wagtailimages_tags.py +2 -2
- wagtail/images/tests/test_admin_views.py +87 -0
- wagtail/images/tests/test_form_overrides.py +1 -1
- wagtail/images/tests/test_models.py +48 -9
- wagtail/images/views/chooser.py +66 -2
- wagtail/locale/en/LC_MESSAGES/django.po +55 -55
- wagtail/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/is_IS/LC_MESSAGES/django.po +3 -3
- wagtail/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/nl/LC_MESSAGES/django.po +11 -2
- wagtail/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ru/LC_MESSAGES/django.po +11 -1
- wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/nl/LC_MESSAGES/django.po +12 -1
- wagtail/locales/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/ru/LC_MESSAGES/django.po +10 -1
- wagtail/locales/views.py +2 -2
- wagtail/models/orderable.py +10 -0
- wagtail/models/pages.py +9 -11
- wagtail/models/sites.py +1 -1
- wagtail/models/workflows.py +8 -5
- wagtail/project_template/home/tests.py +6 -7
- wagtail/project_template/project_name/settings/base.py +9 -9
- wagtail/project_template/requirements.txt +1 -1
- wagtail/query.py +7 -2
- wagtail/rich_text/rewriters.py +1 -1
- wagtail/search/apps.py +4 -49
- wagtail/search/backends/__init__.py +1 -113
- wagtail/search/backends/base.py +1 -547
- wagtail/search/backends/database/__init__.py +1 -50
- wagtail/search/backends/database/fallback.py +1 -253
- wagtail/search/backends/database/mysql/mysql.py +1 -700
- wagtail/search/backends/database/mysql/query.py +1 -258
- wagtail/search/backends/database/postgres/postgres.py +1 -749
- wagtail/search/backends/database/postgres/query.py +1 -83
- wagtail/search/backends/database/postgres/weights.py +1 -63
- wagtail/search/backends/database/sqlite/query.py +1 -294
- wagtail/search/backends/database/sqlite/sqlite.py +1 -719
- wagtail/search/backends/database/sqlite/utils.py +1 -35
- wagtail/search/backends/deprecation.py +45 -0
- wagtail/search/backends/elasticsearch7.py +18 -1260
- wagtail/search/backends/elasticsearch8.py +21 -96
- wagtail/search/backends/elasticsearch9.py +35 -0
- wagtail/search/backends/opensearch2.py +35 -0
- wagtail/search/backends/opensearch3.py +35 -0
- wagtail/search/index.py +1 -358
- wagtail/search/locale/en/LC_MESSAGES/django.po +2 -10
- wagtail/search/management/commands/update_index.py +1 -205
- wagtail/search/management/commands/wagtail_update_index.py +1 -4
- wagtail/search/models.py +32 -158
- wagtail/search/query.py +1 -114
- wagtail/search/queryset.py +1 -43
- wagtail/search/signal_handlers.py +1 -24
- wagtail/search/tasks.py +1 -10
- wagtail/search/tests/test_elasticsearch.py +22 -0
- wagtail/search/utils.py +1 -206
- wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -3
- wagtail/snippets/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ru/LC_MESSAGES/django.po +8 -1
- wagtail/snippets/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/tr/LC_MESSAGES/django.po +8 -1
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
- wagtail/snippets/tests/test_preview.py +5 -6
- wagtail/snippets/tests/test_reordering.py +319 -0
- wagtail/snippets/tests/test_snippets.py +65 -12
- wagtail/snippets/views/snippets.py +16 -0
- wagtail/test/numberformat.py +30 -0
- wagtail/test/settings.py +35 -12
- wagtail/test/testapp/fields.py +12 -0
- wagtail/test/testapp/migrations/0056_commentablejsonpage.py +50 -0
- wagtail/test/testapp/migrations/0057_featurecompletetoy_sort_order.py +23 -0
- wagtail/test/testapp/migrations/0058_customlocktask.py +31 -0
- wagtail/test/testapp/models.py +27 -0
- wagtail/test/testapp/urls.py +1 -0
- wagtail/test/testapp/views.py +18 -2
- wagtail/test/utils/page_tests.py +17 -17
- wagtail/test/utils/template_tests.py +4 -6
- wagtail/test/utils/wagtail_tests.py +1 -2
- wagtail/tests/test_blocks.py +15 -0
- wagtail/tests/test_page_model.py +15 -0
- wagtail/{search/tests → tests}/test_page_search.py +29 -2
- wagtail/tests/test_search_fields.py +69 -0
- wagtail/tests/test_tests.py +62 -6
- wagtail/tests/test_workflow.py +25 -1
- wagtail/users/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/cs/LC_MESSAGES/django.po +3 -0
- wagtail/users/locale/en/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/nl/LC_MESSAGES/django.po +6 -3
- wagtail/users/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ru/LC_MESSAGES/django.po +5 -1
- wagtail/users/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/tr/LC_MESSAGES/django.po +78 -4
- wagtail/users/templates/wagtailusers/users/create.html +2 -0
- wagtail/users/templates/wagtailusers/users/edit.html +2 -0
- wagtail/users/tests/test_admin_views.py +4 -0
- wagtail/users/views/users.py +1 -1
- {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/METADATA +7 -6
- {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/RECORD +322 -328
- wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +0 -13
- wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +0 -13
- wagtail/search/tests/__init__.py +0 -0
- wagtail/search/tests/elasticsearch_common_tests.py +0 -251
- wagtail/search/tests/test_backends.py +0 -1215
- wagtail/search/tests/test_db_backend.py +0 -62
- wagtail/search/tests/test_elasticsearch7_backend.py +0 -1452
- wagtail/search/tests/test_elasticsearch8_backend.py +0 -15
- wagtail/search/tests/test_index_functions.py +0 -256
- wagtail/search/tests/test_indexed_class.py +0 -157
- wagtail/search/tests/test_mysql_backend.py +0 -192
- wagtail/search/tests/test_postgres_backend.py +0 -210
- wagtail/search/tests/test_queries.py +0 -332
- wagtail/search/tests/test_related_fields.py +0 -102
- wagtail/search/tests/test_sqlite_backend.py +0 -52
- wagtail/test/search/__init__.py +0 -0
- wagtail/test/search/apps.py +0 -9
- wagtail/test/search/fixtures/search.json +0 -545
- wagtail/test/search/migrations/0001_initial.py +0 -146
- wagtail/test/search/migrations/0002_bookunindexed.py +0 -43
- wagtail/test/search/migrations/0003_book_summary.py +0 -18
- wagtail/test/search/migrations/__init__.py +0 -0
- wagtail/test/search/models.py +0 -137
- /wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_cell.html → tables/ordering_cell.html} +0 -0
- /wagtail/{search/checks.py → checks.py} +0 -0
- {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/WHEEL +0 -0
- {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/entry_points.txt +0 -0
- {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/licenses/LICENSE +0 -0
- {wagtail-7.1.1.dist-info → wagtail-7.2rc1.dist-info}/top_level.txt +0 -0
|
@@ -1,749 +1 @@
|
|
|
1
|
-
import
|
|
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
|