[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"> </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;"><<</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;">>></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