Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional search endpoint follows site settings #1081

Merged
merged 8 commits into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/source/searching.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ In order to return specific metadata columns, see the documentation of the ``met
This is done in order to match also the partial results of the beginning of a search term(s).
The plone.restapi @search endpoint will not do that for you. You'll have to add it if you want to keep this feature.


Query format
------------

Expand Down Expand Up @@ -163,3 +162,8 @@ You do so by specifying the ``fullobjects`` parameter:
.. warning::

Be aware that this might induce performance issues when retrieving a lot of resources. Normally the search just serializes catalog brains, but with ``fullobjects``, we wake up all the returned objects.


Restrict search results to Plone's search settings
--------------------------------------------------
By default the search endpoint is not excluding any types from its results. To allow the search to follow Plone's search settings schema, pass the ``use_site_search_settings=1`` to the ``@search`` endpoint request. By doing this, the search results will be filtered based on the defined types to be searched and will be sorted according to the default sorting order.
1 change: 1 addition & 0 deletions news/1081.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow passing ``use_site_search_settings=1`` in the ``@search`` endpoint request, to follow Plone's ``ISearchSchema`` settings.
65 changes: 65 additions & 0 deletions src/plone/restapi/search/handler.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# -*- coding: utf-8 -*-
from plone.registry.interfaces import IRegistry
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.interfaces import IZCatalogCompatibleQuery
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.browser.navtree import getNavigationRoot
from zope.component import getMultiAdapter
from zope.component import getUtility


try:
from Products.CMFPlone.factory import _IMREALLYPLONE5 # noqa
from Products.CMFPlone.interfaces import ISearchSchema
except ImportError:
PLONE5 = False
else:
PLONE5 = True


class SearchHandler(object):
Expand Down Expand Up @@ -74,11 +86,64 @@ def search(self, query=None):
else:
fullobjects = False

use_site_search_settings = False

if "use_site_search_settings" in query:
use_site_search_settings = True
del query["use_site_search_settings"]

if PLONE5 and use_site_search_settings:
query = self.filter_query(query)

self._constrain_query_by_path(query)
query = self._parse_query(query)

lazy_resultset = self.catalog.searchResults(**query)
results = getMultiAdapter((lazy_resultset, self.request), ISerializeToJson)(
fullobjects=fullobjects
)

return results

def filter_types(self, types):
plone_utils = getToolByName(self.context, "plone_utils")
if not isinstance(types, list):
types = [types]
return plone_utils.getUserFriendlyTypes(types)

def filter_query(self, query):
registry = getUtility(IRegistry)
search_settings = registry.forInterface(ISearchSchema, prefix="plone")

types = query.get("portal_type", [])
if "query" in types:
types = types["query"]
query["portal_type"] = self.filter_types(types)

# respect effective/expiration date
query["show_inactive"] = False

# respect navigation root
if "path" not in query:
query["path"] = {"query": getNavigationRoot(self.context)}

default_sort_on = search_settings.sort_on

if "sort_on" not in query:
if default_sort_on != "relevance":
query["sort_on"] = self.default_sort_on
elif query["sort_on"] == "relevance":
del query["sort_on"]

if not query.get("sort_order") and (
query.get("sort_on", "") == "Date"
or query.get("sort_on", "") == "effective" # compatibility with Volto
):
query["sort_order"] = "reverse"
elif "sort_order" in query:
del query["sort_order"]

if "sort_order" in query and not query["sort_order"]:
del query["sort_order"]

return query
1 change: 1 addition & 0 deletions src/plone/restapi/services/search/get.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-

from plone.restapi.search.handler import SearchHandler
from plone.restapi.search.utils import unflatten_dotted_dict
from plone.restapi.services import Service
Expand Down
68 changes: 68 additions & 0 deletions src/plone/restapi/tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
from plone.uuid.interfaces import IMutableUUID
from Products.CMFCore.utils import getToolByName
from zope.component import getUtility
from zope.interface import alsoProvides
from zope.interface import noLongerProvides

import six
import transaction
import unittest


try:
from plone.app.layout.navigation.interfaces import INavigationRoot
from Products.CMFPlone.factory import _IMREALLYPLONE5 # noqa
except ImportError:
PLONE5 = False
Expand Down Expand Up @@ -664,6 +668,70 @@ def test_respect_access_inactive_permission(self):
).json()
self.assertEqual(response["items_total"], 1)

@unittest.skipIf(not PLONE5, "No ISearchSchema in Plone 4")
def test_search_use_site_search_settings_for_types(self):
response = self.api_session.get(
"/@search", params={"use_site_search_settings": 1}
).json()
types = set([item["@type"] for item in response["items"]])

self.assertEqual(set(types), set(["Folder", "DXTestDocument"]))

registry = getUtility(IRegistry)
from Products.CMFPlone.interfaces import ISearchSchema

search_settings = registry.forInterface(ISearchSchema, prefix="plone")
old = search_settings.types_not_searched
search_settings.types_not_searched += ("DXTestDocument",)
transaction.commit()

response = self.api_session.get(
"/@search", params={"use_site_search_settings": 1}
).json()
types = set([item["@type"] for item in response["items"]])

self.assertEqual(set(types), set(["Folder"]))
search_settings.types_not_searched = old
transaction.commit()

@unittest.skipIf(not PLONE5, "No ISearchSchema in Plone 4")
def test_search_use_site_search_settings_for_default_sort_order(self):
response = self.api_session.get(
"/@search", params={"use_site_search_settings": 1}
).json()
titles = [
u"Some Folder",
u"Lorem Ipsum",
u"Other Document",
u"Another Folder",
u"Document in second folder",
u"Doc outside folder",
]
self.assertEqual([item["title"] for item in response["items"]], titles)

response = self.api_session.get(
"/@search", params={"use_site_search_settings": 1, "sort_on": "effective"}
).json()
self.assertEqual(
[item["title"] for item in response["items"]][0],
u"Other Document",
)

@unittest.skipIf(not PLONE5, "No ISearchSchema in Plone 4")
def test_search_use_site_search_settings_with_navigation_root(self):

alsoProvides(self.folder, INavigationRoot)
transaction.commit()

response = self.api_session.get(
"/folder/@search", params={"use_site_search_settings": 1}
).json()
titles = [u"Some Folder", u"Lorem Ipsum", u"Other Document"]
self.assertEqual([item["title"] for item in response["items"]], titles)

noLongerProvides(self.folder, INavigationRoot)
transaction.commit()


class TestSearchATFunctional(unittest.TestCase):
layer = PLONE_RESTAPI_AT_FUNCTIONAL_TESTING
Expand Down