Skip to content

Commit

Permalink
Fix AttributeError: 'list' object has no attribute 'lstrip' in a virt…
Browse files Browse the repository at this point in the history
…ual hosting setting when restricting search to multiple paths (path.query is a list).
  • Loading branch information
sunew committed Sep 5, 2018
1 parent 0952713 commit 22d610e
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 5 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Expand Up @@ -4,7 +4,10 @@ Changelog
3.4.5 (unreleased)
------------------

- Nothing changed yet.
Bugfixes:

- Make search work with a path query containing a list of paths in a virtual hosting setting.
[sunew]


3.4.4 (2018-08-31)
Expand Down
18 changes: 18 additions & 0 deletions docs/source/searching.rst
Expand Up @@ -70,6 +70,24 @@ This dictionary will need to be flattened in dotted notation in order to pass it
Again, this is very similar to how `Record Arguments <http://docs.zope.org/zope2/zdgbook/ObjectPublishing.html?highlight=record#record-arguments>`_ are parsed by ZPublisher, except that you can omit the ``:record`` suffix.


Restricting search to multiple paths
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To restrict search to multiple paths, the original query as a Python dictionary would look like this (with an optional depth and sort_on)::

query = {'path': {'query': ('/folder', '/folder2'),
'depth': 2},
'sort_on': 'path'}

This dictionary will need to be flattened in dotted notation in order to pass it in a query string. In order to specify multiple paths, simply repeat the query string parameter (the ``requests`` module will do this automatically for you if you pass it a list of values for a query string parameter).

.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/search_multiple_paths.req

.. literalinclude:: ../../src/plone/restapi/tests/http-examples/search_multiple_paths.resp
:language: http


Data types in queries
^^^^^^^^^^^^^^^^^^^^^

Expand Down
19 changes: 15 additions & 4 deletions src/plone/restapi/search/handler.py
Expand Up @@ -33,7 +33,10 @@ def _constrain_query_by_path(self, query):
if 'path' not in query:
query['path'] = {}

if isinstance(query['path'], str):
if (
isinstance(query['path'], str)
or isinstance(query['path'], list)
):
query['path'] = {'query': query['path']}

# If this is accessed through a VHM the client does not know
Expand All @@ -43,9 +46,17 @@ def _constrain_query_by_path(self, query):
if vhm_physical_path:
path = query['path'].get('query')
if path:
path = path.lstrip('/')
full_path = '/'.join(vhm_physical_path + (path,))
query['path']['query'] = full_path
if isinstance(path, str):
path = path.lstrip('/')
full_path = '/'.join(vhm_physical_path + (path,))
query['path']['query'] = full_path
if isinstance(path, list):
full_paths = []
for p in path:
p = p.lstrip('/')
full_path = '/'.join(vhm_physical_path + (p,))
full_paths.append(full_path)
query['path']['query'] = full_paths

if isinstance(query['path'], dict) and 'query' not in query['path']:
# We either had no 'path' parameter at all, or an incomplete
Expand Down
@@ -0,0 +1,3 @@
GET /plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&sort_on=path&path.depth=2 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
37 changes: 37 additions & 0 deletions src/plone/restapi/tests/http-examples/search_multiple_paths.resp
@@ -0,0 +1,37 @@
HTTP/1.1 200 OK
Content-Type: application/json

{
"@id": "http://localhost:55001/plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&path.depth=2",
"items": [
{
"@id": "http://localhost:55001/plone/folder1",
"@type": "Folder",
"description": "",
"review_state": "private",
"title": "Folder 1"
},
{
"@id": "http://localhost:55001/plone/folder1/doc1",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Lorem Ipsum"
},
{
"@id": "http://localhost:55001/plone/folder2",
"@type": "Folder",
"description": "",
"review_state": "private",
"title": "Folder 2"
},
{
"@id": "http://localhost:55001/plone/folder2/doc2",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Lorem Ipsum"
}
],
"items_total": 4
}
29 changes: 29 additions & 0 deletions src/plone/restapi/tests/test_documentation.py
Expand Up @@ -424,6 +424,35 @@ def test_documentation_search_options(self):
response = self.api_session.get('/@search', params=query)
save_request_and_response_for_docs('search_options', response)

def test_documentation_search_multiple_paths(self):
self.portal.invokeFactory(
'Folder',
id='folder1',
title='Folder 1'
)
self.portal.folder1.invokeFactory(
'Document',
id='doc1',
title='Lorem Ipsum'
)
self.portal.invokeFactory(
'Folder',
id='folder2',
title='Folder 2'
)
self.portal.folder2.invokeFactory(
'Document',
id='doc2',
title='Lorem Ipsum'
)
import transaction
transaction.commit()
query = {'sort_on': 'path',
'path.query': ['/plone/folder1', '/plone/folder2'],
'path.depth': '2'}
response = self.api_session.get('/@search', params=query)
save_request_and_response_for_docs('search_multiple_paths', response)

def test_documentation_search_metadata_fields(self):
self.portal.invokeFactory(
'Document',
Expand Down
101 changes: 101 additions & 0 deletions src/plone/restapi/tests/test_search.py
Expand Up @@ -74,6 +74,23 @@ def setUp(self):
test_bool_field=False,
)

# /plone/folder2
self.folder2 = createContentInContainer(
self.portal, u'Folder',
id=u'folder2',
title=u'Another Folder')

# /plone/folder2/doc
createContentInContainer(
self.folder2, u'DXTestDocument',
id='doc',
title=u'Document in second folder',
start=DateTime(1975, 1, 1, 0, 0),
effective=DateTime(2015, 1, 1, 0, 0),
expires=DateTime(2020, 1, 1, 0, 0),
test_bool_field=False,
)

# /plone/doc-outside-folder
createContentInContainer(
self.portal, u'DXTestDocument',
Expand Down Expand Up @@ -133,10 +150,61 @@ def test_search_in_vhm(self):
u'/folder/other-document'},
set(result_paths(response.json())))

def test_search_in_vhm_multiple_paths(self):
# Install a Virtual Host Monster
if 'virtual_hosting' not in self.app.objectIds():
# If ZopeLite was imported, we have no default virtual
# host monster
from Products.SiteAccess.VirtualHostMonster \
import manage_addVirtualHostMonster
manage_addVirtualHostMonster(self.app, 'virtual_hosting')
transaction.commit()

# path as a list
query = {'path': [
'/folder',
'/folder2']
}

# If we go through the VHM we will get results for multiple paths
# if we only use the part of the path inside the VHM
vhm_url = (
'%s/VirtualHostBase/http/plone.org/plone/VirtualHostRoot/%s' %
(self.app.absolute_url(), '@search'))
response = self.api_session.get(vhm_url, params=query)
self.assertSetEqual(
{u'/folder',
u'/folder/doc',
u'/folder/other-document',
u'/folder2',
u'/folder2/doc'},
set(result_paths(response.json())))

# path as a dict with a query list
query = {'path.query': [
'/folder',
'/folder2']
}

# If we go through the VHM we will get results for multiple paths
# if we only use the part of the path inside the VHM
vhm_url = (
'%s/VirtualHostBase/http/plone.org/plone/VirtualHostRoot/%s' %
(self.app.absolute_url(), '@search'))
response = self.api_session.get(vhm_url, params=query)
self.assertSetEqual(
{u'/folder',
u'/folder/doc',
u'/folder/other-document',
u'/folder2',
u'/folder2/doc'},
set(result_paths(response.json())))

def test_path_gets_prefilled_if_missing_from_path_query_dict(self):
response = self.api_session.get('/@search?path.depth=1')
self.assertSetEqual(
{u'/plone/folder',
u'/plone/folder2',
u'/plone/doc-outside-folder'},
set(result_paths(response.json())))

Expand Down Expand Up @@ -361,6 +429,39 @@ def test_extended_path_index_query(self):
result_paths(response.json())
)

def test_extended_path_index_query_multiple(self):
# path as a list
query = {'path': [
'/'.join(self.folder.getPhysicalPath()),
'/'.join(self.folder2.getPhysicalPath())]
}
response = self.api_session.get('/@search', params=query)

self.assertEqual(
[u'/plone/folder',
u'/plone/folder/doc',
u'/plone/folder/other-document',
u'/plone/folder2',
u'/plone/folder2/doc'],
result_paths(response.json())
)

# path as a dict with a query list
query = {'path.query': [
'/'.join(self.folder.getPhysicalPath()),
'/'.join(self.folder2.getPhysicalPath())]
}
response = self.api_session.get('/@search', params=query)

self.assertEqual(
[u'/plone/folder',
u'/plone/folder/doc',
u'/plone/folder/other-document',
u'/plone/folder2',
u'/plone/folder2/doc'],
result_paths(response.json())
)

def test_extended_path_index_depth_limiting(self):
lvl1 = createContentInContainer(self.portal, u'Folder', id=u'lvl1')
lvl2 = createContentInContainer(lvl1, u'Folder', id=u'lvl2')
Expand Down

0 comments on commit 22d610e

Please sign in to comment.