Skip to content

Commit

Permalink
Merge b94ec1f into 73cf3b9
Browse files Browse the repository at this point in the history
  • Loading branch information
frisi committed Feb 12, 2019
2 parents 73cf3b9 + b94ec1f commit 223b3aa
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 9 deletions.
2 changes: 2 additions & 0 deletions news/426.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix querying `object_provides` for multiple interfaces using 'and' operator.
[fRiSi]
44 changes: 35 additions & 9 deletions src/plone/api/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,28 @@ def get_uuid(obj=None):
return IUUID(obj)


def _parse_object_provides_query(query):
"""Create a query for the object_provides index.
:param query: [required]
:type query: Single (or list of) Interface or Identifier or a KeywordIndex
query for multiple values
(eg. `{'query': [Iface1, Iface2], 'operator': 'or'}`)
"""
operator = 'or'
ifaces = query
if isinstance(query, dict):
operator = query.get('operator', operator)
ifaces = query.get('query', [])
elif not isinstance(query, (list, tuple)):
ifaces = [query]

return {
'query': [getattr(x, '__identifier__', x) for x in ifaces],
'operator': operator,
}


def find(context=None, depth=None, **kwargs):
"""Find content in the portal.
Expand Down Expand Up @@ -631,10 +653,17 @@ def find(context=None, depth=None, **kwargs):
>>> find(path={'query': '/plone/about/team', 'depth': 1})
The `object_provides` index/argument allows Interface objects as well as
identifiers.
identifiers. It also supports querying multiple interfaces combined with
`and` or `or`.
>>> find(object_provides=IATDocument)
- or -
>>> find(object_provides=IATDocument.__identifier__)
- or -
>>> find(object_provides={
... 'query': [IATFolder, INavigationRoot],
... 'operator': 'and',
... })
An empty resultset is returned if no valid indexes are queried.
>>> len(find())
Expand Down Expand Up @@ -668,14 +697,11 @@ def find(context=None, depth=None, **kwargs):
if context is not None:
query['path']['query'] = '/'.join(context.getPhysicalPath())

# Convert interfaces to their identifiers
object_provides = query.get('object_provides', [])
if object_provides:
if not isinstance(object_provides, (list, tuple)):
object_provides = [object_provides]
query['object_provides'] = [
getattr(x, '__identifier__', x) for x in object_provides
]
# Convert interfaces to their identifiers and also allow to query
# multiple values using {'query:[], 'operator':'and|or'}
obj_provides = query.get('object_provides', [])
if obj_provides:
query['object_provides'] = _parse_object_provides_query(obj_provides)

# Make sure we don't dump the whole catalog.
catalog = portal.get_tool('portal_catalog')
Expand Down
73 changes: 73 additions & 0 deletions src/plone/api/tests/test_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from plone import api
from plone.api.content import NEW_LINKINTEGRITY
from plone.api.tests.base import INTEGRATION_TESTING
from plone.app.layout.navigation.interfaces import INavigationRoot
from plone.app.linkintegrity.exceptions import LinkIntegrityNotificationException # NOQA: E501
from plone.app.textfield import RichTextValue
from plone.dexterity.interfaces import IDexterityContent
Expand All @@ -21,6 +22,7 @@
from zope.component import getGlobalSiteManager
from zope.component import getUtility
from zope.container.contained import ContainerModifiedEvent
from zope.interface import directlyProvides
from zope.lifecycleevent import IObjectModifiedEvent
from zope.lifecycleevent import IObjectMovedEvent
from zope.lifecycleevent import modified
Expand Down Expand Up @@ -1020,6 +1022,32 @@ def test_find_interface(self):

self.assertEqual(by_identifier, by_interface)

def test_find_interface_dict(self):
# Find documents by interface combined with 'and'

directlyProvides(self.portal.events, INavigationRoot)
self.portal.events.reindexObject(idxs=['object_provides'])

# standard catalog query using identifiers
brains = api.content.find(
object_provides={
'query': [
IContentish.__identifier__,
INavigationRoot.__identifier__],
'operator': 'and',
},
)
self.assertEqual(len(brains), 1)

# plone.api query using interfaces
brains = api.content.find(
object_provides={
'query': [IContentish, INavigationRoot],
'operator': 'and',
},
)
self.assertEqual(len(brains), 1)

def test_find_dict(self):
# Pass arguments using dict
path = '/'.join(self.portal.about.getPhysicalPath())
Expand Down Expand Up @@ -1056,6 +1084,51 @@ def test_find_dict(self):
documents = api.content.find(**query)
self.assertEqual(len(documents), 0)

def test_find_parse_object_provides_query(self):

parse = api.content._parse_object_provides_query

# single interface
self.assertDictEqual(
parse(IContentish),
{'query': [IContentish.__identifier__],
'operator': 'or'},
)
# single identifier
self.assertDictEqual(
parse(IContentish.__identifier__),
{'query': [IContentish.__identifier__],
'operator': 'or'},
)
# multiple interfaces/identifiers (mixed as list)
self.assertDictEqual(
parse([INavigationRoot, IContentish.__identifier__]),
{'query': [INavigationRoot.__identifier__,
IContentish.__identifier__],
'operator': 'or'},
)
# multiple interfaces/identifiers (mixed as tuple)
self.assertDictEqual(
parse((INavigationRoot, IContentish.__identifier__)),
{'query': [INavigationRoot.__identifier__,
IContentish.__identifier__],
'operator': 'or'},
)
# full blown query - interfaces/identifiers mixed
self.assertDictEqual(
parse({
'query': [INavigationRoot, IContentish.__identifier__],
'operator': 'and',
}),
{
'query': [
INavigationRoot.__identifier__,
IContentish.__identifier__
],
'operator': 'and',
},
)

def test_get_state(self):
"""Test retrieving the workflow state of a content item."""

Expand Down

0 comments on commit 223b3aa

Please sign in to comment.