[Checkins] SVN: z3c.contents/branches/darrylcousins/src/z3c/contents/ Added simple search support to contents view.

Darryl Cousins darryl at darrylcousins.net.nz
Sat Mar 22 21:32:14 EDT 2008


Log message for revision 84858:
  Added simple search support to contents view.
  
  The contained objects are searched by available attributes and __name__ (id in
  container). The search terms are also included in the query string of the
  column header sorting urls. Batching links will also need to be looked at.
  

Changed:
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/BROWSER.txt
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/README.txt
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/browser.py
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/configure.zcml
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/ftesting.zcml
  A   z3c.contents/branches/darrylcousins/src/z3c/contents/header.py
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/interfaces.py
  A   z3c.contents/branches/darrylcousins/src/z3c/contents/search.py
  U   z3c.contents/branches/darrylcousins/src/z3c/contents/testing.py

-=-
Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/BROWSER.txt
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/BROWSER.txt	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/BROWSER.txt	2008-03-23 01:32:13 UTC (rev 84858)
@@ -18,11 +18,14 @@
   >>> browser.handleErrors = False
   >>> URL = 'http://localhost/++skin++ContentsTesting%s'
 
+We have sorting urls in the column headers. Initially the table is sorted on the
+name column ascending.
+
   >>> browser.open(URL % '/')
   >>> printElement(browser, "//table/thead/tr/th", multiple=True)
   <th>X</th>
   <th><a
-    href="?contents-sortOn=contents-renameColumn-1&amp;contents-sortOrder=ascending"
+    href="?contents-sortOn=contents-renameColumn-1&amp;contents-sortOrder=descending"
     title="Sort">Name</a></th>
   <th><a
     href="?contents-sortOn=contents-createdColumn-2&amp;contents-sortOrder=ascending"
@@ -31,8 +34,28 @@
     href="?contents-sortOn=contents-modifiedColumn-3&amp;contents-sortOrder=ascending"
     title="Sort">Modified</a></th>
 
-  >>> #print browser.contents
 
 Sample data set up
 ==================
 
+  >>> container = getRootFolder()
+  >>> from z3c.contents.testing import Content
+  >>> container[u'zero'] = Content('Zero', 0)
+  >>> container[u'first'] = Content('First', 1)
+  >>> container[u'second'] = Content('Second', 2)
+  >>> container[u'third'] = Content('Third', 3)
+  >>> container[u'fourth'] = Content('Fourth', 4)
+
+We can search within the container - this is a simple search on ids and string
+attributes on the contained objects.
+
+  >>> browser.getControl(name="search.widgets.searchterm").value = u'4 second Third'
+  >>> browser.getControl(name="search.buttons.search").click()
+
+Note that the searchterms are also now included in the sorting header link.
+
+  >>> printElement(browser, "//table/thead/tr/th[2]")
+  <th><a
+    href="?contents-sortOn=contents-renameColumn-1&amp;contents-sortOrder=descending&amp;search.widgets.searchterm=4+second+Third"
+    title="Sort">Name</a></th>
+

Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/README.txt
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/README.txt	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/README.txt	2008-03-23 01:32:13 UTC (rev 84858)
@@ -245,7 +245,6 @@
     </div>
   </form>
 
-
 Sorting
 -------
 
@@ -837,7 +836,112 @@
   [(u'fifth', <Content Second 2>), (u'first', <Content First 1>),
    (u'zero', <Content Zero 0>)]
 
+Search
+------
 
+Add an IFind adapter for the search:
+
+  >>> from z3c.contents.interfaces import ISearch
+  >>> from z3c.contents.search import SearchForContainer
+  >>> zope.component.provideAdapter(SearchForContainer,
+  ...     (IContainer, ), provides=ISearch)
+
+The default search adapter matches search terms to the objects id in the
+container or to any possible string attribute.
+
+  >>> searchRequest = TestRequest(form={'search.buttons.search': 'Search',
+  ...                       'search.widgets.searchterm': u'First zero'})
+  >>> alsoProvides(searchRequest, IDivFormLayer)
+  >>> searchPage = browser.ContentsPage(secondContainer, searchRequest)
+  >>> searchPage.update()
+  >>> print searchPage.render()
+  <form action="http://127.0.0.1" method="post"
+        enctype="multipart/form-data" class="edit-form"
+        name="contents" id="contents">
+  ...
+  <tbody>
+    <tr>
+      <td><input type="checkbox" class="checkbox-widget" name="contents-checkBoxColumn-0-selectedItems" value="first"  /></td>
+      <td><a href="http://127.0.0.1/secondContainer/first">first</a></td>
+      <td>01/01/01 01:01</td>
+      <td>02/02/02 02:02</td>
+    </tr>
+    <tr>
+      <td><input type="checkbox" class="checkbox-widget" name="contents-checkBoxColumn-0-selectedItems" value="zero"  /></td>
+      <td><a href="http://127.0.0.1/secondContainer/zero">zero</a></td>
+      <td>01/01/01 01:01</td>
+      <td>02/02/02 02:02</td>
+    </tr>
+  </tbody>
+  ...
+
+Headers
+-------
+
+We have adapters to the columns that support inclusion of links in the headers
+to sort the columns.
+      for="zope.app.container.interfaces.IContainer
+           zope.interface.Interface
+           z3c.contents.interfaces.IContentsPage
+           z3c.contents.column.RenameColumn"
+      provides="z3c.table.interfaces.IColumnHeader"
+
+  >>> from z3c.contents.header import ContentsColumnHeader
+  >>> from z3c.table.interfaces import IColumnHeader
+  >>> zope.component.provideAdapter(ContentsColumnHeader,
+  ...     (IContainer, None, interfaces.IContentsPage, column.RenameColumn),
+  ...      provides=IColumnHeader)
+
+Now we shall see that the name column header includes a link with arguments to
+sort the table by that column.
+
+  >>> headerRequest = TestRequest()
+  >>> alsoProvides(headerRequest, IDivFormLayer)
+  >>> headerPage = browser.ContentsPage(secondContainer, headerRequest)
+  >>> headerPage.update()
+  >>> print headerPage.render()
+  <form action="http://127.0.0.1" method="post"
+        enctype="multipart/form-data" class="edit-form"
+        name="contents" id="contents">
+  ...
+  <thead>
+    <tr>
+      <th>X</th>
+      <th><a
+      href="?contents-sortOn=contents-renameColumn-1&contents-sortOrder=ascending"
+      title="Sort">Name</a></th>
+      <th>Created</th>
+      <th>Modified</th>
+    </tr>
+  </thead>
+  ...
+
+When we perform a search we also expect the the search terms will also be
+included in the query so as to maintain the search across views.
+
+  >>> searchRequest = TestRequest(form={'search.buttons.search': 'Search',
+  ...                       'search.widgets.searchterm': u'First zero'})
+  >>> alsoProvides(searchRequest, IDivFormLayer)
+  >>> searchPage = browser.ContentsPage(secondContainer, searchRequest)
+  >>> searchPage.update()
+  >>> print searchPage.render()
+  <form action="http://127.0.0.1" method="post"
+        enctype="multipart/form-data" class="edit-form"
+        name="contents" id="contents">
+  ...
+  <thead>
+    <tr>
+      <th>X</th>
+      <th><a
+      href="?contents-sortOn=contents-renameColumn-1&contents-sortOrder=ascending&search.widgets.searchterm=First+zero"
+      title="Sort">Name</a></th>
+      <th>Created</th>
+      <th>Modified</th>
+    </tr>
+  </thead>
+  ...
+
+
 Batching
 --------
 

Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/browser.py
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/browser.py	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/browser.py	2008-03-23 01:32:13 UTC (rev 84858)
@@ -31,7 +31,7 @@
 from zope.security.interfaces import Unauthorized
 from zope.traversing.interfaces import TraversalError
 from zope.traversing import api
-
+from zope.app.container.find import SimpleIdFindFilter
 from zope.app.container.interfaces import DuplicateIDError
 
 from z3c.form import button, field
@@ -40,6 +40,7 @@
 from z3c.template.template import getPageTemplate
 
 from z3c.contents import interfaces
+from z3c.contents.search import SimpleAttributeFindFilter
 
 _ = zope.i18nmessageid.MessageFactory('z3c')
 
@@ -62,7 +63,9 @@
 
 
 class ContentsSearch(object):
-    """An adapter for container context to satisfy search form requirements"""
+    """An adapter for container context to satisfy search form requirements
+    
+    """
 
     zope.interface.implements(interfaces.IContentsSearch)
     zope.component.adapts(zope.interface.Interface)
@@ -83,10 +86,15 @@
     template = getPageTemplate()
     fields = field.Fields(interfaces.IContentsSearch)
     prefix = 'search'
+    table = None
 
     @button.buttonAndHandler(_('Search'), name='search')
     def handleSearch(self, action):
-        pass
+        data, errors = self.extractData()
+        if errors:
+            self.status = u'Some error message'
+            return
+        self.table.searchterm = data.get('searchterm', '')
 
 
 # conditions
@@ -130,6 +138,9 @@
     supportsPaste = False
     supportsRename = False
 
+    # sort attributes
+    sortOn = 1 # initial sort on name column
+
     # customize this part
     allowCut = True
     allowCopy = True
@@ -138,6 +149,7 @@
     allowRename = True
 
     prefix = 'contents'
+    searchterm = ''
 
     # error messages
     cutNoItemsMessage = _('No items selected for cut')
@@ -158,14 +170,16 @@
     renameItemNotFoundMessage = _('Item not found')
 
     def update(self):
+
+        self.search = ContentsSearchForm(self.context, self.request)
+        self.search.table = self
+        self.search.update()
+
         # first setup columns and process the items as selected if any
         super(ContentsPage, self).update()
         # second find out if we support paste
         self.clipboard = queryPrincipalClipboard(self.request)
 
-        self.search = ContentsSearchForm(self.context, self.request)
-        self.search.update()
-
         self.setupCopyPasteMove()
         self.updateWidgets()
         self.updateActions()
@@ -196,6 +210,28 @@
         return self.template()
 
     @property
+    def values(self):
+        # not searching
+        if not self.searchterm:
+            return self.context.values()
+
+        # no search adapter for the context
+        try:
+            search = interfaces.ISearch(self.context)
+        except TypeError:
+            return self.context.values()
+
+        # perform the search
+        searchterms = self.searchterm.split(' ')
+
+        # possible enhancement would be to look up these filters as adapters to
+        # the container!
+        result = search.search(id_filters=[SimpleIdFindFilter(searchterms)],
+                            object_filters=[SimpleAttributeFindFilter(searchterms)])
+        return result
+
+
+    @property
     def hasContent(self):
         return bool(self.values)
 

Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/configure.zcml
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/configure.zcml	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/configure.zcml	2008-03-23 01:32:13 UTC (rev 84858)
@@ -42,10 +42,17 @@
       factory="z3c.contents.browser.ContentsSearch"
       />
 
+  <adapter
+     provides="z3c.contents.interfaces.ISearch"
+     for="zope.app.container.interfaces.IReadContainer"
+     permission="zope.Public"
+     factory="z3c.contents.search.SearchForContainer"
+     />
+
   <!-- include also sorting column headers for some columns
        leaving out CheckBoxColumn -->
   <adapter
-      factory="z3c.table.header.SortingColumnHeader"
+      factory="z3c.contents.header.ContentsColumnHeader"
       for="zope.app.container.interfaces.IContainer
            zope.interface.Interface
            z3c.contents.interfaces.IContentsPage
@@ -54,7 +61,7 @@
       />
 
   <adapter
-      factory="z3c.table.header.SortingColumnHeader"
+      factory="z3c.contents.header.ContentsColumnHeader"
       for="zope.app.container.interfaces.IContainer
            zope.interface.Interface
            z3c.contents.interfaces.IContentsPage
@@ -63,7 +70,7 @@
       />
 
   <adapter
-      factory="z3c.table.header.SortingColumnHeader"
+      factory="z3c.contents.header.ContentsColumnHeader"
       for="zope.app.container.interfaces.IContainer
            zope.interface.Interface
            z3c.contents.interfaces.IContentsPage

Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/ftesting.zcml
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/ftesting.zcml	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/ftesting.zcml	2008-03-23 01:32:13 UTC (rev 84858)
@@ -81,4 +81,18 @@
       permission="zope.ManageContent"
       />
 
+  <class class="z3c.contents.testing.Content">
+    <implements
+      interface="zope.annotation.interfaces.IAttributeAnnotatable"
+      />
+    <allow
+      interface="z3c.contents.testing.IContent"
+      />
+    <require
+      permission="zope.ManageContent"
+      set_schema="z3c.contents.testing.IContent"
+      />
+  </class>
+
+
 </configure>

Added: z3c.contents/branches/darrylcousins/src/z3c/contents/header.py
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/header.py	                        (rev 0)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/header.py	2008-03-23 01:32:13 UTC (rev 84858)
@@ -0,0 +1,26 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+$Id:$
+"""
+__docformat__ = "reStructuredText"
+
+
+from z3c.table.header import SortingColumnHeader
+
+class ContentsColumnHeader(SortingColumnHeader):
+    """Sorting column header."""
+
+    request_args = ['search.widgets.searchterm']
+


Property changes on: z3c.contents/branches/darrylcousins/src/z3c/contents/header.py
___________________________________________________________________
Name: svn:keywords
   + Id

Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/interfaces.py
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/interfaces.py	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/interfaces.py	2008-03-23 01:32:13 UTC (rev 84858)
@@ -25,7 +25,8 @@
 
 
 class IContentsPage(interfaces.ITable):
-    """Container management page"""
+    """Container management page
+    """
 
 class IContentsSearch(zope.interface.Interface):
     """We would like to provide a search field for searching within the
@@ -36,3 +37,20 @@
     """
 
     searchterm = zope.schema.TextLine(title=_(u'Search'))
+
+
+class ISearch(zope.interface.Interface):
+    """
+    Search support for containers.
+
+    This is different to z.a.c.interfaces.IFind in that the search method
+    will match **any** filter whereas the find method of IFind will match
+    if **all** filters match.
+    """
+
+    def search(id_filters=None, object_filters=None):
+        """Find object that matches **any** filters in all sub-objects.
+
+        This container itself is not included.
+        """
+

Added: z3c.contents/branches/darrylcousins/src/z3c/contents/search.py
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/search.py	                        (rev 0)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/search.py	2008-03-23 01:32:13 UTC (rev 84858)
@@ -0,0 +1,92 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+$Id:$
+"""
+__docformat__ = "reStructuredText"
+
+import zope.interface
+from zope.app.container.interfaces import (IObjectFindFilter,
+                                           IReadContainer)
+from zope.security.proxy import removeSecurityProxy
+
+from z3c.contents.interfaces import ISearch
+
+class SearchForContainer(object):
+
+    zope.interface.implements(ISearch)
+
+    __used_for__ = IReadContainer
+
+    def __init__(self, context):
+        self._context = context
+
+    def search(self, id_filters=None, object_filters=None):
+        'See ISearch'
+        id_filters = id_filters or []
+        object_filters = object_filters or []
+        result = []
+        container = self._context
+        for id, object in container.items():
+            _search_helper(id, object, container,
+                         id_filters, object_filters,
+                         result)
+        return result
+
+
+def _search_helper(id, object, container, id_filters, object_filters, result):
+    # check id filters if we get a match then return immediately
+    for id_filter in id_filters:
+        if id_filter.matches(id):
+            result.append(object)
+            return
+
+    # now check all object filters
+    for object_filter in object_filters:
+        if object_filter.matches(object):
+            result.append(object)
+            return
+
+    # do we need to check sub containers?
+    if not IReadContainer.providedBy(object):
+        return
+
+    container = object
+    for id, object in container.items():
+        _search_helper(id, object, container, id_filters, object_filters, result)
+
+
+class SimpleAttributeFindFilter(object):
+    """Filter objects on text or integer attributes"""
+    zope.interface.implements(IObjectFindFilter)
+    
+    def __init__(self, terms):
+        self.terms = terms
+    
+    def matches(self, object):
+        """Check if one of the search terms is in any text or integer field of
+        this object
+
+        """
+        # surely a better way to get to the attributes than is done here?
+        object = removeSecurityProxy(object)
+
+        for key in [k for k in dir(object) if not k.startswith('_')]:
+            value = str(getattr(object, key)).lower()
+            for term in self.terms:
+                term = term.lower()
+                if term in value:
+                    return True
+        return False
+


Property changes on: z3c.contents/branches/darrylcousins/src/z3c/contents/search.py
___________________________________________________________________
Name: svn:keywords
   + Id

Modified: z3c.contents/branches/darrylcousins/src/z3c/contents/testing.py
===================================================================
--- z3c.contents/branches/darrylcousins/src/z3c/contents/testing.py	2008-03-22 19:20:43 UTC (rev 84857)
+++ z3c.contents/branches/darrylcousins/src/z3c/contents/testing.py	2008-03-23 01:32:13 UTC (rev 84858)
@@ -16,6 +16,7 @@
 """
 __docformat__ = "reStructuredText"
 
+import zope.interface
 from zope.publisher.interfaces.browser import IBrowserRequest
 
 import z3c.layer.ready2go
@@ -26,8 +27,14 @@
 class IContentsTestBrowserSkin(IContentsTestBrowserLayer):
     """The browser skin."""
 
+class IContent(zope.interface.Interface):
+
+    title = zope.schema.TextLine(title=u'Title')
+    number = zope.schema.Int(title=u'Number')
+
 class Content(object):
     """Sample content which is pickable for copy test."""
+    zope.interface.implements(IContent)
     def __init__(self, title, number):
         self.title = title
         self.number = number



More information about the Checkins mailing list