Back to index

plone3  3.1.7
catalog.py
Go to the documentation of this file.
00001 import itertools
00002 from zope.interface import implements, classProvides
00003 from zope.schema.interfaces import ISource, IContextSourceBinder
00004 
00005 from zope.app.form.browser.interfaces import ISourceQueryView, ITerms
00006 from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
00007 
00008 from plone.app.vocabularies.terms import BrowsableTerm
00009 
00010 from Products.ZCTextIndex.ParseTree import ParseError
00011 from Products.CMFCore.utils import getToolByName
00012 
00013 
00014 def parse_query(query, path_prefix=""):
00015     """ Parse the query string and turn it into a dictionary for querying the
00016         catalog.
00017 
00018         We want to find anything which starts with the given string, so we add
00019         a * at the end of words.
00020 
00021         >>> parse_query('foo')
00022         {'SearchableText': 'foo*'}
00023 
00024         If we have more than one word, each of them should have the * and
00025         they should be combined with the AND operator.
00026 
00027         >>> parse_query('foo bar')
00028         {'SearchableText': 'foo* AND bar*'}
00029 
00030         We also filter out some special characters. They are handled like
00031         spaces and seperate words from each other.
00032 
00033         >>> parse_query('foo +bar some-thing')
00034         {'SearchableText': 'foo* AND bar* AND some* AND thing*'}
00035 
00036         >>> parse_query('what? (spam) *ham')
00037         {'SearchableText': 'what* AND spam* AND ham*'}
00038 
00039         You can also limit searches to paths, if you only supply the path,
00040         then all contents of that folder will be searched. If you supply
00041         additional search words, then all subfolders are searched as well.
00042 
00043         >>> parse_query('path:/dummy')
00044         {'path': {'query': '/dummy', 'depth': 1}}
00045 
00046         >>> parse_query('bar path:/dummy')
00047         {'path': {'query': '/dummy'}, 'SearchableText': 'bar*'}
00048 
00049         >>> parse_query('path:/dummy foo')
00050         {'path': {'query': '/dummy'}, 'SearchableText': 'foo*'}
00051 
00052         If you supply more then one path, then only the last one is used.
00053 
00054         >>> parse_query('path:/dummy path:/spam')
00055         {'path': {'query': '/spam', 'depth': 1}}
00056 
00057         You can also provide a prefix for the path. This is useful for virtual
00058         hosting.
00059 
00060         >>> parse_query('path:/dummy', path_prefix='/portal')
00061         {'path': {'query': '/portal/dummy', 'depth': 1}}
00062 
00063     """
00064     query_parts = query.split()
00065     query = {'SearchableText': []}
00066     for part in query_parts:
00067         if part.startswith('path:'):
00068             path = part[5:]
00069             query['path'] = {'query': path}
00070         else:
00071             query['SearchableText'].append(part)
00072     text = " ".join(query['SearchableText'])
00073     for char in '?-+*()':
00074         text = text.replace(char, ' ')
00075     query['SearchableText'] = " AND ".join(x+"*" for x in text.split())
00076     if query.has_key('path'):
00077         if query['SearchableText'] == '':
00078             del query['SearchableText']
00079             query["path"]["depth"] = 1
00080         query["path"]["query"] = path_prefix + query["path"]["query"]
00081     return query
00082 
00083 
00084 class SearchableTextSource(object):
00085     """
00086       >>> from plone.app.vocabularies.tests.base import Brain
00087       >>> from plone.app.vocabularies.tests.base import DummyContext
00088       >>> from plone.app.vocabularies.tests.base import DummyTool
00089 
00090       >>> context = DummyContext()
00091 
00092       >>> tool = DummyTool('portal_catalog')
00093       >>> rids = ('/1234', '/2345')
00094       >>> def getrid(value):
00095       ...     return value in rids and value or None
00096       >>> tool.getrid = getrid
00097       >>> def call(**values):
00098       ...     if values['SearchableText'].startswith('error'):
00099       ...         raise ParseError
00100       ...     return [Brain(r) for r in rids]
00101       >>> tool.__call__ = call
00102       >>> context.portal_catalog = tool
00103 
00104       >>> tool = DummyTool('portal_url')
00105       >>> def getPortalPath():
00106       ...     return '/'
00107       >>> tool.getPortalPath = getPortalPath
00108       >>> context.portal_url = tool
00109 
00110       >>> source = SearchableTextSource(context)
00111       >>> source
00112       <plone.app.vocabularies.catalog.SearchableTextSource object at ...>
00113 
00114       >>> '1234' in source, '1' in source
00115       (True, False)
00116 
00117       >>> source.search('')
00118       []
00119 
00120       >>> source.search('error')
00121       []
00122 
00123       >>> source.search('foo')
00124       <generator object at ...>
00125 
00126       >>> list(source.search('foo'))
00127       ['1234', '2345']
00128 
00129       >>> list(source.search('bar path:/dummy'))
00130       ['/dummy', '1234', '2345']
00131 
00132       >>> source = SearchableTextSource(context, default_query='default')
00133       >>> list(source.search(''))
00134       ['1234', '2345']
00135     """
00136     implements(ISource)
00137     classProvides(IContextSourceBinder)
00138 
00139     def __init__(self, context, base_query={}, default_query=None):
00140         self.context = context
00141         self.base_query = base_query
00142         self.default_query = default_query
00143         self.catalog = getToolByName(context, "portal_catalog")
00144         self.portal_tool = getToolByName(context, "portal_url")
00145         self.portal_path = self.portal_tool.getPortalPath()
00146 
00147     def __contains__(self, value):
00148         """Return whether the value is available in this source
00149         """
00150         if self.catalog.getrid(self.portal_path + value) is None:
00151             return False
00152         return True
00153 
00154     def search(self, query_string):
00155         query = self.base_query.copy()
00156         if query_string == '':
00157             if self.default_query is not None:
00158                 query.update(parse_query(self.default_query, self.portal_path))
00159             else:
00160                 return []
00161         else:
00162             query.update(parse_query(query_string, self.portal_path))
00163         
00164         try:
00165             results = (x.getPath()[len(self.portal_path):] for x in self.catalog(**query))
00166         except ParseError:
00167             return []
00168         
00169         if query.has_key('path'):
00170             path = query['path']['query'][len(self.portal_path):]
00171             if path != '':
00172                 return itertools.chain((path,), results)
00173         return results
00174 
00175 class SearchableTextSourceBinder(object):
00176     """Use this to instantiate a new SearchableTextSource with custom
00177     parameters. For example:
00178     
00179     target_folder = schema.Choice(
00180         title=_(u"Target folder"),
00181         description=_(u"As a path relative to the portal root"),
00182         required=True,
00183         source=SearchableTextSourceBinder({'is_folderish' : True}),
00184         )
00185 
00186     This ensures that the is_folderish=True is always in the query used.
00187 
00188       >>> query = {'query': 'query'}
00189 
00190       >>> binder = SearchableTextSourceBinder(query)
00191       >>> binder
00192       <plone.app.vocabularies.catalog.SearchableTextSourceBinder object at ...>
00193 
00194       >>> binder.query == query
00195       True
00196 
00197       >>> from plone.app.vocabularies.tests.base import Brain
00198       >>> from plone.app.vocabularies.tests.base import DummyContext
00199       >>> from plone.app.vocabularies.tests.base import DummyTool
00200 
00201       >>> context = DummyContext()
00202 
00203       >>> tool = DummyTool('portal_catalog')
00204       >>> context.portal_catalog = tool
00205 
00206       >>> tool = DummyTool('portal_url')
00207       >>> def getPortalPath():
00208       ...     return '/'
00209       >>> tool.getPortalPath = getPortalPath
00210       >>> context.portal_url = tool
00211 
00212       >>> source = binder(context)
00213       >>> source
00214       <plone.app.vocabularies.catalog.SearchableTextSource object at ...>
00215 
00216       >>> source.base_query == query
00217       True
00218     """
00219     
00220     implements(IContextSourceBinder)
00221     
00222     def __init__(self, query, default_query=None):
00223         self.query = query
00224         self.default_query = default_query
00225         
00226     def __call__(self, context):
00227         return SearchableTextSource(context, base_query=self.query.copy(),
00228                                     default_query=self.default_query)
00229     
00230 
00231 class QuerySearchableTextSourceView(object):
00232     """
00233       >>> from plone.app.vocabularies.tests.base import DummyCatalog
00234       >>> from plone.app.vocabularies.tests.base import DummyContext
00235       >>> from plone.app.vocabularies.tests.base import DummyTool
00236       >>> from plone.app.vocabularies.tests.base import Request
00237 
00238       >>> context = DummyContext()
00239 
00240       >>> rids = ('/1234', '/2345', '/dummy/1234')
00241       >>> tool = DummyCatalog(rids)
00242       >>> context.portal_catalog = tool
00243 
00244       >>> tool = DummyTool('portal_url')
00245       >>> def getPortalPath():
00246       ...     return '/dummy'
00247       >>> tool.getPortalPath = getPortalPath
00248       >>> context.portal_url = tool
00249 
00250       >>> source = SearchableTextSource(context)
00251       >>> source
00252       <plone.app.vocabularies.catalog.SearchableTextSource object at ...>
00253 
00254       >>> view = QuerySearchableTextSourceView(source, Request())
00255       >>> view
00256       <plone.app.vocabularies.catalog.QuerySearchableTextSourceView object ...>
00257 
00258       >>> view.getValue('a')
00259       Traceback (most recent call last):
00260       ...
00261       LookupError: a
00262 
00263       >>> view.getValue('/1234')
00264       '/1234'
00265 
00266       >>> view.getTerm(None) is None
00267       True
00268 
00269       >>> view.getTerm('1234')
00270       <plone.app.vocabularies.terms.BrowsableTerm object at ...>
00271 
00272       >>> view.getTerm('/1234')
00273       <plone.app.vocabularies.terms.BrowsableTerm object at ...>
00274 
00275       >>> template = view.render(name='t')
00276       >>> u'<input type="text" name="t.query" value="" />' in template
00277       True
00278 
00279       >>> u'<input type="submit" name="t.search" value="Search" />' in template
00280       True
00281 
00282       >>> request = Request(form={'t.search' : True, 't.query' : 'value'})
00283       >>> view = QuerySearchableTextSourceView(source, request)
00284       >>> list(view.results('t'))
00285       ['', '/1234', '']
00286 
00287       >>> request = Request(form={'t.search' : True, 't.query' : 'value',
00288       ...                         't.browse.foo' : '/foo'})
00289       >>> view = QuerySearchableTextSourceView(source, request)
00290       >>> list(view.results('t'))
00291       ['foo', '', '/1234', '']
00292     """
00293 
00294     implements(ITerms,
00295                ISourceQueryView)
00296 
00297     template = ViewPageTemplateFile('searchabletextsource.pt')
00298 
00299     def __init__(self, context, request):
00300         self.context = context
00301         self.request = request
00302 
00303     def getTerm(self, value):
00304         if not value:
00305             return None
00306         if (not self.context.portal_path.endswith('/')) \
00307                and (not value.startswith('/')):
00308             value = '/' + value
00309         # get rid for path
00310         rid = self.context.catalog.getrid(self.context.portal_path + value)
00311         # first some defaults
00312         token = value
00313         title = value
00314         browse_token = None
00315         parent_token = None
00316         if rid is not None:
00317             # fetch the brain from the catalog
00318             brain = self.context.catalog._catalog[rid]
00319             title = brain.Title
00320             if brain.is_folderish:
00321                 browse_token = value
00322             parent_token = "/".join(value.split("/")[:-1])
00323         return BrowsableTerm(value, token=token, title=title, 
00324                              description=value,
00325                              browse_token=browse_token,
00326                              parent_token=parent_token)
00327 
00328     def getValue(self, token):
00329         if token not in self.context:
00330             raise LookupError(token)
00331         return token
00332 
00333     def render(self, name):
00334         return self.template(name=name)
00335 
00336     def results(self, name):
00337         query = ''
00338 
00339         # check whether the normal search button was pressed
00340         if name+".search" in self.request.form:
00341             query_fieldname = name+".query"
00342             if query_fieldname in self.request.form:
00343                 query = self.request.form[query_fieldname]
00344 
00345         # check whether a browse button was pressed
00346         browse_prefix = name+".browse."
00347         browse = tuple(x for x in self.request.form
00348                        if x.startswith(browse_prefix))
00349         if len(browse) == 1:
00350             path = browse[0][len(browse_prefix):]
00351             query = "path:" + path
00352             results = self.context.search(query)
00353             if name+".omitbrowsedfolder" in self.request.form:
00354                 results = itertools.ifilter(lambda x: x != path, results)
00355         else:
00356             results = self.context.search(query)
00357 
00358         return results