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"
|
||||
class="loops.browser.node.InlineEdit"
|
||||
attribute="save"
|
||||
permission="zope.View"
|
||||
permission="zope.ManageContent"
|
||||
/>
|
||||
|
||||
<!-- 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(
|
||||
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 '
|
||||
'the results'),
|
||||
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.target = search
|
||||
|
||||
Search views
|
||||
------------
|
||||
|
||||
Now we are ready to create a search view object:
|
||||
|
||||
>>> from zope.publisher.browser import TestRequest
|
||||
|
@ -65,6 +68,25 @@ accessed exactly once per row:
|
|||
>>> searchView.rowNum
|
||||
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
|
||||
view for our page. The submitReplacing method returns a JavaScript call
|
||||
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",
|
||||
"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
|
||||
result set of the search via its `results` property.
|
||||
|
||||
>>> from loops.search.browser import SearchResults
|
||||
>>> form = {'search.2.title': 'yes', 'search.2.text': u'foo' }
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> resultsView = SearchResults(page, request)
|
||||
|
||||
Before accessing the `results` property we have to prepare a catalog.
|
||||
Before accessing the `results` property we have to prepare a (for testing
|
||||
purposes fairly primitive) catalog and a resource we can search for:
|
||||
|
||||
>>> from zope.app.catalog.interfaces import ICatalog
|
||||
>>> class DummyCat(object):
|
||||
... implements(ICatalog)
|
||||
... 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 []
|
||||
>>> 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 cybertools.ajax import innerHtml
|
||||
from cybertools.typology.interfaces import ITypeManager
|
||||
from loops.browser.common import BaseView
|
||||
from loops import util
|
||||
|
||||
_ = MessageFactory('zope')
|
||||
|
||||
|
@ -59,6 +61,12 @@ class Search(BaseView):
|
|||
self.maxRowNum = 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):
|
||||
self.registerDojo()
|
||||
return 'return submitReplacing("%s", "%s", "%s")' % (
|
||||
|
@ -76,31 +84,73 @@ 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:resource:*')
|
||||
text = request.get('search.2.text')
|
||||
if not text and '*' in type: # there should be some sort of selection...
|
||||
return set()
|
||||
useTitle = request.get('search.2.title')
|
||||
useFull = request.get('search.2.full')
|
||||
r1 = set()
|
||||
cat = component.getUtility(ICatalog)
|
||||
if useFull and text and not type.startswith('loops:concept:'):
|
||||
criteria = {'loops_resource_textng': {'query': text},}
|
||||
r1 = set(cat.searchResults(**criteria))
|
||||
if not r3 and not text and '*' in type: # there should be some sort of selection...
|
||||
return result
|
||||
if text or not '*' in type:
|
||||
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())
|
||||
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('*'):
|
||||
start = type[:-1]
|
||||
end = start + '\x7f'
|
||||
else:
|
||||
start = end = type
|
||||
criteria = {'loops_type': (start, end),}
|
||||
if useTitle and text:
|
||||
if text:
|
||||
criteria['loops_title'] = text
|
||||
r2 = set(cat.searchResults(**criteria))
|
||||
result = r1.union(r2)
|
||||
result = [r for r in result if r.getLoopsRoot() == self.loopsRoot]
|
||||
result.sort(key=lambda x: x.title.lower())
|
||||
return self.viewIterator(result)
|
||||
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
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<input type="hidden" name="search.resultsId" value="1.results"
|
||||
tal:attributes="value resultsId" />
|
||||
<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" />
|
||||
</tal:block>
|
||||
<tr tal:condition="nothing">
|
||||
|
@ -68,7 +68,7 @@
|
|||
value param" />
|
||||
<input type="button" value="−"
|
||||
title="Remove search parameter"
|
||||
tal:condition="python: param not in ['type', 'text']" />
|
||||
tal:condition="python: param not in ['type', 'text', 'concept']" />
|
||||
</td>
|
||||
<td>
|
||||
<div metal:use-macro="macros/?param" />
|
||||
|
@ -84,7 +84,7 @@
|
|||
<select name="text"
|
||||
tal:attributes="name string:$namePrefix.text;
|
||||
id string:$idPrefix.text;">
|
||||
<tal:types repeat="type view/typesForSearch">
|
||||
<tal:types repeat="type item/typesForSearch">
|
||||
<option value="loops:*"
|
||||
i18n:translate=""
|
||||
tal:attributes="value type/token"
|
||||
|
@ -114,7 +114,7 @@
|
|||
<label for="full"
|
||||
tal:attributes="for string:$idPrefix.full">Full text</label>
|
||||
<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"
|
||||
tal:attributes="name string:$namePrefix.text;
|
||||
id string:$idPrefix.text;" />
|
||||
|
@ -125,23 +125,29 @@
|
|||
</metal:text>
|
||||
|
||||
|
||||
<!-- obsolete -->
|
||||
<tr metal:define-macro="search_row_selectable" id="search.row.1.1">
|
||||
<td>
|
||||
<input type="button" value="−"
|
||||
title="Remove search parameter" />
|
||||
</td>
|
||||
<td tal:define="selection search_selection | string:type">
|
||||
<select>
|
||||
<option value="type"
|
||||
tal:attributes="selected python: selection=='type'">Type</option>
|
||||
<option value="text"
|
||||
tal:attributes="selected python: selection=='text'">Text</option>
|
||||
<option value="attribute">Attribute value</option>
|
||||
<option value="concept">Concept</option>
|
||||
</select>
|
||||
<input metal:use-macro="template/macros/?selection" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<metal:text define-macro="concept">
|
||||
<div>
|
||||
<h3>Search via related conceps</h3>
|
||||
<label for="type"
|
||||
tal:attributes="for string:$idPrefix.type">Type:</label>
|
||||
<select name="type"
|
||||
tal:attributes="name string:$namePrefix.type;
|
||||
id string:$idPrefix.type;">
|
||||
<tal:types repeat="type item/conceptTypesForSearch">
|
||||
<option value="loops:*"
|
||||
i18n:translate=""
|
||||
tal:attributes="value type/token"
|
||||
tal:content="type/title">Topic</option>
|
||||
</tal:types>
|
||||
</select>
|
||||
<label for="text"
|
||||
tal:attributes="for string:$idPrefix.text">Search text:</label>
|
||||
<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