moved query logic to query module; use standard text index instead of TextIndexNG

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1344 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2006-09-17 11:01:16 +00:00
parent 2d25015202
commit 92f6a06e01
9 changed files with 191 additions and 120 deletions

View file

@ -317,8 +317,8 @@ Index attributes adapter
>>> from loops.resource import IndexAttributes >>> from loops.resource import IndexAttributes
>>> idx = IndexAttributes(doc1) >>> idx = IndexAttributes(doc1)
>>> idx.text() >>> idx.text() is None
u'doc1 Zope Info' True
>>> idx.title() >>> idx.title()
u'doc1 Zope Info' u'doc1 Zope Info'

View file

@ -539,6 +539,15 @@
permission="zope.View" permission="zope.View"
/> />
<zope:adapter
name="listchildren"
for="loops.interfaces.INode
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.browser.node.ListChildren"
permission="zope.View"
/>
<!-- inner HTML views --> <!-- inner HTML views -->
<page <page

View file

@ -72,10 +72,14 @@ class NodeView(BaseView):
cm.register('portlet_left', 'navigation', title='Navigation', cm.register('portlet_left', 'navigation', title='Navigation',
subMacro=self.template.macros['menu']) subMacro=self.template.macros['menu'])
if not IUnauthenticatedPrincipal.providedBy(self.request.principal): if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
cm.register('portlet_right', 'actions', title='Actions', cm.register('portlet_right', 'clipboard', title='Clipboard',
subMacro=self.template.macros['actions']) subMacro=self.template.macros['clipboard'])
cm.register('portlet_right', 'personal', title='Personal Items', # this belongs to loops.organize; how to register portlets
subMacro=self.template.macros['personal']) # from sub- (other) packages?
# see controller / configurator: use multiple configurators;
# register additional configurators (adapters) from within package.
cm.register('portlet_right', 'worklist', title='Worklist',
subMacro=self.template.macros['worklist'])
@Lazy @Lazy
def view(self): def view(self):
@ -316,26 +320,27 @@ class InlineEdit(NodeView):
# special (named) views for nodes # special (named) views for nodes
class ListPages(NodeView): class SpecialNodeView(NodeView):
macroName = None # to be provided by subclass
@Lazy @Lazy
def macro(self): def macro(self):
return self.template.macros['listpages'] return self.template.macros[self.macroName]
@Lazy @Lazy
def view(self): def view(self):
return self return self
class ListResources(NodeView): class ListPages(SpecialNodeView):
macroName = 'listpages'
@Lazy class ListResources(SpecialNodeView):
def macro(self): macroName = 'listresources'
return self.template.macros['listresources']
@Lazy class ListChildren(SpecialNodeView):
def view(self): macroName = 'listchildren'
return self
class ConfigureView(NodeView): class ConfigureView(NodeView):

View file

@ -144,6 +144,28 @@
</metal:resources> </metal:resources>
<metal:children define-macro="listchildren"
tal:define="target nocall:item/target">
<div class="content-1"
tal:content="structure item/body"
tal:attributes="ondblclick python:
target and target.openEditWindow('configure.html')
or item.openEditWindow();
id string:${view/itemNum}.body;">
Listing
</div><br />
<div tal:attributes="ondblclick python: target.openEditWindow('configure.html')"
tal:define="item nocall:item/target"
tal:condition="nocall:target">
<div tal:repeat="related item/children">
<a href="#"
tal:attributes="href string:${view/url}/.target${related/uniqueId}"
tal:content="related/title">Resource Title</a>
</div>
</div>
</metal:children>
<!-- menu --> <!-- menu -->
<metal:menu define-macro="menu" <metal:menu define-macro="menu"
@ -171,13 +193,13 @@
<!-- portlets --> <!-- portlets -->
<metal:actions define-macro="actions"> <metal:actions define-macro="clipboard">
<div class="menu-2">Add Resource</div> <div class="menu-2">loops Development</div>
</metal:actions> </metal:actions>
<metal:actions define-macro="personal"> <metal:actions define-macro="worklist">
<div class="menu-2">Clipboard</div> <div class="menu-2">Create Resource...</div>
</metal:actions> </metal:actions>
@ -193,10 +215,6 @@
</form> </form>
</div> </div>
<!-- dojoType="Editor"
items="formatblock;|;insertunorderedlist;insertorderedlist;|;bold;italic;|;createLink;"
-->
<!-- edit and other links --> <!-- edit and other links -->

View file

@ -291,9 +291,9 @@ class IndexAttributes(object):
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
# obsolete, use TextIndexNG (with indexableContent() method) instead
def text(self): def text(self):
context = self.context context = self.context
# TODO: include attributes provided by concept type
return ' '.join((zapi.getName(context), context.title,)) return ' '.join((zapi.getName(context), context.title,))
def title(self): def title(self):

100
query.py
View file

@ -22,9 +22,11 @@ Query management stuff.
$Id$ $Id$
""" """
from zope import schema, component
from zope.interface import Interface, Attribute, implements from zope.interface import Interface, Attribute, implements
from zope.app import traversing
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope import schema from zope.app.catalog.interfaces import ICatalog
from loops.interfaces import IConcept from loops.interfaces import IConcept
from loops.common import AdapterBase 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): class IQueryConcept(Interface):
""" The schema for the query type. """ The schema for the query type.
""" """

View file

@ -234,10 +234,14 @@ class IndexAttributes(object):
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
# obsolete, use TextIndexNG (with indexableContent() method) instead
def text(self): def text(self):
context = self.context 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): def title(self):
context = self.context context = self.context
@ -245,6 +249,8 @@ class IndexAttributes(object):
class IndexableResource(object): class IndexableResource(object):
""" Used for TextIndexNG.
"""
implements(IIndexableContent) implements(IIndexableContent)
adapts(IResource) adapts(IResource)

View file

@ -152,8 +152,8 @@ purposes fairly primitive) catalog and a resource we can search for:
Search via related concepts Search via related concepts
--------------------------- ---------------------------
We first have to prepare some test concepts (topics); we also assign our test We first have to prepare some test concepts (topics); we also assign our test
resource (rplone) from above to one of the topics: resource (rplone) from above to one of the topics:
>>> czope = concepts['zope'] = Concept('Zope') >>> czope = concepts['zope'] = Concept('Zope')
>>> czope2 = concepts['zope2'] = Concept('Zope 2') >>> czope2 = concepts['zope2'] = Concept('Zope 2')
@ -166,11 +166,11 @@ Search via related concepts
>>> czope2.assignChild(cplone) >>> czope2.assignChild(cplone)
>>> rplone.assignConcept(cplone) >>> rplone.assignConcept(cplone)
Now we can fill our search form and execute the query; note that all concepts 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 found are listed, plus all their children and all resources associated
with them: 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) >>> request = TestRequest(form=form)
>>> resultsView = SearchResults(page, request) >>> resultsView = SearchResults(page, request)
>>> results = list(resultsView.results) >>> results = list(resultsView.results)
@ -179,7 +179,7 @@ Search via related concepts
>>> results[0].context.__name__ >>> results[0].context.__name__
u'plone' 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) >>> request = TestRequest(form=form)
>>> resultsView = SearchResults(page, request) >>> resultsView = SearchResults(page, request)
>>> results = list(resultsView.results) >>> results = list(resultsView.results)
@ -187,3 +187,11 @@ Search via related concepts
1 1
>>> results[0].context.__name__ >>> results[0].context.__name__
u'zope3' 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...

View file

@ -23,9 +23,8 @@ loops.search package.
$Id$ $Id$
""" """
from zope.app import zapi
from zope import interface, component 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.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.formlib.namedtemplate import NamedTemplate, NamedTemplateImplementation from zope.formlib.namedtemplate import NamedTemplate, NamedTemplateImplementation
@ -34,6 +33,7 @@ from zope.i18nmessageid import MessageFactory
from cybertools.ajax import innerHtml from cybertools.ajax import innerHtml
from cybertools.typology.interfaces import ITypeManager from cybertools.typology.interfaces import ITypeManager
from loops.browser.common import BaseView from loops.browser.common import BaseView
from loops.query import ConceptQuery, FullQuery
from loops import util from loops import util
from loops.util import _ from loops.util import _
@ -51,10 +51,6 @@ class Search(BaseView):
def macro(self): def macro(self):
return template.macros['search'] return template.macros['search']
@Lazy
def catalog(self):
return component.getUtility(ICatalog)
@property @property
def rowNum(self): def rowNum(self):
""" Return the rowNum to be used for identifying the current search """ Return the rowNum to be used for identifying the current search
@ -84,19 +80,11 @@ class Search(BaseView):
""" """
request = self.request request = self.request
request.response.setHeader('Content-Type', 'text/plain; charset=UTF-8') 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:*' type = request.get('searchType') or 'loops:concept:*'
if type.endswith('*'): result = ConceptQuery(self).query(title=title, type=type)
start = type[:-1] # simple way to provide JSON format:
end = start + '\x7f' return str(sorted([[`o.title`[2:-1], `traversing.api.getName(o)`[2:-1]]
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]]
for o in result])).replace('\\\\x', '\\x') for o in result])).replace('\\\\x', '\\x')
def submitReplacing(self, targetId, formId, view): def submitReplacing(self, targetId, formId, view):
@ -116,79 +104,18 @@ class SearchResults(BaseView):
def __call__(self): def __call__(self):
return innerHtml(self) return innerHtml(self)
@Lazy
def catalog(self):
return component.getUtility(ICatalog)
@Lazy @Lazy
def results(self): def results(self):
result = set()
request = self.request request = self.request
r3 = self.queryConcepts()
type = request.get('search.1.text', 'loops:*') type = request.get('search.1.text', 'loops:*')
text = request.get('search.2.text') text = request.get('search.2.text')
if not r3 and not text and '*' in type: # there should be some sort of selection... useTitle = request.get('search.2.title')
return result useFull = request.get('search.2.full')
#if r3 and type != 'loops:*': conceptType = request.get('search.3.type', 'loops:concept:*')
# typeName = type.split(':')[-1] conceptTitle = request.get('search.3.text_selected')
# r3 = set(o for o in r3 if self.isType(o, typeName)) result = FullQuery(self).query(text=text, type=type,
#if text or not '*' in loops: useTitle=useTitle, useFull=useFull,
if text or type != 'loops:*': # TODO: this may be highly inefficient! see above conceptTitle=conceptTitle, conceptType= conceptType)
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
result = sorted(result, key=lambda x: x.title.lower()) result = sorted(result, key=lambda x: x.title.lower())
return self.viewIterator(result) 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