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

Exclude some folders from search #248

Merged
merged 6 commits into from
Mar 6, 2024
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
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Changelog
[cekk]
- Improve types test for their schema, required fields, fieldsets.
[cekk]
- Add *exclude_from_search* indexer and behavior, and enable for Document and Folder.
[cekk]
- Add custom adapter for IZCatalogCompatibleQuery to force all anonymous @search calls to skip items excluded from search.
[cekk]
- Set *exclude_from_search* to True in all Documents/Folders automatically created in createSubfolders event handler,
and add an upgrade-step that fix already created ones.
[cekk]

6.1.14 (2024-02-20)
-------------------
Expand Down
1 change: 1 addition & 0 deletions base.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ eggs = createcoverage
[versions]
# Don't use a released version of design.plone.contenttypes
design.plone.contenttypes =
plone.restapi =

[sources]
#collective.volto.blocksfield = git https://github.com/collective/collective.volto.blocksfield.git [email protected]:collective/collective.volto.blocksfield.git branch=main
Expand Down
2 changes: 2 additions & 0 deletions src/design/plone/contenttypes/adapters/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@
name="text"
/>

<adapter factory=".query.ZCatalogCompatibleQueryAdapter" />

</configure>
26 changes: 26 additions & 0 deletions src/design/plone/contenttypes/adapters/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from design.plone.contenttypes.interfaces import IDesignPloneContenttypesLayer
from plone.restapi.interfaces import IZCatalogCompatibleQuery
from plone.restapi.search.query import ZCatalogCompatibleQueryAdapter as BaseAdapter
from zope.component import adapter
from zope.interface import implementer
from zope.interface import Interface
from plone import api


@implementer(IZCatalogCompatibleQuery)
@adapter(Interface, IDesignPloneContenttypesLayer)
class ZCatalogCompatibleQueryAdapter(BaseAdapter):
""" """

def __call__(self, query):
"""
Do not show excluded from search items when anonymous are performing
some catalog searches
"""
query = super().__call__(query=query)

if api.user.is_anonymous():
# For the anonymous user, only content that is not "excluded from the search" is found
query["exclude_from_search"] = False

return query
8 changes: 8 additions & 0 deletions src/design/plone/contenttypes/behaviors/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,12 @@
marker=".update_note.IUpdateNote"
/>

<plone:behavior
name="design.plone.contenttypes.behavior.exclude_from_search"
title="Exclude from search"
description="Campo per escludere un contenuto dalle ricerche del sito."
factory=".exclude_from_search.ExcludeFromSearch"
provides=".exclude_from_search.IExcludeFromSearch"
marker=".exclude_from_search.IExcludeFromSearch"
/>
</configure>
37 changes: 37 additions & 0 deletions src/design/plone/contenttypes/behaviors/exclude_from_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from design.plone.contenttypes import _
from plone.autoform.interfaces import IFormFieldProvider
from plone.dexterity.interfaces import IDexterityContent
from plone.supermodel import model
from zope import schema
from zope.component import adapter
from zope.interface import implementer
from zope.interface import provider


@provider(IFormFieldProvider)
class IExcludeFromSearch(model.Schema):
""" """

exclude_from_search = schema.Bool(
title=_("exclude_from_search_label", default="Escludi dalla ricerca"),
description=_(
"help_exclude_from_search",
default="Se selezionato, questo contenuto non verrà mostrato nelle ricerche del sito per gli utenti anonimi.",
),
required=False,
default=False,
)
model.fieldset(
"settings",
fields=["exclude_from_search"],
)


@implementer(IExcludeFromSearch)
@adapter(IDexterityContent)
class ExcludeFromSearch(object):
""" """

def __init__(self, context):
self.context = context
19 changes: 14 additions & 5 deletions src/design/plone/contenttypes/events/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,15 @@
}
],
"Servizio": [
{"id": "modulistica", "title": "Modulistica", "contains": ("File", "Link")},
{"id": "allegati", "title": "Allegati", "contains": ("File", "Link")},
{
"id": "modulistica",
"title": "Modulistica",
"allowed_types": ("File", "Link"),
},
{"id": "allegati", "title": "Allegati", "allowed_types": ("File", "Link")},
],
"UnitaOrganizzativa": [
{"id": "allegati", "title": "Allegati", "contains": ("File",)},
{"id": "allegati", "title": "Allegati", "allowed_types": ("File",)},
],
}

Expand All @@ -160,14 +164,19 @@ def createSubfolders(context, event):
return
for mapping in subfolders_mapping:
if mapping["id"] not in context.keys():
portal_type = mapping.get("type", "Document")
child = api.content.create(
container=context,
type=mapping.get("type", "Document"),
type=portal_type,
title=mapping["title"],
id=mapping["id"],
)
create_default_blocks(context=child)
if portal_type == "Document":
create_default_blocks(context=child)

if portal_type in ["Folder", "Document"]:
child.exclude_from_search = True
child.reindexObject(idxs=["exclude_from_search"])
# select constraints
if mapping.get("allowed_types", ()):
constraintsChild = ISelectableConstrainTypes(child)
Expand Down
5 changes: 5 additions & 0 deletions src/design/plone/contenttypes/indexers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ def parent(context):
"UID": obj_parent.UID(),
"@id": obj_parent.absolute_url(),
}


@indexer(IDexterityContent)
def exclude_from_search(context):
return getattr(context.aq_base, "exclude_from_search", False)
4 changes: 4 additions & 0 deletions src/design/plone/contenttypes/indexers/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
factory=".punto_di_contatto.PuntoDiContattoMoreTextToIndex"
name="IPuntoDiContatto"
/>
<adapter
factory=".common.exclude_from_search"
name="exclude_from_search"
/>
<!-- only metadata -->

</configure>
3 changes: 3 additions & 0 deletions src/design/plone/contenttypes/profiles/default/catalog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
<index name="data_conclusione_incarico" meta_type="DateIndex">
<indexed_attr value="data_conclusione_incarico"/>
</index>
<index name="exclude_from_search" meta_type="BooleanIndex">
<indexed_attr value="exclude_from_search"/>
</index>

<!-- metadata -->
<column value="tipologia_notizia" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<metadata>
<version>7100</version>
<version>7200</version>
<dependencies>
<dependency>profile-redturtle.bandi:default</dependency>
<dependency>profile-collective.venue:default</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<element value="kitconcept.seo" />
<element value="plone.constraintypes" />
<element value="plone.leadimage" />
<element value="design.plone.contenttypes.behavior.exclude_from_search" />
</property>
<property name="view_methods">
<element value="document_view" />
Expand Down
13 changes: 13 additions & 0 deletions src/design/plone/contenttypes/profiles/default/types/Folder.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<object xmlns:i18n="http://xml.zope.org/namespaces/i18n"
meta_type="Dexterity FTI"
name="Folder"
i18n:domain="plone"
>

<property name="behaviors"
purge="False"
>
<element value="design.plone.contenttypes.behavior.exclude_from_search" />
</property>
</object>
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
from design.plone.contenttypes.testing import (
DESIGN_PLONE_CONTENTTYPES_API_FUNCTIONAL_TESTING,
)

from plone import api
from plone.app.testing import setRoles
from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.app.testing import TEST_USER_ID
from plone.app.testing.helpers import logout
from plone.indexer.interfaces import IIndexableObject
from plone.restapi.interfaces import IZCatalogCompatibleQuery
from plone.restapi.testing import RelativeSession
from transaction import commit
from zope.component import getMultiAdapter
from zope.component import queryMultiAdapter
import unittest


class ExcludeFromSearchFunctionalTest(unittest.TestCase):
layer = DESIGN_PLONE_CONTENTTYPES_API_FUNCTIONAL_TESTING
maxDiff = None

def setUp(self):
self.portal = self.layer["portal"]
self.request = self.layer["request"]
self.portal_url = self.portal.absolute_url()
self.catalog = api.portal.get_tool("portal_catalog")
setRoles(self.portal, TEST_USER_ID, ["Manager"])

api.user.create(
email="[email protected]",
username="foo",
password="secret!!!",
)

self.news = api.content.create(
container=self.portal,
type="News Item",
title="Test News",
)

self.document = api.content.create(
container=self.portal,
type="Document",
title="Test Document",
)

api.content.transition(obj=self.news, transition="publish")
api.content.transition(obj=self.news["multimedia"], transition="publish")
api.content.transition(obj=self.document, transition="publish")

commit()

self.api_session = RelativeSession(self.portal_url)
self.api_session.headers.update({"Accept": "application/json"})
self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)

self.api_session_foo = RelativeSession(self.portal_url)
self.api_session_foo.headers.update({"Accept": "application/json"})
self.api_session_foo.auth = ("foo", "secret!!!")

self.api_session_anon = RelativeSession(self.portal_url)
self.api_session_anon.headers.update({"Accept": "application/json"})

def tearDown(self):
self.api_session.close()
self.api_session_anon.close()

def test_exclude_from_search_indexer_for_item_without_behavior(self):
"""
news item does not have the behavior, so it has False by default
"""
self.assertRaises(AttributeError, getattr, self.news, "exclude_from_search")
adapter = queryMultiAdapter((self.news, self.catalog), IIndexableObject)
self.assertFalse(adapter.exclude_from_search)

def test_exclude_from_search_indexer_for_item_with_behavior_enabled(self):
""" """
self.assertFalse(self.document.exclude_from_search)
adapter = queryMultiAdapter((self.document, self.catalog), IIndexableObject)
self.assertFalse(adapter.exclude_from_search)

def test_exclude_from_search_indexer_for_item_with_behavior_enabled_and_set(self):
""" """
self.assertTrue(self.news["multimedia"].exclude_from_search)
adapter = queryMultiAdapter(
(self.news["multimedia"], self.catalog), IIndexableObject
)
self.assertTrue(adapter.exclude_from_search)

def test_adapter_do_not_append_anything_to_query_for_auth_users(self):
catalog_compatible_query = getMultiAdapter(
(self.portal, self.request), IZCatalogCompatibleQuery
)({})
self.assertEqual({}, catalog_compatible_query)

def test_adapter_append_exclude_from_search_to_query_for_anon_users(self):
logout()
catalog_compatible_query = getMultiAdapter(
(self.portal, self.request), IZCatalogCompatibleQuery
)({})
self.assertEqual(catalog_compatible_query, {"exclude_from_search": False})

def test_search_return_excluded_contents_for_logged_users(self):
""" """
resp = self.api_session.get(
"/@search", params={"SearchableText": "multimedia"}
).json()
self.assertEqual(resp["items_total"], 1)

resp = self.api_session_foo.get(
"/@search", params={"SearchableText": "multimedia"}
).json()
self.assertEqual(resp["items_total"], 1)

def test_search_do_not_return_excluded_contents_for_anon_users(self):
""" """
resp = self.api_session_anon.get(
"/@search", params={"SearchableText": "multimedia"}
).json()
self.assertEqual(resp["items_total"], 0)
2 changes: 2 additions & 0 deletions src/design/plone/contenttypes/tests/test_ct_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def test_behaviors_enabled_for_document(self):
"design.plone.contenttypes.behavior.show_modified",
"kitconcept.seo",
"plone.constraintypes",
"design.plone.contenttypes.behavior.exclude_from_search",
"plone.leadimage",
"volto.preview_image",
),
Expand Down Expand Up @@ -128,6 +129,7 @@ def test_document_fields_settings_fieldset(self):
"id",
"versioning_enabled",
"show_modified",
"exclude_from_search",
"changeNote",
],
)
Expand Down
48 changes: 48 additions & 0 deletions src/design/plone/contenttypes/tests/test_ct_folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-

from design.plone.contenttypes.testing import (
DESIGN_PLONE_CONTENTTYPES_API_FUNCTIONAL_TESTING,
)
from plone import api
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.restapi.testing import RelativeSession

import unittest


class TestFolderSchema(unittest.TestCase):
layer = DESIGN_PLONE_CONTENTTYPES_API_FUNCTIONAL_TESTING

def setUp(self):
self.app = self.layer["app"]
self.portal = self.layer["portal"]
self.request = self.layer["request"]
self.portal_url = self.portal.absolute_url()
setRoles(self.portal, TEST_USER_ID, ["Manager"])

self.api_session = RelativeSession(self.portal_url)
self.api_session.headers.update({"Accept": "application/json"})
self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)

def tearDown(self):
self.api_session.close()

def test_behaviors_enabled_for_folder(self):
portal_types = api.portal.get_tool(name="portal_types")
self.assertEqual(
portal_types["Folder"].behaviors,
(
"plone.dublincore",
"plone.namefromtitle",
"plone.allowdiscussion",
"plone.excludefromnavigation",
"plone.shortname",
"plone.constraintypes",
"plone.relateditems",
"plone.nextprevioustoggle",
"design.plone.contenttypes.behavior.exclude_from_search",
),
)
Loading
Loading