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