[CMF-checkins] CVS: CMF/CMFCore - ActionInformation.py:1.2 ActionProviderBase.py:1.2 Expression.py:1.2 ActionsTool.py:1.20 CatalogTool.py:1.22 MemberDataTool.py:1.13 MembershipTool.py:1.17 PortalFolder.py:1.29 RegistrationTool.py:1.8 SkinsTool.py:1.12 TypesTool.py:1.27 UndoTool.py:1.5 WorkflowTool.py:1.20

Andrew Sawyers andrew@zope.com
Fri, 4 Jan 2002 14:50:35 -0500


Update of /cvs-repository/CMF/CMFCore
In directory cvs.zope.org:/tmp/cvs-serv10417/CMFCore

Modified Files:
	ActionsTool.py CatalogTool.py MemberDataTool.py 
	MembershipTool.py PortalFolder.py RegistrationTool.py 
	SkinsTool.py TypesTool.py UndoTool.py WorkflowTool.py 
Added Files:
	ActionInformation.py ActionProviderBase.py Expression.py 
Log Message:

*Merging andrew_ttw_actions-branch into the head
*Adds TTW configuration of Action Providers
*Allows tools to have actions managed TTW
*Added unittests for ActionTool, ActionInformation, and
Expression
*Added TTW action management to all stock CMF tools
*Added Expression module for allowing actions and conditions of
actions to be TALES.


=== CMF/CMFCore/ActionInformation.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001 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
+# 
+##############################################################################
+
+"""Basic action list tool.
+
+$Id$
+"""
+__version__='$Revision$'[11:-2]
+
+from utils import SimpleItemWithProperties, _dtmldir, getToolByName
+import CMFCorePermissions
+from AccessControl import ClassSecurityInfo
+from Acquisition import aq_inner, aq_parent
+from Globals import InitializeClass, DTMLFile
+
+class ActionInformation(SimpleItemWithProperties):
+    """
+    Represent a single action which the user can select from a list
+    and execut in some context.
+    """
+    _isActionInformation = 1
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    manage_options = (SimpleItemWithProperties.manage_options[:1] +
+                      ({'label': 'Actions',
+                       'action': 'manage_editActionsForm'},) +
+                       SimpleItemWithProperties.manage_options[1:])
+    security = ClassSecurityInfo()
+    security.declareProtected(CMFCorePermissions.ManagePortal
+                            , 'manage_editProperties'
+                            , 'manage_changeProperties'
+                            , 'manage_propertiesForm'
+                             )
+
+    _basic_properties = (
+                         {'id': 'title', 'type': 'string', 'mode': 'w', 'label': 'Title'}
+                       , {'id': 'description', 'type': 'text', 'mode': 'w', 
+                         'label': 'Description'}
+                       , {'id': 'category', 'type': 'string', 'mode': 'w', 
+                         'label': 'Category'}
+                       , {'id': 'priority', 'type': 'boolean', 'mode':  'w', 'label': 'Priority'}
+                         )
+
+    title = ''
+    description = ''
+    category = ''
+    priority = 0
+    visible = 1
+    _action = ''
+
+    def __init__(self
+               , id
+               , title=''
+               , description=''
+               , category='object'
+               , condition=''
+               , permissions=()
+               , priority=10
+               , visible=1
+               , action=''):
+       """
+       Setup an instance
+       """
+       self.id = id
+       self.title = title
+       self.description = description
+       self.category = category 
+       self.condition = condition
+       self.permissions = permissions
+       self.priority = priority 
+       self.visible = visible
+       self._action = action
+
+
+    security.declareProtected(CMFCorePermissions.View, 'Title')
+    def Title(self):
+        """
+        Return the Action title - name
+        """
+        if self.title:
+            return self.title
+        else:
+            return self.getId()
+
+    security.declareProtected(CMFCorePermissions.View, 'Description')
+    def Description(self):
+        """
+        Return a description of the action
+        """
+        return self.description
+
+    security.declarePrivate('testCondition')
+    def testCondition(self, ec):
+        """
+        Evaluate condition and return 0 or 1
+        """
+        if self.condition:
+            return self.condition(ec)
+        else:
+            return 1
+
+    security.declarePublic('getAction')
+    def getAction(self, ec):
+        """
+        Return the action, which is an TALES expresssion
+        """
+        if self._action:
+            aa = self._action(ec)
+        else:
+            aa = ''
+        action = {}
+        action['id'] = self.id
+        action['name'] = self.Title()
+        action['url'] = aa 
+        action['permissions'] = self.getPermissions()
+        action['category'] = self.getCategory()
+        action['visible'] = self.getVisibility()
+        return action 
+
+    security.declarePublic('getActionExpression')
+    def getActionExpression(self):
+        """
+        If not an empty string or None, return the 
+        text of the expression otherwise return '' 
+        """
+        if self._action:
+            return self._action.text
+        else:
+            return self._action
+
+    security.declarePublic('getCondition')
+    def getCondition(self):
+        """
+        If not an empty string or None, return the 
+        text of the expression otherwise
+        return ''
+        """
+        if self.condition:
+            return self.condition.text
+        else:
+            return self.condition
+
+    security.declarePublic('getPermission')
+    def getPermissions(self):
+        """
+        Return the permission if any required for a user to
+        execute the action
+        """
+        return self.permissions
+
+    security.declarePublic('getCategory')
+    def getCategory(self):
+        """
+        Return the category for which the action is
+        """
+        if self.category:
+            return self.category
+        else:
+            return 'object'
+
+    security.declarePublic('getVisibility')
+    def getVisibility(self):
+        """
+        Return boolean for whether the action
+        is visible in the UI
+        """
+        return self.visible
+
+    security.declarePublic('getPriority')
+    def getPriority(self):
+        """
+        Return integer priority for sorting
+        Not used....keep and implement or toss?
+        """
+        if self.priority:
+            return self.priority
+        else:
+            return 10
+
+InitializeClass(ActionInformation)
+
+class oai:
+    #Provided for backwards compatability
+    # Provides information that may be needed when constructing the list of
+    # available actions.
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    def __init__(self, tool, folder, object=None):
+        self.portal = portal = aq_parent(aq_inner(tool))
+        membership = getToolByName(tool, 'portal_membership')
+        self.isAnonymous = membership.isAnonymousUser()
+        self.portal_url = portal.absolute_url()
+        if folder is not None:
+            self.folder_url = folder.absolute_url()
+            self.folder = folder
+        else:
+            self.folder_url = self.portal_url
+            self.folder = portal
+        self.content = object
+        if object is not None:
+            self.content_url = object.absolute_url()
+        else:
+            self.content_url = None
+
+    def __getitem__(self, name):
+        # Mapping interface for easy string formatting.
+        if name[:1] == '_':
+            raise KeyError, name
+        if hasattr(self, name):
+            return getattr(self, name)
+        raise KeyError, name
+


=== CMF/CMFCore/ActionProviderBase.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001 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 OFS.SimpleItem import SimpleItem
+from Globals import DTMLFile
+from CMFCorePermissions import ManagePortal
+from utils import _dtmldir, cookString
+from AccessControl import ClassSecurityInfo
+from ActionInformation import ActionInformation
+from Expression import Expression
+
+
+"""Basic action list tool.
+
+$Id$
+"""
+__version__='$Revision$'[11:-2]
+
+class ActionProviderBase:
+    """
+    Provide ActionTabs and management methods for ActionProviders
+    """
+    _actions = []
+    security = ClassSecurityInfo()
+    _actions_form = DTMLFile( 'editToolsActions', _dtmldir )
+
+    manage_options = ({ 'label' : 'Actions', 'action' : 'manage_editActionsForm' }
+                    , 
+                     )
+
+    security.declarePrivate('listActions')
+    def listActions(self):
+        """
+        Return all the actions defined by a tool
+        """
+        if self._actions:
+            return self._actions
+        else:
+            return None
+
+    security.declareProtected(ManagePortal, 'manage_editActionsForm')
+    def manage_editActionsForm(self, REQUEST, manage_tabs_message=None):
+        """
+        Shows the 'Actions' management tab.
+        """
+        actions = []
+        if self.listActions() is not None:
+            for a in self.listActions():
+                a1 = {}
+                a1['id'] = a.getId()
+                a1['name'] = a.Title()
+                p = a.getPermissions()
+                if p:
+                    a1['permission'] = p[0]
+                else:
+                    a1['permission'] = ''
+                a1['category'] = a.getCategory() or 'object'
+                a1['visible'] = a.getVisibility()
+                if a._action:
+                    a1['action'] = a.getActionExpression()
+                else:
+                    a1['action'] = ''
+                if a.condition:
+                    a1['condition'] = a.getCondition()
+                else:
+                    a1['condition'] = ''
+                actions.append(a1)
+                # possible_permissions is in AccessControl.Role.RoleManager.
+        pp = self.possible_permissions()
+        return self._actions_form(self, REQUEST,
+                                  actions=actions,
+                                  possible_permissions=pp,
+                                  management_view='Actions',
+                                  manage_tabs_message=manage_tabs_message)
+
+    security.declareProtected(ManagePortal, 'addAction')
+    def addAction( self
+                 , id
+                 , name
+                 , action
+                 , condition
+                 , permission
+                 , category
+                 , visible=1
+                 , REQUEST=None
+                 ):
+        """
+        Adds an action to the list.
+        """
+        al = self._actions 
+        if not name:
+            raise ValueError('A name is required.')
+        if action:
+            a_expr = Expression(text=str(action))
+        else:
+            a_expr = ''
+        if condition:
+            c_expr = Expression(text=str(condition))
+        else:
+            c_expr = ''
+        al.append(ActionInformation(id=str(id)
+                                         , title=str(name)
+                                         , action=a_expr
+                                         , condition=c_expr
+                                         , permissions=(str(permission),)
+                                         , category=str(category)
+                                         , visible=int(visible)
+                                          ))
+        self._actions = al
+        if REQUEST is not None:
+            return self.manage_editActionsForm(
+                REQUEST, manage_tabs_message='Added.')
+    
+    security.declareProtected(ManagePortal, 'changeActions')
+    def changeActions(self, properties=None, REQUEST=None):
+        """
+        Changes the _actions.
+        """
+        if properties is None:
+            properties = REQUEST
+        actions = []
+        for idx in range(len(self._actions)):
+            s_idx = str(idx)
+            action = {
+                'id': str(properties.get('id_' + s_idx, '')),
+                'name': str(properties.get('name_' + s_idx, '')),
+                'action': str(properties.get('action_' + s_idx, '')),
+                'condition': str(properties.get('condition_' + s_idx, '')),
+                'permissions':
+                (properties.get('permission_' + s_idx, ()),),
+                'category': str(properties.get('category_' + s_idx, 'object')),
+                'visible': properties.get('visible_' + s_idx, 0),
+                }
+            if not action['name']:
+                raise ValueError('A name is required.')
+            a = self._actions[idx]
+            a.id = action['id']
+            a.title = action['name']
+            if action['action'] is not '':
+                a._action = Expression(text=action['action'])
+            else:
+                a._action = ''
+            if action['condition'] is not '':
+                a.condition = Expression(text=action['condition'])
+            else:
+                a.condition = ''
+            a.permissions = action['permissions']
+            a.category = action['category']
+            a.visible = action['visible']
+        if REQUEST is not None:
+            return self.manage_editActionsForm(REQUEST, manage_tabs_message=
+                                               'Actions changed.')
+
+    security.declareProtected(ManagePortal, 'deleteActions')
+    def deleteActions(self, selections=(), REQUEST=None):
+        """
+        Deletes actions.
+        """
+        actions = list(self._actions)
+        sels = list(map(int, selections))  # Convert to a list of integers.
+        sels.sort()
+        sels.reverse()
+        for idx in sels:
+            del actions[idx]
+        self._actions = actions
+        if REQUEST is not None:
+            return self.manage_editActionsForm(
+                REQUEST, manage_tabs_message=(
+                'Deleted %d action(s).' % len(sels)))
+
+    security.declareProtected(ManagePortal, 'moveUpActions')
+    def moveUpActions(self, selections=(), REQUEST=None):
+        """
+        Moves the specified actions up one slot.
+        """
+        actions = list(self._actions)
+        sels = list(map(int, selections))  # Convert to a list of integers.
+        sels.sort()
+        for idx in sels:
+            idx2 = idx - 1
+            if idx2 < 0:
+                # Wrap to the bottom.
+                idx2 = len(actions) - 1
+            # Swap.
+            a = actions[idx2]
+            actions[idx2] = actions[idx]
+            actions[idx] = a
+        self._actions = actions
+        if REQUEST is not None:
+            return self.manage_editActionsForm(
+                REQUEST, manage_tabs_message=(
+                'Moved up %d action(s).' % len(sels)))
+
+    security.declareProtected(ManagePortal, 'moveDownActions')
+    def moveDownActions(self, selections=(), REQUEST=None):
+        """
+        Moves the specified actions down one slot.
+        """
+        actions = list(self._actions)
+        sels = list(map(int, selections))  # Convert to a list of integers.
+        sels.sort()
+        sels.reverse()
+        for idx in sels:
+            idx2 = idx + 1
+            if idx2 >= len(actions):
+                # Wrap to the top.
+                idx2 = 0
+            # Swap.
+            a = actions[idx2]
+            actions[idx2] = actions[idx]
+            actions[idx] = a
+        self._actions = actions
+        if REQUEST is not None:
+            return self.manage_editActionsForm(
+                REQUEST, manage_tabs_message=(
+                'Moved down %d action(s).' % len(sels)))


=== CMF/CMFCore/Expression.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001 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
+# 
+##############################################################################
+'''
+Expressions in a web-configurable workflow.
+$Id$
+'''
+__version__='$Revision$'[11:-2]
+
+import Globals
+from Globals import Persistent
+from Acquisition import aq_inner, aq_parent
+from AccessControl import getSecurityManager, ClassSecurityInfo
+
+from utils import getToolByName
+from Products.PageTemplates.Expressions import getEngine
+from Products.PageTemplates.TALES import SafeMapping
+from Products.PageTemplates.PageTemplate import ModuleImporter
+
+
+class Expression (Persistent):
+    text = ''
+    _v_compiled = None
+
+    security = ClassSecurityInfo()
+
+    def __init__(self, text):
+        self.text = text
+        self._v_compiled = getEngine().compile(text)
+
+    def __call__(self, econtext):
+        compiled = self._v_compiled
+        if compiled is None:
+            compiled = self._v_compiled = getEngine().compile(self.text)
+        # ?? Maybe expressions should manipulate the security
+        # context stack.
+        res = compiled(econtext)
+        if isinstance(res, Exception):
+            raise res
+        #print 'returning %s from %s' % (`res`, self.text)
+        return res
+
+Globals.InitializeClass(Expression)
+
+
+def createExprContext(folder, portal, object):
+    '''
+    An expression context provides names for TALES expressions.
+    '''
+    pm = getToolByName(portal, 'portal_membership')
+    if object is None:
+        object_url = ''
+    else:
+        object_url = object.absolute_url()
+    if pm.isAnonymousUser():
+        member = None
+    else:
+        member = pm.getAuthenticatedMember()
+    data = {
+        'object_url':   object_url,
+        'folder_url':   folder.absolute_url(),
+        'portal_url':   portal.absolute_url(),
+        'object':       object,
+        'folder':       folder,
+        'portal':       portal,
+        'nothing':      None,
+        'request':      getattr( object, 'REQUEST', None ),
+        'modules':      ModuleImporter,
+        'member':       member,
+        }
+    return getEngine().getContext(data)
+


=== CMF/CMFCore/ActionsTool.py 1.19 => 1.20 ===
 
 
-from utils import UniqueObject, _getAuthenticatedUser, _checkPermission
-from utils import getToolByName, _dtmldir
+import OFS
+from utils import UniqueObject, SimpleItemWithProperties, _getAuthenticatedUser, _checkPermission
+from utils import getToolByName, _dtmldir, cookString
 import CMFCorePermissions
 from OFS.SimpleItem import SimpleItem
 from Globals import InitializeClass, DTMLFile, package_home
@@ -27,60 +28,57 @@
 from Acquisition import aq_base, aq_inner, aq_parent
 from AccessControl import ClassSecurityInfo
 from string import join
+from Expression import Expression, createExprContext
+from ActionInformation import ActionInformation, oai
+from ActionProviderBase import ActionProviderBase
 
-class ActionInformation:
-    # Provides information that may be needed when constructing the list of
-    # available actions.
-    __allow_access_to_unprotected_subobjects__ = 1
-
-    def __init__(self, tool, folder, object=None):
-        self.portal = portal = aq_parent(aq_inner(tool))
-        membership = getToolByName(tool, 'portal_membership')
-        self.isAnonymous = membership.isAnonymousUser()
-        self.portal_url = portal.absolute_url()
-        if folder is not None:
-            self.folder_url = folder.absolute_url()
-            self.folder = folder
-        else:
-            self.folder_url = self.portal_url
-            self.folder = portal
-        self.content = object
-        if object is not None:
-            self.content_url = object.absolute_url()
-        else:
-            self.content_url = None
 
-    def __getitem__(self, name):
-        # Mapping interface for easy string formatting.
-        if name[:1] == '_':
-            raise KeyError, name
-        if hasattr(self, name):
-            return getattr(self, name)
-        raise KeyError, name
-
-
-class ActionsTool (UniqueObject, SimpleItem):
+class ActionsTool(UniqueObject, OFS.Folder.Folder, ActionProviderBase):
     """
         Weave together the various sources of "actions" which are apropos
         to the current user and context.
     """
     id = 'portal_actions'
+    _actions = [ActionInformation(id='folderContents'
+                                , title='Folder contents'
+                                , action=Expression(
+               text='string: ${folder_url}/folder_contents')
+                                , condition=Expression(
+               text='python: folder is not object') 
+                                , permissions=('List folder contents',)
+                                , category='object'
+                                , visible=1
+                                 )
+              , ActionInformation(id='folderContents'
+                                , title='Folder contents'
+                                , action=Expression(
+               text='string: ${folder_url}/folder_contents')
+                                , condition=Expression(
+               text='python: folder is object')
+                                , permissions=('List folder contents',)
+                                , category='folder'
+                                , visible=1
+                                 )]
+
     meta_type = 'CMF Actions Tool'
 
-    action_providers = ( 'portal_actions'
-                       , 'portal_memberdata'
-                       , 'portal_registration'
-                       , 'portal_discussion'
-                       , 'portal_membership'
-                       , 'portal_workflow'
-                       , 'portal_undo'
-                       )
+    action_providers = ('portal_membership'
+                      , 'portal_actions'
+                      , 'portal_registration'
+                      , 'portal_discussion'
+                      , 'portal_undo'
+                      , 'portal_syndication'
+                      , 'portal_workflow'
+                      , 'portal_properties')
 
     security = ClassSecurityInfo()
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
-                     , 
-                     ) + SimpleItem.manage_options
+    manage_options = ( ActionProviderBase.manage_options +
+                      ({'label' : 'Action Providers', 'action' : 'manage_actionProviders'}
+                     ,   { 'label' : 'Overview', 'action' : 'manage_overview' }
+                     ,
+                     ) + OFS.Folder.Folder.manage_options
+                     ) 
 
     #
     #   ZMI methods
@@ -88,19 +86,55 @@
     security.declareProtected( CMFCorePermissions.ManagePortal
                              , 'manage_overview' )
     manage_overview = DTMLFile( 'explainActionsTool', _dtmldir )
+    manage_actionProviders = DTMLFile('manageActionProviders', _dtmldir)
 
 
     #
     # Programmatically manipulate the list of action providers
     #
 
+    security.declarePrivate('listActions')
+    def listActions(self, info=None):
+        """
+        Lists actions available through the tool.
+        """
+        return self._actions
+
     security.declareProtected( CMFCorePermissions.ManagePortal
                              , 'listActionProviders'
                              )
-    def listActionProviders( self ):
+    def listActionProviders(self):
        """ returns a sequence of action providers known by this tool """
        return self.action_providers
 
+    security.declareProtected(CMFCorePermissions.ManagePortal
+                            , 'manage_aproviders')
+    def manage_aproviders(self
+                        , apname=''
+                        , chosen=()
+                        , add_provider=0
+                        , del_provider=0
+                        , REQUEST=None):
+        """
+        Manage TTW Action Providers
+        """
+        providers = list(self.listActionProviders())
+        new_providers = []
+        if add_provider:
+            providers.append(apname)
+        elif del_provider:
+            for item in providers:
+                if item not in chosen:
+                    new_providers.append(item)
+            providers = new_providers
+        self.action_providers = providers
+        if REQUEST is not None:
+            return self.manage_actionProviders(self
+                                             , REQUEST
+                                             , manage_tabs_message='Properties changed.')
+        
+
+
     security.declareProtected( CMFCorePermissions.ManagePortal
                              , 'addActionProvider'
                              )
@@ -111,26 +145,6 @@
             p_new = p_old + ( provider_name, )
             self.action_providers = p_new
 
-    security.declarePrivate('listActions')
-    def listActions(self, info):
-        """
-        List actions available from this tool
-        """
-        if info.isAnonymous:
-            return None
-        else:
-            actions = []
-            folder_url = info.folder_url   
-            content_url = info.content_url   
-            if folder_url is not None: 
-                actions.append(
-                    { 'name'          : 'Folder contents'
-                    , 'url'        : folder_url + '/folder_contents'
-                    , 'permissions'   : ['List folder contents']
-                    , 'category'      : 'folder'
-                   })
-            return actions
-
     security.declareProtected( CMFCorePermissions.ManagePortal
                              , 'deleteActionProvider'
                              )
@@ -162,16 +176,24 @@
                     break
                 else:
                     folder = aq_parent(aq_inner(folder))
-
-        info = ActionInformation(self, folder, object)
-
+        ec = createExprContext(folder, portal, object)
+        ai_objs = []
         actions = []
+        info = oai(self, folder, object)
         # Include actions from specific tools.
         for provider_name in self.listActionProviders():
             provider = getattr(self, provider_name)
             a = provider.listActions(info)
-            if a:
-                actions.extend(list(a))
+            if a and type(a[0]) is not type({}):
+                ai_objs.extend(list(a))
+            else:
+                for i in a: 
+                    actions.append(i)
+
+        if ai_objs:
+            for ai in ai_objs:
+                if ai.testCondition(ec):
+                    actions.append(ai.getAction(ec))
 
         # Include actions from object.
         if object is not None:
@@ -181,7 +203,7 @@
             if ti is not None:
                 defs = ti.getActions()
                 if defs:
-                    c_url = info.content_url
+                    c_url = object.absolute_url()
                     for d in defs:
                         a = d['action']
                         if a:
@@ -197,7 +219,7 @@
                             'visible': d.get('visible', 1),
                             })
             if hasattr(base, 'listActions'):
-                a = object.listActions(info)
+                a = object.listActions()
                 if a:
                     actions.extend(list(a))
 


=== CMF/CMFCore/CatalogTool.py 1.21 => 1.22 ===
 from AccessControl import ClassSecurityInfo
 from utils import mergedLocalRoles
+from ActionProviderBase import ActionProviderBase
+from ActionInformation import ActionInformation
+from Expression import Expression
 import os
 import CMFCorePermissions
 from Acquisition import aq_base
@@ -64,16 +67,19 @@
         return list(allowed.keys())
 
 
-class CatalogTool (UniqueObject, ZCatalog):
+class CatalogTool (UniqueObject, ZCatalog, ActionProviderBase):
     '''This is a ZCatalog that filters catalog queries.
     '''
     id = 'portal_catalog'
     meta_type = 'CMF Catalog'
     security = ClassSecurityInfo()
+    _actions = []
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
+    manage_options = ( ZCatalog.manage_options +
+                      ActionProviderBase.manage_options +
+                      ({ 'label' : 'Overview', 'action' : 'manage_overview' }
                      , 
-                     ) + ZCatalog.manage_options
+                     ))
 
     def __init__(self):
         ZCatalog.__init__(self, self.getId())
@@ -82,6 +88,14 @@
     #
     #   Subclass extension interface
     #
+    security.declarePrivate('listActions')
+    def listActions(self, info=None):
+        """
+        Return a list of action information instances 
+        provided via tool
+        """
+        return self._actions
+
     security.declarePublic( 'enumerateIndexes' ) # Subclass can call
     def enumerateIndexes( self ):
         #   Return a list of ( index_name, type ) pairs for the initial


=== CMF/CMFCore/MemberDataTool.py 1.12 => 1.13 ===
 from CMFCorePermissions import ViewManagementScreens
 import CMFCorePermissions
+from ActionProviderBase import ActionProviderBase
 
 _marker = []  # Create a new marker object.
 
 
-class MemberDataTool (UniqueObject, SimpleItem, PropertyManager):
+class MemberDataTool (UniqueObject, SimpleItem, PropertyManager, ActionProviderBase):
     '''This tool wraps user objects, making them act as Member objects.
     '''
     id = 'portal_memberdata'
     meta_type = 'CMF Member Data Tool'
+    _actions = []
     _v_temps = None
     _properties = ()
 
     security = ClassSecurityInfo()
 
-    manage_options=( ( { 'label' : 'Overview'
+    manage_options=( ActionProviderBase.manage_options +
+                     ({ 'label' : 'Overview'
                        , 'action' : 'manage_overview'
                        }
                      , { 'label' : 'Contents'
@@ -83,8 +86,11 @@
     #   'portal_memberdata' interface methods
     #
     security.declarePrivate('listActions')
-    def listActions(self, info):
-        return None
+    def listActions(self, info=None):
+        """
+        Return actions provided via tool.
+        """
+        return self._actions
 
     security.declarePrivate('getMemberDataContents')
     def getMemberDataContents(self):


=== CMF/CMFCore/MembershipTool.py 1.16 => 1.17 ===
 from CMFCorePermissions import ManagePortal
 import CMFCorePermissions
+from ActionProviderBase import ActionProviderBase
 import Acquisition
 
 default_member_content = '''Default page for %s
@@ -37,22 +38,23 @@
   in the Tool Box on the left.
 '''
 
-class MembershipTool (UniqueObject, SimpleItem):
+class MembershipTool (UniqueObject, SimpleItem, ActionProviderBase):
     # This tool accesses member data through an acl_users object.
     # It can be replaced with something that accesses member data in
     # a different way.
     id = 'portal_membership'
     meta_type = 'CMF Membership Tool'
-
+    _actions = []
     security = ClassSecurityInfo()
 
-    manage_options=( { 'label' : 'Overview'
-                     , 'action' : 'manage_overview'
-                     }
-                   , { 'label' : 'Configuration'
+    manage_options=( ({ 'label' : 'Configuration'
                      , 'action' : 'manage_mapRoles'
-                     }
-                   ) + SimpleItem.manage_options
+                     },) +
+                     ActionProviderBase.manage_options + 
+                   ( { 'label' : 'Overview'
+                     , 'action' : 'manage_overview'
+                     },
+                   ) + SimpleItem.manage_options)
 
     #
     #   ZMI methods
@@ -387,7 +389,7 @@
 
 
     security.declarePrivate('listActions')
-    def listActions(self, info):
+    def listActions(self):
         return None
 
     security.declarePublic('getHomeFolder')


=== CMF/CMFCore/PortalFolder.py 1.28 => 1.29 ===
                                   , 'category'      : 'folder'
                                   }
-                                , { 'id'            : 'syndication'
-                                  , 'name'          : 'Syndication'
-                                  , 'action'        : 'synPropertiesForm'
-                                  , 'permissions'   : (ManageProperties,)
-                                  , 'category'      : 'folder'
-                                  }
                                 )
                              }
                            ,


=== CMF/CMFCore/RegistrationTool.py 1.7 => 1.8 ===
 import CMFCorePermissions
 import string, random
+from ActionProviderBase import ActionProviderBase
 
 
-class RegistrationTool (UniqueObject, SimpleItem):
+class RegistrationTool (UniqueObject, SimpleItem, ActionProviderBase):
     # This tool creates and modifies users by making calls
     # to portal_membership.
     id = 'portal_registration'
@@ -37,9 +38,10 @@
 
     security = ClassSecurityInfo()
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
+    manage_options = (ActionProviderBase.manage_options +
+                     ({ 'label' : 'Overview', 'action' : 'manage_overview' }
                      , 
-                     ) + SimpleItem.manage_options
+                     ) + SimpleItem.manage_options)
 
     #
     #   ZMI methods
@@ -51,9 +53,6 @@
     #
     #   'portal_registration' interface methods
     #
-    security.declarePrivate('listActions')
-    def listActions(self, info):
-        return none
 
     security.declarePublic('isRegistrationAllowed')
     def isRegistrationAllowed(self, REQUEST):


=== CMF/CMFCore/SkinsTool.py 1.11 => 1.12 ===
 from AccessControl import ClassSecurityInfo
 from CMFCorePermissions import ManagePortal, AccessContentsInformation
+from ActionProviderBase import ActionProviderBase
+from ActionInformation import ActionInformation
+from Expression import Expression
 
 from OFS.Image import Image
 from OFS.DTMLMethod import DTMLMethod
@@ -53,20 +56,23 @@
                   'action':'manage_propertiesForm'}]
     return tuple(rval)
 
-class SkinsTool(UniqueObject, SkinsContainer, PortalFolder):
+class SkinsTool(UniqueObject, SkinsContainer, PortalFolder, ActionProviderBase):
     '''
     This tool is used to supply skins to a portal.
     '''
 
     id = 'portal_skins'
     meta_type = 'CMF Skins Tool'
+    _actions = []
     cookie_persistence = 0
 
     security = ClassSecurityInfo()
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
+    manage_options = ( modifiedOptions() +
+                      ({ 'label' : 'Overview', 'action' : 'manage_overview' }
                      , 
-                     ) + modifiedOptions()
+                     ) + ActionProviderBase.manage_options
+                     )
 
     def __init__(self):
         self.selections = PersistentMapping()
@@ -89,6 +95,14 @@
     request_varname = 'portal_skin'
     allow_any = 0
     selections = None
+
+    security.declarePrivate('listActions')
+    def listActions(self, info=None):
+        """
+        Return a list of actions information instances
+        provided by the tool.
+        """
+        return self._actions
 
     security.declareProtected(ManagePortal, 'manage_propertiesForm')
     manage_propertiesForm = DTMLFile('dtml/skinProps', globals())


=== CMF/CMFCore/TypesTool.py 1.26 => 1.27 ===
 from Acquisition import aq_base
 import Products, CMFCorePermissions
+from ActionProviderBase import ActionProviderBase
+from ActionInformation import ActionInformation
+from Expression import Expression
 
 from CMFCorePermissions import View, ManagePortal, AccessContentsInformation
 
@@ -501,18 +504,21 @@
                , ScriptableTypeInformation.meta_type
                )
 
-class TypesTool( UniqueObject, OFS.Folder.Folder ):
+class TypesTool( UniqueObject, OFS.Folder.Folder, ActionProviderBase ):
     """
         Provides a configurable registry of portal content types.
     """
     id = 'portal_types'
     meta_type = 'CMF Types Tool'
+    _actions = []
 
     security = ClassSecurityInfo()
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
+    manage_options = ( OFS.Folder.Folder.manage_options +
+                      ActionProviderBase.manage_options +
+                      ({ 'label' : 'Overview', 'action' : 'manage_overview' }
                      , 
-                     ) + OFS.Folder.Folder.manage_options
+                     ))
 
     #
     #   ZMI methods
@@ -520,6 +526,14 @@
     security.declareProtected( CMFCorePermissions.ManagePortal
                              , 'manage_overview' )
     manage_overview = DTMLFile( 'explainTypesTool', _dtmldir )
+
+    security.declarePrivate('listActions')
+    def listActions(self, info=None):
+        """
+        Return a list of action information instances
+        for actions provided via tool
+        """
+        return self._actions
 
     def all_meta_types(self):
         all = TypesTool.inheritedAttribute('all_meta_types')(self)


=== CMF/CMFCore/UndoTool.py 1.4 => 1.5 ===
 from string import split
 from AccessControl import ClassSecurityInfo
-
+from Expression import Expression
+from ActionInformation import ActionInformation
+from ActionProviderBase import ActionProviderBase
 from CMFCorePermissions import ManagePortal, UndoChanges, ListUndoableChanges
 
-class UndoTool (UniqueObject, SimpleItem):
+class UndoTool (UniqueObject, SimpleItem, ActionProviderBase):
     id = 'portal_undo'
     meta_type = 'CMF Undo Tool'
     # This tool is used to undo changes.
+    _actions = [ActionInformation(id='undo'
+                                , title='Undo'
+                                , action=Expression(
+               text='string: ${portal_url}/undo_form')
+                                , condition=Expression(
+               text='member') 
+                                , permissions=(ListUndoableChanges,)
+                                , category='global'
+                                , visible=1
+                                 )]
 
 
     security = ClassSecurityInfo()
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
+    manage_options = ( ActionProviderBase.manage_options +
+                       SimpleItem.manage_options +
+                       ({ 'label' : 'Overview', 'action' : 'manage_overview' }
                      , 
-                     ) + SimpleItem.manage_options
-
+                     ))
     #
     #   ZMI methods
     #
@@ -46,14 +59,11 @@
     manage_overview = DTMLFile( 'explainUndoTool', _dtmldir )
 
     security.declarePrivate('listActions')
-    def listActions( self, info ):
-        if info.isAnonymous:
-            return []
-        return [ { 'name': 'Undo'
-                 , 'url': 'undo_form'
-                 , 'permissions': [ ListUndoableChanges ]
-                 , 'category': 'global'
-                 } ]
+    def listActions(self, info=None):
+        """
+        List actions available through tool
+        """
+        return self._actions
 
     #
     #   'portal_undo' interface methods


=== CMF/CMFCore/WorkflowTool.py 1.19 => 1.20 ===
     security = ClassSecurityInfo()
 
-    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
-                     , { 'label' : 'Workflows'
+    manage_options = ( { 'label' : 'Workflows'
                        , 'action' : 'manage_selectWorkflows'
                        }
+                     , { 'label' : 'Overview', 'action' : 'manage_overview' }
                      ) + Folder.manage_options
 
     #