introduced simple search using concept hierarchy
git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1329 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
parent
01680a666a
commit
029632fde7
5 changed files with 190 additions and 48 deletions
|
@ -553,7 +553,7 @@
|
||||||
for="loops.interfaces.INode"
|
for="loops.interfaces.INode"
|
||||||
class="loops.browser.node.InlineEdit"
|
class="loops.browser.node.InlineEdit"
|
||||||
attribute="save"
|
attribute="save"
|
||||||
permission="zope.View"
|
permission="zope.ManageContent"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- render file or image assigned to a node as target -->
|
<!-- render file or image assigned to a node as target -->
|
||||||
|
|
2
query.py
2
query.py
|
@ -53,7 +53,7 @@ class IQueryConcept(Interface):
|
||||||
|
|
||||||
viewName = schema.TextLine(
|
viewName = schema.TextLine(
|
||||||
title=_(u'Adapter/View name'),
|
title=_(u'Adapter/View name'),
|
||||||
description=_(u'The name of the (mulit-) adapter (typically a view) '
|
description=_(u'The name of the (multi-) adapter (typically a view) '
|
||||||
'to be used for the query and for presenting '
|
'to be used for the query and for presenting '
|
||||||
'the results'),
|
'the results'),
|
||||||
default=u'',
|
default=u'',
|
||||||
|
|
|
@ -50,6 +50,9 @@ In addition we create a concept that holds the search page and a node
|
||||||
>>> page = views['page'] = Node('Search Page')
|
>>> page = views['page'] = Node('Search Page')
|
||||||
>>> page.target = search
|
>>> page.target = search
|
||||||
|
|
||||||
|
Search views
|
||||||
|
------------
|
||||||
|
|
||||||
Now we are ready to create a search view object:
|
Now we are ready to create a search view object:
|
||||||
|
|
||||||
>>> from zope.publisher.browser import TestRequest
|
>>> from zope.publisher.browser import TestRequest
|
||||||
|
@ -65,6 +68,25 @@ accessed exactly once per row:
|
||||||
>>> searchView.rowNum
|
>>> searchView.rowNum
|
||||||
2
|
2
|
||||||
|
|
||||||
|
The search view provides vocabularies for types that allow the selection
|
||||||
|
of types to search for; this needs an ITypeManager adapter registered via
|
||||||
|
zcml in real life:
|
||||||
|
|
||||||
|
>>> from loops.type import LoopsTypeManager
|
||||||
|
>>> component.provideAdapter(LoopsTypeManager)
|
||||||
|
|
||||||
|
>>> t = searchView.typesForSearch()
|
||||||
|
>>> len(t)
|
||||||
|
11
|
||||||
|
>>> t.getTermByToken('loops:resource:*').title
|
||||||
|
'Any Resource'
|
||||||
|
|
||||||
|
>>> t = searchView.conceptTypesForSearch()
|
||||||
|
>>> len(t)
|
||||||
|
5
|
||||||
|
>>> t.getTermByToken('loops:concept:*').title
|
||||||
|
'Any Concept'
|
||||||
|
|
||||||
To execute the search in the context of a node we have to set up a node
|
To execute the search in the context of a node we have to set up a node
|
||||||
view for our page. The submitReplacing method returns a JavaScript call
|
view for our page. The submitReplacing method returns a JavaScript call
|
||||||
that will replace the results part on the search page; as this registers
|
that will replace the results part on the search page; as this registers
|
||||||
|
@ -82,22 +104,86 @@ a controller attribute for the search view.
|
||||||
'return submitReplacing("1.results", "1.search.form",
|
'return submitReplacing("1.results", "1.search.form",
|
||||||
"http://127.0.0.1/loops/views/page/.target19/@@searchresults.html")'
|
"http://127.0.0.1/loops/views/page/.target19/@@searchresults.html")'
|
||||||
|
|
||||||
|
Basic (text/title) search
|
||||||
|
-------------------------
|
||||||
|
|
||||||
The searchresults.html view, i.e. the SearchResults view class provides the
|
The searchresults.html view, i.e. the SearchResults view class provides the
|
||||||
result set of the search via its `results` property.
|
result set of the search via its `results` property.
|
||||||
|
|
||||||
>>> from loops.search.browser import SearchResults
|
Before accessing the `results` property we have to prepare a (for testing
|
||||||
>>> form = {'search.2.title': 'yes', 'search.2.text': u'foo' }
|
purposes fairly primitive) catalog and a resource we can search for:
|
||||||
>>> request = TestRequest(form=form)
|
|
||||||
>>> resultsView = SearchResults(page, request)
|
|
||||||
|
|
||||||
Before accessing the `results` property we have to prepare a catalog.
|
|
||||||
|
|
||||||
>>> from zope.app.catalog.interfaces import ICatalog
|
>>> from zope.app.catalog.interfaces import ICatalog
|
||||||
>>> class DummyCat(object):
|
>>> class DummyCat(object):
|
||||||
... implements(ICatalog)
|
... implements(ICatalog)
|
||||||
... def searchResults(self, **criteria):
|
... def searchResults(self, **criteria):
|
||||||
|
... name = criteria.get('loops_title')
|
||||||
|
... type = criteria.get('loops_type', ('resource',))
|
||||||
|
... if name:
|
||||||
|
... if 'concept' in type[0]:
|
||||||
|
... result = concepts.get(name)
|
||||||
|
... else:
|
||||||
|
... result = resources.get(name)
|
||||||
|
... if result:
|
||||||
|
... return [result]
|
||||||
... return []
|
... return []
|
||||||
>>> component.provideUtility(DummyCat())
|
>>> component.provideUtility(DummyCat())
|
||||||
|
|
||||||
>>> list(resultsView.results)
|
>>> from loops.resource import Resource
|
||||||
[]
|
>>> rplone = resources['plone'] = Resource()
|
||||||
|
|
||||||
|
>>> from loops.search.browser import SearchResults
|
||||||
|
>>> form = {'search.2.title': True, 'search.2.text': u'plone'}
|
||||||
|
>>> request = TestRequest(form=form)
|
||||||
|
>>> resultsView = SearchResults(page, request)
|
||||||
|
|
||||||
|
>>> results = list(resultsView.results)
|
||||||
|
>>> len(results)
|
||||||
|
1
|
||||||
|
>>> results[0].context == rplone
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> form = {'search.2.title': True, 'search.2.text': u'foo'}
|
||||||
|
>>> request = TestRequest(form=form)
|
||||||
|
>>> resultsView = SearchResults(page, request)
|
||||||
|
>>> len(list(resultsView.results))
|
||||||
|
0
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
>>> czope = concepts['zope'] = Concept('Zope')
|
||||||
|
>>> czope2 = concepts['zope2'] = Concept('Zope 2')
|
||||||
|
>>> czope3 = concepts['zope3'] = Concept('Zope 3')
|
||||||
|
>>> cplone = concepts['plone'] = Concept('Plone')
|
||||||
|
>>> for c in (czope, czope2, czope3, cplone):
|
||||||
|
... c.conceptType = topic
|
||||||
|
>>> czope.assignChild(czope2)
|
||||||
|
>>> czope.assignChild(czope3)
|
||||||
|
>>> 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:
|
||||||
|
|
||||||
|
>>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': u'zope'}
|
||||||
|
>>> request = TestRequest(form=form)
|
||||||
|
>>> resultsView = SearchResults(page, request)
|
||||||
|
>>> results = list(resultsView.results)
|
||||||
|
>>> len(results)
|
||||||
|
5
|
||||||
|
>>> results[0].context.__name__
|
||||||
|
u'plone'
|
||||||
|
|
||||||
|
>>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': u'zope3'}
|
||||||
|
>>> request = TestRequest(form=form)
|
||||||
|
>>> resultsView = SearchResults(page, request)
|
||||||
|
>>> results = list(resultsView.results)
|
||||||
|
>>> len(results)
|
||||||
|
1
|
||||||
|
>>> results[0].context.__name__
|
||||||
|
u'zope3'
|
||||||
|
|
|
@ -31,7 +31,9 @@ from zope.formlib.namedtemplate import NamedTemplate, NamedTemplateImplementatio
|
||||||
from zope.i18nmessageid import MessageFactory
|
from zope.i18nmessageid import MessageFactory
|
||||||
|
|
||||||
from cybertools.ajax import innerHtml
|
from cybertools.ajax import innerHtml
|
||||||
|
from cybertools.typology.interfaces import ITypeManager
|
||||||
from loops.browser.common import BaseView
|
from loops.browser.common import BaseView
|
||||||
|
from loops import util
|
||||||
|
|
||||||
_ = MessageFactory('zope')
|
_ = MessageFactory('zope')
|
||||||
|
|
||||||
|
@ -59,6 +61,12 @@ class Search(BaseView):
|
||||||
self.maxRowNum = n
|
self.maxRowNum = n
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
def conceptTypesForSearch(self):
|
||||||
|
general = [('loops:concept:*', 'Any Concept'),]
|
||||||
|
return util.KeywordVocabulary(general + sorted([(t.tokenForSearch, t.title)
|
||||||
|
for t in ITypeManager(self.context).types
|
||||||
|
if 'concept' in t.qualifiers]))
|
||||||
|
|
||||||
def submitReplacing(self, targetId, formId, view):
|
def submitReplacing(self, targetId, formId, view):
|
||||||
self.registerDojo()
|
self.registerDojo()
|
||||||
return 'return submitReplacing("%s", "%s", "%s")' % (
|
return 'return submitReplacing("%s", "%s", "%s")' % (
|
||||||
|
@ -76,31 +84,73 @@ 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:resource:*')
|
type = request.get('search.1.text', 'loops:resource:*')
|
||||||
text = request.get('search.2.text')
|
text = request.get('search.2.text')
|
||||||
if not text and '*' in type: # there should be some sort of selection...
|
if not r3 and not text and '*' in type: # there should be some sort of selection...
|
||||||
return set()
|
return result
|
||||||
useTitle = request.get('search.2.title')
|
if text or not '*' in type:
|
||||||
useFull = request.get('search.2.full')
|
useTitle = request.get('search.2.title')
|
||||||
r1 = set()
|
useFull = request.get('search.2.full')
|
||||||
cat = component.getUtility(ICatalog)
|
r1 = set()
|
||||||
if useFull and text and not type.startswith('loops:concept:'):
|
cat = self.catalog
|
||||||
criteria = {'loops_resource_textng': {'query': text},}
|
if useFull and text and not type.startswith('loops:concept:'):
|
||||||
r1 = set(cat.searchResults(**criteria))
|
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())
|
||||||
|
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 not text and '*' in type:
|
||||||
|
return None
|
||||||
if type.endswith('*'):
|
if type.endswith('*'):
|
||||||
start = type[:-1]
|
start = type[:-1]
|
||||||
end = start + '\x7f'
|
end = start + '\x7f'
|
||||||
else:
|
else:
|
||||||
start = end = type
|
start = end = type
|
||||||
criteria = {'loops_type': (start, end),}
|
criteria = {'loops_type': (start, end),}
|
||||||
if useTitle and text:
|
if text:
|
||||||
criteria['loops_title'] = text
|
criteria['loops_title'] = text
|
||||||
r2 = set(cat.searchResults(**criteria))
|
queue = list(cat.searchResults(**criteria))
|
||||||
result = r1.union(r2)
|
concepts = []
|
||||||
result = [r for r in result if r.getLoopsRoot() == self.loopsRoot]
|
while queue:
|
||||||
result.sort(key=lambda x: x.title.lower())
|
c = queue.pop(0)
|
||||||
return self.viewIterator(result)
|
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
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<input type="hidden" name="search.resultsId" value="1.results"
|
<input type="hidden" name="search.resultsId" value="1.results"
|
||||||
tal:attributes="value resultsId" />
|
tal:attributes="value resultsId" />
|
||||||
<table cellpadding="3">
|
<table cellpadding="3">
|
||||||
<tal:block repeat="search_param python: ['type', 'text']">
|
<tal:block repeat="search_param python: ['type', 'text', 'concept']">
|
||||||
<metal:row use-macro="macros/search_row" />
|
<metal:row use-macro="macros/search_row" />
|
||||||
</tal:block>
|
</tal:block>
|
||||||
<tr tal:condition="nothing">
|
<tr tal:condition="nothing">
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
value param" />
|
value param" />
|
||||||
<input type="button" value="−"
|
<input type="button" value="−"
|
||||||
title="Remove search parameter"
|
title="Remove search parameter"
|
||||||
tal:condition="python: param not in ['type', 'text']" />
|
tal:condition="python: param not in ['type', 'text', 'concept']" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div metal:use-macro="macros/?param" />
|
<div metal:use-macro="macros/?param" />
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
<select name="text"
|
<select name="text"
|
||||||
tal:attributes="name string:$namePrefix.text;
|
tal:attributes="name string:$namePrefix.text;
|
||||||
id string:$idPrefix.text;">
|
id string:$idPrefix.text;">
|
||||||
<tal:types repeat="type view/typesForSearch">
|
<tal:types repeat="type item/typesForSearch">
|
||||||
<option value="loops:*"
|
<option value="loops:*"
|
||||||
i18n:translate=""
|
i18n:translate=""
|
||||||
tal:attributes="value type/token"
|
tal:attributes="value type/token"
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
<label for="full"
|
<label for="full"
|
||||||
tal:attributes="for string:$idPrefix.full">Full text</label>
|
tal:attributes="for string:$idPrefix.full">Full text</label>
|
||||||
<label for="text"
|
<label for="text"
|
||||||
tal:attributes="for string:$idPrefix.text">Search words:</label>
|
tal:attributes="for string:$idPrefix.text">Search text:</label>
|
||||||
<input type="text" name="text"
|
<input type="text" name="text"
|
||||||
tal:attributes="name string:$namePrefix.text;
|
tal:attributes="name string:$namePrefix.text;
|
||||||
id string:$idPrefix.text;" />
|
id string:$idPrefix.text;" />
|
||||||
|
@ -125,23 +125,29 @@
|
||||||
</metal:text>
|
</metal:text>
|
||||||
|
|
||||||
|
|
||||||
<!-- obsolete -->
|
<metal:text define-macro="concept">
|
||||||
<tr metal:define-macro="search_row_selectable" id="search.row.1.1">
|
<div>
|
||||||
<td>
|
<h3>Search via related conceps</h3>
|
||||||
<input type="button" value="−"
|
<label for="type"
|
||||||
title="Remove search parameter" />
|
tal:attributes="for string:$idPrefix.type">Type:</label>
|
||||||
</td>
|
<select name="type"
|
||||||
<td tal:define="selection search_selection | string:type">
|
tal:attributes="name string:$namePrefix.type;
|
||||||
<select>
|
id string:$idPrefix.type;">
|
||||||
<option value="type"
|
<tal:types repeat="type item/conceptTypesForSearch">
|
||||||
tal:attributes="selected python: selection=='type'">Type</option>
|
<option value="loops:*"
|
||||||
<option value="text"
|
i18n:translate=""
|
||||||
tal:attributes="selected python: selection=='text'">Text</option>
|
tal:attributes="value type/token"
|
||||||
<option value="attribute">Attribute value</option>
|
tal:content="type/title">Topic</option>
|
||||||
<option value="concept">Concept</option>
|
</tal:types>
|
||||||
</select>
|
</select>
|
||||||
<input metal:use-macro="template/macros/?selection" />
|
<label for="text"
|
||||||
</td>
|
tal:attributes="for string:$idPrefix.text">Search text:</label>
|
||||||
</tr>
|
<input type="text" name="text"
|
||||||
|
tal:attributes="name string:$namePrefix.text;
|
||||||
|
id string:$idPrefix.text;" />
|
||||||
|
<input type="button" value="+"
|
||||||
|
title="Add type"
|
||||||
|
tal:condition="nothing" />
|
||||||
|
</div>
|
||||||
|
</metal:text>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue