From 9b7f05cb4d9dd700141f68687dc7aa71d44042a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Calonne?= <aurel@nexedi.com> Date: Thu, 6 Sep 2007 13:53:47 +0000 Subject: [PATCH] initial import of HBTreeFolder product git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@16118 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/HBTreeFolder2/CHANGES.txt | 4 + product/HBTreeFolder2/CMFHBTreeFolder.py | 101 +++ product/HBTreeFolder2/HBTreeFolder2.py | 605 ++++++++++++++++++ product/HBTreeFolder2/README.txt | 2 + product/HBTreeFolder2/__init__.py | 48 ++ product/HBTreeFolder2/btreefolder2.gif | Bin 0 -> 179 bytes product/HBTreeFolder2/contents.dtml | 164 +++++ product/HBTreeFolder2/folderAdd.dtml | 67 ++ product/HBTreeFolder2/tests/__init__.py | 1 + .../HBTreeFolder2/tests/testHBTreeFolder2.py | 219 +++++++ product/HBTreeFolder2/version.txt | 1 + 11 files changed, 1212 insertions(+) create mode 100755 product/HBTreeFolder2/CHANGES.txt create mode 100755 product/HBTreeFolder2/CMFHBTreeFolder.py create mode 100755 product/HBTreeFolder2/HBTreeFolder2.py create mode 100755 product/HBTreeFolder2/README.txt create mode 100755 product/HBTreeFolder2/__init__.py create mode 100755 product/HBTreeFolder2/btreefolder2.gif create mode 100755 product/HBTreeFolder2/contents.dtml create mode 100755 product/HBTreeFolder2/folderAdd.dtml create mode 100755 product/HBTreeFolder2/tests/__init__.py create mode 100755 product/HBTreeFolder2/tests/testHBTreeFolder2.py create mode 100755 product/HBTreeFolder2/version.txt diff --git a/product/HBTreeFolder2/CHANGES.txt b/product/HBTreeFolder2/CHANGES.txt new file mode 100755 index 0000000000..baf1329bf2 --- /dev/null +++ b/product/HBTreeFolder2/CHANGES.txt @@ -0,0 +1,4 @@ +Version 1.0.0 + + - Initial version + diff --git a/product/HBTreeFolder2/CMFHBTreeFolder.py b/product/HBTreeFolder2/CMFHBTreeFolder.py new file mode 100755 index 0000000000..7b6b5b351e --- /dev/null +++ b/product/HBTreeFolder2/CMFHBTreeFolder.py @@ -0,0 +1,101 @@ +############################################################################## +# +# 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 +# +############################################################################## + +from AccessControl.SecurityInfo import ClassSecurityInfo +from Globals import InitializeClass +from Products.HBTreeFolder2.HBTreeFolder2 import HBTreeFolder2Base + +try: + from Products.CMFCore.PortalFolder import PortalFolderBase as PortalFolder +except ImportError: + from Products.CMFCore.PortalFolder import PortalFolder + +from Products.CMFCore.PortalFolder import factory_type_information as PortalFolder_FTI +from Products.CMFCore.utils import getToolByName + +_actions = PortalFolder_FTI[0]['actions'] + +factory_type_information = ( { 'id' : 'CMF HBTree Folder', + 'meta_type' : 'CMF HBTree Folder', + 'description' : """\ +CMF folder designed to hold a lot of objects.""", + 'icon' : 'folder_icon.gif', + 'product' : 'CMFCore', + 'factory' : 'manage_addCMFHBTreeFolder', + 'filter_content_types' : 0, + 'immediate_view' : 'folder_edit_form', + 'actions' : _actions, + }, + ) + + +def manage_addCMFHBTreeFolder(dispatcher, id, title='', REQUEST=None): + """Adds a new HBTreeFolder object with id *id*. + """ + id = str(id) + ob = CMFHBTreeFolder(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 CMFHBTreeFolder(HBTreeFolder2Base, PortalFolder): + """HBTree folder for CMF sites. + """ + meta_type = 'CMF HBTree Folder' + security = ClassSecurityInfo() + + def __init__(self, id, title=''): + PortalFolder.__init__(self, id, title) + HBTreeFolder2Base.__init__(self, id) + + def _checkId(self, id, allow_dup=0): + PortalFolder._checkId(self, id, allow_dup) + HBTreeFolder2Base._checkId(self, id, allow_dup) + + + def allowedContentTypes(self): + """ + List type info objects for types which can be added in + this folder. + """ + result = [] + portal_types = getToolByName(self, 'portal_types') + myType = portal_types.getTypeInfo(self) + + if myType is not None: + allowed_types_to_check = [] + if myType.filter_content_types: + for portal_type in myType.allowed_content_types: + contentType = portal_types.getTypeInfo(portal_type) + if contentType is None: + raise AttributeError, "Portal type '%s' does not exist " \ + "and should not be allowed in '%s'" % \ + (portal_type, self.getPortalType()) + result.append(contentType) + else: + for contentType in portal_types.listTypeInfo(self): + if myType.allowType(contentType.getId()): + result.append(contentType) + else: + result = portal_types.listTypeInfo() + + return filter( + lambda typ, container=self: typ.isConstructionAllowed(container), + result) + + +InitializeClass(CMFHBTreeFolder) diff --git a/product/HBTreeFolder2/HBTreeFolder2.py b/product/HBTreeFolder2/HBTreeFolder2.py new file mode 100755 index 0000000000..2eb52fa269 --- /dev/null +++ b/product/HBTreeFolder2/HBTreeFolder2.py @@ -0,0 +1,605 @@ +############################################################################## +# +# 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 +# +############################################################################## + +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, PersistentMapping +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, LazyFilter, LazyCat + + +manage_addHBTreeFolder2Form = DTMLFile('folderAdd', globals()) + +def manage_addHBTreeFolder2(dispatcher, id, title='', REQUEST=None): + """Adds a new HBTreeFolder object with id *id*. + """ + id = str(id) + ob = HBTreeFolder2(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 +MAX_OBJECT_PER_LEVEL = 1000 +H_SEPARATOR = '-' + +class ExhaustedUniqueIdsError (Exception): + pass + + +class HBTreeFolder2Base (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()) + + _htree = None # OOBTree: { id -> object } + _count = None # A BTrees.Length + _v_nextid = 0 # The integer component of the next generated ID + title = '' + _tree_list = None + + + def __init__(self, id=None): + if id is not None: + self.id = id + self._initBTrees() + + def _initBTrees(self): + self._htree = OOBTree() + self._count = Length() + self._tree_list = PersistentMapping() + + def initBTrees(self): + """ """ + return self._initBTrees() + + 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 HBTreeFolder2 at %s." % path + else: + return ("Fixed count mismatch in HBTreeFolder2 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 HBTreeFolder2 at %s." % path + else: + return ("Fixed HBTreeFolder2 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. + """ + def hCheck(htree): + """ + Recursively check the btree + """ + check(htree) + for key in htree.keys(): + if not htree.has_key(key): + raise AssertionError( + "Missing value for key: %s" % repr(key)) + else: + ob = htree[key] + if isinstance(ob, OOBTree): + hCheck(ob) + return 1 + + from BTrees.check import check + path = '/'.join(self.getPhysicalPath()) + try: + return hCheck(self._htree) + except AssertionError: + LOG('HBTreeFolder2', WARNING, + 'Detected damage to %s. Fixing now.' % path, + error=sys.exc_info()) + try: + self._htree = OOBTree(self._htree) # XXX hFix needed + except: + LOG('HBTreeFolder2', ERROR, 'Failed to fix %s.' % path, + error=sys.exc_info()) + raise + else: + LOG('HBTreeFolder2', INFO, 'Fixed %s.' % path) + return 0 + + def hashId(self, id): + """Return a tuple of ids + """ + id_list = str(id).split(H_SEPARATOR) # We use '-' as the separator by default + if len(id_list) > 1: + return tuple(id_list) + else: + return [id,] + +# try: # We then try int hashing +# id_int = int(id) +# except ValueError: +# return id_list +# result = [] +# while id_int: +# result.append(id_int % MAX_OBJECT_PER_LEVEL) +# id_int = id_int / MAX_OBJECT_PER_LEVEL +# result.reverse() +# return tuple(result) + + def _getOb(self, id, default=_marker): + """ + Return the named object from the folder. + """ + htree = self._htree + ob = htree + id_list = self.hashId(id) + for sub_id in id_list[0:-1]: + if default is _marker: + ob = ob[sub_id] + else: + ob = ob.get(sub_id, _marker) + if ob is _marker: + return default + if default is _marker: + ob = ob[id] + else: + ob = ob.get(id, _marker) + if ob is _marker: + return default + return ob.__of__(self) + + def _setOb(self, id, object): + """Store the named object in the folder. + """ + htree = self._htree + id_list = self.hashId(id) + for idx in xrange(len(id_list[0:-1])): + sub_id = id_list[idx] + if not htree.has_key(sub_id): + # Create a new index and index it + htree[sub_id] = OOBTree() + if isinstance(sub_id, int) or isinstance(sub_id, long): + tree_id = 0 + for id in id_list[:idx+1]: + tree_id = tree_id + id * MAX_OBJECT_PER_LEVEL + else: + tree_id = H_SEPARATOR.join(id_list[:idx+1]) + self._tree_list[tree_id] = None + + htree = htree[sub_id] + # set object in subtree + ob_id = id_list[-1] + if htree.has_key(id): + raise KeyError('There is already an item named "%s".' % id) + htree[id] = object + self._count.change(1) + + def _delOb(self, id): + """Remove the named object from the folder. + """ + htree = self._htree + id_list = self.hashId(id) + for sub_id in id_list[0:-1]: + htree = htree[sub_id] + del htree[id] + self._count.change(-1) + + 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. + """ + htree = self._htree + id_list = self.hashId(id) + for sub_id in id_list[0:-1]: + if not isinstance(htree, OOBTree): + return 0 + if not htree.has_key(sub_id): + return 0 + htree = htree[sub_id] + if not htree.has_key(id): + return 0 + return 1 + + + security.declareProtected(access_contents_information, + 'treeIds') + def treeIds(self, base_id=None): + """ Return a list of subtree ids + """ + tree = self._getTree(base_id=base_id) + return [x for x in self._htree.keys() if isinstance(self._htree[x], OOBTree)] + + + def _getTree(self, base_id): + """ Return the tree wich has the base_id + """ + htree = self._htree + id_list = self.hashId(base_id) + for sub_id in id_list: + if not isinstance(htree, OOBTree): + return None + if not htree.has_key(sub_id): + raise IndexError, base_id + htree = htree[sub_id] + return htree + + def _getTreeIdList(self, htree=None): + """ recursively build a list of btree ids + """ + if htree is None: + htree = self._htree + btree_list = [None,] + else: + btree_list = [] + for obj_id in htree.keys(): + obj = htree[obj_id] + if isinstance(obj, OOBTree): + btree_list.extend(["%s-%s"%(obj_id, x) for x in self._getTreeIdList(htree=obj)]) + btree_list.append(obj_id) + + return btree_list + + security.declareProtected(access_contents_information, + 'getTreeIdList') + def getTreeIdList(self, htree=None): + """ Return list of all tree ids + """ + if self._tree_list is None or len(self._tree_list.keys()) == 0: + tree_list = self._getTreeIdList(htree=htree) + self._tree_list = PersistentMapping() + for tree in tree_list: + self._tree_list[tree] = None + return sorted(self._tree_list.keys()) + + + def _treeObjectValues(self, base_id=None): + """ return object values for a given btree + """ + if base_id is not None: + return LazyFilter(self._isNotBTree, self._getTree("%s" %base_id).values()) + else: + return LazyFilter(self._isNotBTree, self._htree.values()) + + def _treeObjectIds(self, base_id=None): + """ return object ids for a given btree + """ + if base_id is not None: + return LazyFilter(self._checkObjectId, self._getTree("%s" %base_id).keys()) + else: + return LazyFilter(self._checkObjectId, self._htree.keys()) + + def _isNotBTree(self, obj): + """ test object is not a btree + """ + if isinstance(obj, OOBTree): + return False + else: + return True + + def _checkObjectId(self, id): + """ test id is not in btree id list + """ + return not self._tree_list.has_key(id) + + security.declareProtected(access_contents_information, + 'ObjectValues') + def objectValues(self, base_id=None): + return LazyMap(self._getOb, self.objectIds(base_id)) + + + security.declareProtected(access_contents_information, + 'objectIds') + def objectIds(self, base_id=None): + if base_id is None: + return LazyCat(LazyMap(self._treeObjectIds, self.getTreeIdList())) + else: + return self._treeObjectIds(base_id=base_id) + + + 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._htree.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._htree + 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._htree.get(name) + if res is None: + raise AttributeError, name + return res + + +Globals.InitializeClass(HBTreeFolder2Base) + + +class HBTreeFolder2 (HBTreeFolder2Base, Folder): + """BTreeFolder2 based on OFS.Folder. + """ + meta_type = 'HBTreeFolder2' + + def _checkId(self, id, allow_dup=0): + Folder._checkId(self, id, allow_dup) + HBTreeFolder2Base._checkId(self, id, allow_dup) + + +Globals.InitializeClass(HBTreeFolder2) + diff --git a/product/HBTreeFolder2/README.txt b/product/HBTreeFolder2/README.txt new file mode 100755 index 0000000000..ccbf8103d0 --- /dev/null +++ b/product/HBTreeFolder2/README.txt @@ -0,0 +1,2 @@ +TODO + diff --git a/product/HBTreeFolder2/__init__.py b/product/HBTreeFolder2/__init__.py new file mode 100755 index 0000000000..64f81d5ef5 --- /dev/null +++ b/product/HBTreeFolder2/__init__.py @@ -0,0 +1,48 @@ +############################################################################## +# +# 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 +# +############################################################################## + +import HBTreeFolder2 + +def initialize(context): + + context.registerClass( + HBTreeFolder2.HBTreeFolder2, + constructors=(HBTreeFolder2.manage_addHBTreeFolder2Form, + HBTreeFolder2.manage_addHBTreeFolder2), + icon='btreefolder2.gif', + ) + + #context.registerHelp() + #context.registerHelpTitle('Zope Help') + + context.registerBaseClass(HBTreeFolder2.HBTreeFolder2) + + try: + from Products.CMFCore import utils + except ImportError: + # CMF not installed + pass + else: + # CMF installed; make available a special folder type. + import CMFHBTreeFolder + ADD_FOLDERS_PERMISSION = 'Add portal folders' + + utils.ContentInit( + 'CMF HBTree Folder', + content_types=(CMFHBTreeFolder.CMFHBTreeFolder,), + permission=ADD_FOLDERS_PERMISSION, + extra_constructors=(CMFHBTreeFolder.manage_addCMFHBTreeFolder,), + fti=CMFHBTreeFolder.factory_type_information + ).initialize(context) + diff --git a/product/HBTreeFolder2/btreefolder2.gif b/product/HBTreeFolder2/btreefolder2.gif new file mode 100755 index 0000000000000000000000000000000000000000..725e514aedf421be0b267a31f332aa31568ea53b GIT binary patch literal 179 zcmZ?wbhEHb6krfwc+9}CWy_X~jEwj18M?X{7A;z|dpARGZ*N0GLqI^lfddErg8%~% zfEge{@h1x-0|P&U4oDPa1_MjNhm)S#t`ax9E`7T7tt5WtT%k=GIj=XJSy9szc*CLc zNXH||o`eY>Zl1JbbyJ)uQ_-_mUO9lnfXDS-k0uY-yeLhxg8`}wZZBKQGtIM=<56Vv XryHd<M-tD!f1fNA+vLPA$Y2csedbUg literal 0 HcmV?d00001 diff --git a/product/HBTreeFolder2/contents.dtml b/product/HBTreeFolder2/contents.dtml new file mode 100755 index 0000000000..4fadeb4360 --- /dev/null +++ b/product/HBTreeFolder2/contents.dtml @@ -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> diff --git a/product/HBTreeFolder2/folderAdd.dtml b/product/HBTreeFolder2/folderAdd.dtml new file mode 100755 index 0000000000..1a8c3f66d2 --- /dev/null +++ b/product/HBTreeFolder2/folderAdd.dtml @@ -0,0 +1,67 @@ +<dtml-let form_title="'Add HBTreeFolder2'"> +<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 HBTreeFolder2 may be able to handle a larger number +of objects than a standard BTreeFolder because it use btree of +btree to store objects. +It is more efficient than the original BTreeFolder2 product, +but you must provide well constructed id according to hashId method +</p> + +<FORM ACTION="manage_addHBTreeFolder2" 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> diff --git a/product/HBTreeFolder2/tests/__init__.py b/product/HBTreeFolder2/tests/__init__.py new file mode 100755 index 0000000000..d2f3ead134 --- /dev/null +++ b/product/HBTreeFolder2/tests/__init__.py @@ -0,0 +1 @@ +"""Python package.""" diff --git a/product/HBTreeFolder2/tests/testHBTreeFolder2.py b/product/HBTreeFolder2/tests/testHBTreeFolder2.py new file mode 100755 index 0000000000..a32a5a1eac --- /dev/null +++ b/product/HBTreeFolder2/tests/testHBTreeFolder2.py @@ -0,0 +1,219 @@ +############################################################################## +# +# 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 +# +############################################################################## + +import unittest +import ZODB +import Testing +import Zope +from Products.HBTreeFolder2.HBTreeFolder2 \ + import HBTreeFolder2, ExhaustedUniqueIdsError +from OFS.ObjectManager import BadRequestException +from OFS.Folder import Folder +from Acquisition import aq_base + + +class HBTreeFolder2Tests(unittest.TestCase): + + def getBase(self, ob): + # This is overridden in subclasses. + return aq_base(ob) + + def setUp(self): + self.f = HBTreeFolder2('root') + ff = HBTreeFolder2('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 = HBTreeFolder2('item3') + self.f._setOb(f3.id, f3) + lst = list(self.f.objectIds()) + lst.sort() + self.assertEqual(lst, ['item', 'item3']) + + 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 = HBTreeFolder2('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 = HBTreeFolder2('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 = HBTreeFolder2('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 = HBTreeFolder2(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._htree[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 HBTree. + """ + 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(HBTreeFolder2Tests), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/product/HBTreeFolder2/version.txt b/product/HBTreeFolder2/version.txt new file mode 100755 index 0000000000..0b69fa3114 --- /dev/null +++ b/product/HBTreeFolder2/version.txt @@ -0,0 +1 @@ +HBTreeFolder2-1.0.0 -- 2.30.9