Skip to content

Commit

Permalink
Implement content.find.
Browse files Browse the repository at this point in the history
1. Implement .find to map to portal_catalog.searchResults / .__call__.
2. Allow passing in Interface objects as well as Interface.__identifier__ strings.
3. Context implicitly becomes the portal root if needed.
4. Returns an empty resultset when the query has no valid indexes.
  • Loading branch information
jaroel committed Feb 9, 2015
1 parent c0487c8 commit 652557c
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 6 deletions.
78 changes: 72 additions & 6 deletions docs/content.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,84 @@ The following operations will get objects from the stucture above, including usi
Find content objects
--------------------

You can use the *catalog* to search for content.
Here is a simple example:
You can use the find function to search for content.

Finding all Documents:

.. code-block:: python
from plone import api
documents = api.content.find(portal_type='Document')
.. invisible-code-block: python
self.assertGreater(len(documents), 0)
Finding all Documents within a context:

.. code-block:: python
from plone import api
documents = api.content.find(
context=api.portal.get(), portal_type='Document')
.. invisible-code-block: python
self.assertGreater(len(documents), 0)
Limit search depth:

.. code-block:: python
from plone import api
catalog = api.portal.get_tool(name='portal_catalog')
documents = catalog(portal_type='Document')
documents = api.content.find(depth=1, portal_type='Document')
.. invisible-code-block: python
self.assertEqual(catalog.__class__.__name__, 'CatalogTool')
self.assertEqual(len(documents), 3)
self.assertGreater(len(documents), 0)
Limit search depth within a context:

.. code-block:: python
from plone import api
documents = api.content.find(
context=api.portal.get(), depth=1, portal_type='Document')
.. invisible-code-block: python
self.assertGreater(len(documents), 0)
Search by interface:

.. code-block:: python
from plone import api
from Products.ATContentTypes.interfaces.document import IATDocument
documents = api.content.find(object_provides=IATDocument)
.. invisible-code-block: python
self.assertGreater(len(documents), 0)
Combining multiple arguments:

.. code-block:: python
from plone import api
from Products.ATContentTypes.interfaces.document import IATDocument
documents = api.content.find(
context=api.portal.get(), depth=2, object_provides=IATDocument,
SearchableText='Team')
.. invisible-code-block: python
self.assertGreater(len(documents), 0)
More information about how to use the catalog may be found in the `Plone Documentation <http://docs.plone.org/develop/plone/searching_and_indexing/index.html>`_.
Note that the catalog returns *brains* (metadata stored in indexes) and not objects.
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def read(*rnames):
'Products.statusmessages',
'Zope2', # Globals, OFS(tests),
'decorator',
'plone.app.contentlisting',
'plone.app.uuid',
'plone.uuid',
'setuptools',
Expand All @@ -55,6 +56,7 @@ def read(*rnames):
'zest.releaser',
],
'test': [
'Products.Archetypes',
'Products.MailHost',
'Products.CMFPlone',
'Products.ZCatalog',
Expand Down
90 changes: 90 additions & 0 deletions src/plone/api/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from Products.CMFCore.interfaces import ISiteRoot
from Products.CMFCore.WorkflowCore import WorkflowException
from copy import copy as _copy
from plone.app.contentlisting.interfaces import IContentListing
from plone.api import portal
from plone.api.exc import InvalidParameterError
from plone.api.validation import at_least_one_of
Expand Down Expand Up @@ -473,3 +474,92 @@ def get_uuid(obj=None):
:Example: :ref:`content_get_uuid_example`
"""
return IUUID(obj)


def find(context=None, depth=None, **kwargs):
"""Find content in the portal.
Find works alike catalog(). Indexes are passing in as arguments with the
search query as the values.
Specify indexes as arguments:
>>> find(portal_type='Document')
or combinations of indexes.
>>> find(portal_type='Document', SearchableText='Team')
Differences to using the catalog directly are:
The context argument allows passing in an context object, instead
of path='/'.join(context.getPhysicalPath().
>>> find(context=context)
- or -
>>> find(context=context, portal_type='Document')
Specifing the search depth is supported using the `depth` argument.
>>> find(depth=1)
Using `depth` needs a context for it's path. If no context is passed, the
portal root is used.
>>> find(context=portal, depth=1, portal_type='Document')
- or -
>>> find(depth=1, portal_type='Document')
The path can be queried directly, too:
>>> find(path={'query': '/plone/about/team', 'depth': 1})
The `object_provides` index/argument allows Interface objects as well as
identifiers.
>>> find(object_provides=IATDocument)
- or -
>>> find(object_provides=IATDocument.__identifier__)
An empty resultset is returned if no valid indexes are queried.
>>> len(find())
>>> 0
:param context: Context for the search
:type obj: Content object
:param depth: How far in the content tree we want to search from context
:type obj: Content object
:returns: ContentListing objects
:rtype: ContentListing
:Example: :ref:`content_find_example`
"""
query = {}
query.update(**kwargs)

# Passing a context or depth overrides the exising path query

This comment has been minimized.

Copy link
@bogdangi

bogdangi Feb 11, 2015

There is a typo "exising" => "existing"

if context or depth:
query['path'] = {}

# Limit search depth
if depth is not None:
# If we don't have a context, we'll assume the portal root.
if context is None:
context = portal.get()
query['path']['depth'] = depth

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):
object_provides = [object_provides]
for k, v in enumerate(object_provides):
if not isinstance(v, basestring):
object_provides[k] = v.__identifier__
query['object_provides'] = object_provides

# Make sure we don't dump the whole catalog.
catalog = portal.get_tool('portal_catalog')
indexes = catalog.indexes()
valid_indexes = [index for index in query if index in indexes]
if not valid_indexes:
return IContentListing([])

return IContentListing(catalog(**query))
78 changes: 78 additions & 0 deletions src/plone/api/tests/test_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,84 @@ def test_delete_multiple(self):
assert 'copy_of_about' not in container
assert 'about' not in container['events']

def test_find(self):
"""Test the finding of content in various ways."""

# Find documents
documents = api.content.find(portal_type='Document')
self.assertEqual(len(documents), 2)

def test_find_empty_query(self):
"""Make sure an empty query yields no results"""

documents = api.content.find()
self.assertEqual(len(documents), 0)

def test_find_invalid_indexes(self):
"""Make sure invalid indexes yield no results"""

# All invalid indexes yields no results
documents = api.content.find(invalid_index='henk')
self.assertEqual(len(documents), 0)

# But at least one valid index does.
documents = api.content.find(
invalid_index='henk', portal_type='Document')
self.assertEqual(len(documents), 2)

def test_find_context(self):
# Find documents in context
documents = api.content.find(
context=self.portal.about, portal_type='Document')
self.assertEqual(len(documents), 2)
documents = api.content.find(
context=self.portal.events, portal_type='Document')
self.assertEqual(len(documents), 0)

def test_find_depth(self):
# Limit search depth from portal root
documents = api.content.find(depth=2, portal_type='Document')
self.assertEqual(len(documents), 2)
documents = api.content.find(depth=1, portal_type='Document')
self.assertEqual(len(documents), 0)

# Limit search depth with explicit context
documents = api.content.find(
context=self.portal.about, depth=1, portal_type='Document')
self.assertEqual(len(documents), 2)
documents = api.content.find(
context=self.portal.about, depth=0, portal_type='Document')
self.assertEqual(len(documents), 0)

def test_find_interface(self):
# Find documents by interface or it's identier

This comment has been minimized.

Copy link
@bogdangi

bogdangi Feb 11, 2015

there is a typo I think "identier" => "identifier"

from Products.ATContentTypes.interfaces.document import IATDocument

identifier = IATDocument.__identifier__
documents = api.content.find(object_provides=identifier)
self.assertEqual(len(documents), 2)

documents = api.content.find(object_provides=IATDocument)
self.assertEqual(len(documents), 2)

def test_find_dict(self):
# Pass arguments using dict
path = '/'.join(self.portal.about.getPhysicalPath())

query = {
'portal_type': 'Document',
'path': {'query': path, 'depth': 2}
}
documents = api.content.find(**query)
self.assertEqual(len(documents), 2)

query = {
'portal_type': 'Document',
'path': {'query': path, 'depth': 0}
}
documents = api.content.find(**query)
self.assertEqual(len(documents), 0)

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

Expand Down

0 comments on commit 652557c

Please sign in to comment.