===================================================================
loops.search - Provide search functionality for the loops framework
===================================================================

  ($Id$)


Let's do some basic set up

  >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
  >>> site = placefulSetUp(True)

  >>> from zope import component, interface
  >>> from zope.interface import implements

and setup a simple loops site with a concept manager and some concepts
(with all the type machinery, what in real life is done via standard
ZCML setup):

  >>> from loops.concept import Concept
  >>> from loops.type import ConceptType, TypeConcept
  >>> from loops.interfaces import ITypeConcept
  >>> from loops.base import Loops
  >>> from loops.tests.setup import TestSite
  >>> t = TestSite(site)
  >>> concepts, resources, views = t.setup()

  >>> loopsRoot = site['loops']
  >>> query = concepts['query']
  >>> topic = concepts['topic'] = Concept(u'Topic')

In addition we create a concept that holds the search page and a node
(page) that links to this concept:

  >>> search = concepts['search'] = Concept(u'Search')
  >>> search.conceptType = query

  >>> from loops.view import 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
  >>> from loops.search.browser import Search
  >>> searchView = Search(search, TestRequest())

The search view provides values for identifying the search form itself
and the parameter rows; the rowNum is auto-incremented, so it should be
accessed exactly once per row:

  >>> searchView.rowNum
  1
  >>> 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)
  9
  >>> t.getTermByToken('loops:resource:*').title
  'Any Resource'

  >>> t = searchView.conceptTypesForSearch()
  >>> len(t)
  4
  >>> 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
the dojo library with the view's controller we also have to supply
a controller attribute for the search view.

  >>> from loops.browser.node import NodeView
  >>> request = TestRequest()
  >>> pageView = NodeView(page, request)

  >>> from cybertools.browser.liquid.controller import Controller
  >>> searchView.controller = Controller(searchView, request)

  >>> searchView.submitReplacing('1.results', '1.search.form', pageView)
  'submitReplacing("1.results", "1.search.form",
       "http://127.0.0.1/loops/views/page/.target29/@@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.

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):
  ...         result = []
  ...         name = criteria.get('loops_title')
  ...         if name and name.endswith('*'): name = name[:-1]
  ...         typeToken = criteria.get('loops_type', ('resource',))
  ...         if name or typeToken:
  ...             if 'concept' in typeToken[0]:
  ...                  if name:
  ...                      result = concepts.get(name)
  ...                  else:
  ...                      tp = concepts[typeToken[0].split(':')[-1]]
  ...                      result = list(tp.getChildren())
  ...             else:
  ...                 result = resources.get(name)
  ...         if not result: return []
  ...         return type(result) is list and result or [result]
  >>> component.provideUtility(DummyCat())

  >>> 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(u'Zope')
  >>> czope2 = concepts['zope2'] = Concept(u'Zope 2')
  >>> czope3 = concepts['zope3'] = Concept(u'Zope 3')
  >>> cplone = concepts['plone'] = Concept(u'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:

  >>> from loops import util
  >>> uid = util.getUidForObject(concepts['zope'])
  >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': uid}
  >>> request = TestRequest(form=form)
  >>> resultsView = SearchResults(page, request)
  >>> results = list(resultsView.results)
  >>> len(results)
  5
  >>> results[0].context.__name__
  u'plone'

  >>> uid = util.getUidForObject(concepts['zope3'])
  >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': uid}
  >>> request = TestRequest(form=form)
  >>> resultsView = SearchResults(page, request)
  >>> results = list(resultsView.results)
  >>> len(results)
  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:

  >>> form = {'searchType': 'loops:concept:topic', 'name': u'zope'}
  >>> request = TestRequest(form=form)
  >>> view = Search(page, request)
  >>> view.listConcepts()
  u"{identifier: 'id', items: [{label: 'Zope (Topic)', name: 'Zope', id: '33'}]}"

Preset Concept Types on Search Forms
------------------------------------

Often we want to include certain types in our search. We can instruct
the search form to include lines for these types by giving these types
a certain qualifier, via the option attribute of the type interface.

Let's start with a new type, the customer type.

  >>> customer = concepts['customer']
  >>> custType = ITypeConcept(customer)
  >>> custType.options
  []

  >>> cust1 = concepts['cust1'] = Concept(u'Zope Corporation')
  >>> cust2 = concepts['cust2'] = Concept(u'cyberconcepts')
  >>> for c in (cust1, cust2): c.conceptType = customer

  >>> from cybertools.typology.interfaces import IType
  >>> IType(cust1).qualifiers
  ('concept',)

  >>> searchView = Search(search, TestRequest())
  >>> list(searchView.presetSearchTypes)
  []

We can now add a 'search' qualifier to the customer type's options
and thus include the customer type in the preset search types.

  >>> custType.options = ('qualifier:search',)
  >>> IType(cust1).qualifiers
  ('concept', 'search')
  >>> searchView = Search(search, TestRequest())
  >>> list(searchView.presetSearchTypes)
  [{'token': 'loops:concept:customer', 'title': u'Customer'}]

  >>> searchView.conceptsForType('loops:concept:customer')
  [{'token': 'none', 'title': u'not selected'},
   {'token': '47', 'title': u'Zope Corporation'},
   {'token': '49', 'title': u'cyberconcepts'}]

Let's use this new search option for querying:

  >>> form = {'search.4.text_selected': u'47'}
  >>> resultsView = SearchResults(page, TestRequest(form=form))
  >>> results = list(resultsView.results)
  >>> results[0].title
  u'Zope Corporation'


Automatic Filtering
-------------------

TODO - more to come...

