[Zope-Checkins] SVN: Zope/trunk/lib/python/Products/BTreeFolder2/ added BTreeFolder2

Andreas Jung andreas at andreas-jung.com
Mon Apr 25 09:29:39 EDT 2005


Log message for revision 30156:
  added BTreeFolder2
  

Changed:
  A   Zope/trunk/lib/python/Products/BTreeFolder2/
  A   Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.py
  A   Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.pyc
  A   Zope/trunk/lib/python/Products/BTreeFolder2/CHANGES.txt
  A   Zope/trunk/lib/python/Products/BTreeFolder2/CMFBTreeFolder.py
  A   Zope/trunk/lib/python/Products/BTreeFolder2/README.txt
  A   Zope/trunk/lib/python/Products/BTreeFolder2/__init__.py
  A   Zope/trunk/lib/python/Products/BTreeFolder2/__init__.pyc
  A   Zope/trunk/lib/python/Products/BTreeFolder2/btreefolder2.gif
  A   Zope/trunk/lib/python/Products/BTreeFolder2/contents.dtml
  A   Zope/trunk/lib/python/Products/BTreeFolder2/folderAdd.dtml
  A   Zope/trunk/lib/python/Products/BTreeFolder2/tests/
  A   Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.py
  A   Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.pyc
  A   Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.py
  A   Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.pyc
  A   Zope/trunk/lib/python/Products/BTreeFolder2/version.txt

-=-
Added: Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.py
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.py	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.py	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,514 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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
+#
+##############################################################################
+"""BTreeFolder2
+
+$Id: BTreeFolder2.py,v 1.27 2004/03/17 22:49:25 urbanape Exp $
+"""
+
+import sys
+from cgi import escape
+from urllib import quote
+from random import randint
+from types import StringType
+
+import Globals
+from Globals import DTMLFile
+from Globals import Persistent
+from Acquisition import aq_base
+from BTrees.OOBTree import OOBTree
+from BTrees.OIBTree import OIBTree, union
+from BTrees.Length import Length
+from ZODB.POSException import ConflictError
+from OFS.ObjectManager import BadRequestException, BeforeDeleteException
+from OFS.Folder import Folder
+from AccessControl import getSecurityManager, ClassSecurityInfo
+from AccessControl.Permissions import access_contents_information, \
+     view_management_screens
+from zLOG import LOG, INFO, ERROR, WARNING
+from Products.ZCatalog.Lazy import LazyMap
+
+
+manage_addBTreeFolderForm = DTMLFile('folderAdd', globals())
+
+def manage_addBTreeFolder(dispatcher, id, title='', REQUEST=None):
+    """Adds a new BTreeFolder object with id *id*.
+    """
+    id = str(id)
+    ob = BTreeFolder2(id)
+    ob.title = str(title)
+    dispatcher._setObject(id, ob)
+    ob = dispatcher._getOb(id)
+    if REQUEST is not None:
+        return dispatcher.manage_main(dispatcher, REQUEST, update_menu=1)
+
+
+listtext0 = '''<select name="ids:list" multiple="multiple" size="%s">
+'''
+listtext1 = '''<option value="%s">%s</option>
+'''
+listtext2 = '''</select>
+'''
+
+
+_marker = []  # Create a new marker object.
+
+MAX_UNIQUEID_ATTEMPTS = 1000
+
+class ExhaustedUniqueIdsError (Exception):
+    pass
+
+
+class BTreeFolder2Base (Persistent):
+    """Base for BTree-based folders.
+    """
+
+    security = ClassSecurityInfo()
+
+    manage_options=(
+        ({'label':'Contents', 'action':'manage_main',},
+         ) + Folder.manage_options[1:]
+        )
+
+    security.declareProtected(view_management_screens,
+                              'manage_main')
+    manage_main = DTMLFile('contents', globals())
+
+    _tree = None      # OOBTree: { id -> object }
+    _count = None     # A BTrees.Length
+    _v_nextid = 0     # The integer component of the next generated ID
+    _mt_index = None  # OOBTree: { meta_type -> OIBTree: { id -> 1 } }
+    title = ''
+
+
+    def __init__(self, id=None):
+        if id is not None:
+            self.id = id
+        self._initBTrees()
+
+    def _initBTrees(self):
+        self._tree = OOBTree()
+        self._count = Length()
+        self._mt_index = OOBTree()
+
+
+    def _populateFromFolder(self, source):
+        """Fill this folder with the contents of another folder.
+        """
+        for name in source.objectIds():
+            value = source._getOb(name, None)
+            if value is not None:
+                self._setOb(name, aq_base(value))
+
+
+    security.declareProtected(view_management_screens, 'manage_fixCount')
+    def manage_fixCount(self):
+        """Calls self._fixCount() and reports the result as text.
+        """
+        old, new = self._fixCount()
+        path = '/'.join(self.getPhysicalPath())
+        if old == new:
+            return "No count mismatch detected in BTreeFolder2 at %s." % path
+        else:
+            return ("Fixed count mismatch in BTreeFolder2 at %s. "
+                    "Count was %d; corrected to %d" % (path, old, new))
+
+
+    def _fixCount(self):
+        """Checks if the value of self._count disagrees with
+        len(self.objectIds()). If so, corrects self._count. Returns the
+        old and new count values. If old==new, no correction was
+        performed.
+        """
+        old = self._count()
+        new = len(self.objectIds())
+        if old != new:
+            self._count.set(new)
+        return old, new
+
+
+    security.declareProtected(view_management_screens, 'manage_cleanup')
+    def manage_cleanup(self):
+        """Calls self._cleanup() and reports the result as text.
+        """
+        v = self._cleanup()
+        path = '/'.join(self.getPhysicalPath())
+        if v:
+            return "No damage detected in BTreeFolder2 at %s." % path
+        else:
+            return ("Fixed BTreeFolder2 at %s.  "
+                    "See the log for more details." % path)
+
+
+    def _cleanup(self):
+        """Cleans up errors in the BTrees.
+
+        Certain ZODB bugs have caused BTrees to become slightly insane.
+        Fortunately, there is a way to clean up damaged BTrees that
+        always seems to work: make a new BTree containing the items()
+        of the old one.
+
+        Returns 1 if no damage was detected, or 0 if damage was
+        detected and fixed.
+        """
+        from BTrees.check import check
+        path = '/'.join(self.getPhysicalPath())
+        try:
+            check(self._tree)
+            for key in self._tree.keys():
+                if not self._tree.has_key(key):
+                    raise AssertionError(
+                        "Missing value for key: %s" % repr(key))
+            check(self._mt_index)
+            for key, value in self._mt_index.items():
+                if (not self._mt_index.has_key(key)
+                    or self._mt_index[key] is not value):
+                    raise AssertionError(
+                        "Missing or incorrect meta_type index: %s"
+                        % repr(key))
+                check(value)
+                for k in value.keys():
+                    if not value.has_key(k):
+                        raise AssertionError(
+                            "Missing values for meta_type index: %s"
+                            % repr(key))
+            return 1
+        except AssertionError:
+            LOG('BTreeFolder2', WARNING,
+                'Detected damage to %s. Fixing now.' % path,
+                error=sys.exc_info())
+            try:
+                self._tree = OOBTree(self._tree)
+                mt_index = OOBTree()
+                for key, value in self._mt_index.items():
+                    mt_index[key] = OIBTree(value)
+                self._mt_index = mt_index
+            except:
+                LOG('BTreeFolder2', ERROR, 'Failed to fix %s.' % path,
+                    error=sys.exc_info())
+                raise
+            else:
+                LOG('BTreeFolder2', INFO, 'Fixed %s.' % path)
+            return 0
+
+
+    def _getOb(self, id, default=_marker):
+        """Return the named object from the folder.
+        """
+        tree = self._tree
+        if default is _marker:
+            ob = tree[id]
+            return ob.__of__(self)
+        else:
+            ob = tree.get(id, _marker)
+            if ob is _marker:
+                return default
+            else:
+                return ob.__of__(self)
+
+
+    def _setOb(self, id, object):
+        """Store the named object in the folder.
+        """
+        tree = self._tree
+        if tree.has_key(id):
+            raise KeyError('There is already an item named "%s".' % id)
+        tree[id] = object
+        self._count.change(1)
+        # Update the meta type index.
+        mti = self._mt_index
+        meta_type = getattr(object, 'meta_type', None)
+        if meta_type is not None:
+            ids = mti.get(meta_type, None)
+            if ids is None:
+                ids = OIBTree()
+                mti[meta_type] = ids
+            ids[id] = 1
+
+
+    def _delOb(self, id):
+        """Remove the named object from the folder.
+        """
+        tree = self._tree
+        meta_type = getattr(tree[id], 'meta_type', None)
+        del tree[id]
+        self._count.change(-1)
+        # Update the meta type index.
+        if meta_type is not None:
+            mti = self._mt_index
+            ids = mti.get(meta_type, None)
+            if ids is not None and ids.has_key(id):
+                del ids[id]
+                if not ids:
+                    # Removed the last object of this meta_type.
+                    # Prune the index.
+                    del mti[meta_type]
+
+
+    security.declareProtected(view_management_screens, 'getBatchObjectListing')
+    def getBatchObjectListing(self, REQUEST=None):
+        """Return a structure for a page template to show the list of objects.
+        """
+        if REQUEST is None:
+            REQUEST = {}
+        pref_rows = int(REQUEST.get('dtpref_rows', 20))
+        b_start = int(REQUEST.get('b_start', 1))
+        b_count = int(REQUEST.get('b_count', 1000))
+        b_end = b_start + b_count - 1
+        url = self.absolute_url() + '/manage_main'
+        idlist = self.objectIds()  # Pre-sorted.
+        count = self.objectCount()
+
+        if b_end < count:
+            next_url = url + '?b_start=%d' % (b_start + b_count)
+        else:
+            b_end = count
+            next_url = ''
+
+        if b_start > 1:
+            prev_url = url + '?b_start=%d' % max(b_start - b_count, 1)
+        else:
+            prev_url = ''
+
+        formatted = []
+        formatted.append(listtext0 % pref_rows)
+        for i in range(b_start - 1, b_end):
+            optID = escape(idlist[i])
+            formatted.append(listtext1 % (escape(optID, quote=1), optID))
+        formatted.append(listtext2)
+        return {'b_start': b_start, 'b_end': b_end,
+                'prev_batch_url': prev_url,
+                'next_batch_url': next_url,
+                'formatted_list': ''.join(formatted)}
+
+
+    security.declareProtected(view_management_screens,
+                              'manage_object_workspace')
+    def manage_object_workspace(self, ids=(), REQUEST=None):
+        '''Redirects to the workspace of the first object in
+        the list.'''
+        if ids and REQUEST is not None:
+            REQUEST.RESPONSE.redirect(
+                '%s/%s/manage_workspace' % (
+                self.absolute_url(), quote(ids[0])))
+        else:
+            return self.manage_main(self, REQUEST)
+
+
+    security.declareProtected(access_contents_information,
+                              'tpValues')
+    def tpValues(self):
+        """Ensures the items don't show up in the left pane.
+        """
+        return ()
+
+
+    security.declareProtected(access_contents_information,
+                              'objectCount')
+    def objectCount(self):
+        """Returns the number of items in the folder."""
+        return self._count()
+
+
+    security.declareProtected(access_contents_information, 'has_key')
+    def has_key(self, id):
+        """Indicates whether the folder has an item by ID.
+        """
+        return self._tree.has_key(id)
+
+
+    security.declareProtected(access_contents_information,
+                              'objectIds')
+    def objectIds(self, spec=None):
+        # Returns a list of subobject ids of the current object.
+        # If 'spec' is specified, returns objects whose meta_type
+        # matches 'spec'.
+        if spec is not None:
+            if isinstance(spec, StringType):
+                spec = [spec]
+            mti = self._mt_index
+            set = None
+            for meta_type in spec:
+                ids = mti.get(meta_type, None)
+                if ids is not None:
+                    set = union(set, ids)
+            if set is None:
+                return ()
+            else:
+                return set.keys()
+        else:
+            return self._tree.keys()
+
+
+    security.declareProtected(access_contents_information,
+                              'objectValues')
+    def objectValues(self, spec=None):
+        # Returns a list of actual subobjects of the current object.
+        # If 'spec' is specified, returns only objects whose meta_type
+        # match 'spec'.
+        return LazyMap(self._getOb, self.objectIds(spec))
+
+
+    security.declareProtected(access_contents_information,
+                              'objectItems')
+    def objectItems(self, spec=None):
+        # Returns a list of (id, subobject) tuples of the current object.
+        # If 'spec' is specified, returns only objects whose meta_type match
+        # 'spec'
+        return LazyMap(lambda id, _getOb=self._getOb: (id, _getOb(id)),
+                       self.objectIds(spec))
+
+
+    security.declareProtected(access_contents_information,
+                              'objectMap')
+    def objectMap(self):
+        # Returns a tuple of mappings containing subobject meta-data.
+        return LazyMap(lambda (k, v):
+                       {'id': k, 'meta_type': getattr(v, 'meta_type', None)},
+                       self._tree.items(), self._count())
+
+    # superValues() looks for the _objects attribute, but the implementation
+    # would be inefficient, so superValues() support is disabled.
+    _objects = ()
+
+
+    security.declareProtected(access_contents_information,
+                              'objectIds_d')
+    def objectIds_d(self, t=None):
+        ids = self.objectIds(t)
+        res = {}
+        for id in ids:
+            res[id] = 1
+        return res
+
+
+    security.declareProtected(access_contents_information,
+                              'objectMap_d')
+    def objectMap_d(self, t=None):
+        return self.objectMap()
+
+
+    def _checkId(self, id, allow_dup=0):
+        if not allow_dup and self.has_key(id):
+            raise BadRequestException, ('The id "%s" is invalid--'
+                                        'it is already in use.' % id)
+
+
+    def _setObject(self, id, object, roles=None, user=None, set_owner=1):
+        v=self._checkId(id)
+        if v is not None: id=v
+
+        # If an object by the given id already exists, remove it.
+        if self.has_key(id):
+            self._delObject(id)
+
+        self._setOb(id, object)
+        object = self._getOb(id)
+
+        if set_owner:
+            object.manage_fixupOwnershipAfterAdd()
+
+            # Try to give user the local role "Owner", but only if
+            # no local roles have been set on the object yet.
+            if hasattr(object, '__ac_local_roles__'):
+                if object.__ac_local_roles__ is None:
+                    user=getSecurityManager().getUser()
+                    if user is not None:
+                        userid=user.getId()
+                        if userid is not None:
+                            object.manage_setLocalRoles(userid, ['Owner'])
+
+        object.manage_afterAdd(object, self)
+        return id
+
+
+    def _delObject(self, id, dp=1):
+        object = self._getOb(id)
+        try:
+            object.manage_beforeDelete(object, self)
+        except BeforeDeleteException, ob:
+            raise
+        except ConflictError:
+            raise
+        except:
+            LOG('Zope', ERROR, 'manage_beforeDelete() threw',
+                error=sys.exc_info())
+        self._delOb(id)
+
+
+    # Aliases for mapping-like access.
+    __len__ = objectCount
+    keys = objectIds
+    values = objectValues
+    items = objectItems
+
+    # backward compatibility
+    hasObject = has_key
+
+    security.declareProtected(access_contents_information, 'get')
+    def get(self, name, default=None):
+        return self._getOb(name, default)
+
+
+    # Utility for generating unique IDs.
+
+    security.declareProtected(access_contents_information, 'generateId')
+    def generateId(self, prefix='item', suffix='', rand_ceiling=999999999):
+        """Returns an ID not used yet by this folder.
+
+        The ID is unlikely to collide with other threads and clients.
+        The IDs are sequential to optimize access to objects
+        that are likely to have some relation.
+        """
+        tree = self._tree
+        n = self._v_nextid
+        attempt = 0
+        while 1:
+            if n % 4000 != 0 and n <= rand_ceiling:
+                id = '%s%d%s' % (prefix, n, suffix)
+                if not tree.has_key(id):
+                    break
+            n = randint(1, rand_ceiling)
+            attempt = attempt + 1
+            if attempt > MAX_UNIQUEID_ATTEMPTS:
+                # Prevent denial of service
+                raise ExhaustedUniqueIdsError
+        self._v_nextid = n + 1
+        return id
+
+    def __getattr__(self, name):
+        # Boo hoo hoo!  Zope 2 prefers implicit acquisition over traversal
+        # to subitems, and __bobo_traverse__ hooks don't work with
+        # restrictedTraverse() unless __getattr__() is also present.
+        # Oh well.
+        res = self._tree.get(name)
+        if res is None:
+            raise AttributeError, name
+        return res
+
+
+Globals.InitializeClass(BTreeFolder2Base)
+
+
+class BTreeFolder2 (BTreeFolder2Base, Folder):
+    """BTreeFolder2 based on OFS.Folder.
+    """
+    meta_type = 'BTreeFolder2'
+
+    def _checkId(self, id, allow_dup=0):
+        Folder._checkId(self, id, allow_dup)
+        BTreeFolder2Base._checkId(self, id, allow_dup)
+    
+
+Globals.InitializeClass(BTreeFolder2)
+

Added: Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.pyc
===================================================================
(Binary files differ)


Property changes on: Zope/trunk/lib/python/Products/BTreeFolder2/BTreeFolder2.pyc
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope/trunk/lib/python/Products/BTreeFolder2/CHANGES.txt
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/CHANGES.txt	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/CHANGES.txt	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,21 @@
+Version 1.0.1
+
+  - ConflictError was swallowed by _delObject. This could break code
+    expecting to do cleanups before deletion.
+
+  - Renamed hasObject() to has_key().  hasObject() conflicted with
+    another product.
+
+  - You can now visit objects whose names have a trailing space.
+
+Version 1.0
+
+  - BTreeFolder2s now use an icon contributed by Chris Withers.
+
+  - Since recent ZODB releases have caused minor corruption in BTrees,
+    there is now a manage_cleanup method for fixing damaged BTrees
+    contained in BTreeFolders.
+
+Version 0.5.1
+
+  - Fixed the CMFBTreeFolder constructor.

Added: Zope/trunk/lib/python/Products/BTreeFolder2/CMFBTreeFolder.py
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/CMFBTreeFolder.py	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/CMFBTreeFolder.py	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,68 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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
+#
+##############################################################################
+"""CMFBTreeFolder
+
+$Id: CMFBTreeFolder.py,v 1.2 2002/10/30 14:54:18 shane Exp $
+"""
+
+import Globals
+from BTreeFolder2 import BTreeFolder2Base
+from Products.CMFCore.PortalFolder import PortalFolder
+import Products.CMFCore.PortalFolder
+
+
+_actions = Products.CMFCore.PortalFolder.factory_type_information[0]['actions']
+
+factory_type_information = ( { 'id'             : 'CMF BTree Folder',
+                               'meta_type'      : 'CMF BTree Folder',
+                               'description'    : """\
+CMF folder designed to hold a lot of objects.""",
+                               'icon'           : 'folder_icon.gif',
+                               'product'        : 'BTreeFolder2',
+                               'factory'        : 'manage_addCMFBTreeFolder',
+                               'filter_content_types' : 0,
+                               'immediate_view' : 'folder_edit_form',
+                               'actions'        : _actions,
+                               },
+                           )
+
+
+def manage_addCMFBTreeFolder(dispatcher, id, title='', REQUEST=None):
+    """Adds a new BTreeFolder object with id *id*.
+    """
+    id = str(id)
+    ob = CMFBTreeFolder(id)
+    ob.title = str(title)
+    dispatcher._setObject(id, ob)
+    ob = dispatcher._getOb(id)
+    if REQUEST is not None:
+        REQUEST['RESPONSE'].redirect(ob.absolute_url() + '/manage_main' )
+
+
+class CMFBTreeFolder(BTreeFolder2Base, PortalFolder):
+    """BTree folder for CMF sites.
+    """
+    meta_type = 'CMF BTree Folder'
+
+    def __init__(self, id, title=''):
+        PortalFolder.__init__(self, id, title)
+        BTreeFolder2Base.__init__(self, id)
+
+    def _checkId(self, id, allow_dup=0):
+        PortalFolder._checkId(self, id, allow_dup)
+        BTreeFolder2Base._checkId(self, id, allow_dup)
+
+
+Globals.InitializeClass(CMFBTreeFolder)
+

Added: Zope/trunk/lib/python/Products/BTreeFolder2/README.txt
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/README.txt	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/README.txt	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,124 @@
+
+Contact
+=======
+
+Shane Hathaway
+Zope Corporation
+shane at zope dot com
+
+
+BTreeFolder2 Product
+====================
+
+BTreeFolder2 is a Zope product that acts like a Zope folder but can
+store many more items.
+
+When you fill a Zope folder with too many items, both Zope and your
+browser get overwhelmed.  Zope has to load and store a large folder
+object, and the browser has to render large HTML tables repeatedly.
+Zope can store a lot of objects, but it has trouble storing a lot of
+objects in a single standard folder.
+
+Zope Corporation once had an extensive discussion on the subject.  It
+was decided that we would expand standard folders to handle large
+numbers of objects gracefully.  Unfortunately, Zope folders are used
+and extended in so many ways today that it would be difficult to
+modify standard folders in a way that would be compatible with all
+Zope products.
+
+So the BTreeFolder product was born.  It stored all subobjects in a
+ZODB BTree, a structure designed to allow many items without loading
+them all into memory.  It also rendered the contents of the folder as
+a simple select list rather than a table.  Most browsers have no
+trouble rendering large select lists.
+
+But there was still one issue remaining.  BTreeFolders still stored
+the ID of all subobjects in a single database record.  If you put tens
+of thousands of items in a single BTreeFolder, you would still be
+loading and storing a multi-megabyte folder object.  Zope can do this,
+but not quickly, and not without bloating the database.
+
+BTreeFolder2 solves this issue.  It stores not only the subobjects but
+also the IDs of the subobjects in a BTree.  It also batches the list
+of items in the UI, showing only 1000 items at a time.  So if you
+write your application carefully, you can use a BTreeFolder2 to store
+as many items as will fit in physical storage.
+
+There are products that depend on the internal structure of the
+original BTreeFolder, however.  So rather than risk breaking those
+products, the product has been renamed.  You can have both products
+installed at the same time.  If you're developing new applications,
+you should use BTreeFolder2.
+
+
+Installation
+============
+
+Untar BTreeFolder2 in your Products directory and restart Zope.
+BTreeFolder2 will now be available in your "Add" drop-down.
+
+Additionally, if you have CMF installed, the BTreeFolder2 product also
+provides the "CMF BTree Folder" addable type.
+
+
+Usage
+=====
+
+The BTreeFolder2 user interface shows a list of items rather than a
+series of checkboxes.  To visit an item, select it in the list and
+click the "edit" button.
+
+BTreeFolder2 objects provide Python dictionary-like methods to make them
+easier to use in Python code than standard folders::
+
+    has_key(key)
+    keys()
+    values()
+    items()
+    get(key, default=None)
+    __len__()
+
+keys(), values(), and items() return sequences, but not necessarily
+tuples or lists.  Use len(folder) to call the __len__() method.  The
+objects returned by values() and items() have acquisition wrappers.
+
+BTreeFolder2 also provides a method for generating unique,
+non-overlapping IDs::
+
+    generateId(prefix='item', suffix='', rand_ceiling=999999999)
+
+The ID returned by this method is guaranteed to not clash with any
+other ID in the folder.  Use the returned value as the ID for new
+objects.  The generated IDs tend to be sequential so that objects that
+are likely related in some way get loaded together.
+
+BTreeFolder2 implements the full Folder interface, with the exception
+that the superValues() method does not return any items.  To implement
+the method in the way the Zope codebase expects would undermine the
+performance benefits gained by using BTreeFolder2.
+
+
+Repairing BTree Damage
+======================
+
+Certain ZODB bugs in the past have caused minor corruption in BTrees.
+Fortunately, the damage is apparently easy to repair.  As of version
+1.0, BTreeFolder2 provides a 'manage_cleanup' method that will check
+the internal structure of existing BTreeFolder2 instances and repair
+them if necessary.  Many thanks to Tim Peters, who fixed the BTrees
+code and provided a function for checking a BTree.
+
+Visit a BTreeFolder2 instance through the web as a manager.  Add
+"manage_cleanup" to the end of the URL and request that URL.  It may
+take some time to load and fix the entire structure.  If problems are
+detected, information will be added to the event log.
+
+
+Future
+======
+
+BTreeFolder2 will be maintained for Zope 2.  Zope 3, however, is not
+likely to require BTreeFolder, since the intention is to make Zope 3
+folders gracefully expand to support many items.
+
+

Added: Zope/trunk/lib/python/Products/BTreeFolder2/__init__.py
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/__init__.py	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/__init__.py	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,51 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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
+#
+##############################################################################
+__doc__='''BTreeFolder2 Product Initialization
+$Id: __init__.py,v 1.4 2003/08/21 17:03:52 shane Exp $'''
+__version__='$Revision: 1.4 $'[11:-2]
+
+import BTreeFolder2
+
+def initialize(context):
+
+    context.registerClass(
+        BTreeFolder2.BTreeFolder2,
+        constructors=(BTreeFolder2.manage_addBTreeFolderForm,
+                      BTreeFolder2.manage_addBTreeFolder),
+        icon='btreefolder2.gif',
+        )
+
+    #context.registerHelp()
+    #context.registerHelpTitle('Zope Help')
+
+    context.registerBaseClass(BTreeFolder2.BTreeFolder2)
+
+    try:
+        from Products.CMFCore import utils
+    except ImportError:
+        # CMF not installed
+        pass
+    else:
+        # CMF installed; make available a special folder type.
+        import CMFBTreeFolder
+        ADD_FOLDERS_PERMISSION = 'Add portal folders'
+
+        utils.ContentInit(
+            'CMF BTree Folder',
+            content_types=(CMFBTreeFolder.CMFBTreeFolder,),
+            permission=ADD_FOLDERS_PERMISSION,
+            extra_constructors=(CMFBTreeFolder.manage_addCMFBTreeFolder,),
+            fti=CMFBTreeFolder.factory_type_information
+            ).initialize(context)
+

Added: Zope/trunk/lib/python/Products/BTreeFolder2/__init__.pyc
===================================================================
(Binary files differ)


Property changes on: Zope/trunk/lib/python/Products/BTreeFolder2/__init__.pyc
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope/trunk/lib/python/Products/BTreeFolder2/btreefolder2.gif
===================================================================
(Binary files differ)


Property changes on: Zope/trunk/lib/python/Products/BTreeFolder2/btreefolder2.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope/trunk/lib/python/Products/BTreeFolder2/contents.dtml
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/contents.dtml	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/contents.dtml	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,164 @@
+<dtml-let form_title="'Contents'">
+<dtml-if manage_page_header>
+  <dtml-var manage_page_header>
+<dtml-else>
+  <html><head><title>&dtml-form_title;</title></head>
+  <body bgcolor="#ffffff">
+</dtml-if>
+</dtml-let>
+<dtml-var manage_tabs>
+
+<script type="text/javascript">
+<!-- 
+
+isSelected = false;
+
+function toggleSelect() {
+  elem = document.objectItems.elements['ids:list'];
+  if (isSelected == false) {
+    for (i = 0; i < elem.options.length; i++) {
+      elem.options[i].selected = true;
+    }
+    isSelected = true;
+    document.objectItems.selectButton.value = "Deselect All";
+    return isSelected;
+  }
+  else {
+    for (i = 0; i < elem.options.length; i++) {
+      elem.options[i].selected = false;
+    }
+    isSelected = false;
+    document.objectItems.selectButton.value = "Select All";
+    return isSelected;
+  }
+}
+
+//-->
+</script>
+
+<dtml-unless skey><dtml-call expr="REQUEST.set('skey', 'id')"></dtml-unless>
+<dtml-unless rkey><dtml-call expr="REQUEST.set('rkey', '')"></dtml-unless>
+
+<!-- Add object widget -->
+<br />
+<dtml-if filtered_meta_types>
+  <table width="100%" cellspacing="0" cellpadding="0" border="0">
+  <tr>
+  <td align="left" valign="top">&nbsp;</td>
+  <td align="right" valign="top">
+  <div class="form-element">
+  <form action="&dtml-URL1;/" method="get">
+  <dtml-if "_.len(filtered_meta_types) > 1">
+    <select class="form-element" name=":action" 
+     onChange="location.href='&dtml-URL1;/'+this.options[this.selectedIndex].value">
+    <option value="manage_workspace" disabled>Select type to add...</option>
+    <dtml-in filtered_meta_types mapping sort=name>
+    <option value="&dtml.url_quote-action;">&dtml-name;</option>
+    </dtml-in>
+    </select>
+    <input class="form-element" type="submit" name="submit" value=" Add " />
+  <dtml-else>
+    <dtml-in filtered_meta_types mapping sort=name>
+    <input type="hidden" name=":method" value="&dtml.url_quote-action;" />
+    <input class="form-element" type="submit" name="submit" value=" Add &dtml-name;" />
+    </dtml-in>
+  </dtml-if>
+  </form>
+  </div>
+  </td>
+  </tr>
+  </table>
+</dtml-if>
+
+<form action="&dtml-URL1;/" name="objectItems" method="post">
+<dtml-if objectCount>
+<dtml-with expr="getBatchObjectListing(REQUEST)" mapping>
+
+<p>
+<dtml-if prev_batch_url><a href="&dtml-prev_batch_url;">&lt;&lt;</a></dtml-if>
+<em>Items <dtml-var b_start> through <dtml-var b_end> of <dtml-var objectCount></em>
+<dtml-if next_batch_url><a href="&dtml-next_batch_url;">&gt;&gt;</a></dtml-if>
+</p>
+
+<dtml-var formatted_list>
+
+<table cellspacing="0" cellpadding="2" border="0">
+<tr>
+  <td align="left" valign="top" width="16"></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <input class="form-element" type="submit"
+   name="manage_object_workspace:method" value="Edit" />
+  <dtml-unless dontAllowCopyAndPaste>
+  <input class="form-element" type="submit" name="manage_renameForm:method" 
+   value="Rename" />
+  <input class="form-element" type="submit" name="manage_cutObjects:method" 
+   value="Cut" /> 
+  <input class="form-element" type="submit" name="manage_copyObjects:method" 
+   value="Copy" />
+  <dtml-if cb_dataValid>
+  <input class="form-element" type="submit" name="manage_pasteObjects:method" 
+   value="Paste" />
+  </dtml-if>
+  </dtml-unless>
+  <dtml-if "_.SecurityCheckPermission('Delete objects',this())">
+  <input class="form-element" type="submit" name="manage_delObjects:method" 
+   value="Delete" />
+  </dtml-if>
+  <dtml-if "_.SecurityCheckPermission('Import/Export objects', this())">
+  <input class="form-element" type="submit" 
+   name="manage_importExportForm:method" 
+   value="Import/Export" />
+  </dtml-if>
+<script type="text/javascript">
+<!-- 
+if (document.forms[0]) {
+  document.write('<input class="form-element" type="submit" name="selectButton" value="Select All" onClick="toggleSelect(); return false">')
+  }
+//-->
+</script>
+  </div>
+  </td>
+</tr>
+</table>
+
+</dtml-with>
+<dtml-else>
+<table cellspacing="0" cellpadding="2" border="0">
+<tr>
+<td>
+<div class="std-text">
+There are currently no items in <em>&dtml-title_or_id;</em>
+<br /><br />
+</div>
+<dtml-unless dontAllowCopyAndPaste>
+<dtml-if cb_dataValid>
+<div class="form-element">
+<input class="form-element" type="submit" name="manage_pasteObjects:method" 
+ value="Paste" />
+</div>
+</dtml-if>
+</dtml-unless>
+<dtml-if "_.SecurityCheckPermission('Import/Export objects', this())">
+<input class="form-element" type="submit"
+  name="manage_importExportForm:method" value="Import/Export" />
+</dtml-if>
+</td>
+</tr>
+</table>
+</dtml-if>
+</form>
+
+<dtml-if update_menu>
+<script type="text/javascript">
+<!--
+window.parent.update_menu();
+//-->
+</script>
+</dtml-if>
+
+<dtml-if manage_page_footer>
+  <dtml-var manage_page_footer>
+<dtml-else>
+  </body></html>
+</dtml-if>

Added: Zope/trunk/lib/python/Products/BTreeFolder2/folderAdd.dtml
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/folderAdd.dtml	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/folderAdd.dtml	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,67 @@
+<dtml-let form_title="'Add BTreeFolder2'">
+<dtml-if manage_page_header>
+  <dtml-var manage_page_header>
+  <dtml-var manage_form_title>
+<dtml-else>
+  <html><head><title>&dtml-form_title;</title></head>
+  <body bgcolor="#ffffff">
+  <h2>&dtml-form_title;</h2>
+</dtml-if>
+</dtml-let>
+
+<p class="form-help">
+A Folder contains other objects. Use Folders to organize your
+web objects in to logical groups.
+</p>
+
+<p class="form-help">
+A BTreeFolder2 may be able to handle a larger number
+of objects than a standard folder because it does not need to
+activate other subobjects in order to access a single subobject.
+It is more efficient than the original BTreeFolder product,
+but does not provide attribute access.
+</p>
+
+<FORM ACTION="manage_addBTreeFolder" METHOD="POST">
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value="Add" />
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<dtml-if manage_page_footer>
+  <dtml-var manage_page_footer>
+<dtml-else>
+  </body></html>
+</dtml-if>

Added: Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.py
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.py	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.py	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1 @@
+"""Python package."""

Added: Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.pyc
===================================================================
(Binary files differ)


Property changes on: Zope/trunk/lib/python/Products/BTreeFolder2/tests/__init__.pyc
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.py
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.py	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.py	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1,237 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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
+#
+##############################################################################
+"""Unit tests for BTreeFolder2.
+
+$Id: testBTreeFolder2.py,v 1.8 2004/03/15 20:31:40 shane Exp $
+"""
+
+import unittest
+import ZODB
+import Testing
+import Zope2
+from Products.BTreeFolder2.BTreeFolder2 \
+     import BTreeFolder2, ExhaustedUniqueIdsError
+from OFS.ObjectManager import BadRequestException
+from OFS.Folder import Folder
+from Acquisition import aq_base
+
+
+class BTreeFolder2Tests(unittest.TestCase):
+
+    def getBase(self, ob):
+        # This is overridden in subclasses.
+        return aq_base(ob)
+
+    def setUp(self):
+        self.f = BTreeFolder2('root')
+        ff = BTreeFolder2('item')
+        self.f._setOb(ff.id, ff)
+        self.ff = self.f._getOb(ff.id)
+
+    def testAdded(self):
+        self.assertEqual(self.ff.id, 'item')
+
+    def testCount(self):
+        self.assertEqual(self.f.objectCount(), 1)
+        self.assertEqual(self.ff.objectCount(), 0)
+        self.assertEqual(len(self.f), 1)
+        self.assertEqual(len(self.ff), 0)
+
+    def testObjectIds(self):
+        self.assertEqual(list(self.f.objectIds()), ['item'])
+        self.assertEqual(list(self.f.keys()), ['item'])
+        self.assertEqual(list(self.ff.objectIds()), [])
+        f3 = BTreeFolder2('item3')
+        self.f._setOb(f3.id, f3)
+        lst = list(self.f.objectIds())
+        lst.sort()
+        self.assertEqual(lst, ['item', 'item3'])
+
+    def testObjectIdsWithMetaType(self):
+        f2 = Folder()
+        f2.id = 'subfolder'
+        self.f._setOb(f2.id, f2)
+        mt1 = self.ff.meta_type
+        mt2 = Folder.meta_type
+        self.assertEqual(list(self.f.objectIds(mt1)), ['item'])
+        self.assertEqual(list(self.f.objectIds((mt1,))), ['item'])
+        self.assertEqual(list(self.f.objectIds(mt2)), ['subfolder'])
+        lst = list(self.f.objectIds([mt1, mt2]))
+        lst.sort()
+        self.assertEqual(lst, ['item', 'subfolder'])
+        self.assertEqual(list(self.f.objectIds('blah')), [])
+
+    def testObjectValues(self):
+        values = self.f.objectValues()
+        self.assertEqual(len(values), 1)
+        self.assertEqual(values[0].id, 'item')
+        # Make sure the object is wrapped.
+        self.assert_(values[0] is not self.getBase(values[0]))
+
+    def testObjectItems(self):
+        items = self.f.objectItems()
+        self.assertEqual(len(items), 1)
+        id, val = items[0]
+        self.assertEqual(id, 'item')
+        self.assertEqual(val.id, 'item')
+        # Make sure the object is wrapped.
+        self.assert_(val is not self.getBase(val))
+
+    def testHasKey(self):
+        self.assert_(self.f.hasObject('item'))  # Old spelling
+        self.assert_(self.f.has_key('item'))  # New spelling
+
+    def testDelete(self):
+        self.f._delOb('item')
+        self.assertEqual(list(self.f.objectIds()), [])
+        self.assertEqual(self.f.objectCount(), 0)
+
+    def testObjectMap(self):
+        map = self.f.objectMap()
+        self.assertEqual(list(map), [{'id': 'item', 'meta_type':
+                                      self.ff.meta_type}])
+        # I'm not sure why objectMap_d() exists, since it appears to be
+        # the same as objectMap(), but it's implemented by Folder.
+        self.assertEqual(list(self.f.objectMap_d()), list(self.f.objectMap()))
+
+    def testObjectIds_d(self):
+        self.assertEqual(self.f.objectIds_d(), {'item': 1})
+
+    def testCheckId(self):
+        self.assertEqual(self.f._checkId('xyz'), None)
+        self.assertRaises(BadRequestException, self.f._checkId, 'item')
+        self.assertRaises(BadRequestException, self.f._checkId, 'REQUEST')
+
+    def testSetObject(self):
+        f2 = BTreeFolder2('item2')
+        self.f._setObject(f2.id, f2)
+        self.assert_(self.f.has_key('item2'))
+        self.assertEqual(self.f.objectCount(), 2)
+
+    def testWrapped(self):
+        # Verify that the folder returns wrapped versions of objects.
+        base = self.getBase(self.f._getOb('item'))
+        self.assert_(self.f._getOb('item') is not base)
+        self.assert_(self.f['item'] is not base)
+        self.assert_(self.f.get('item') is not base)
+        self.assert_(self.getBase(self.f._getOb('item')) is base)
+
+    def testGenerateId(self):
+        ids = {}
+        for n in range(10):
+            ids[self.f.generateId()] = 1
+        self.assertEqual(len(ids), 10)  # All unique
+        for id in ids.keys():
+            self.f._checkId(id)  # Must all be valid
+
+    def testGenerateIdDenialOfServicePrevention(self):
+        for n in range(10):
+            item = Folder()
+            item.id = 'item%d' % n
+            self.f._setOb(item.id, item)
+        self.f.generateId('item', rand_ceiling=20)  # Shouldn't be a problem
+        self.assertRaises(ExhaustedUniqueIdsError,
+                          self.f.generateId, 'item', rand_ceiling=9)
+
+    def testReplace(self):
+        old_f = Folder()
+        old_f.id = 'item'
+        inner_f = BTreeFolder2('inner')
+        old_f._setObject(inner_f.id, inner_f)
+        self.ff._populateFromFolder(old_f)
+        self.assertEqual(self.ff.objectCount(), 1)
+        self.assert_(self.ff.has_key('inner'))
+        self.assertEqual(self.getBase(self.ff._getOb('inner')), inner_f)
+
+    def testObjectListing(self):
+        f2 = BTreeFolder2('somefolder')
+        self.f._setObject(f2.id, f2)
+        # Hack in an absolute_url() method that works without context.
+        self.f.absolute_url = lambda: ''
+        info = self.f.getBatchObjectListing()
+        self.assertEqual(info['b_start'], 1)
+        self.assertEqual(info['b_end'], 2)
+        self.assertEqual(info['prev_batch_url'], '')
+        self.assertEqual(info['next_batch_url'], '')
+        self.assert_(info['formatted_list'].find('</select>') > 0)
+        self.assert_(info['formatted_list'].find('item') > 0)
+        self.assert_(info['formatted_list'].find('somefolder') > 0)
+
+        # Ensure batching is working.
+        info = self.f.getBatchObjectListing({'b_count': 1})
+        self.assertEqual(info['b_start'], 1)
+        self.assertEqual(info['b_end'], 1)
+        self.assertEqual(info['prev_batch_url'], '')
+        self.assert_(info['next_batch_url'] != '')
+        self.assert_(info['formatted_list'].find('item') > 0)
+        self.assert_(info['formatted_list'].find('somefolder') < 0)
+
+        info = self.f.getBatchObjectListing({'b_start': 2})
+        self.assertEqual(info['b_start'], 2)
+        self.assertEqual(info['b_end'], 2)
+        self.assert_(info['prev_batch_url'] != '')
+        self.assertEqual(info['next_batch_url'], '')
+        self.assert_(info['formatted_list'].find('item') < 0)
+        self.assert_(info['formatted_list'].find('somefolder') > 0)
+
+    def testObjectListingWithSpaces(self):
+        # The option list must use value attributes to preserve spaces.
+        name = " some folder "
+        f2 = BTreeFolder2(name)
+        self.f._setObject(f2.id, f2)
+        self.f.absolute_url = lambda: ''
+        info = self.f.getBatchObjectListing()
+        expect = '<option value="%s">%s</option>' % (name, name)
+        self.assert_(info['formatted_list'].find(expect) > 0)
+
+    def testCleanup(self):
+        self.assert_(self.f._cleanup())
+        key = TrojanKey('a')
+        self.f._tree[key] = 'b'
+        self.assert_(self.f._cleanup())
+        key.value = 'z'
+
+        # With a key in the wrong place, there should now be damage.
+        self.assert_(not self.f._cleanup())
+        # Now it's fixed.
+        self.assert_(self.f._cleanup())
+        # Verify the management interface also works,
+        # but don't test return values.
+        self.f.manage_cleanup()
+        key.value = 'a'
+        self.f.manage_cleanup()
+
+
+class TrojanKey:
+    """Pretends to be a consistent, immutable, humble citizen...
+
+    then sweeps the rug out from under the BTree.
+    """
+    def __init__(self, value):
+        self.value = value
+
+    def __cmp__(self, other):
+        return cmp(self.value, other)
+
+    def __hash__(self):
+        return hash(self.value)
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(BTreeFolder2Tests),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.pyc
===================================================================
(Binary files differ)


Property changes on: Zope/trunk/lib/python/Products/BTreeFolder2/tests/testBTreeFolder2.pyc
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope/trunk/lib/python/Products/BTreeFolder2/version.txt
===================================================================
--- Zope/trunk/lib/python/Products/BTreeFolder2/version.txt	2005-04-25 10:01:22 UTC (rev 30155)
+++ Zope/trunk/lib/python/Products/BTreeFolder2/version.txt	2005-04-25 13:29:39 UTC (rev 30156)
@@ -0,0 +1 @@
+BTreeFolder2-1.0.1



More information about the Zope-Checkins mailing list