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:
parent
2d25015202
commit
92f6a06e01
9 changed files with 191 additions and 120 deletions
|
@ -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'
|
||||
|
|
|
@ -539,6 +539,15 @@
|
|||
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 -->
|
||||
|
||||
<page
|
||||
|
|
|
@ -72,10 +72,14 @@ class NodeView(BaseView):
|
|||
cm.register('portlet_left', 'navigation', title='Navigation',
|
||||
subMacro=self.template.macros['menu'])
|
||||
if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
|
||||
cm.register('portlet_right', 'actions', title='Actions',
|
||||
subMacro=self.template.macros['actions'])
|
||||
cm.register('portlet_right', 'personal', title='Personal Items',
|
||||
subMacro=self.template.macros['personal'])
|
||||
cm.register('portlet_right', 'clipboard', title='Clipboard',
|
||||
subMacro=self.template.macros['clipboard'])
|
||||
# this belongs to loops.organize; how to register portlets
|
||||
# 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
|
||||
def view(self):
|
||||
|
@ -316,26 +320,27 @@ class InlineEdit(NodeView):
|
|||
|
||||
# special (named) views for nodes
|
||||
|
||||
class ListPages(NodeView):
|
||||
class SpecialNodeView(NodeView):
|
||||
|
||||
macroName = None # to be provided by subclass
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.template.macros['listpages']
|
||||
return self.template.macros[self.macroName]
|
||||
|
||||
@Lazy
|
||||
def view(self):
|
||||
return self
|
||||
|
||||
|
||||
class ListResources(NodeView):
|
||||
class ListPages(SpecialNodeView):
|
||||
macroName = 'listpages'
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.template.macros['listresources']
|
||||
class ListResources(SpecialNodeView):
|
||||
macroName = 'listresources'
|
||||
|
||||
@Lazy
|
||||
def view(self):
|
||||
return self
|
||||
class ListChildren(SpecialNodeView):
|
||||
macroName = 'listchildren'
|
||||
|
||||
|
||||
class ConfigureView(NodeView):
|
||||
|
|
|
@ -144,6 +144,28 @@
|
|||
</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 -->
|
||||
|
||||
<metal:menu define-macro="menu"
|
||||
|
@ -171,13 +193,13 @@
|
|||
|
||||
<!-- portlets -->
|
||||
|
||||
<metal:actions define-macro="actions">
|
||||
<div class="menu-2">Add Resource</div>
|
||||
<metal:actions define-macro="clipboard">
|
||||
<div class="menu-2">loops Development</div>
|
||||
</metal:actions>
|
||||
|
||||
|
||||
<metal:actions define-macro="personal">
|
||||
<div class="menu-2">Clipboard</div>
|
||||
<metal:actions define-macro="worklist">
|
||||
<div class="menu-2">Create Resource...</div>
|
||||
</metal:actions>
|
||||
|
||||
|
||||
|
@ -193,10 +215,6 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- dojoType="Editor"
|
||||
items="formatblock;|;insertunorderedlist;insertorderedlist;|;bold;italic;|;createLink;"
|
||||
-->
|
||||
|
||||
|
||||
<!-- edit and other links -->
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
100
query.py
100
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.
|
||||
"""
|
||||
|
|
10
resource.py
10
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)
|
||||
|
|
|
@ -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...
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue