diff --git a/README.txt b/README.txt
index 00fa563..1bb2288 100755
--- a/README.txt
+++ b/README.txt
@@ -317,8 +317,8 @@ Index attributes adapter
>>> from loops.resource import IndexAttributes
>>> idx = IndexAttributes(doc1)
- >>> idx.text()
- u'doc1 Zope Info'
+ >>> idx.text() is None
+ True
>>> idx.title()
u'doc1 Zope Info'
diff --git a/browser/configure.zcml b/browser/configure.zcml
index 85909a1..e1fef37 100644
--- a/browser/configure.zcml
+++ b/browser/configure.zcml
@@ -539,6 +539,15 @@
permission="zope.View"
/>
+
+
+
+
+ Listing
+
+
+
+
+
-
-
+
+
-
-
+
+
@@ -193,10 +215,6 @@
-
-
diff --git a/concept.py b/concept.py
index 7594388..9e47451 100644
--- a/concept.py
+++ b/concept.py
@@ -291,9 +291,9 @@ class IndexAttributes(object):
def __init__(self, context):
self.context = context
- # obsolete, use TextIndexNG (with indexableContent() method) instead
def text(self):
context = self.context
+ # TODO: include attributes provided by concept type
return ' '.join((zapi.getName(context), context.title,))
def title(self):
diff --git a/query.py b/query.py
index 83f690a..fed9ee0 100644
--- a/query.py
+++ b/query.py
@@ -22,9 +22,11 @@ Query management stuff.
$Id$
"""
+from zope import schema, component
from zope.interface import Interface, Attribute, implements
+from zope.app import traversing
from zope.cachedescriptors.property import Lazy
-from zope import schema
+from zope.app.catalog.interfaces import ICatalog
from loops.interfaces import IConcept
from loops.common import AdapterBase
@@ -41,6 +43,102 @@ class IQuery(Interface):
"""
+class BaseQuery(object):
+
+ implements(IQuery)
+
+ def __init__(self, context):
+ self.context = context
+
+ @Lazy
+ def catalog(self):
+ return component.getUtility(ICatalog)
+
+ @Lazy
+ def loopsRoot(self):
+ return self.context.context.getLoopsRoot()
+
+ def queryConcepts(self, title=None, type=None):
+ if type.endswith('*'):
+ start = type[:-1]
+ end = start + '\x7f'
+ else:
+ start = end = type
+ cat = self.catalog
+ if title:
+ result = cat.searchResults(loops_type=(start, end), loops_title=title)
+ else:
+ result = cat.searchResults(loops_type=(start, end))
+ result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot)
+ return result
+
+ def queryConceptsWithChildren(self, title=None, type=None):
+ if title: # there are a few characters that the index doesn't like
+ title = title.replace('(', ' ').replace(')', ' ')
+ if not title and (type is None or '*' in type):
+ return None
+ result = set()
+ queue = list(self.queryConcepts(title=title, type=type))
+ concepts = []
+ while queue:
+ c = queue.pop(0)
+ concepts.append(c)
+ for child in c.getChildren():
+ # TODO: check for tree level, use relevance factors, ...
+ if child not in queue and child not in concepts:
+ queue.append(child)
+ for c in concepts:
+ result.add(c)
+ result.update(c.getResources())
+ return result
+
+
+class FullQuery(BaseQuery):
+
+ def query(self, text=None, type=None, useTitle=True, useFull=False,
+ conceptTitle=None, conceptType=None):
+ result = set()
+ rc = self.queryConceptsWithChildren(title=conceptTitle, type=conceptType)
+ if not rc and not text and '*' in type: # there should be some sort of selection...
+ return result
+ if text or type != 'loops:*': # TODO: this may be highly inefficient!
+ cat = self.catalog
+ if useFull and text:
+ criteria = {'loops_text': text}
+ r1 = set(cat.searchResults(**criteria))
+ else:
+ r1 = set()
+ if type.endswith('*'):
+ start = type[:-1]
+ end = start + '\x7f'
+ else:
+ start = end = type
+ criteria = {'loops_type': (start, end),}
+ if useTitle and text:
+ criteria['loops_title'] = text
+ r2 = set(cat.searchResults(**criteria))
+ result = r1.union(r2)
+ if rc is not None:
+ if result:
+ result = result.intersection(rc)
+ else:
+ result = rc
+ result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot)
+ return result
+
+
+class ConceptQuery(BaseQuery):
+ """ Find concepts of type `type` whose title starts with `title`.
+ """
+
+ def query(self, title=None, type=None):
+ if title and not title.endswith('*'):
+ title += '*'
+ return self.queryConcepts(title=title, type=type)
+
+
+# QueryConcept: concept objects that allow querying the database.
+
class IQueryConcept(Interface):
""" The schema for the query type.
"""
diff --git a/resource.py b/resource.py
index 301dd86..e8b6dbd 100644
--- a/resource.py
+++ b/resource.py
@@ -234,10 +234,14 @@ class IndexAttributes(object):
def __init__(self, context):
self.context = context
- # obsolete, use TextIndexNG (with indexableContent() method) instead
def text(self):
context = self.context
- return ' '.join((zapi.getName(context), context.title, context.data)).strip()
+ if not context.contentType.startswith('text'):
+ return None
+ data = context.data
+ # TODO: transform to plain text
+ #return ' '.join((zapi.getName(context), context.title, data)).strip()
+ return data
def title(self):
context = self.context
@@ -245,6 +249,8 @@ class IndexAttributes(object):
class IndexableResource(object):
+ """ Used for TextIndexNG.
+ """
implements(IIndexableContent)
adapts(IResource)
diff --git a/search/README.txt b/search/README.txt
index c26e2fe..877f655 100755
--- a/search/README.txt
+++ b/search/README.txt
@@ -152,8 +152,8 @@ purposes fairly primitive) catalog and a resource we can search for:
Search via related concepts
---------------------------
- We first have to prepare some test concepts (topics); we also assign our test
- resource (rplone) from above to one of the topics:
+We first have to prepare some test concepts (topics); we also assign our test
+resource (rplone) from above to one of the topics:
>>> czope = concepts['zope'] = Concept('Zope')
>>> czope2 = concepts['zope2'] = Concept('Zope 2')
@@ -166,11 +166,11 @@ Search via related concepts
>>> czope2.assignChild(cplone)
>>> rplone.assignConcept(cplone)
- Now we can fill our search form and execute the query; note that all concepts
- found are listed, plus all their children and all resources associated
- with them:
+Now we can fill our search form and execute the query; note that all concepts
+found are listed, plus all their children and all resources associated
+with them:
- >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': u'zope'}
+ >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text_selected': u'zope'}
>>> request = TestRequest(form=form)
>>> resultsView = SearchResults(page, request)
>>> results = list(resultsView.results)
@@ -179,7 +179,7 @@ Search via related concepts
>>> results[0].context.__name__
u'plone'
- >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': u'zope3'}
+ >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text_selected': u'zope3'}
>>> request = TestRequest(form=form)
>>> resultsView = SearchResults(page, request)
>>> results = list(resultsView.results)
@@ -187,3 +187,11 @@ Search via related concepts
1
>>> results[0].context.__name__
u'zope3'
+
+To support easy entry of concepts to search for we can preselect the available
+concepts (optionally restricted to a certain type) by entering text parts
+of the concepts' titles:
+
+TODO...
+
+
diff --git a/search/browser.py b/search/browser.py
index 9ecea95..2f01439 100644
--- a/search/browser.py
+++ b/search/browser.py
@@ -23,9 +23,8 @@ loops.search package.
$Id$
"""
-from zope.app import zapi
from zope import interface, component
-from zope.app.catalog.interfaces import ICatalog
+from zope.app import traversing
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.formlib.namedtemplate import NamedTemplate, NamedTemplateImplementation
@@ -34,6 +33,7 @@ from zope.i18nmessageid import MessageFactory
from cybertools.ajax import innerHtml
from cybertools.typology.interfaces import ITypeManager
from loops.browser.common import BaseView
+from loops.query import ConceptQuery, FullQuery
from loops import util
from loops.util import _
@@ -51,10 +51,6 @@ class Search(BaseView):
def macro(self):
return template.macros['search']
- @Lazy
- def catalog(self):
- return component.getUtility(ICatalog)
-
@property
def rowNum(self):
""" Return the rowNum to be used for identifying the current search
@@ -84,19 +80,11 @@ class Search(BaseView):
"""
request = self.request
request.response.setHeader('Content-Type', 'text/plain; charset=UTF-8')
- text = request.get('searchString', '').replace('(', ' ').replace(')', ' ')
+ title = request.get('searchString', '').replace('(', ' ').replace(')', ' ')
type = request.get('searchType') or 'loops:concept:*'
- if type.endswith('*'):
- start = type[:-1]
- end = start + '\x7f'
- else:
- start = end = type
- cat = self.catalog
- if text:
- result = cat.searchResults(loops_type=(start, end), loops_text=text+'*')
- else:
- result = cat.searchResults(loops_type=(start, end))
- return str(sorted([[`o.title`[2:-1], `zapi.getName(o)`[2:-1]]
+ result = ConceptQuery(self).query(title=title, type=type)
+ # simple way to provide JSON format:
+ return str(sorted([[`o.title`[2:-1], `traversing.api.getName(o)`[2:-1]]
for o in result])).replace('\\\\x', '\\x')
def submitReplacing(self, targetId, formId, view):
@@ -116,79 +104,18 @@ class SearchResults(BaseView):
def __call__(self):
return innerHtml(self)
- @Lazy
- def catalog(self):
- return component.getUtility(ICatalog)
-
@Lazy
def results(self):
- result = set()
request = self.request
- r3 = self.queryConcepts()
type = request.get('search.1.text', 'loops:*')
text = request.get('search.2.text')
- if not r3 and not text and '*' in type: # there should be some sort of selection...
- return result
- #if r3 and type != 'loops:*':
- # typeName = type.split(':')[-1]
- # r3 = set(o for o in r3 if self.isType(o, typeName))
- #if text or not '*' in loops:
- if text or type != 'loops:*': # TODO: this may be highly inefficient! see above
- useTitle = request.get('search.2.title')
- useFull = request.get('search.2.full')
- r1 = set()
- cat = self.catalog
- if useFull and text and not type.startswith('loops:concept:'):
- criteria = {'loops_resource_textng': {'query': text},}
- r1 = set(cat.searchResults(**criteria))
- if type.endswith('*'):
- start = type[:-1]
- end = start + '\x7f'
- else:
- start = end = type
- criteria = {'loops_type': (start, end),}
- if useTitle and text:
- criteria['loops_title'] = text
- r2 = set(cat.searchResults(**criteria))
- result = r1.union(r2)
- result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot)
- if r3 is not None:
- if result:
- result = result.intersection(r3)
- else:
- result = r3
+ useTitle = request.get('search.2.title')
+ useFull = request.get('search.2.full')
+ conceptType = request.get('search.3.type', 'loops:concept:*')
+ conceptTitle = request.get('search.3.text_selected')
+ result = FullQuery(self).query(text=text, type=type,
+ useTitle=useTitle, useFull=useFull,
+ conceptTitle=conceptTitle, conceptType= conceptType)
result = sorted(result, key=lambda x: x.title.lower())
return self.viewIterator(result)
- def queryConcepts(self):
- result = set()
- cat = self.catalog
- request = self.request
- type = request.get('search.3.type', 'loops:concept:*')
- text = request.get('search.3.text')
- if text: # there are a few characters that the index doesn't like
- text = text.replace('(', ' ').replace(')', ' ')
- if not text and '*' in type:
- return None
- if type.endswith('*'):
- start = type[:-1]
- end = start + '\x7f'
- else:
- start = end = type
- criteria = {'loops_type': (start, end),}
- if text:
- criteria['loops_title'] = text
- queue = list(cat.searchResults(**criteria))
- concepts = []
- while queue:
- c = queue.pop(0)
- concepts.append(c)
- for child in c.getChildren():
- # TODO: check for tree level, use relevance factors, ...
- if child not in queue and child not in concepts:
- queue.append(child)
- for c in concepts:
- result.add(c)
- result.update(c.getResources())
- return result
-