diff --git a/browser/configure.zcml b/browser/configure.zcml index 059a379..85909a1 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -553,7 +553,7 @@ for="loops.interfaces.INode" class="loops.browser.node.InlineEdit" attribute="save" - permission="zope.View" + permission="zope.ManageContent" /> diff --git a/query.py b/query.py index 814a3b6..f61f0e9 100644 --- a/query.py +++ b/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'', diff --git a/search/README.txt b/search/README.txt index 8e7cbf4..c26e2fe 100755 --- a/search/README.txt +++ b/search/README.txt @@ -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' diff --git a/search/browser.py b/search/browser.py index 800a972..fb4f15c 100644 --- a/search/browser.py +++ b/search/browser.py @@ -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 diff --git a/search/search.pt b/search/search.pt index 297c368..7761b29 100644 --- a/search/search.pt +++ b/search/search.pt @@ -17,7 +17,7 @@ - + @@ -68,7 +68,7 @@ value param" />   + tal:condition="python: param not in ['type', 'text', 'concept']" />  - - - - + +
+

Search via related conceps

+ +    + + +   +
+
@@ -84,7 +84,7 @@ @@ -125,23 +125,29 @@ - -
-   - - - -