vendor/CMF/1.5.2/DCWorkflow

changeset 0:e89e53b8c32e DCWorkflow tip

Vendor import of CMF 1.5.2
author fguillaume
date Wed, 20 Jul 2005 16:21:26 +0000
parents
children
files CHANGES.txt ContainerTab.py DCWorkflow.py DEPENDENCIES.txt Default.py Expression.py Guard.py README.txt Scripts.py States.py Transitions.py Variables.py WorkflowUIMixin.py Worklists.py __init__.py configure.zcml doc/actbox.stx doc/basics.stx doc/examples/staging/checkin.py doc/examples/staging/checkout.py doc/examples/staging/retractStages.py doc/examples/staging/updateProductionStage.py doc/examples/staging/updateReviewStage.py doc/expressions.stx doc/howto.stx doc/worklists.stx dtml/guard.dtml dtml/state_groups.pt dtml/state_permissions.dtml dtml/state_properties.dtml dtml/state_variables.dtml dtml/states.dtml dtml/transition_properties.dtml dtml/transition_variables.dtml dtml/transitions.dtml dtml/variable_properties.dtml dtml/variables.dtml dtml/workflow_groups.pt dtml/workflow_permissions.dtml dtml/workflow_properties.dtml dtml/worklist_properties.dtml dtml/worklists.dtml help/001-overview.stx help/002-expressions.stx help/003-guards.stx help/004-actionbox.stx help/011-states.stx help/021-transition.stx help/031-variables.stx help/041-worklists.stx help/051-scripts.stx help/061-permissions.stx images/script.gif images/state.gif images/transition.gif images/variable.gif images/workflow.gif images/worklist.gif permissions.py tests/__init__.py tests/test_DCWorkflow.py tests/test_all.py tests/test_guard.py tests/test_roles.py utils.py version.txt
diffstat 66 files changed, 5174 insertions(+), 0 deletions(-) [+]
line diff
     1.1 new file mode 100644
     1.2 --- /dev/null
     1.3 +++ b/CHANGES.txt
     1.4 @@ -0,0 +1,102 @@
     1.5 +
     1.6 +Next release
     1.7 +
     1.8 +- Added support for groups.  Although the implementation is currently
     1.9 +tied to a particular implementation of groups, it should be
    1.10 +easy to generalize to any product that adds groups to Zope.
    1.11 +
    1.12 +- Added a pop-up expression reference.
    1.13 +
    1.14 +- Gave Managers the option to bypass all guards.
    1.15 +
    1.16 +- Workflow can now help decide whether object types appear in add
    1.17 +menus.  This requires help from the types tool.
    1.18 +
    1.19 +
    1.20 +Version 0.5
    1.21 +
    1.22 +- Help documentation contributed by John Morton.
    1.23 +
    1.24 +- In the ZMI, states are now careful to show only transitions that
    1.25 +still exist.
    1.26 +
    1.27 +- States now have a description, in addition to a title.
    1.28 +
    1.29 +- Worklist variables now allow matches with multiple values (separated
    1.30 +by semicolons) and use Python string formatting for dynamic matches.
    1.31 +
    1.32 +- Fixed issues with ObjectMoved exceptions.
    1.33 +
    1.34 +- Fixed the default permissions for new DCWorkflow instances.
    1.35 +
    1.36 +- Expression.py was moved to CMFCore.
    1.37 +
    1.38 +
    1.39 +Version 0.4.2
    1.40 +
    1.41 +- Fixed getInfoFor() using patch from Sebastien.Bigaret@inqual.com.  Thanks!
    1.42 +
    1.43 +- executeTransition(): Optionally preserve (by copying) unchanged
    1.44 +status variables.
    1.45 +
    1.46 +- Updated to ZPL 2.0.
    1.47 +
    1.48 +- Added scripts that get executed after a transition.
    1.49 +
    1.50 +
    1.51 +Version 0.4.1
    1.52 +
    1.53 +- Corrected an expression in the classic workflow implementation.
    1.54 +
    1.55 +- Made expressions work again in scripts by removing the requirement
    1.56 +  that "REQUEST" exist.
    1.57 +
    1.58 +
    1.59 +Version 0.4
    1.60 +
    1.61 +- Thanks to Ulrich Eck (ueck@net-labs.de), you can now set variables
    1.62 +  on states and transitions.  Great job!
    1.63 +
    1.64 +- Changed expressions to TALES.  This means that it is now required that
    1.65 +  you have the PageTemplates product installed; see
    1.66 +  http://www.zope.org/Members/4am/ZPT .
    1.67 +  Your expressions will need to be written
    1.68 +  again, but they should be a lot cleaner now.
    1.69 +
    1.70 +- Added a second default workflow that closely resembles the "classic"
    1.71 +  default workflow.  Just visit a workflow tool and click "Add workflow"
    1.72 +  then select "Web-configurable workflow [Classic]".
    1.73 +
    1.74 +
    1.75 +Version 0.3 (never officially released)
    1.76 +
    1.77 +- Fixed guard expressions.  Thanks to Jens Quade!
    1.78 +
    1.79 +- Implemented updateRoleMappingsFor(), a new addition to the
    1.80 +WorkflowDefinition interface.
    1.81 +
    1.82 +- Added getPortal() to the expression namespace.  Again, thanks to Jens
    1.83 +Quade.
    1.84 +
    1.85 +- DCWorkflow is now aware of ObjectDeleted and ObjectMoved messages.
    1.86 + 
    1.87 +- getObjectContainer() added to expressions.
    1.88 + 
    1.89 +- What is passed to scripts is now an object whose attributes are from
    1.90 +  the expression namespace.
    1.91 +
    1.92 +
    1.93 +Version 0.2
    1.94 +
    1.95 +- As suggested by Seb Bacon, simplified by making transitions trigger on
    1.96 +their own IDs.  Thanks!
    1.97 + 
    1.98 +- Added script invocation just before execution of a transition.  Scripts
    1.99 +are passed an expression namespace as the first parameter.
   1.100 + 
   1.101 +- Replaced the name "action_or_method" with "transition".
   1.102 +
   1.103 +
   1.104 +Version 0.1
   1.105 +
   1.106 +- Initial release.
     2.1 new file mode 100644
     2.2 --- /dev/null
     2.3 +++ b/ContainerTab.py
     2.4 @@ -0,0 +1,119 @@
     2.5 +##############################################################################
     2.6 +#
     2.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     2.8 +# 
     2.9 +# This software is subject to the provisions of the Zope Public License,
    2.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    2.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    2.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    2.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    2.14 +# FOR A PARTICULAR PURPOSE.
    2.15 +# 
    2.16 +##############################################################################
    2.17 +""" A convenient base class for representing a container as a management tab.
    2.18 +
    2.19 +$Id: ContainerTab.py,v 1.6 2004/08/12 15:07:44 jens Exp $
    2.20 +"""
    2.21 +
    2.22 +from OFS.Folder import Folder
    2.23 +from OFS.SimpleItem import Item_w__name__
    2.24 +from Acquisition import aq_base, aq_inner, aq_parent
    2.25 +
    2.26 +_marker = []  # Create a new marker object.
    2.27 +
    2.28 +class ContainerTab (Folder):
    2.29 +
    2.30 +    def __init__(self, id):
    2.31 +        self.id = id
    2.32 +        self._mapping = {}
    2.33 +
    2.34 +    def getId(self):
    2.35 +        return self.id
    2.36 +
    2.37 +    def manage_options(self):
    2.38 +        parent = aq_parent(aq_inner(self))
    2.39 +        res = []
    2.40 +        options = parent.manage_options
    2.41 +        if callable(options):
    2.42 +            options = options()
    2.43 +        for item in options:
    2.44 +            item = item.copy()
    2.45 +            item['action'] = '../' + item['action']
    2.46 +            res.append(item)
    2.47 +        return res
    2.48 +
    2.49 +    def manage_workspace(self, RESPONSE):
    2.50 +        '''
    2.51 +        Redirects to the primary option.
    2.52 +        '''
    2.53 +        RESPONSE.redirect(self.absolute_url() + '/manage_main')
    2.54 +
    2.55 +    def _checkId(self, id, allow_dup=0):
    2.56 +        if not allow_dup:
    2.57 +            if self._mapping.has_key(id):
    2.58 +                raise 'Bad Request', 'The id "%s" is already in use.' % id
    2.59 +        return Folder._checkId(self, id, allow_dup)
    2.60 +
    2.61 +    def _getOb(self, name, default=_marker):
    2.62 +        mapping = self._mapping
    2.63 +        if mapping.has_key(name):
    2.64 +            res = mapping[name]
    2.65 +            if hasattr(res, '__of__'):
    2.66 +                res = res.__of__(self)
    2.67 +            return res
    2.68 +        else:
    2.69 +            if default is _marker:
    2.70 +                raise KeyError, name
    2.71 +            return default
    2.72 +
    2.73 +    def __getattr__(self, name):
    2.74 +        ob = self._mapping.get(name, None)
    2.75 +        if ob is not None:
    2.76 +            return ob
    2.77 +        raise AttributeError, name
    2.78 +
    2.79 +    def _setOb(self, name, value):
    2.80 +        mapping = self._mapping
    2.81 +        mapping[name] = aq_base(value)
    2.82 +        self._mapping = mapping  # Trigger persistence.
    2.83 +
    2.84 +    def _delOb(self, name):
    2.85 +        mapping = self._mapping
    2.86 +        del mapping[name]
    2.87 +        self._mapping = mapping  # Trigger persistence.
    2.88 +
    2.89 +    def get(self, name, default=None):
    2.90 +        if self._mapping.has_key(name):
    2.91 +            return self[name]
    2.92 +        else:
    2.93 +            return default
    2.94 +
    2.95 +    def has_key(self, key):
    2.96 +        return self._mapping.has_key(key)
    2.97 +
    2.98 +    def objectIds(self, spec=None):
    2.99 +        # spec is not important for now...
   2.100 +        return self._mapping.keys()
   2.101 +
   2.102 +    def keys(self):
   2.103 +        return self._mapping.keys()
   2.104 +
   2.105 +    def items(self):
   2.106 +        return map(lambda id, self=self: (id, self._getOb(id)),
   2.107 +                   self._mapping.keys())
   2.108 +
   2.109 +    def values(self):
   2.110 +        return map(lambda id, self=self: self._getOb(id),
   2.111 +                   self._mapping.keys())
   2.112 +
   2.113 +    def manage_renameObjects(self, ids=[], new_ids=[], REQUEST=None):
   2.114 +        """Rename several sub-objects"""
   2.115 +        if len(ids) != len(new_ids):
   2.116 +            raise 'Bad Request', 'Please rename each listed object.'
   2.117 +        for i in range(len(ids)):
   2.118 +            if ids[i] != new_ids[i]:
   2.119 +                self.manage_renameObject(ids[i], new_ids[i])
   2.120 +        if REQUEST is not None:
   2.121 +            return self.manage_main(REQUEST)
   2.122 +        return None
   2.123 +
     3.1 new file mode 100644
     3.2 --- /dev/null
     3.3 +++ b/DCWorkflow.py
     3.4 @@ -0,0 +1,588 @@
     3.5 +##############################################################################
     3.6 +#
     3.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     3.8 +#
     3.9 +# This software is subject to the provisions of the Zope Public License,
    3.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    3.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    3.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    3.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    3.14 +# FOR A PARTICULAR PURPOSE.
    3.15 +#
    3.16 +##############################################################################
    3.17 +""" Web-configurable workflow.
    3.18 +
    3.19 +$Id: DCWorkflow.py,v 1.36.2.2 2005/04/26 16:01:34 anguenot Exp $
    3.20 +"""
    3.21 +
    3.22 +# Zope
    3.23 +from AccessControl import ClassSecurityInfo
    3.24 +from AccessControl import getSecurityManager
    3.25 +from AccessControl import Unauthorized
    3.26 +from Acquisition import aq_inner
    3.27 +from Acquisition import aq_parent
    3.28 +from App.Undo import UndoSupport
    3.29 +from DocumentTemplate.DT_Util import TemplateDict
    3.30 +from Globals import InitializeClass
    3.31 +from OFS.Folder import Folder
    3.32 +from OFS.ObjectManager import bad_id
    3.33 +
    3.34 +# CMFCore
    3.35 +from Products.CMFCore.interfaces.portal_workflow \
    3.36 +        import WorkflowDefinition as IWorkflowDefinition
    3.37 +from Products.CMFCore.utils import getToolByName
    3.38 +from Products.CMFCore.WorkflowCore import ObjectDeleted
    3.39 +from Products.CMFCore.WorkflowCore import ObjectMoved
    3.40 +from Products.CMFCore.WorkflowCore import WorkflowException
    3.41 +from Products.CMFCore.WorkflowTool import addWorkflowFactory
    3.42 +
    3.43 +# DCWorkflow
    3.44 +from permissions import ManagePortal
    3.45 +from utils import _dtmldir
    3.46 +from utils import modifyRolesForPermission
    3.47 +from utils import modifyRolesForGroup
    3.48 +from WorkflowUIMixin import WorkflowUIMixin
    3.49 +from Transitions import TRIGGER_AUTOMATIC
    3.50 +from Transitions import TRIGGER_USER_ACTION
    3.51 +from Transitions import TRIGGER_WORKFLOW_METHOD
    3.52 +from Expression import StateChangeInfo
    3.53 +from Expression import createExprContext
    3.54 +
    3.55 +
    3.56 +def checkId(id):
    3.57 +    res = bad_id(id)
    3.58 +    if res != -1 and res is not None:
    3.59 +        raise ValueError, 'Illegal ID'
    3.60 +    return 1
    3.61 +
    3.62 +
    3.63 +class DCWorkflowDefinition (WorkflowUIMixin, Folder):
    3.64 +    '''
    3.65 +    This class is the workflow engine and the container for the
    3.66 +    workflow definition.
    3.67 +    UI methods are in WorkflowUIMixin.
    3.68 +    '''
    3.69 +
    3.70 +    __implements__ = IWorkflowDefinition
    3.71 +
    3.72 +    meta_type = 'Workflow'
    3.73 +    title = 'DC Workflow Definition'
    3.74 +    _isAWorkflow = 1
    3.75 +
    3.76 +    state_var = 'state'
    3.77 +    initial_state = None
    3.78 +
    3.79 +    states = None
    3.80 +    transitions = None
    3.81 +    variables = None
    3.82 +    worklists = None
    3.83 +    scripts = None
    3.84 +
    3.85 +    permissions = ()
    3.86 +    groups = ()     # Names of groups managed by this workflow.
    3.87 +    roles = None  # The role names managed by this workflow.
    3.88 +    # If roles is None, listRoles() provides a default.
    3.89 +
    3.90 +    creation_guard = None  # The guard that can veto object creation.
    3.91 +
    3.92 +    manager_bypass = 0  # Boolean: 'Manager' role bypasses guards
    3.93 +
    3.94 +    manage_options = (
    3.95 +        {'label': 'Properties', 'action': 'manage_properties'},
    3.96 +        {'label': 'States', 'action': 'states/manage_main'},
    3.97 +        {'label': 'Transitions', 'action': 'transitions/manage_main'},
    3.98 +        {'label': 'Variables', 'action': 'variables/manage_main'},
    3.99 +        {'label': 'Worklists', 'action': 'worklists/manage_main'},
   3.100 +        {'label': 'Scripts', 'action': 'scripts/manage_main'},
   3.101 +        {'label': 'Permissions', 'action': 'manage_permissions'},
   3.102 +        {'label': 'Groups', 'action': 'manage_groups'},
   3.103 +        )
   3.104 +
   3.105 +    security = ClassSecurityInfo()
   3.106 +    security.declareObjectProtected(ManagePortal)
   3.107 +
   3.108 +    def __init__(self, id):
   3.109 +        self.id = id
   3.110 +        from States import States
   3.111 +        self._addObject(States('states'))
   3.112 +        from Transitions import Transitions
   3.113 +        self._addObject(Transitions('transitions'))
   3.114 +        from Variables import Variables
   3.115 +        self._addObject(Variables('variables'))
   3.116 +        from Worklists import Worklists
   3.117 +        self._addObject(Worklists('worklists'))
   3.118 +        from Scripts import Scripts
   3.119 +        self._addObject(Scripts('scripts'))
   3.120 +
   3.121 +    def _addObject(self, ob):
   3.122 +        id = ob.getId()
   3.123 +        setattr(self, id, ob)
   3.124 +        self._objects = self._objects + (
   3.125 +            {'id': id, 'meta_type': ob.meta_type},)
   3.126 +
   3.127 +    #
   3.128 +    # Workflow engine.
   3.129 +    #
   3.130 +
   3.131 +    def _getStatusOf(self, ob):
   3.132 +        tool = aq_parent(aq_inner(self))
   3.133 +        status = tool.getStatusOf(self.id, ob)
   3.134 +        if status is None:
   3.135 +            return {}
   3.136 +        else:
   3.137 +            return status
   3.138 +
   3.139 +    def _getWorkflowStateOf(self, ob, id_only=0):
   3.140 +        tool = aq_parent(aq_inner(self))
   3.141 +        status = tool.getStatusOf(self.id, ob)
   3.142 +        if status is None:
   3.143 +            state = self.initial_state
   3.144 +        else:
   3.145 +            state = status.get(self.state_var, None)
   3.146 +            if state is None:
   3.147 +                state = self.initial_state
   3.148 +        if id_only:
   3.149 +            return state
   3.150 +        else:
   3.151 +            return self.states.get(state, None)
   3.152 +
   3.153 +    def _getPortalRoot(self):
   3.154 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   3.155 +
   3.156 +    security.declarePrivate('getCatalogVariablesFor')
   3.157 +    def getCatalogVariablesFor(self, ob):
   3.158 +        '''
   3.159 +        Allows this workflow to make workflow-specific variables
   3.160 +        available to the catalog, making it possible to implement
   3.161 +        worklists in a simple way.
   3.162 +        Returns a mapping containing the catalog variables
   3.163 +        that apply to ob.
   3.164 +        '''
   3.165 +        res = {}
   3.166 +        status = self._getStatusOf(ob)
   3.167 +        for id, vdef in self.variables.items():
   3.168 +            if vdef.for_catalog:
   3.169 +                if status.has_key(id):
   3.170 +                    value = status[id]
   3.171 +
   3.172 +                # Not set yet.  Use a default.
   3.173 +                elif vdef.default_expr is not None:
   3.174 +                    ec = createExprContext(StateChangeInfo(ob, self, status))
   3.175 +                    value = vdef.default_expr(ec)
   3.176 +                else:
   3.177 +                    value = vdef.default_value
   3.178 +
   3.179 +                res[id] = value
   3.180 +        # Always provide the state variable.
   3.181 +        state_var = self.state_var
   3.182 +        res[state_var] = status.get(state_var, self.initial_state)
   3.183 +        return res
   3.184 +
   3.185 +    security.declarePrivate('listObjectActions')
   3.186 +    def listObjectActions(self, info):
   3.187 +        '''
   3.188 +        Allows this workflow to
   3.189 +        include actions to be displayed in the actions box.
   3.190 +        Called only when this workflow is applicable to
   3.191 +        info.object.
   3.192 +        Returns the actions to be displayed to the user.
   3.193 +        '''
   3.194 +        ob = info.object
   3.195 +        sdef = self._getWorkflowStateOf(ob)
   3.196 +        if sdef is None:
   3.197 +            return None
   3.198 +        res = []
   3.199 +        for tid in sdef.transitions:
   3.200 +            tdef = self.transitions.get(tid, None)
   3.201 +            if tdef is not None and tdef.trigger_type == TRIGGER_USER_ACTION:
   3.202 +                if tdef.actbox_name:
   3.203 +                    if self._checkTransitionGuard(tdef, ob):
   3.204 +                        res.append((tid, {
   3.205 +                            'id': tid,
   3.206 +                            'name': tdef.actbox_name % info,
   3.207 +                            'url': tdef.actbox_url % info,
   3.208 +                            'permissions': (),  # Predetermined.
   3.209 +                            'category': tdef.actbox_category,
   3.210 +                            'transition': tdef}))
   3.211 +        res.sort()
   3.212 +        return [ result[1] for result in res ]
   3.213 +
   3.214 +    security.declarePrivate('listGlobalActions')
   3.215 +    def listGlobalActions(self, info):
   3.216 +        '''
   3.217 +        Allows this workflow to
   3.218 +        include actions to be displayed in the actions box.
   3.219 +        Called on every request.
   3.220 +        Returns the actions to be displayed to the user.
   3.221 +        '''
   3.222 +        if not self.worklists:
   3.223 +            return None  # Optimization
   3.224 +        sm = getSecurityManager()
   3.225 +        portal = self._getPortalRoot()
   3.226 +        res = []
   3.227 +        fmt_data = None
   3.228 +        for id, qdef in self.worklists.items():
   3.229 +            if qdef.actbox_name:
   3.230 +                guard = qdef.guard
   3.231 +                if guard is None or guard.check(sm, self, portal):
   3.232 +                    searchres = None
   3.233 +                    var_match_keys = qdef.getVarMatchKeys()
   3.234 +                    if var_match_keys:
   3.235 +                        # Check the catalog for items in the worklist.
   3.236 +                        catalog = getToolByName(self, 'portal_catalog')
   3.237 +                        kw = {}
   3.238 +                        for k in var_match_keys:
   3.239 +                            v = qdef.getVarMatch(k)
   3.240 +                            kw[k] = [ x % info for x in v ]
   3.241 +                        searchres = catalog.searchResults(**kw)
   3.242 +                        if not searchres:
   3.243 +                            continue
   3.244 +                    if fmt_data is None:
   3.245 +                        fmt_data = TemplateDict()
   3.246 +                        fmt_data._push(info)
   3.247 +                    fmt_data._push({'count': len(searchres)})
   3.248 +                    res.append((id, {'name': qdef.actbox_name % fmt_data,
   3.249 +                                     'url': qdef.actbox_url % fmt_data,
   3.250 +                                     'permissions': (),  # Predetermined.
   3.251 +                                     'category': qdef.actbox_category}))
   3.252 +                    fmt_data._pop()
   3.253 +        res.sort()
   3.254 +        return [ result[1] for result in res ]
   3.255 +
   3.256 +    security.declarePrivate('isActionSupported')
   3.257 +    def isActionSupported(self, ob, action, **kw):
   3.258 +        '''
   3.259 +        Returns a true value if the given action name
   3.260 +        is possible in the current state.
   3.261 +        '''
   3.262 +        sdef = self._getWorkflowStateOf(ob)
   3.263 +        if sdef is None:
   3.264 +            return 0
   3.265 +        if action in sdef.transitions:
   3.266 +            tdef = self.transitions.get(action, None)
   3.267 +            if (tdef is not None and
   3.268 +                tdef.trigger_type == TRIGGER_USER_ACTION and
   3.269 +                self._checkTransitionGuard(tdef, ob, **kw)):
   3.270 +                return 1
   3.271 +        return 0
   3.272 +
   3.273 +    security.declarePrivate('doActionFor')
   3.274 +    def doActionFor(self, ob, action, comment='', **kw):
   3.275 +        '''
   3.276 +        Allows the user to request a workflow action.  This method
   3.277 +        must perform its own security checks.
   3.278 +        '''
   3.279 +        kw['comment'] = comment
   3.280 +        sdef = self._getWorkflowStateOf(ob)
   3.281 +        if sdef is None:
   3.282 +            raise WorkflowException, 'Object is in an undefined state'
   3.283 +        if action not in sdef.transitions:
   3.284 +            raise Unauthorized(action)
   3.285 +        tdef = self.transitions.get(action, None)
   3.286 +        if tdef is None or tdef.trigger_type != TRIGGER_USER_ACTION:
   3.287 +            raise WorkflowException, (
   3.288 +                'Transition %s is not triggered by a user action' % action)
   3.289 +        if not self._checkTransitionGuard(tdef, ob, **kw):
   3.290 +            raise Unauthorized(action)
   3.291 +        self._changeStateOf(ob, tdef, kw)
   3.292 +
   3.293 +    security.declarePrivate('isWorkflowMethodSupported')
   3.294 +    def isWorkflowMethodSupported(self, ob, method_id):
   3.295 +        '''
   3.296 +        Returns a true value if the given workflow method
   3.297 +        is supported in the current state.
   3.298 +        '''
   3.299 +        sdef = self._getWorkflowStateOf(ob)
   3.300 +        if sdef is None:
   3.301 +            return 0
   3.302 +        if method_id in sdef.transitions:
   3.303 +            tdef = self.transitions.get(method_id, None)
   3.304 +            if (tdef is not None and
   3.305 +                tdef.trigger_type == TRIGGER_WORKFLOW_METHOD and
   3.306 +                self._checkTransitionGuard(tdef, ob)):
   3.307 +                return 1
   3.308 +        return 0
   3.309 +
   3.310 +    security.declarePrivate('wrapWorkflowMethod')
   3.311 +    def wrapWorkflowMethod(self, ob, method_id, func, args, kw):
   3.312 +        '''
   3.313 +        Allows the user to request a workflow action.  This method
   3.314 +        must perform its own security checks.
   3.315 +        '''
   3.316 +        sdef = self._getWorkflowStateOf(ob)
   3.317 +        if sdef is None:
   3.318 +            raise WorkflowException, 'Object is in an undefined state'
   3.319 +        if method_id not in sdef.transitions:
   3.320 +            raise Unauthorized(method_id)
   3.321 +        tdef = self.transitions.get(method_id, None)
   3.322 +        if tdef is None or tdef.trigger_type != TRIGGER_WORKFLOW_METHOD:
   3.323 +            raise WorkflowException, (
   3.324 +                'Transition %s is not triggered by a workflow method'
   3.325 +                % method_id)
   3.326 +        if not self._checkTransitionGuard(tdef, ob):
   3.327 +            raise Unauthorized(method_id)
   3.328 +        res = func(*args, **kw)
   3.329 +        try:
   3.330 +            self._changeStateOf(ob, tdef)
   3.331 +        except ObjectDeleted:
   3.332 +            # Re-raise with a different result.
   3.333 +            raise ObjectDeleted(res)
   3.334 +        except ObjectMoved, ex:
   3.335 +            # Re-raise with a different result.
   3.336 +            raise ObjectMoved(ex.getNewObject(), res)
   3.337 +        return res
   3.338 +
   3.339 +    security.declarePrivate('isInfoSupported')
   3.340 +    def isInfoSupported(self, ob, name):
   3.341 +        '''
   3.342 +        Returns a true value if the given info name is supported.
   3.343 +        '''
   3.344 +        if name == self.state_var:
   3.345 +            return 1
   3.346 +        vdef = self.variables.get(name, None)
   3.347 +        if vdef is None:
   3.348 +            return 0
   3.349 +        return 1
   3.350 +
   3.351 +    security.declarePrivate('getInfoFor')
   3.352 +    def getInfoFor(self, ob, name, default):
   3.353 +        '''
   3.354 +        Allows the user to request information provided by the
   3.355 +        workflow.  This method must perform its own security checks.
   3.356 +        '''
   3.357 +        if name == self.state_var:
   3.358 +            return self._getWorkflowStateOf(ob, 1)
   3.359 +        vdef = self.variables[name]
   3.360 +        if vdef.info_guard is not None and not vdef.info_guard.check(
   3.361 +            getSecurityManager(), self, ob):
   3.362 +            return default
   3.363 +        status = self._getStatusOf(ob)
   3.364 +        if status is not None and status.has_key(name):
   3.365 +            value = status[name]
   3.366 +
   3.367 +        # Not set yet.  Use a default.
   3.368 +        elif vdef.default_expr is not None:
   3.369 +            ec = createExprContext(StateChangeInfo(ob, self, status))
   3.370 +            value = vdef.default_expr(ec)
   3.371 +        else:
   3.372 +            value = vdef.default_value
   3.373 +
   3.374 +        return value
   3.375 +
   3.376 +    security.declarePrivate('allowCreate')
   3.377 +    def allowCreate(self, container, type_name):
   3.378 +        """Returns true if the user is allowed to create a workflow instance.
   3.379 +
   3.380 +        The object passed to the guard is the prospective container.
   3.381 +        """
   3.382 +        if self.creation_guard is not None:
   3.383 +            return self.creation_guard.check(
   3.384 +                getSecurityManager(), self, container)
   3.385 +        return 1
   3.386 +
   3.387 +    security.declarePrivate('notifyCreated')
   3.388 +    def notifyCreated(self, ob):
   3.389 +        """Notifies this workflow after an object has been created and added.
   3.390 +        """
   3.391 +        try:
   3.392 +            self._changeStateOf(ob, None)
   3.393 +        except ( ObjectDeleted, ObjectMoved ):
   3.394 +            # Swallow.
   3.395 +            pass
   3.396 +
   3.397 +    security.declarePrivate('notifyBefore')
   3.398 +    def notifyBefore(self, ob, action):
   3.399 +        '''
   3.400 +        Notifies this workflow of an action before it happens,
   3.401 +        allowing veto by exception.  Unless an exception is thrown, either
   3.402 +        a notifySuccess() or notifyException() can be expected later on.
   3.403 +        The action usually corresponds to a method name.
   3.404 +        '''
   3.405 +        pass
   3.406 +
   3.407 +    security.declarePrivate('notifySuccess')
   3.408 +    def notifySuccess(self, ob, action, result):
   3.409 +        '''
   3.410 +        Notifies this workflow that an action has taken place.
   3.411 +        '''
   3.412 +        pass
   3.413 +
   3.414 +    security.declarePrivate('notifyException')
   3.415 +    def notifyException(self, ob, action, exc):
   3.416 +        '''
   3.417 +        Notifies this workflow that an action failed.
   3.418 +        '''
   3.419 +        pass
   3.420 +
   3.421 +    security.declarePrivate('updateRoleMappingsFor')
   3.422 +    def updateRoleMappingsFor(self, ob):
   3.423 +        """Changes the object permissions according to the current state.
   3.424 +        """
   3.425 +        changed = 0
   3.426 +        sdef = self._getWorkflowStateOf(ob)
   3.427 +        if sdef is None:
   3.428 +            return 0
   3.429 +        # Update the role -> permission map.
   3.430 +        if self.permissions:
   3.431 +            for p in self.permissions:
   3.432 +                roles = []
   3.433 +                if sdef.permission_roles is not None:
   3.434 +                    roles = sdef.permission_roles.get(p, roles)
   3.435 +                if modifyRolesForPermission(ob, p, roles):
   3.436 +                    changed = 1
   3.437 +        # Update the group -> role map.
   3.438 +        groups = self.getGroups()
   3.439 +        managed_roles = self.getRoles()
   3.440 +        if groups and managed_roles:
   3.441 +            for group in groups:
   3.442 +                roles = ()
   3.443 +                if sdef.group_roles is not None:
   3.444 +                    roles = sdef.group_roles.get(group, ())
   3.445 +                if modifyRolesForGroup(ob, group, roles, managed_roles):
   3.446 +                    changed = 1
   3.447 +        return changed
   3.448 +
   3.449 +    def _checkTransitionGuard(self, t, ob, **kw):
   3.450 +        guard = t.guard
   3.451 +        if guard is None:
   3.452 +            return 1
   3.453 +        if guard.check(getSecurityManager(), self, ob, **kw):
   3.454 +            return 1
   3.455 +        return 0
   3.456 +
   3.457 +    def _findAutomaticTransition(self, ob, sdef):
   3.458 +        tdef = None
   3.459 +        for tid in sdef.transitions:
   3.460 +            t = self.transitions.get(tid, None)
   3.461 +            if t is not None and t.trigger_type == TRIGGER_AUTOMATIC:
   3.462 +                if self._checkTransitionGuard(t, ob):
   3.463 +                    tdef = t
   3.464 +                    break
   3.465 +        return tdef
   3.466 +        
   3.467 +    def _changeStateOf(self, ob, tdef=None, kwargs=None):
   3.468 +        '''
   3.469 +        Changes state.  Can execute multiple transitions if there are
   3.470 +        automatic transitions.  tdef set to None means the object
   3.471 +        was just created.
   3.472 +        '''
   3.473 +        moved_exc = None
   3.474 +        while 1:
   3.475 +            try:
   3.476 +                sdef = self._executeTransition(ob, tdef, kwargs)
   3.477 +            except ObjectMoved, moved_exc:
   3.478 +                ob = moved_exc.getNewObject()
   3.479 +                sdef = self._getWorkflowStateOf(ob)
   3.480 +                # Re-raise after all transitions.
   3.481 +            if sdef is None:
   3.482 +                break
   3.483 +            tdef = self._findAutomaticTransition(ob, sdef)
   3.484 +            if tdef is None:
   3.485 +                # No more automatic transitions.
   3.486 +                break
   3.487 +            # Else continue.
   3.488 +        if moved_exc is not None:
   3.489 +            # Re-raise.
   3.490 +            raise moved_exc
   3.491 +
   3.492 +    def _executeTransition(self, ob, tdef=None, kwargs=None):
   3.493 +        '''
   3.494 +        Private method.
   3.495 +        Puts object in a new state.
   3.496 +        '''
   3.497 +        sci = None
   3.498 +        econtext = None
   3.499 +        moved_exc = None
   3.500 +
   3.501 +        # Figure out the old and new states.
   3.502 +        old_sdef = self._getWorkflowStateOf(ob)
   3.503 +        old_state = old_sdef.getId()
   3.504 +        if tdef is None:
   3.505 +            new_state = self.initial_state
   3.506 +            former_status = {}
   3.507 +        else:
   3.508 +            new_state = tdef.new_state_id
   3.509 +            if not new_state:
   3.510 +                # Stay in same state.
   3.511 +                new_state = old_state
   3.512 +            former_status = self._getStatusOf(ob)
   3.513 +        new_sdef = self.states.get(new_state, None)
   3.514 +        if new_sdef is None:
   3.515 +            raise WorkflowException, (
   3.516 +                'Destination state undefined: ' + new_state)
   3.517 +
   3.518 +        # Execute the "before" script.
   3.519 +        if tdef is not None and tdef.script_name:
   3.520 +            script = self.scripts[tdef.script_name]
   3.521 +            # Pass lots of info to the script in a single parameter.
   3.522 +            sci = StateChangeInfo(
   3.523 +                ob, self, former_status, tdef, old_sdef, new_sdef, kwargs)
   3.524 +            try:
   3.525 +                script(sci)  # May throw an exception.
   3.526 +            except ObjectMoved, moved_exc:
   3.527 +                ob = moved_exc.getNewObject()
   3.528 +                # Re-raise after transition
   3.529 +
   3.530 +        # Update variables.
   3.531 +        state_values = new_sdef.var_values
   3.532 +        if state_values is None: state_values = {}
   3.533 +        tdef_exprs = None
   3.534 +        if tdef is not None: tdef_exprs = tdef.var_exprs
   3.535 +        if tdef_exprs is None: tdef_exprs = {}
   3.536 +        status = {}
   3.537 +        for id, vdef in self.variables.items():
   3.538 +            if not vdef.for_status:
   3.539 +                continue
   3.540 +            expr = None
   3.541 +            if state_values.has_key(id):
   3.542 +                value = state_values[id]
   3.543 +            elif tdef_exprs.has_key(id):
   3.544 +                expr = tdef_exprs[id]
   3.545 +            elif not vdef.update_always and former_status.has_key(id):
   3.546 +                # Preserve former value
   3.547 +                value = former_status[id]
   3.548 +            else:
   3.549 +                if vdef.default_expr is not None:
   3.550 +                    expr = vdef.default_expr
   3.551 +                else:
   3.552 +                    value = vdef.default_value
   3.553 +            if expr is not None:
   3.554 +                # Evaluate an expression.
   3.555 +                if econtext is None:
   3.556 +                    # Lazily create the expression context.
   3.557 +                    if sci is None:
   3.558 +                        sci = StateChangeInfo(
   3.559 +                            ob, self, former_status, tdef,
   3.560 +                            old_sdef, new_sdef, kwargs)
   3.561 +                    econtext = createExprContext(sci)
   3.562 +                value = expr(econtext)
   3.563 +            status[id] = value
   3.564 +
   3.565 +        # Update state.
   3.566 +        status[self.state_var] = new_state
   3.567 +        tool = aq_parent(aq_inner(self))
   3.568 +        tool.setStatusOf(self.id, ob, status)
   3.569 +
   3.570 +        # Update role to permission assignments.
   3.571 +        self.updateRoleMappingsFor(ob)
   3.572 +
   3.573 +        # Execute the "after" script.
   3.574 +        if tdef is not None and tdef.after_script_name:
   3.575 +            script = self.scripts[tdef.after_script_name]
   3.576 +            # Pass lots of info to the script in a single parameter.
   3.577 +            sci = StateChangeInfo(
   3.578 +                ob, self, status, tdef, old_sdef, new_sdef, kwargs)
   3.579 +            script(sci)  # May throw an exception.
   3.580 +
   3.581 +        # Return the new state object.
   3.582 +        if moved_exc is not None:
   3.583 +            # Propagate the notification that the object has moved.
   3.584 +            raise moved_exc
   3.585 +        else:
   3.586 +            return new_sdef
   3.587 +
   3.588 +InitializeClass(DCWorkflowDefinition)
   3.589 +
   3.590 +
   3.591 +addWorkflowFactory(DCWorkflowDefinition, id='dc_workflow',
   3.592 +                   title='Web-configurable workflow')
     4.1 new file mode 100644
     4.2 --- /dev/null
     4.3 +++ b/DEPENDENCIES.txt
     4.4 @@ -0,0 +1,2 @@
     4.5 +Zope >= 2.7.0
     4.6 +CMFCore
     5.1 new file mode 100644
     5.2 --- /dev/null
     5.3 +++ b/Default.py
     5.4 @@ -0,0 +1,321 @@
     5.5 +##############################################################################
     5.6 +#
     5.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     5.8 +#
     5.9 +# This software is subject to the provisions of the Zope Public License,
    5.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    5.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    5.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    5.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    5.14 +# FOR A PARTICULAR PURPOSE.
    5.15 +#
    5.16 +##############################################################################
    5.17 +""" Programmatically creates a workflow.
    5.18 +
    5.19 +$Id: Default.py,v 1.17 2004/08/12 15:07:44 jens Exp $
    5.20 +"""
    5.21 +
    5.22 +from Products.CMFCore.WorkflowTool import addWorkflowFactory
    5.23 +
    5.24 +from DCWorkflow import DCWorkflowDefinition
    5.25 +from permissions import AccessContentsInformation
    5.26 +from permissions import ModifyPortalContent
    5.27 +from permissions import RequestReview
    5.28 +from permissions import ReviewPortalContent
    5.29 +from permissions import View
    5.30 +
    5.31 +p_access = AccessContentsInformation
    5.32 +p_modify = ModifyPortalContent
    5.33 +p_request = RequestReview
    5.34 +p_review = ReviewPortalContent
    5.35 +p_view = View
    5.36 +
    5.37 +r_anon = 'Anonymous'
    5.38 +r_manager = 'Manager'
    5.39 +r_reviewer = 'Reviewer'
    5.40 +r_owner = 'Owner'
    5.41 +r_member = 'Member'
    5.42 +
    5.43 +
    5.44 +
    5.45 +def setupDefaultWorkflowRev2(wf):
    5.46 +    '''
    5.47 +    Sets up a DCWorkflow with the addition of a visible state,
    5.48 +    the show and hide transitions, and corresponding changes.
    5.49 +    wf is a DCWorkflow instance.
    5.50 +    '''
    5.51 +    wf.setProperties(title='CMF default workflow [Revision 2]')
    5.52 +
    5.53 +    for s in ('private', 'visible', 'pending', 'published'):
    5.54 +        wf.states.addState(s)
    5.55 +    for t in ('publish', 'reject', 'retract', 'submit', 'hide', 'show'):
    5.56 +        wf.transitions.addTransition(t)
    5.57 +    for v in ('action', 'actor', 'comments', 'review_history', 'time'):
    5.58 +        wf.variables.addVariable(v)
    5.59 +    for l in ('reviewer_queue',):
    5.60 +        wf.worklists.addWorklist(l)
    5.61 +    for p in (p_access, p_modify, p_view):
    5.62 +        wf.addManagedPermission(p)
    5.63 +
    5.64 +    wf.states.setInitialState('visible')
    5.65 +
    5.66 +    sdef = wf.states['private']
    5.67 +    sdef.setProperties(
    5.68 +        title='Visible and editable only by owner',
    5.69 +        transitions=('show',))
    5.70 +    sdef.setPermission(p_access, 0, (r_manager, r_owner))
    5.71 +    sdef.setPermission(p_view, 0, (r_manager, r_owner))
    5.72 +    sdef.setPermission(p_modify, 0, (r_manager, r_owner))
    5.73 +
    5.74 +    sdef = wf.states['pending']
    5.75 +    sdef.setProperties(
    5.76 +        title='Waiting for reviewer',
    5.77 +        transitions=('hide', 'publish', 'reject', 'retract',))
    5.78 +    sdef.setPermission(p_access, 1, (r_manager, r_owner, r_reviewer))
    5.79 +    sdef.setPermission(p_view, 1, (r_manager, r_owner, r_reviewer))
    5.80 +    sdef.setPermission(p_modify, 0, (r_manager, r_reviewer))
    5.81 +
    5.82 +    sdef = wf.states['published']
    5.83 +    sdef.setProperties(
    5.84 +        title='Public',
    5.85 +        transitions=('reject', 'retract',))
    5.86 +    sdef.setPermission(p_access, 1, (r_anon, r_manager))
    5.87 +    sdef.setPermission(p_view, 1, (r_anon, r_manager))
    5.88 +    sdef.setPermission(p_modify, 0, (r_manager,))
    5.89 +
    5.90 +    sdef = wf.states['visible']
    5.91 +    sdef.setProperties(
    5.92 +        title='Visible but not published',
    5.93 +        transitions=('hide', 'publish', 'submit',))
    5.94 +    sdef.setPermission(p_access, 1, (r_anon, r_manager, r_reviewer))
    5.95 +    sdef.setPermission(p_view, 1, (r_anon, r_manager, r_reviewer))
    5.96 +    sdef.setPermission(p_modify, 0, (r_manager, r_owner))
    5.97 +
    5.98 +    tdef = wf.transitions['hide']
    5.99 +    tdef.setProperties(
   5.100 +        title='Member makes content private',
   5.101 +        new_state_id='private',
   5.102 +        actbox_name='Make private',
   5.103 +        actbox_url='%(content_url)s/content_hide_form',
   5.104 +        props={'guard_roles':r_owner})
   5.105 +
   5.106 +    tdef = wf.transitions['publish']
   5.107 +    tdef.setProperties(
   5.108 +        title='Reviewer publishes content',
   5.109 +        new_state_id='published',
   5.110 +        actbox_name='Publish',
   5.111 +        actbox_url='%(content_url)s/content_publish_form',
   5.112 +        props={'guard_permissions':p_review})
   5.113 +
   5.114 +    tdef = wf.transitions['reject']
   5.115 +    tdef.setProperties(
   5.116 +        title='Reviewer rejects submission',
   5.117 +        new_state_id='visible',
   5.118 +        actbox_name='Reject',
   5.119 +        actbox_url='%(content_url)s/content_reject_form',
   5.120 +        props={'guard_permissions':p_review})
   5.121 +
   5.122 +    tdef = wf.transitions['retract']
   5.123 +    tdef.setProperties(
   5.124 +        title='Member retracts submission',
   5.125 +        new_state_id='visible',
   5.126 +        actbox_name='Retract',
   5.127 +        actbox_url='%(content_url)s/content_retract_form',
   5.128 +        props={'guard_permissions':p_request})
   5.129 +
   5.130 +    tdef = wf.transitions['show']
   5.131 +    tdef.setProperties(
   5.132 +        title='Member makes content visible',
   5.133 +        new_state_id='visible',
   5.134 +        actbox_name='Make visible',
   5.135 +        actbox_url='%(content_url)s/content_show_form',
   5.136 +        props={'guard_roles':r_owner})
   5.137 +
   5.138 +    tdef = wf.transitions['submit']
   5.139 +    tdef.setProperties(
   5.140 +        title='Member requests publishing',
   5.141 +        new_state_id='pending',
   5.142 +        actbox_name='Submit',
   5.143 +        actbox_url='%(content_url)s/content_submit_form',
   5.144 +        props={'guard_permissions':p_request})
   5.145 +
   5.146 +    wf.variables.setStateVar('review_state')
   5.147 +
   5.148 +    vdef = wf.variables['action']
   5.149 +    vdef.setProperties(description='The last transition',
   5.150 +                       default_expr='transition/getId|nothing',
   5.151 +                       for_status=1, update_always=1)
   5.152 +
   5.153 +    vdef = wf.variables['actor']
   5.154 +    vdef.setProperties(description='The ID of the user who performed '
   5.155 +                       'the last transition',
   5.156 +                       default_expr='user/getId',
   5.157 +                       for_status=1, update_always=1)
   5.158 +
   5.159 +    vdef = wf.variables['comments']
   5.160 +    vdef.setProperties(description='Comments about the last transition',
   5.161 +                       default_expr="python:state_change.kwargs.get('comment', '')",
   5.162 +                       for_status=1, update_always=1)
   5.163 +
   5.164 +    vdef = wf.variables['review_history']
   5.165 +    vdef.setProperties(description='Provides access to workflow history',
   5.166 +                       default_expr="state_change/getHistory",
   5.167 +                       props={'guard_permissions':
   5.168 +                              p_request + ';' + p_review})
   5.169 +
   5.170 +    vdef = wf.variables['time']
   5.171 +    vdef.setProperties(description='Time of the last transition',
   5.172 +                       default_expr="state_change/getDateTime",
   5.173 +                       for_status=1, update_always=1)
   5.174 +
   5.175 +    ldef = wf.worklists['reviewer_queue']
   5.176 +    ldef.setProperties(description='Reviewer tasks',
   5.177 +                       actbox_name='Pending (%(count)d)',
   5.178 +                       actbox_url='%(portal_url)s/search?review_state=pending',
   5.179 +                       props={'var_match_review_state':'pending',
   5.180 +                              'guard_permissions':p_review})
   5.181 +    
   5.182 +
   5.183 +def createDefaultWorkflowRev2(id):
   5.184 +    '''
   5.185 +    '''
   5.186 +    ob = DCWorkflowDefinition(id)
   5.187 +    setupDefaultWorkflowRev2(ob)
   5.188 +    return ob
   5.189 +
   5.190 +addWorkflowFactory(createDefaultWorkflowRev2, id='default_workflow',
   5.191 +                   title='Web-configurable workflow [Revision 2]')
   5.192 +
   5.193 +
   5.194 +
   5.195 +
   5.196 +
   5.197 +
   5.198 +
   5.199 +
   5.200 +
   5.201 +def setupDefaultWorkflowClassic(wf):
   5.202 +    '''
   5.203 +    Sets up a DCWorkflow as close as possible to the old DefaultWorkflow,
   5.204 +    with only the private, pending, and published states.
   5.205 +    wf is a DCWorkflow instance.
   5.206 +    '''
   5.207 +    wf.setProperties(title='CMF default workflow [Classic]')
   5.208 +
   5.209 +    for s in ('private', 'pending', 'published'):
   5.210 +        wf.states.addState(s)
   5.211 +    for t in ('publish', 'reject', 'retract', 'submit'):
   5.212 +        wf.transitions.addTransition(t)
   5.213 +    for v in ('action', 'actor', 'comments', 'review_history', 'time'):
   5.214 +        wf.variables.addVariable(v)
   5.215 +    for l in ('reviewer_queue',):
   5.216 +        wf.worklists.addWorklist(l)
   5.217 +    for p in (p_access, p_modify, p_view):
   5.218 +        wf.addManagedPermission(p)
   5.219 +
   5.220 +    wf.states.setInitialState('private')
   5.221 +
   5.222 +    sdef = wf.states['private']
   5.223 +    sdef.setProperties(
   5.224 +        title='Non-visible and editable only by owner',
   5.225 +        transitions=('submit', 'publish',))
   5.226 +    sdef.setPermission(p_access, 0, (r_manager, r_owner))
   5.227 +    sdef.setPermission(p_view, 0, (r_manager, r_owner))
   5.228 +    sdef.setPermission(p_modify, 0, (r_manager, r_owner))
   5.229 +
   5.230 +    sdef = wf.states['pending']
   5.231 +    sdef.setProperties(
   5.232 +        title='Waiting for reviewer',
   5.233 +        transitions=('publish', 'reject', 'retract',))
   5.234 +    sdef.setPermission(p_access, 0, (r_manager, r_owner, r_reviewer))
   5.235 +    sdef.setPermission(p_view, 0, (r_manager, r_owner, r_reviewer))
   5.236 +    sdef.setPermission(p_modify, 0, (r_manager, r_reviewer))
   5.237 +
   5.238 +    sdef = wf.states['published']
   5.239 +    sdef.setProperties(
   5.240 +        title='Public',
   5.241 +        transitions=('reject', 'retract',))
   5.242 +    sdef.setPermission(p_access, 1, (r_anon, r_manager))
   5.243 +    sdef.setPermission(p_view, 1, (r_anon, r_manager))
   5.244 +    sdef.setPermission(p_modify, 0, (r_manager,))
   5.245 +
   5.246 +    tdef = wf.transitions['publish']
   5.247 +    tdef.setProperties(
   5.248 +        title='Reviewer publishes content',
   5.249 +        new_state_id='published',
   5.250 +        actbox_name='Publish',
   5.251 +        actbox_url='%(content_url)s/content_publish_form',
   5.252 +        props={'guard_permissions':p_review})
   5.253 +
   5.254 +    tdef = wf.transitions['reject']
   5.255 +    tdef.setProperties(
   5.256 +        title='Reviewer rejects submission',
   5.257 +        new_state_id='private',
   5.258 +        actbox_name='Reject',
   5.259 +        actbox_url='%(content_url)s/content_reject_form',
   5.260 +        props={'guard_permissions':p_review})
   5.261 +
   5.262 +    tdef = wf.transitions['retract']
   5.263 +    tdef.setProperties(
   5.264 +        title='Member retracts submission',
   5.265 +        new_state_id='private',
   5.266 +        actbox_name='Retract',
   5.267 +        actbox_url='%(content_url)s/content_retract_form',
   5.268 +        props={'guard_permissions':p_request})
   5.269 +
   5.270 +    tdef = wf.transitions['submit']
   5.271 +    tdef.setProperties(
   5.272 +        title='Member requests publishing',
   5.273 +        new_state_id='pending',
   5.274 +        actbox_name='Submit',
   5.275 +        actbox_url='%(content_url)s/content_submit_form',
   5.276 +        props={'guard_permissions':p_request})
   5.277 +
   5.278 +    wf.variables.setStateVar('review_state')
   5.279 +
   5.280 +    vdef = wf.variables['action']
   5.281 +    vdef.setProperties(description='The last transition',
   5.282 +                       default_expr='transition/getId|nothing',
   5.283 +                       for_status=1, update_always=1)
   5.284 +
   5.285 +    vdef = wf.variables['actor']
   5.286 +    vdef.setProperties(description='The ID of the user who performed '
   5.287 +                       'the last transition',
   5.288 +                       default_expr='user/getId',
   5.289 +                       for_status=1, update_always=1)
   5.290 +
   5.291 +    vdef = wf.variables['comments']
   5.292 +    vdef.setProperties(description='Comments about the last transition',
   5.293 +                       default_expr="python:state_change.kwargs.get('comment', '')",
   5.294 +                       for_status=1, update_always=1)
   5.295 +
   5.296 +    vdef = wf.variables['review_history']
   5.297 +    vdef.setProperties(description='Provides access to workflow history',
   5.298 +                       default_expr="state_change/getHistory",
   5.299 +                       props={'guard_permissions':
   5.300 +                              p_request + ';' + p_review})
   5.301 +
   5.302 +    vdef = wf.variables['time']
   5.303 +    vdef.setProperties(description='Time of the last transition',
   5.304 +                       default_expr="state_change/getDateTime",
   5.305 +                       for_status=1, update_always=1)
   5.306 +
   5.307 +    ldef = wf.worklists['reviewer_queue']
   5.308 +    ldef.setProperties(description='Reviewer tasks',
   5.309 +                       actbox_name='Pending (%(count)d)',
   5.310 +                       actbox_url='%(portal_url)s/search?review_state=pending',
   5.311 +                       props={'var_match_review_state':'pending',
   5.312 +                              'guard_permissions':p_review})
   5.313 +    
   5.314 +
   5.315 +
   5.316 +def createDefaultWorkflowClassic(id):
   5.317 +    '''
   5.318 +    '''
   5.319 +    ob = DCWorkflowDefinition(id)
   5.320 +    setupDefaultWorkflowClassic(ob)
   5.321 +    return ob
   5.322 +
   5.323 +addWorkflowFactory(createDefaultWorkflowClassic, id='default_workflow',
   5.324 +                   title='Web-configurable workflow [Classic]')
   5.325 +
     6.1 new file mode 100644
     6.2 --- /dev/null
     6.3 +++ b/Expression.py
     6.4 @@ -0,0 +1,120 @@
     6.5 +##############################################################################
     6.6 +#
     6.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     6.8 +# 
     6.9 +# This software is subject to the provisions of the Zope Public License,
    6.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    6.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    6.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    6.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    6.14 +# FOR A PARTICULAR PURPOSE.
    6.15 +# 
    6.16 +##############################################################################
    6.17 +""" Expressions in a web-configurable workflow.
    6.18 +
    6.19 +$Id: Expression.py,v 1.13.2.1 2005/04/15 09:01:32 jens Exp $
    6.20 +"""
    6.21 +
    6.22 +import Globals
    6.23 +from Globals import Persistent
    6.24 +from Acquisition import aq_inner, aq_parent
    6.25 +from AccessControl import getSecurityManager, ClassSecurityInfo
    6.26 +from DateTime import DateTime
    6.27 +
    6.28 +from Products.CMFCore.WorkflowCore import ObjectDeleted, ObjectMoved
    6.29 +from Products.CMFCore.Expression import Expression
    6.30 +from Products.PageTemplates.Expressions import getEngine
    6.31 +from Products.PageTemplates.TALES import SafeMapping
    6.32 +from Products.PageTemplates.Expressions import SecureModuleImporter
    6.33 +
    6.34 +class StateChangeInfo:
    6.35 +    '''
    6.36 +    Provides information for expressions and scripts.
    6.37 +    '''
    6.38 +    _date = None
    6.39 +
    6.40 +    ObjectDeleted = ObjectDeleted
    6.41 +    ObjectMoved = ObjectMoved
    6.42 +
    6.43 +    security = ClassSecurityInfo()
    6.44 +    security.setDefaultAccess('allow')
    6.45 +
    6.46 +    def __init__(self, object, workflow, status=None, transition=None,
    6.47 +                 old_state=None, new_state=None, kwargs=None):
    6.48 +        if kwargs is None:
    6.49 +            kwargs = {}
    6.50 +        else:
    6.51 +            # Don't allow mutation
    6.52 +            kwargs = SafeMapping(kwargs)
    6.53 +        if status is None:
    6.54 +            tool = aq_parent(aq_inner(workflow))
    6.55 +            status = tool.getStatusOf(workflow.id, object)
    6.56 +            if status is None:
    6.57 +                status = {}
    6.58 +        if status:
    6.59 +            # Don't allow mutation
    6.60 +            status = SafeMapping(status)
    6.61 +        self.object = object
    6.62 +        self.workflow = workflow
    6.63 +        self.old_state = old_state
    6.64 +        self.new_state = new_state
    6.65 +        self.transition = transition
    6.66 +        self.status = status
    6.67 +        self.kwargs = kwargs
    6.68 +
    6.69 +    def __getitem__(self, name):
    6.70 +        if name[:1] != '_' and hasattr(self, name):
    6.71 +            return getattr(self, name)
    6.72 +        raise KeyError, name
    6.73 +
    6.74 +    def getHistory(self):
    6.75 +        wf = self.workflow
    6.76 +        tool = aq_parent(aq_inner(wf))
    6.77 +        wf_id = wf.id
    6.78 +        h = tool.getHistoryOf(wf_id, self.object)
    6.79 +        if h:
    6.80 +            return map(lambda dict: dict.copy(), h)  # Don't allow mutation
    6.81 +        else:
    6.82 +            return ()
    6.83 +
    6.84 +    def getPortal(self):
    6.85 +        ob = self.object
    6.86 +        while ob is not None and not getattr(ob, '_isPortalRoot', 0):
    6.87 +            ob = aq_parent(aq_inner(ob))
    6.88 +        return ob
    6.89 +
    6.90 +    def getDateTime(self):
    6.91 +        date = self._date
    6.92 +        if not date:
    6.93 +            date = self._date = DateTime()
    6.94 +        return date
    6.95 +
    6.96 +Globals.InitializeClass(StateChangeInfo)
    6.97 +
    6.98 +
    6.99 +def createExprContext(sci):
   6.100 +    '''
   6.101 +    An expression context provides names for TALES expressions.
   6.102 +    '''
   6.103 +    ob = sci.object
   6.104 +    wf = sci.workflow
   6.105 +    container = aq_parent(aq_inner(ob))
   6.106 +    data = {
   6.107 +        'here':         ob,
   6.108 +        'object':       ob,
   6.109 +        'container':    container,
   6.110 +        'folder':       container,
   6.111 +        'nothing':      None,
   6.112 +        'root':         wf.getPhysicalRoot(),
   6.113 +        'request':      getattr( ob, 'REQUEST', None ),
   6.114 +        'modules':      SecureModuleImporter,
   6.115 +        'user':         getSecurityManager().getUser(),
   6.116 +        'state_change': sci,
   6.117 +        'transition':   sci.transition,
   6.118 +        'status':       sci.status,
   6.119 +        'kwargs':       sci.kwargs,
   6.120 +        'workflow':     wf,
   6.121 +        'scripts':      wf.scripts,
   6.122 +        }
   6.123 +    return getEngine().getContext(data)
   6.124 +
     7.1 new file mode 100644
     7.2 --- /dev/null
     7.3 +++ b/Guard.py
     7.4 @@ -0,0 +1,180 @@
     7.5 +##############################################################################
     7.6 +#
     7.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     7.8 +#
     7.9 +# This software is subject to the provisions of the Zope Public License,
    7.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    7.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    7.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    7.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    7.14 +# FOR A PARTICULAR PURPOSE.
    7.15 +#
    7.16 +##############################################################################
    7.17 +""" Guard conditions in a web-configurable workflow.
    7.18 +
    7.19 +$Id: Guard.py,v 1.16.2.1 2005/04/26 15:59:42 anguenot Exp $
    7.20 +"""
    7.21 +
    7.22 +from cgi import escape
    7.23 +
    7.24 +from Globals import DTMLFile
    7.25 +from Globals import InitializeClass
    7.26 +from Globals import Persistent
    7.27 +from AccessControl import ClassSecurityInfo
    7.28 +from Acquisition import Explicit
    7.29 +from Acquisition import aq_base
    7.30 +
    7.31 +from Products.CMFCore.utils import _checkPermission
    7.32 +
    7.33 +from Expression import Expression
    7.34 +from Expression import StateChangeInfo
    7.35 +from Expression import createExprContext
    7.36 +from permissions import ManagePortal
    7.37 +from utils import _dtmldir
    7.38 +
    7.39 +
    7.40 +class Guard (Persistent, Explicit):
    7.41 +    permissions = ()
    7.42 +    roles = ()
    7.43 +    groups = ()
    7.44 +    expr = None
    7.45 +
    7.46 +    security = ClassSecurityInfo()
    7.47 +    security.declareObjectProtected(ManagePortal)
    7.48 +
    7.49 +    guardForm = DTMLFile('guard', _dtmldir)
    7.50 +
    7.51 +    def check(self, sm, wf_def, ob, **kw):
    7.52 +        """Checks conditions in this guard.
    7.53 +        """
    7.54 +        u_roles = None
    7.55 +        if wf_def.manager_bypass:
    7.56 +            # Possibly bypass.
    7.57 +            u_roles = sm.getUser().getRolesInContext(ob)
    7.58 +            if 'Manager' in u_roles:
    7.59 +                return 1
    7.60 +        if self.permissions:
    7.61 +            for p in self.permissions:
    7.62 +                if _checkPermission(p, ob):
    7.63 +                    break
    7.64 +            else:
    7.65 +                return 0
    7.66 +        if self.roles:
    7.67 +            # Require at least one of the given roles.
    7.68 +            if u_roles is None:
    7.69 +                u_roles = sm.getUser().getRolesInContext(ob)
    7.70 +            for role in self.roles:
    7.71 +                if role in u_roles:
    7.72 +                    break
    7.73 +            else:
    7.74 +                return 0
    7.75 +        if self.groups:
    7.76 +            # Require at least one of the specified groups.
    7.77 +            u = sm.getUser()
    7.78 +            b = aq_base( u )
    7.79 +            if hasattr( b, 'getGroupsInContext' ):
    7.80 +                u_groups = u.getGroupsInContext( ob )
    7.81 +            elif hasattr( b, 'getGroups' ):
    7.82 +                u_groups = u.getGroups()
    7.83 +            else:
    7.84 +                u_groups = ()
    7.85 +            for group in self.groups:
    7.86 +                if group in u_groups:
    7.87 +                    break
    7.88 +            else:
    7.89 +                return 0
    7.90 +        expr = self.expr
    7.91 +        if expr is not None:
    7.92 +            econtext = createExprContext(
    7.93 +                StateChangeInfo(ob, wf_def, kwargs=kw))
    7.94 +            res = expr(econtext)
    7.95 +            if not res:
    7.96 +                return 0
    7.97 +        return 1
    7.98 +
    7.99 +    security.declareProtected(ManagePortal, 'getSummary')
   7.100 +    def getSummary(self):
   7.101 +        # Perhaps ought to be in DTML.
   7.102 +        res = []
   7.103 +        if self.permissions:
   7.104 +            res.append('Requires permission:')
   7.105 +            res.append(formatNameUnion(self.permissions))
   7.106 +        if self.roles:
   7.107 +            if res:
   7.108 +                res.append('<br/>')
   7.109 +            res.append('Requires role:')
   7.110 +            res.append(formatNameUnion(self.roles))
   7.111 +        if self.groups:
   7.112 +            if res:
   7.113 +                res.append('<br/>')
   7.114 +            res.append('Requires group:')
   7.115 +            res.append(formatNameUnion(self.groups))
   7.116 +        if self.expr is not None:
   7.117 +            if res:
   7.118 +                res.append('<br/>')
   7.119 +            res.append('Requires expr:')
   7.120 +            res.append('<code>' + escape(self.expr.text) + '</code>')
   7.121 +        return ' '.join(res)
   7.122 +
   7.123 +    def changeFromProperties(self, props):
   7.124 +        '''
   7.125 +        Returns 1 if changes were specified.
   7.126 +        '''
   7.127 +        if props is None:
   7.128 +            return 0
   7.129 +        res = 0
   7.130 +        s = props.get('guard_permissions', None)
   7.131 +        if s:
   7.132 +            res = 1
   7.133 +            p = [ permission.strip() for permission in s.split(';') ]
   7.134 +            self.permissions = tuple(p)
   7.135 +        s = props.get('guard_roles', None)
   7.136 +        if s:
   7.137 +            res = 1
   7.138 +            r = [ role.strip() for role in s.split(';') ]
   7.139 +            self.roles = tuple(r)
   7.140 +        s = props.get('guard_groups', None)
   7.141 +        if s:
   7.142 +            res = 1
   7.143 +            g = [ group.strip() for group in s.split(';') ]
   7.144 +            self.groups = tuple(g)
   7.145 +        s = props.get('guard_expr', None)
   7.146 +        if s:
   7.147 +            res = 1
   7.148 +            self.expr = Expression(s)
   7.149 +        return res
   7.150 +
   7.151 +    security.declareProtected(ManagePortal, 'getPermissionsText')
   7.152 +    def getPermissionsText(self):
   7.153 +        if not self.permissions:
   7.154 +            return ''
   7.155 +        return '; '.join(self.permissions)
   7.156 +
   7.157 +    security.declareProtected(ManagePortal, 'getRolesText')
   7.158 +    def getRolesText(self):
   7.159 +        if not self.roles:
   7.160 +            return ''
   7.161 +        return '; '.join(self.roles)
   7.162 +
   7.163 +    security.declareProtected(ManagePortal, 'getGroupsText')
   7.164 +    def getGroupsText(self):
   7.165 +        if not self.groups:
   7.166 +            return ''
   7.167 +        return '; '.join(self.groups)
   7.168 +
   7.169 +    security.declareProtected(ManagePortal, 'getExprText')
   7.170 +    def getExprText(self):
   7.171 +        if not self.expr:
   7.172 +            return ''
   7.173 +        return str(self.expr.text)
   7.174 +
   7.175 +InitializeClass(Guard)
   7.176 +
   7.177 +
   7.178 +def formatNameUnion(names):
   7.179 +    escaped = ['<code>' + escape(name) + '</code>' for name in names]
   7.180 +    if len(escaped) == 2:
   7.181 +        return ' or '.join(escaped)
   7.182 +    elif len(escaped) > 2:
   7.183 +        escaped[-1] = ' or ' + escaped[-1]
   7.184 +    return '; '.join(escaped)
     8.1 new file mode 100644
     8.2 --- /dev/null
     8.3 +++ b/README.txt
     8.4 @@ -0,0 +1,10 @@
     8.5 +
     8.6 +There is some documentation in the 'doc' directory.
     8.7 +
     8.8 +I encourage you to experiment with what's available and invite you to
     8.9 +ask questions about the tool on zope-cmf@zope.org.  Future development
    8.10 +depends entirely on people's needs, so speak up!
    8.11 +
    8.12 +Shane Hathaway
    8.13 +Zope Corporation
    8.14 +shane@zope.com
     9.1 new file mode 100644
     9.2 --- /dev/null
     9.3 +++ b/Scripts.py
     9.4 @@ -0,0 +1,41 @@
     9.5 +##############################################################################
     9.6 +#
     9.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     9.8 +#
     9.9 +# This software is subject to the provisions of the Zope Public License,
    9.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    9.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    9.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    9.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    9.14 +# FOR A PARTICULAR PURPOSE.
    9.15 +#
    9.16 +##############################################################################
    9.17 +""" Scripts in a web-configurable workflow.
    9.18 +
    9.19 +$Id: Scripts.py,v 1.6.2.2 2005/06/14 18:09:32 sidnei Exp $
    9.20 +"""
    9.21 +
    9.22 +from OFS.Folder import Folder
    9.23 +from Globals import InitializeClass
    9.24 +from AccessControl import ClassSecurityInfo
    9.25 +
    9.26 +from ContainerTab import ContainerTab
    9.27 +from permissions import ManagePortal
    9.28 +
    9.29 +
    9.30 +class Scripts (ContainerTab):
    9.31 +    """A container for workflow scripts"""
    9.32 +
    9.33 +    meta_type = 'Workflow Scripts'
    9.34 +
    9.35 +    security = ClassSecurityInfo()
    9.36 +    security.declareObjectProtected(ManagePortal)
    9.37 +
    9.38 +    def manage_main(self, client=None, REQUEST=None, **kw):
    9.39 +        '''
    9.40 +        '''
    9.41 +        kw['management_view'] = 'Scripts'
    9.42 +        m = Folder.manage_main.__of__(self)
    9.43 +        return m(self, client, REQUEST, **kw)
    9.44 +
    9.45 +InitializeClass(Scripts)
    10.1 new file mode 100644
    10.2 --- /dev/null
    10.3 +++ b/States.py
    10.4 @@ -0,0 +1,304 @@
    10.5 +##############################################################################
    10.6 +#
    10.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    10.8 +#
    10.9 +# This software is subject to the provisions of the Zope Public License,
   10.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   10.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   10.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   10.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   10.14 +# FOR A PARTICULAR PURPOSE.
   10.15 +#
   10.16 +##############################################################################
   10.17 +""" States in a web-configurable workflow.
   10.18 +
   10.19 +$Id: States.py,v 1.16.2.1 2005/04/07 16:37:47 yuppie Exp $
   10.20 +"""
   10.21 +
   10.22 +from AccessControl import ClassSecurityInfo
   10.23 +from Acquisition import aq_inner
   10.24 +from Acquisition import aq_parent
   10.25 +from Globals import DTMLFile
   10.26 +from Globals import InitializeClass
   10.27 +from Globals import PersistentMapping
   10.28 +from OFS.SimpleItem import SimpleItem
   10.29 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   10.30 +
   10.31 +from ContainerTab import ContainerTab
   10.32 +from permissions import ManagePortal
   10.33 +from utils import _dtmldir
   10.34 +
   10.35 +
   10.36 +class StateDefinition(SimpleItem):
   10.37 +    """State definition"""
   10.38 +
   10.39 +    meta_type = 'Workflow State'
   10.40 +
   10.41 +    manage_options = (
   10.42 +        {'label': 'Properties', 'action': 'manage_properties'},
   10.43 +        {'label': 'Permissions', 'action': 'manage_permissions'},
   10.44 +        {'label': 'Groups', 'action': 'manage_groups'},
   10.45 +        {'label': 'Variables', 'action': 'manage_variables'},
   10.46 +        )
   10.47 +
   10.48 +    title = ''
   10.49 +    description = ''
   10.50 +    transitions = ()  # The ids of possible transitions.
   10.51 +    permission_roles = None  # { permission: [role] or (role,) }
   10.52 +    group_roles = None  # { group name : (role,) }
   10.53 +    var_values = None  # PersistentMapping if set.  Overrides transition exprs.
   10.54 +
   10.55 +    security = ClassSecurityInfo()
   10.56 +    security.declareObjectProtected(ManagePortal)
   10.57 +
   10.58 +    def __init__(self, id):
   10.59 +        self.id = id
   10.60 +
   10.61 +    def getId(self):
   10.62 +        return self.id
   10.63 +
   10.64 +    def getWorkflow(self):
   10.65 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   10.66 +
   10.67 +    def getTransitions(self):
   10.68 +        return filter(self.getWorkflow().transitions.has_key,
   10.69 +                      self.transitions)
   10.70 +
   10.71 +    def getTransitionTitle(self, tid):
   10.72 +        t = self.getWorkflow().transitions.get(tid, None)
   10.73 +        if t is not None:
   10.74 +            return t.title
   10.75 +        return ''
   10.76 +
   10.77 +    def getAvailableTransitionIds(self):
   10.78 +        return self.getWorkflow().transitions.keys()
   10.79 +
   10.80 +    def getAvailableVarIds(self):
   10.81 +        return self.getWorkflow().variables.keys()
   10.82 +
   10.83 +    def getManagedPermissions(self):
   10.84 +        return list(self.getWorkflow().permissions)
   10.85 +
   10.86 +    def getAvailableRoles(self):
   10.87 +        return self.getWorkflow().getAvailableRoles()
   10.88 +
   10.89 +    def getPermissionInfo(self, p):
   10.90 +        """Returns the list of roles to be assigned to a permission.
   10.91 +        """
   10.92 +        roles = None
   10.93 +        if self.permission_roles:
   10.94 +            roles = self.permission_roles.get(p, None)
   10.95 +        if roles is None:
   10.96 +            return {'acquired':1, 'roles':[]}
   10.97 +        else:
   10.98 +            if isinstance(roles, tuple):
   10.99 +                acq = 0
  10.100 +            else:
  10.101 +                acq = 1
  10.102 +            return {'acquired':acq, 'roles':list(roles)}
  10.103 +
  10.104 +    def getGroupInfo(self, group):
  10.105 +        """Returns the list of roles to be assigned to a group.
  10.106 +        """
  10.107 +        if self.group_roles:
  10.108 +            return self.group_roles.get(group, ())
  10.109 +        return ()
  10.110 +
  10.111 +    _properties_form = DTMLFile('state_properties', _dtmldir)
  10.112 +
  10.113 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
  10.114 +        """Show state properties ZMI form."""
  10.115 +        return self._properties_form(REQUEST,
  10.116 +                                     management_view='Properties',
  10.117 +                                     manage_tabs_message=manage_tabs_message,
  10.118 +                                     )
  10.119 +
  10.120 +    def setProperties(self, title='', transitions=(), REQUEST=None, description=''):
  10.121 +        """Set the properties for this State."""
  10.122 +        self.title = str(title)
  10.123 +        self.description = str(description)
  10.124 +        self.transitions = tuple(map(str, transitions))
  10.125 +        if REQUEST is not None:
  10.126 +            return self.manage_properties(REQUEST, 'Properties changed.')
  10.127 +
  10.128 +
  10.129 +    _variables_form = DTMLFile('state_variables', _dtmldir)
  10.130 +
  10.131 +    def manage_variables(self, REQUEST, manage_tabs_message=None):
  10.132 +        """Show State variables ZMI form."""
  10.133 +        return self._variables_form(REQUEST,
  10.134 +                                     management_view='Variables',
  10.135 +                                     manage_tabs_message=manage_tabs_message,
  10.136 +                                     )
  10.137 +
  10.138 +    def getVariableValues(self):
  10.139 +        """Get VariableValues for management UI."""
  10.140 +        vv = self.var_values
  10.141 +        if vv is None:
  10.142 +            return []
  10.143 +        else:
  10.144 +            return vv.items()
  10.145 +
  10.146 +    def getWorkflowVariables(self):
  10.147 +        """Get all variables that are available from the workflow and
  10.148 +        not handled yet.
  10.149 +        """
  10.150 +        wf_vars = self.getAvailableVarIds()
  10.151 +        if self.var_values is None:
  10.152 +            return wf_vars
  10.153 +        ret = []
  10.154 +        for vid in wf_vars:
  10.155 +            if not self.var_values.has_key(vid):
  10.156 +                ret.append(vid)
  10.157 +        return ret
  10.158 +
  10.159 +    def addVariable(self,id,value,REQUEST=None):
  10.160 +        """Add a WorkflowVariable to State."""
  10.161 +        if self.var_values is None:
  10.162 +            self.var_values = PersistentMapping()
  10.163 +
  10.164 +        self.var_values[id] = value
  10.165 +
  10.166 +        if REQUEST is not None:
  10.167 +            return self.manage_variables(REQUEST, 'Variable added.')
  10.168 +
  10.169 +    def deleteVariables(self,ids=[],REQUEST=None):
  10.170 +        """Delete a WorkflowVariable from State."""
  10.171 +        vv = self.var_values
  10.172 +        for id in ids:
  10.173 +            if vv.has_key(id):
  10.174 +                del vv[id]
  10.175 +
  10.176 +        if REQUEST is not None:
  10.177 +            return self.manage_variables(REQUEST, 'Variables deleted.')
  10.178 +
  10.179 +    def setVariables(self, ids=[], REQUEST=None):
  10.180 +        """Set values for Variables set by this State."""
  10.181 +        if self.var_values is None:
  10.182 +            self.var_values = PersistentMapping()
  10.183 +
  10.184 +        vv = self.var_values
  10.185 +
  10.186 +        if REQUEST is not None:
  10.187 +            for id in vv.keys():
  10.188 +                fname = 'varval_%s' % id
  10.189 +                vv[id] = str(REQUEST[fname])
  10.190 +            return self.manage_variables(REQUEST, 'Variables changed.')
  10.191 +
  10.192 +
  10.193 +
  10.194 +    _permissions_form = DTMLFile('state_permissions', _dtmldir)
  10.195 +
  10.196 +    def manage_permissions(self, REQUEST, manage_tabs_message=None):
  10.197 +        """Present TTW UI for managing this State's permissions."""
  10.198 +        return self._permissions_form(REQUEST,
  10.199 +                                     management_view='Permissions',
  10.200 +                                     manage_tabs_message=manage_tabs_message,
  10.201 +                                     )
  10.202 +
  10.203 +    def setPermissions(self, REQUEST):
  10.204 +        """Set the permissions in REQUEST for this State."""
  10.205 +        pr = self.permission_roles
  10.206 +        if pr is None:
  10.207 +            self.permission_roles = pr = PersistentMapping()
  10.208 +        pr.clear()
  10.209 +        for p in self.getManagedPermissions():
  10.210 +            roles = []
  10.211 +            acquired = REQUEST.get('acquire_' + p, 0)
  10.212 +            for r in self.getAvailableRoles():
  10.213 +                if REQUEST.get('%s|%s' % (p, r), 0):
  10.214 +                    roles.append(r)
  10.215 +            roles.sort()
  10.216 +            if not acquired:
  10.217 +                roles = tuple(roles)
  10.218 +            pr[p] = roles
  10.219 +        return self.manage_permissions(REQUEST, 'Permissions changed.')
  10.220 +
  10.221 +    def setPermission(self, permission, acquired, roles):
  10.222 +        """Set a permission for this State."""
  10.223 +        pr = self.permission_roles
  10.224 +        if pr is None:
  10.225 +            self.permission_roles = pr = PersistentMapping()
  10.226 +        if acquired:
  10.227 +            roles = list(roles)
  10.228 +        else:
  10.229 +            roles = tuple(roles)
  10.230 +        pr[permission] = roles
  10.231 +
  10.232 +    manage_groups = PageTemplateFile('state_groups.pt', _dtmldir)
  10.233 +
  10.234 +    def setGroups(self, REQUEST, RESPONSE=None):
  10.235 +        """Set the group to role mappings in REQUEST for this State.
  10.236 +        """
  10.237 +        map = self.group_roles
  10.238 +        if map is None:
  10.239 +            self.group_roles = map = PersistentMapping()
  10.240 +        map.clear()
  10.241 +        all_roles = self.getWorkflow().getRoles()
  10.242 +        for group in self.getWorkflow().getGroups():
  10.243 +            roles = []
  10.244 +            for role in all_roles:
  10.245 +                if REQUEST.get('%s|%s' % (group, role), 0):
  10.246 +                    roles.append(role)
  10.247 +            roles.sort()
  10.248 +            roles = tuple(roles)
  10.249 +            map[group] = roles
  10.250 +        if RESPONSE is not None:
  10.251 +            RESPONSE.redirect(
  10.252 +                "%s/manage_groups?manage_tabs_message=Groups+changed."
  10.253 +                % self.absolute_url())
  10.254 +
  10.255 +InitializeClass(StateDefinition)
  10.256 +
  10.257 +
  10.258 +class States(ContainerTab):
  10.259 +    """A container for state definitions"""
  10.260 +
  10.261 +    meta_type = 'Workflow States'
  10.262 +
  10.263 +    security = ClassSecurityInfo()
  10.264 +    security.declareObjectProtected(ManagePortal)
  10.265 +
  10.266 +    all_meta_types = ({'name':StateDefinition.meta_type,
  10.267 +                       'action':'addState',
  10.268 +                       },)
  10.269 +
  10.270 +    _manage_states = DTMLFile('states', _dtmldir)
  10.271 +
  10.272 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  10.273 +        '''
  10.274 +        '''
  10.275 +        return self._manage_states(REQUEST,
  10.276 +                                   management_view='States',
  10.277 +                                   manage_tabs_message=manage_tabs_message,
  10.278 +                                   )
  10.279 +
  10.280 +    def addState(self, id, REQUEST=None):
  10.281 +        '''
  10.282 +        '''
  10.283 +        sdef = StateDefinition(id)
  10.284 +        self._setObject(id, sdef)
  10.285 +        if REQUEST is not None:
  10.286 +            return self.manage_main(REQUEST, 'State added.')
  10.287 +
  10.288 +    def deleteStates(self, ids, REQUEST=None):
  10.289 +        '''
  10.290 +        '''
  10.291 +        for id in ids:
  10.292 +            self._delObject(id)
  10.293 +        if REQUEST is not None:
  10.294 +            return self.manage_main(REQUEST, 'State(s) removed.')
  10.295 +
  10.296 +    def setInitialState(self, id=None, ids=None, REQUEST=None):
  10.297 +        '''
  10.298 +        '''
  10.299 +        if not id:
  10.300 +            if len(ids) != 1:
  10.301 +                raise ValueError, 'One and only one state must be selected'
  10.302 +            id = ids[0]
  10.303 +        id = str(id)
  10.304 +        aq_parent(aq_inner(self)).initial_state = id
  10.305 +        if REQUEST is not None:
  10.306 +            return self.manage_main(REQUEST, 'Initial state selected.')
  10.307 +
  10.308 +InitializeClass(States)
    11.1 new file mode 100644
    11.2 --- /dev/null
    11.3 +++ b/Transitions.py
    11.4 @@ -0,0 +1,260 @@
    11.5 +##############################################################################
    11.6 +#
    11.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    11.8 +#
    11.9 +# This software is subject to the provisions of the Zope Public License,
   11.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   11.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   11.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   11.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   11.14 +# FOR A PARTICULAR PURPOSE.
   11.15 +#
   11.16 +##############################################################################
   11.17 +""" Transitions in a web-configurable workflow.
   11.18 +
   11.19 +$Id: Transitions.py,v 1.13 2004/08/12 15:07:44 jens Exp $
   11.20 +"""
   11.21 +
   11.22 +from OFS.SimpleItem import SimpleItem
   11.23 +from Globals import DTMLFile
   11.24 +from Globals import PersistentMapping
   11.25 +from Globals import InitializeClass
   11.26 +from Acquisition import aq_inner
   11.27 +from Acquisition import aq_parent
   11.28 +from AccessControl import ClassSecurityInfo
   11.29 +
   11.30 +from ContainerTab import ContainerTab
   11.31 +from Guard import Guard
   11.32 +from permissions import ManagePortal
   11.33 +from utils import _dtmldir
   11.34 +from Expression import Expression
   11.35 +
   11.36 +TRIGGER_AUTOMATIC = 0
   11.37 +TRIGGER_USER_ACTION = 1
   11.38 +TRIGGER_WORKFLOW_METHOD = 2
   11.39 +
   11.40 +
   11.41 +class TransitionDefinition (SimpleItem):
   11.42 +    """Transition definition"""
   11.43 +
   11.44 +    meta_type = 'Workflow Transition'
   11.45 +
   11.46 +    security = ClassSecurityInfo()
   11.47 +    security.declareObjectProtected(ManagePortal)
   11.48 +
   11.49 +    title = ''
   11.50 +    description = ''
   11.51 +    new_state_id = ''
   11.52 +    trigger_type = TRIGGER_USER_ACTION
   11.53 +    guard = None
   11.54 +    actbox_name = ''
   11.55 +    actbox_url = ''
   11.56 +    actbox_category = 'workflow'
   11.57 +    var_exprs = None  # A mapping.
   11.58 +    script_name = None  # Executed before transition
   11.59 +    after_script_name = None  # Executed after transition
   11.60 +
   11.61 +    manage_options = (
   11.62 +        {'label': 'Properties', 'action': 'manage_properties'},
   11.63 +        {'label': 'Variables', 'action': 'manage_variables'},
   11.64 +        )
   11.65 +
   11.66 +    def __init__(self, id):
   11.67 +        self.id = id
   11.68 +
   11.69 +    def getId(self):
   11.70 +        return self.id
   11.71 +
   11.72 +    def getGuardSummary(self):
   11.73 +        res = None
   11.74 +        if self.guard is not None:
   11.75 +            res = self.guard.getSummary()
   11.76 +        return res
   11.77 +
   11.78 +    def getGuard(self):
   11.79 +        if self.guard is not None:
   11.80 +            return self.guard
   11.81 +        else:
   11.82 +            return Guard().__of__(self)  # Create a temporary guard.
   11.83 +
   11.84 +    def getVarExprText(self, id):
   11.85 +        if not self.var_exprs:
   11.86 +            return ''
   11.87 +        else:
   11.88 +            expr = self.var_exprs.get(id, None)
   11.89 +            if expr is not None:
   11.90 +                return expr.text
   11.91 +            else:
   11.92 +                return ''
   11.93 +
   11.94 +    def getWorkflow(self):
   11.95 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   11.96 +
   11.97 +    def getAvailableStateIds(self):
   11.98 +        return self.getWorkflow().states.keys()
   11.99 +
  11.100 +    def getAvailableScriptIds(self):
  11.101 +        return self.getWorkflow().scripts.keys()
  11.102 +
  11.103 +    def getAvailableVarIds(self):
  11.104 +        return self.getWorkflow().variables.keys()
  11.105 +
  11.106 +    _properties_form = DTMLFile('transition_properties', _dtmldir)
  11.107 +
  11.108 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
  11.109 +        '''
  11.110 +        '''
  11.111 +        return self._properties_form(REQUEST,
  11.112 +                                     management_view='Properties',
  11.113 +                                     manage_tabs_message=manage_tabs_message,
  11.114 +                                     )
  11.115 +
  11.116 +    def setProperties(self, title, new_state_id,
  11.117 +                      trigger_type=TRIGGER_USER_ACTION, script_name='',
  11.118 +                      after_script_name='',
  11.119 +                      actbox_name='', actbox_url='',
  11.120 +                      actbox_category='workflow',
  11.121 +                      props=None, REQUEST=None, description=''):
  11.122 +        '''
  11.123 +        '''
  11.124 +        self.title = str(title)
  11.125 +        self.description = str(description)
  11.126 +        self.new_state_id = str(new_state_id)
  11.127 +        self.trigger_type = int(trigger_type)
  11.128 +        self.script_name = str(script_name)
  11.129 +        self.after_script_name = str(after_script_name)
  11.130 +        g = Guard()
  11.131 +        if g.changeFromProperties(props or REQUEST):
  11.132 +            self.guard = g
  11.133 +        else:
  11.134 +            self.guard = None
  11.135 +        self.actbox_name = str(actbox_name)
  11.136 +        self.actbox_url = str(actbox_url)
  11.137 +        self.actbox_category = str(actbox_category)
  11.138 +        if REQUEST is not None:
  11.139 +            return self.manage_properties(REQUEST, 'Properties changed.')
  11.140 +
  11.141 +    _variables_form = DTMLFile('transition_variables', _dtmldir)
  11.142 +
  11.143 +    def manage_variables(self, REQUEST, manage_tabs_message=None):
  11.144 +        '''
  11.145 +        '''
  11.146 +        return self._variables_form(REQUEST,
  11.147 +                                     management_view='Variables',
  11.148 +                                     manage_tabs_message=manage_tabs_message,
  11.149 +                                     )
  11.150 +
  11.151 +    def getVariableExprs(self):
  11.152 +        ''' get variable exprs for management UI
  11.153 +        '''
  11.154 +        ve = self.var_exprs
  11.155 +        if ve is None:
  11.156 +            return []
  11.157 +        else:
  11.158 +            ret = []
  11.159 +            for key in ve.keys():
  11.160 +                ret.append((key,self.getVarExprText(key)))
  11.161 +            return ret
  11.162 +
  11.163 +    def getWorkflowVariables(self):
  11.164 +        ''' get all variables that are available form
  11.165 +            workflow and not handled yet.
  11.166 +        '''
  11.167 +        wf_vars = self.getAvailableVarIds()
  11.168 +        if self.var_exprs is None:
  11.169 +                return wf_vars
  11.170 +        ret = []
  11.171 +        for vid in wf_vars:
  11.172 +            if not self.var_exprs.has_key(vid):
  11.173 +                ret.append(vid)
  11.174 +        return ret
  11.175 +
  11.176 +    def addVariable(self, id, text, REQUEST=None):
  11.177 +        '''
  11.178 +        Add a variable expression.
  11.179 +        '''
  11.180 +        if self.var_exprs is None:
  11.181 +            self.var_exprs = PersistentMapping()
  11.182 +
  11.183 +        expr = None
  11.184 +        if text:
  11.185 +          expr = Expression(str(text))
  11.186 +        self.var_exprs[id] = expr
  11.187 +
  11.188 +        if REQUEST is not None:
  11.189 +            return self.manage_variables(REQUEST, 'Variable added.')
  11.190 +
  11.191 +    def deleteVariables(self,ids=[],REQUEST=None):
  11.192 +        ''' delete a WorkflowVariable from State
  11.193 +        '''
  11.194 +        ve = self.var_exprs
  11.195 +        for id in ids:
  11.196 +            if ve.has_key(id):
  11.197 +                del ve[id]
  11.198 +
  11.199 +        if REQUEST is not None:
  11.200 +            return self.manage_variables(REQUEST, 'Variables deleted.')
  11.201 +
  11.202 +    def setVariables(self, ids=[], REQUEST=None):
  11.203 +        ''' set values for Variables set by this state
  11.204 +        '''
  11.205 +        if self.var_exprs is None:
  11.206 +            self.var_exprs = PersistentMapping()
  11.207 +
  11.208 +        ve = self.var_exprs
  11.209 +
  11.210 +        if REQUEST is not None:
  11.211 +            for id in ve.keys():
  11.212 +                fname = 'varexpr_%s' % id
  11.213 +
  11.214 +                val = REQUEST[fname]
  11.215 +                expr = None
  11.216 +                if val:
  11.217 +                    expr = Expression(str(REQUEST[fname]))
  11.218 +                ve[id] = expr
  11.219 +
  11.220 +            return self.manage_variables(REQUEST, 'Variables changed.')
  11.221 +
  11.222 +InitializeClass(TransitionDefinition)
  11.223 +
  11.224 +
  11.225 +class Transitions (ContainerTab):
  11.226 +    """A container for transition definitions"""
  11.227 +
  11.228 +    meta_type = 'Workflow Transitions'
  11.229 +
  11.230 +    security = ClassSecurityInfo()
  11.231 +    security.declareObjectProtected(ManagePortal)
  11.232 +
  11.233 +    all_meta_types = ({'name':TransitionDefinition.meta_type,
  11.234 +                       'action':'addTransition',
  11.235 +                       },)
  11.236 +
  11.237 +    _manage_transitions = DTMLFile('transitions', _dtmldir)
  11.238 +
  11.239 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  11.240 +        '''
  11.241 +        '''
  11.242 +        return self._manage_transitions(
  11.243 +            REQUEST,
  11.244 +            management_view='Transitions',
  11.245 +            manage_tabs_message=manage_tabs_message,
  11.246 +            )
  11.247 +
  11.248 +    def addTransition(self, id, REQUEST=None):
  11.249 +        '''
  11.250 +        '''
  11.251 +        tdef = TransitionDefinition(id)
  11.252 +        self._setObject(id, tdef)
  11.253 +        if REQUEST is not None:
  11.254 +            return self.manage_main(REQUEST, 'Transition added.')
  11.255 +
  11.256 +    def deleteTransitions(self, ids, REQUEST=None):
  11.257 +        '''
  11.258 +        '''
  11.259 +        for id in ids:
  11.260 +            self._delObject(id)
  11.261 +        if REQUEST is not None:
  11.262 +            return self.manage_main(REQUEST, 'Transition(s) removed.')
  11.263 +
  11.264 +InitializeClass(Transitions)
    12.1 new file mode 100644
    12.2 --- /dev/null
    12.3 +++ b/Variables.py
    12.4 @@ -0,0 +1,167 @@
    12.5 +##############################################################################
    12.6 +#
    12.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    12.8 +#
    12.9 +# This software is subject to the provisions of the Zope Public License,
   12.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   12.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   12.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   12.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   12.14 +# FOR A PARTICULAR PURPOSE.
   12.15 +#
   12.16 +##############################################################################
   12.17 +""" Variables in a web-configurable workflow.
   12.18 +
   12.19 +$Id: Variables.py,v 1.10.2.1 2005/04/07 16:37:47 yuppie Exp $
   12.20 +"""
   12.21 +
   12.22 +from AccessControl import ClassSecurityInfo
   12.23 +from Acquisition import aq_inner
   12.24 +from Acquisition import aq_parent
   12.25 +from Globals import DTMLFile
   12.26 +from Globals import InitializeClass
   12.27 +from OFS.SimpleItem import SimpleItem
   12.28 +
   12.29 +from ContainerTab import ContainerTab
   12.30 +from Expression import Expression
   12.31 +from Guard import Guard
   12.32 +from permissions import ManagePortal
   12.33 +from utils import _dtmldir
   12.34 +
   12.35 +
   12.36 +class VariableDefinition(SimpleItem):
   12.37 +    """Variable definition"""
   12.38 +
   12.39 +    meta_type = 'Workflow Variable'
   12.40 +
   12.41 +    security = ClassSecurityInfo()
   12.42 +    security.declareObjectProtected(ManagePortal)
   12.43 +
   12.44 +    description = ''
   12.45 +    for_catalog = 1
   12.46 +    for_status = 1
   12.47 +    default_value = ''
   12.48 +    default_expr = None  # Overrides default_value if set
   12.49 +    info_guard = None
   12.50 +    update_always = 1
   12.51 +
   12.52 +    manage_options = (
   12.53 +        {'label': 'Properties', 'action': 'manage_properties'},
   12.54 +        )
   12.55 +
   12.56 +    def __init__(self, id):
   12.57 +        self.id = id
   12.58 +
   12.59 +    def getDefaultExprText(self):
   12.60 +        if not self.default_expr:
   12.61 +            return ''
   12.62 +        else:
   12.63 +            return self.default_expr.text
   12.64 +
   12.65 +    def getInfoGuard(self):
   12.66 +        if self.info_guard is not None:
   12.67 +            return self.info_guard
   12.68 +        else:
   12.69 +            return Guard().__of__(self)  # Create a temporary guard.
   12.70 +
   12.71 +    def getInfoGuardSummary(self):
   12.72 +        res = None
   12.73 +        if self.info_guard is not None:
   12.74 +            res = self.info_guard.getSummary()
   12.75 +        return res
   12.76 +
   12.77 +    _properties_form = DTMLFile('variable_properties', _dtmldir)
   12.78 +
   12.79 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
   12.80 +        '''
   12.81 +        '''
   12.82 +        return self._properties_form(REQUEST,
   12.83 +                                     management_view='Properties',
   12.84 +                                     manage_tabs_message=manage_tabs_message,
   12.85 +                                     )
   12.86 +
   12.87 +    def setProperties(self, description,
   12.88 +                      default_value='', default_expr='',
   12.89 +                      for_catalog=0, for_status=0,
   12.90 +                      update_always=0,
   12.91 +                      props=None, REQUEST=None):
   12.92 +        '''
   12.93 +        '''
   12.94 +        self.description = str(description)
   12.95 +        self.default_value = str(default_value)
   12.96 +        if default_expr:
   12.97 +            self.default_expr = Expression(default_expr)
   12.98 +        else:
   12.99 +            self.default_expr = None
  12.100 +
  12.101 +        g = Guard()
  12.102 +        if g.changeFromProperties(props or REQUEST):
  12.103 +            self.info_guard = g
  12.104 +        else:
  12.105 +            self.info_guard = None
  12.106 +        self.for_catalog = bool(for_catalog)
  12.107 +        self.for_status = bool(for_status)
  12.108 +        self.update_always = bool(update_always)
  12.109 +        if REQUEST is not None:
  12.110 +            return self.manage_properties(REQUEST, 'Properties changed.')
  12.111 +
  12.112 +InitializeClass(VariableDefinition)
  12.113 +
  12.114 +
  12.115 +class Variables(ContainerTab):
  12.116 +    """A container for variable definitions"""
  12.117 +
  12.118 +    meta_type = 'Workflow Variables'
  12.119 +
  12.120 +    all_meta_types = ({'name':VariableDefinition.meta_type,
  12.121 +                       'action':'addVariable',
  12.122 +                       },)
  12.123 +
  12.124 +    _manage_variables = DTMLFile('variables', _dtmldir)
  12.125 +
  12.126 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  12.127 +        '''
  12.128 +        '''
  12.129 +        return self._manage_variables(
  12.130 +            REQUEST,
  12.131 +            management_view='Variables',
  12.132 +            manage_tabs_message=manage_tabs_message,
  12.133 +            )
  12.134 +
  12.135 +    def addVariable(self, id, REQUEST=None):
  12.136 +        '''
  12.137 +        '''
  12.138 +        vdef = VariableDefinition(id)
  12.139 +        self._setObject(id, vdef)
  12.140 +        if REQUEST is not None:
  12.141 +            return self.manage_main(REQUEST, 'Variable added.')
  12.142 +
  12.143 +    def deleteVariables(self, ids, REQUEST=None):
  12.144 +        '''
  12.145 +        '''
  12.146 +        for id in ids:
  12.147 +            self._delObject(id)
  12.148 +        if REQUEST is not None:
  12.149 +            return self.manage_main(REQUEST, 'Variable(s) removed.')
  12.150 +
  12.151 +    def _checkId(self, id, allow_dup=0):
  12.152 +        wf_def = aq_parent(aq_inner(self))
  12.153 +        if id == wf_def.state_var:
  12.154 +            raise 'Bad Request', '"%s" is used for keeping state.' % id
  12.155 +        return ContainerTab._checkId(self, id, allow_dup)
  12.156 +
  12.157 +    def getStateVar(self):
  12.158 +        wf_def = aq_parent(aq_inner(self))
  12.159 +        return wf_def.state_var
  12.160 +
  12.161 +    def setStateVar(self, id, REQUEST=None):
  12.162 +        '''
  12.163 +        '''
  12.164 +        wf_def = aq_parent(aq_inner(self))
  12.165 +        if id != wf_def.state_var:
  12.166 +            self._checkId(id)
  12.167 +            wf_def.state_var = str(id)
  12.168 +        if REQUEST is not None:
  12.169 +            return self.manage_main(REQUEST, 'Set state variable.')
  12.170 +
  12.171 +InitializeClass(Variables)
    13.1 new file mode 100644
    13.2 --- /dev/null
    13.3 +++ b/WorkflowUIMixin.py
    13.4 @@ -0,0 +1,220 @@
    13.5 +##############################################################################
    13.6 +#
    13.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    13.8 +# 
    13.9 +# This software is subject to the provisions of the Zope Public License,
   13.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   13.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   13.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   13.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   13.14 +# FOR A PARTICULAR PURPOSE.
   13.15 +# 
   13.16 +##############################################################################
   13.17 +""" Web-configurable workflow UI.
   13.18 +
   13.19 +$Id: WorkflowUIMixin.py,v 1.13 2004/08/12 15:07:44 jens Exp $
   13.20 +"""
   13.21 +
   13.22 +import os
   13.23 +
   13.24 +from Globals import DTMLFile
   13.25 +from Globals import InitializeClass
   13.26 +from AccessControl import ClassSecurityInfo
   13.27 +from Acquisition import aq_get
   13.28 +
   13.29 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   13.30 +
   13.31 +from permissions import ManagePortal
   13.32 +from Guard import Guard
   13.33 +from utils import _dtmldir
   13.34 +
   13.35 +try:
   13.36 +    #
   13.37 +    #   XXX: 2004/04/28  This factoring *has* to go;  if necessary,
   13.38 +    #         this module could have a hook function, which the dependent
   13.39 +    #         module could replace.
   13.40 +    #
   13.41 +
   13.42 +    # If base_cms exists, include the roles it defines.
   13.43 +    from Products.base_cms.permissions import getDefaultRolePermissionMap
   13.44 +except ImportError:
   13.45 +    def getDefaultRolePermissionMap():
   13.46 +        return {}
   13.47 +
   13.48 +
   13.49 +class WorkflowUIMixin:
   13.50 +    '''
   13.51 +    '''
   13.52 +
   13.53 +    security = ClassSecurityInfo()
   13.54 +
   13.55 +    security.declareProtected(ManagePortal, 'manage_properties')
   13.56 +    manage_properties = DTMLFile('workflow_properties', _dtmldir)
   13.57 +    manage_groups = PageTemplateFile('workflow_groups.pt', _dtmldir)
   13.58 +
   13.59 +    security.declareProtected(ManagePortal, 'setProperties')
   13.60 +    def setProperties(self, title, manager_bypass=0, props=None, REQUEST=None):
   13.61 +        """Sets basic properties.
   13.62 +        """
   13.63 +        self.title = str(title)
   13.64 +        self.manager_bypass = manager_bypass and 1 or 0
   13.65 +        g = Guard()
   13.66 +        if g.changeFromProperties(props or REQUEST):
   13.67 +            self.creation_guard = g
   13.68 +        else:
   13.69 +            self.creation_guard = None
   13.70 +        if REQUEST is not None:
   13.71 +            return self.manage_properties(
   13.72 +                REQUEST, manage_tabs_message='Properties changed.')
   13.73 +
   13.74 +    _permissions_form = DTMLFile('workflow_permissions', _dtmldir)
   13.75 +
   13.76 +    security.declareProtected(ManagePortal, 'manage_permissions')
   13.77 +    def manage_permissions(self, REQUEST, manage_tabs_message=None):
   13.78 +        """Displays the form for choosing which permissions to manage.
   13.79 +        """
   13.80 +        return self._permissions_form(REQUEST,
   13.81 +                                      management_view='Permissions',
   13.82 +                                      manage_tabs_message=manage_tabs_message,
   13.83 +                                      )
   13.84 +
   13.85 +    security.declareProtected(ManagePortal, 'addManagedPermission')
   13.86 +    def addManagedPermission(self, p, REQUEST=None):
   13.87 +        """Adds to the list of permissions to manage.
   13.88 +        """
   13.89 +        if p in self.permissions:
   13.90 +            raise ValueError, 'Already a managed permission: ' + p
   13.91 +        if REQUEST is not None and p not in self.getPossiblePermissions():
   13.92 +            raise ValueError, 'Not a valid permission name:' + p
   13.93 +        self.permissions = self.permissions + (p,)
   13.94 +        if REQUEST is not None:
   13.95 +            return self.manage_permissions(
   13.96 +                REQUEST, manage_tabs_message='Permission added.')
   13.97 +
   13.98 +    security.declareProtected(ManagePortal, 'delManagedPermissions')
   13.99 +    def delManagedPermissions(self, ps, REQUEST=None):
  13.100 +        """Removes from the list of permissions to manage.
  13.101 +        """
  13.102 +        if ps:
  13.103 +            l = list(self.permissions)
  13.104 +            for p in ps:
  13.105 +                l.remove(p)
  13.106 +            self.permissions = tuple(l)
  13.107 +        if REQUEST is not None:
  13.108 +            return self.manage_permissions(
  13.109 +                REQUEST, manage_tabs_message='Permission(s) removed.')
  13.110 +
  13.111 +    security.declareProtected(ManagePortal, 'getPossiblePermissions')
  13.112 +    def getPossiblePermissions(self):
  13.113 +        """Returns the list of all permissions that can be managed.
  13.114 +        """
  13.115 +        # possible_permissions is in AccessControl.Role.RoleManager.
  13.116 +        return list(self.possible_permissions())
  13.117 +
  13.118 +    security.declareProtected(ManagePortal, 'getGroups')
  13.119 +    def getGroups(self):
  13.120 +        """Returns the names of groups managed by this workflow.
  13.121 +        """
  13.122 +        return tuple(self.groups)
  13.123 +
  13.124 +    security.declareProtected(ManagePortal, 'getAvailableGroups')
  13.125 +    def getAvailableGroups(self):
  13.126 +        """Returns a list of available group names.
  13.127 +        """
  13.128 +        gf = aq_get( self, '__allow_groups__', None, 1 )
  13.129 +        if gf is None:
  13.130 +            return ()
  13.131 +        try:
  13.132 +            groups = gf.searchGroups()
  13.133 +        except AttributeError:
  13.134 +            return ()
  13.135 +        else:
  13.136 +            return [g['id'] for g in groups]
  13.137 +
  13.138 +    security.declareProtected(ManagePortal, 'addGroup')
  13.139 +    def addGroup(self, group, RESPONSE=None):
  13.140 +        """Adds a group by name.
  13.141 +        """
  13.142 +        if group not in self.getAvailableGroups():
  13.143 +            raise ValueError(group)
  13.144 +        self.groups = self.groups + (group,)
  13.145 +        if RESPONSE is not None:
  13.146 +            RESPONSE.redirect(
  13.147 +                "%s/manage_groups?manage_tabs_message=Added+group."
  13.148 +                % self.absolute_url())
  13.149 +
  13.150 +    security.declareProtected(ManagePortal, 'delGroups')
  13.151 +    def delGroups(self, groups, RESPONSE=None):
  13.152 +        """Removes groups by name.
  13.153 +        """
  13.154 +        self.groups = tuple([g for g in self.groups if g not in groups])
  13.155 +        if RESPONSE is not None:
  13.156 +            RESPONSE.redirect(
  13.157 +                "%s/manage_groups?manage_tabs_message=Groups+removed."
  13.158 +                % self.absolute_url())
  13.159 +
  13.160 +    security.declareProtected(ManagePortal, 'getAvailableRoles')
  13.161 +    def getAvailableRoles(self):
  13.162 +        """Returns the acquired roles mixed with base_cms roles.
  13.163 +        """
  13.164 +        roles = list(self.valid_roles())
  13.165 +        for role in getDefaultRolePermissionMap().keys():
  13.166 +            if role not in roles:
  13.167 +                roles.append(role)
  13.168 +        roles.sort()
  13.169 +        return roles
  13.170 +
  13.171 +    security.declareProtected(ManagePortal, 'getRoles')
  13.172 +    def getRoles(self):
  13.173 +        """Returns the list of roles managed by this workflow.
  13.174 +        """
  13.175 +        roles = self.roles
  13.176 +        if roles is not None:
  13.177 +            return roles
  13.178 +        roles = getDefaultRolePermissionMap().keys()
  13.179 +        if roles:
  13.180 +            # Map the base_cms roles by default.
  13.181 +            roles.sort()
  13.182 +            return roles
  13.183 +        return self.valid_roles()
  13.184 +
  13.185 +    security.declareProtected(ManagePortal, 'setRoles')
  13.186 +    def setRoles(self, roles, RESPONSE=None):
  13.187 +        """Changes the list of roles mapped to groups by this workflow.
  13.188 +        """
  13.189 +        avail = self.getAvailableRoles()
  13.190 +        for role in roles:
  13.191 +            if role not in avail:
  13.192 +                raise ValueError(role)
  13.193 +        self.roles = tuple(roles)
  13.194 +        if RESPONSE is not None:
  13.195 +            RESPONSE.redirect(
  13.196 +                "%s/manage_groups?manage_tabs_message=Roles+changed."
  13.197 +                % self.absolute_url())
  13.198 +
  13.199 +    security.declareProtected(ManagePortal, 'getGuard')
  13.200 +    def getGuard(self):
  13.201 +        """Returns the initiation guard.
  13.202 +
  13.203 +        If no init guard has been created, returns a temporary object.
  13.204 +        """
  13.205 +        if self.creation_guard is not None:
  13.206 +            return self.creation_guard
  13.207 +        else:
  13.208 +            return Guard().__of__(self)  # Create a temporary guard.
  13.209 +
  13.210 +    security.declarePublic('guardExprDocs')
  13.211 +    def guardExprDocs(self):
  13.212 +        """Returns documentation on guard expressions.
  13.213 +        """
  13.214 +        here = os.path.dirname(__file__)
  13.215 +        fn = os.path.join(here, 'doc', 'expressions.stx')
  13.216 +        f = open(fn, 'rt')
  13.217 +        try:
  13.218 +            text = f.read()
  13.219 +        finally:
  13.220 +            f.close()
  13.221 +        from DocumentTemplate.DT_Var import structured_text
  13.222 +        return structured_text(text)
  13.223 +
  13.224 +InitializeClass(WorkflowUIMixin)
    14.1 new file mode 100644
    14.2 --- /dev/null
    14.3 +++ b/Worklists.py
    14.4 @@ -0,0 +1,182 @@
    14.5 +##############################################################################
    14.6 +#
    14.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    14.8 +#
    14.9 +# This software is subject to the provisions of the Zope Public License,
   14.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   14.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   14.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   14.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   14.14 +# FOR A PARTICULAR PURPOSE.
   14.15 +#
   14.16 +##############################################################################
   14.17 +""" Worklists in a web-configurable workflow.
   14.18 +
   14.19 +$Id: Worklists.py,v 1.12.2.1 2005/04/07 16:37:47 yuppie Exp $
   14.20 +"""
   14.21 +
   14.22 +from AccessControl import ClassSecurityInfo
   14.23 +from Acquisition import aq_inner
   14.24 +from Acquisition import aq_parent
   14.25 +from Globals import DTMLFile
   14.26 +from Globals import InitializeClass
   14.27 +from Globals import PersistentMapping
   14.28 +from OFS.SimpleItem import SimpleItem
   14.29 +
   14.30 +from ContainerTab import ContainerTab
   14.31 +from Guard import Guard
   14.32 +from permissions import ManagePortal
   14.33 +from utils import _dtmldir
   14.34 +
   14.35 +
   14.36 +class WorklistDefinition(SimpleItem):
   14.37 +    """Worklist definiton"""
   14.38 +
   14.39 +    meta_type = 'Worklist'
   14.40 +
   14.41 +    security = ClassSecurityInfo()
   14.42 +    security.declareObjectProtected(ManagePortal)
   14.43 +
   14.44 +    description = ''
   14.45 +    var_matches = None  # Compared with catalog when set.
   14.46 +    actbox_name = ''
   14.47 +    actbox_url = ''
   14.48 +    actbox_category = 'global'
   14.49 +    guard = None
   14.50 +
   14.51 +    manage_options = (
   14.52 +        {'label': 'Properties', 'action': 'manage_properties'},
   14.53 +        )
   14.54 +
   14.55 +    def __init__(self, id):
   14.56 +        self.id = id
   14.57 +
   14.58 +    def getGuard(self):
   14.59 +        if self.guard is not None:
   14.60 +            return self.guard
   14.61 +        else:
   14.62 +            return Guard().__of__(self)  # Create a temporary guard.
   14.63 +
   14.64 +    def getGuardSummary(self):
   14.65 +        res = None
   14.66 +        if self.guard is not None:
   14.67 +            res = self.guard.getSummary()
   14.68 +        return res
   14.69 +
   14.70 +    def getWorkflow(self):
   14.71 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   14.72 +
   14.73 +    def getAvailableCatalogVars(self):
   14.74 +        res = []
   14.75 +        res.append(self.getWorkflow().state_var)
   14.76 +        for id, vdef in self.getWorkflow().variables.items():
   14.77 +            if vdef.for_catalog:
   14.78 +                res.append(id)
   14.79 +        res.sort()
   14.80 +        return res
   14.81 +
   14.82 +    def getVarMatchKeys(self):
   14.83 +        if self.var_matches:
   14.84 +            return self.var_matches.keys()
   14.85 +        else:
   14.86 +            return []
   14.87 +
   14.88 +    def getVarMatch(self, id):
   14.89 +        if self.var_matches:
   14.90 +            matches = self.var_matches.get(id, ())
   14.91 +            if not isinstance(matches, tuple):
   14.92 +                # Old version, convert it.
   14.93 +                matches = (matches,)
   14.94 +                self.var_matches[id] = matches
   14.95 +            return matches
   14.96 +        else:
   14.97 +            return ()
   14.98 +
   14.99 +    def getVarMatchText(self, id):
  14.100 +        values = self.getVarMatch(id)
  14.101 +        return '; '.join(values)
  14.102 +
  14.103 +    _properties_form = DTMLFile('worklist_properties', _dtmldir)
  14.104 +
  14.105 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
  14.106 +        '''
  14.107 +        '''
  14.108 +        return self._properties_form(REQUEST,
  14.109 +                                     management_view='Properties',
  14.110 +                                     manage_tabs_message=manage_tabs_message,
  14.111 +                                     )
  14.112 +
  14.113 +    def setProperties(self, description,
  14.114 +                      actbox_name='', actbox_url='', actbox_category='global',
  14.115 +                      props=None, REQUEST=None):
  14.116 +        '''
  14.117 +        '''
  14.118 +        if props is None:
  14.119 +            props = REQUEST
  14.120 +        self.description = str(description)
  14.121 +        for key in self.getAvailableCatalogVars():
  14.122 +            # Populate var_matches.
  14.123 +            fieldname = 'var_match_%s' % key
  14.124 +            v = props.get(fieldname, '')
  14.125 +            if v:
  14.126 +                if not self.var_matches:
  14.127 +                    self.var_matches = PersistentMapping()
  14.128 +                v = [ var.strip() for var in v.split(';') ]
  14.129 +                self.var_matches[key] = tuple(v)
  14.130 +            else:
  14.131 +                if self.var_matches and self.var_matches.has_key(key):
  14.132 +                    del self.var_matches[key]
  14.133 +        self.actbox_name = str(actbox_name)
  14.134 +        self.actbox_url = str(actbox_url)
  14.135 +        self.actbox_category = str(actbox_category)
  14.136 +        g = Guard()
  14.137 +        if g.changeFromProperties(props or REQUEST):
  14.138 +            self.guard = g
  14.139 +        else:
  14.140 +            self.guard = None
  14.141 +        if REQUEST is not None:
  14.142 +            return self.manage_properties(REQUEST, 'Properties changed.')
  14.143 +
  14.144 +InitializeClass(WorklistDefinition)
  14.145 +
  14.146 +
  14.147 +class Worklists(ContainerTab):
  14.148 +    """A container for worklist definitions"""
  14.149 +
  14.150 +    meta_type = 'Worklists'
  14.151 +
  14.152 +    security = ClassSecurityInfo()
  14.153 +    security.declareObjectProtected(ManagePortal)
  14.154 +
  14.155 +    all_meta_types = ({'name':WorklistDefinition.meta_type,
  14.156 +                       'action':'addWorklist',
  14.157 +                       },)
  14.158 +
  14.159 +    _manage_worklists = DTMLFile('worklists', _dtmldir)
  14.160 +
  14.161 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  14.162 +        '''
  14.163 +        '''
  14.164 +        return self._manage_worklists(
  14.165 +            REQUEST,
  14.166 +            management_view='Worklists',
  14.167 +            manage_tabs_message=manage_tabs_message,
  14.168 +            )
  14.169 +
  14.170 +    def addWorklist(self, id, REQUEST=None):
  14.171 +        '''
  14.172 +        '''
  14.173 +        qdef = WorklistDefinition(id)
  14.174 +        self._setObject(id, qdef)
  14.175 +        if REQUEST is not None:
  14.176 +            return self.manage_main(REQUEST, 'Worklist added.')
  14.177 +
  14.178 +    def deleteWorklists(self, ids, REQUEST=None):
  14.179 +        '''
  14.180 +        '''
  14.181 +        for id in ids:
  14.182 +            self._delObject(id)
  14.183 +        if REQUEST is not None:
  14.184 +            return self.manage_main(REQUEST, 'Worklist(s) removed.')
  14.185 +
  14.186 +InitializeClass(Worklists)
    15.1 new file mode 100644
    15.2 --- /dev/null
    15.3 +++ b/__init__.py
    15.4 @@ -0,0 +1,43 @@
    15.5 +##############################################################################
    15.6 +#
    15.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    15.8 +# 
    15.9 +# This software is subject to the provisions of the Zope Public License,
   15.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   15.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   15.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   15.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   15.14 +# FOR A PARTICULAR PURPOSE.
   15.15 +# 
   15.16 +##############################################################################
   15.17 +""" Web-configurable workflow.
   15.18 +
   15.19 +$Id: __init__.py,v 1.6 2004/08/12 15:07:44 jens Exp $
   15.20 +"""
   15.21 +
   15.22 +from Products.CMFCore.utils import registerIcon
   15.23 +import DCWorkflow, States, Transitions, Variables, Worklists, Scripts
   15.24 +import Default
   15.25 +
   15.26 +
   15.27 +def initialize(context):
   15.28 +    
   15.29 +    context.registerHelp(directory='help')
   15.30 +    context.registerHelpTitle('DCWorkflow')
   15.31 +    
   15.32 +    registerIcon(DCWorkflow.DCWorkflowDefinition,
   15.33 +                 'images/workflow.gif', globals())
   15.34 +    registerIcon(States.States,
   15.35 +                 'images/state.gif', globals())
   15.36 +    States.StateDefinition.icon = States.States.icon
   15.37 +    registerIcon(Transitions.Transitions,
   15.38 +                 'images/transition.gif', globals())
   15.39 +    Transitions.TransitionDefinition.icon = Transitions.Transitions.icon
   15.40 +    registerIcon(Variables.Variables,
   15.41 +                 'images/variable.gif', globals())
   15.42 +    Variables.VariableDefinition.icon = Variables.Variables.icon
   15.43 +    registerIcon(Worklists.Worklists,
   15.44 +                 'images/worklist.gif', globals())
   15.45 +    Worklists.WorklistDefinition.icon = Worklists.Worklists.icon
   15.46 +    registerIcon(Scripts.Scripts,
   15.47 +                 'images/script.gif', globals())
    16.1 new file mode 100644
    16.2 --- /dev/null
    16.3 +++ b/configure.zcml
    16.4 @@ -0,0 +1,10 @@
    16.5 +<configure
    16.6 +    xmlns:five="http://namespaces.zope.org/five"
    16.7 +    >
    16.8 +
    16.9 +  <five:implements
   16.10 +      class=".DCWorkflow.DCWorkflowDefinition"
   16.11 +      interface="Products.CMFCore.interfaces.IWorkflowDefinition"
   16.12 +      />
   16.13 +
   16.14 +</configure>
    17.1 new file mode 100644
    17.2 --- /dev/null
    17.3 +++ b/doc/actbox.stx
    17.4 @@ -0,0 +1,30 @@
    17.5 +
    17.6 +Names and URLs for the actions box can be formatted using standard Python
    17.7 +string formatting.  An example::
    17.8 +
    17.9 +  %(content_url)s/content_submit_form
   17.10 +
   17.11 +The string '%(content_url)s' will be replaced by the value of content_url.
   17.12 +The following names are available:
   17.13 +
   17.14 +- portal_url
   17.15 +
   17.16 +- folder_url
   17.17 +
   17.18 +- content_url
   17.19 +
   17.20 +- user_id
   17.21 +
   17.22 +- count (Available in work lists only. Represents the number of items in
   17.23 +  the work list.)
   17.24 +
   17.25 +The following names are also available, in case there is any use for them.
   17.26 +They are not strings.
   17.27 +
   17.28 +- portal
   17.29 +
   17.30 +- folder
   17.31 +
   17.32 +- content
   17.33 +
   17.34 +- isAnonymous
    18.1 new file mode 100644
    18.2 --- /dev/null
    18.3 +++ b/doc/basics.stx
    18.4 @@ -0,0 +1,59 @@
    18.5 +
    18.6 +This product can be added to the portal_workflow tool of CMF site.
    18.7 +It provides fully customizable workflow.
    18.8 +
    18.9 +To see an example, after installing DCWorkflow, using the "Contents"
   18.10 +tab of your portal_workflow instance add a "CMF default workflow (rev 2)"
   18.11 +instance.  The other way to create it is to add a "Workflow" object,
   18.12 +selecting "dc_workflow" as the type.  The second way will create a
   18.13 +blank workflow.
   18.14 +
   18.15 +This tool is easiest to use if you draw a state diagram first.  Your
   18.16 +diagram should have:
   18.17 +
   18.18 +- States (bubbles)
   18.19 +
   18.20 +- Transitions (arrows)
   18.21 +
   18.22 +- Variables (both in states and transitions)
   18.23 +
   18.24 +Remember to consider all the states your content can be in.  Consider
   18.25 +the actions users will perform to make the transitions between states.
   18.26 +And consider not only who will be allowed to perform what functions,
   18.27 +but also who will be *required* to perform certain functions.
   18.28 +
   18.29 +On the "States" tab, add a state with a simple ID for each state on
   18.30 +your diagram.  On the "Transitions" tab, add a transition with a simple
   18.31 +ID for each group of arrows that point to the same state and have
   18.32 +similar characteristics.  Then for each state choose which transitions
   18.33 +are allowed to leave that state.
   18.34 +
   18.35 +Variables are useful for keeping track of things that aren't very well
   18.36 +represented as separate states, such as counters or information about
   18.37 +the action that was last performed.  You can create variables that get
   18.38 +stored alongside the workflow state and you can make those variables
   18.39 +available in catalog searches.  Some variables, such as the review
   18.40 +history, should not be stored at all.  Those variables are accessible
   18.41 +through the getInfoFor() interface.
   18.42 +
   18.43 +Worklists are a way to make people aware of tasks they are required
   18.44 +to perform.  Worklists are implemented as a catalog query that puts
   18.45 +actions in the actions box when there is some task the user needs to
   18.46 +perform.  Most of the time you just need to enter a state ID,
   18.47 +a role name, and the information to put in the actions box.
   18.48 +
   18.49 +You can manage all of the actions a user can perform on an object by
   18.50 +setting up permissions to be managed by the workflow.  Using the
   18.51 +"Permissions" tab, select which permissions should be state-dependent.
   18.52 +Then in each state, using the "permissions" tab, set up the
   18.53 +role to permission mappings appropriate for that state.
   18.54 +
   18.55 +Finally, you can extend the workflow with scripts.  Scripts can be
   18.56 +External Methods, Python Scripts, DTML methods, or any other callable
   18.57 +Zope object.  They are accessible by name in expressions.  Scripts
   18.58 +are invoked with a state_change object as the first argument; see
   18.59 +expressions.stx.
   18.60 +
   18.61 +Once you've crafted your workflow, you hook it up with a content type
   18.62 +by using the portal_workflow top-level "Workflows" tab.  Specify the
   18.63 +workflow name in the target content type's box.
    19.1 new file mode 100644
    19.2 --- /dev/null
    19.3 +++ b/doc/examples/staging/checkin.py
    19.4 @@ -0,0 +1,7 @@
    19.5 +## Script (Python) "checkin"
    19.6 +##parameters=sci
    19.7 +# Check in the object to a Zope version repository, disallowing changes.
    19.8 +object = sci.object
    19.9 +vt = object.portal_versions
   19.10 +if vt.isCheckedOut(object):
   19.11 +  vt.checkin(object, sci.kwargs.get('comment', ''))
    20.1 new file mode 100644
    20.2 --- /dev/null
    20.3 +++ b/doc/examples/staging/checkout.py
    20.4 @@ -0,0 +1,10 @@
    20.5 +## Script (Python) "checkout"
    20.6 +##parameters=sci
    20.7 +# Check out the object from a repository, allowing changes.
    20.8 +#
    20.9 +# For workflows that control staging, it makes sense to call this script
   20.10 +# before all transitions.
   20.11 +object = sci.object
   20.12 +vt = object.portal_versions
   20.13 +if not vt.isCheckedOut(object):
   20.14 +  vt.checkout(object)
    21.1 new file mode 100644
    21.2 --- /dev/null
    21.3 +++ b/doc/examples/staging/retractStages.py
    21.4 @@ -0,0 +1,6 @@
    21.5 +## Script (Python) "retractStages"
    21.6 +##parameters=sci
    21.7 +# Remove the object from the given stages.
    21.8 +object = sci.object
    21.9 +st = object.portal_staging
   21.10 +st.removeStages(object, ['review', 'prod'])
    22.1 new file mode 100644
    22.2 --- /dev/null
    22.3 +++ b/doc/examples/staging/updateProductionStage.py
    22.4 @@ -0,0 +1,7 @@
    22.5 +## Script (Python) "updateProductionStage"
    22.6 +##parameters=sci
    22.7 +# Copy the object in development to review and production.
    22.8 +object = sci.object
    22.9 +st = object.portal_staging
   22.10 +st.updateStages(object, 'dev', ['review', 'prod'],
   22.11 +                sci.kwargs.get('comment', ''))
    23.1 new file mode 100644
    23.2 --- /dev/null
    23.3 +++ b/doc/examples/staging/updateReviewStage.py
    23.4 @@ -0,0 +1,7 @@
    23.5 +## Script (Python) "updateReviewStage"
    23.6 +##parameters=sci
    23.7 +# Copy the object in development to review.
    23.8 +object = sci.object
    23.9 +st = object.portal_staging
   23.10 +st.updateStages(object, 'dev', ['review'],
   23.11 +                sci.kwargs.get('comment', ''))
    24.1 new file mode 100644
    24.2 --- /dev/null
    24.3 +++ b/doc/expressions.stx
    24.4 @@ -0,0 +1,52 @@
    24.5 +
    24.6 +DCWorkflow Expressions
    24.7 +
    24.8 +  Expressions in DCWorkflow are TALES expressions.
    24.9 +(See the <a href="http://zope.org/Documentation/Books/ZopeBook">Zope Book</a>
   24.10 +for background on page templates and TALES.)
   24.11 +Some of the contexts have slightly different meanings from what is provided
   24.12 +for expressions in page templates.
   24.13 +
   24.14 +- 'here' -- The content object
   24.15 +
   24.16 +- 'container' -- The content object's container
   24.17 +
   24.18 +Several other contexts are also provided.
   24.19 +
   24.20 +- 'state_change' -- A special object containing info about the state change
   24.21 +
   24.22 +- 'transition' -- The transition object being executed
   24.23 +
   24.24 +- 'status' -- The former status
   24.25 +
   24.26 +- 'workflow' -- The workflow definition object
   24.27 +
   24.28 +- 'scripts' -- The scripts in the workflow definition object
   24.29 +
   24.30 +'state_change' objects provide the following attributes:
   24.31 +
   24.32 +- 'state_change.status' -- a mapping containing the workflow status.
   24.33 +
   24.34 +- 'state_change.object' -- the object being modified by workflow.
   24.35 +
   24.36 +- 'state_change.workflow' -- the workflow definition object.
   24.37 +
   24.38 +- 'state_change.transition' -- the transition object being executed.
   24.39 +
   24.40 +- 'state_change.old_state' -- the former state object.
   24.41 +
   24.42 +- 'state_change.new_state' -- the destination state object.
   24.43 +
   24.44 +- 'state_change.kwargs' -- the keyword arguments passed to the
   24.45 +  doActionFor() method.
   24.46 +
   24.47 +- 'state_change.getHistory()' -- returns a copy of the object's workflow
   24.48 +   history.
   24.49 +
   24.50 +- 'state_change.getPortal()' -- returns the root of the portal.
   24.51 +
   24.52 +- 'state_change.ObjectDeleted' and 'ObjectMoved' -- exceptions that
   24.53 +  can be raised by scripts to indicate to the workflow that an object
   24.54 +  has been moved or deleted.
   24.55 +
   24.56 +- 'state_change.getDateTime()' -- returns the DateTime of the transition.
    25.1 new file mode 100644
    25.2 --- /dev/null
    25.3 +++ b/doc/howto.stx
    25.4 @@ -0,0 +1,46 @@
    25.5 +From: John Morton <jwm@plain.co.nz>
    25.6 +
    25.7 +Here's how I generally go about putting together a new workflow:
    25.8 +
    25.9 +1. Draw up a state diagram with the nodes as states and the arcs as
   25.10 +   transitions. I usually do this on paper, then do a good copy in dia
   25.11 +   or some other diagram tool, so I have an image to go with the
   25.12 +   documentation. I usually spot several corner cases in the process.
   25.13 +
   25.14 +2. Start by creating an example DCworkflow, rather than a new one, as
   25.15 +   it's faster to delete all the states and transitions than it is to
   25.16 +   create all the standard review_state variables.
   25.17 +
   25.18 +3. In the permissions tab, select all the permissions that you want the
   25.19 +   workflow to govern.
   25.20 +
   25.21 +4. Define any extra variables that you need.
   25.22 +
   25.23 +5. Set up the states for your workflow, one for each node in your state
   25.24 +   diagram. Try to stick to the standard names for a publication
   25.25 +   workflow, as some badly behaved products have states like 'published'
   25.26 +   hardcoded into their searches (ie CalendarTool, last I looked). Set
   25.27 +   up the permissions on the states now, as well. I find that using
   25.28 +   acquisition for the site visible states, and using explicit
   25.29 +   permissions for the private and interim states works well. Reviewer
   25.30 +   roles should either have view permissions on every state or you
   25.31 +   should change the appropriate skins to take them somewhere sensible
   25.32 +   after a transition or they'll end up with an ugly access denied page
   25.33 +   after sending some content back to private state.
   25.34 +
   25.35 +6. Set up any scripts that you'll need for transitions - pre and post
   25.36 +   transition scripts and ones to handle complex guard conditions. Just
   25.37 +   set up skeletons for now, if you haven't though through all the
   25.38 +   details.
   25.39 +
   25.40 +7. Create your transitions from all the arcs on your state diagram. You
   25.41 +   should be able to pick the right destination state as all your states
   25.42 +   are already defined, and set up the right scripts to run, as you've
   25.43 +   defined those as well. It's worth noting that the guards are or'ed -
   25.44 +   if any guard matches, the transition can occur. You can specify more
   25.45 +   than one permission, role or expression by separating them with a
   25.46 +   semicolon.
   25.47 +
   25.48 +That about covers it. By working in this order, you tend to step through
   25.49 +the creation process one tab at a time, rather than switching back and
   25.50 +forth. I find it tends to be faster and less confusing that way.
    26.1 new file mode 100644
    26.2 --- /dev/null
    26.3 +++ b/doc/worklists.stx
    26.4 @@ -0,0 +1,7 @@
    26.5 +
    26.6 +Worklists are predefined catalog queries.
    26.7 +
    26.8 +When defining a variable match, you can enter a list of possible matches
    26.9 +separated by semicolons. In addition, the matches will be formatted
   26.10 +according to the rules described in "actbox.stx":actbox.stx.
   26.11 +This way you can enter "%(user_id)s" to match only the current user.
    27.1 new file mode 100644
    27.2 --- /dev/null
    27.3 +++ b/dtml/guard.dtml
    27.4 @@ -0,0 +1,26 @@
    27.5 +<script type="text/javascript">
    27.6 +function guardExprDocs() {
    27.7 +  window.open('guardExprDocs', '', 'width=640, height=480, resizable, scrollbars, status');
    27.8 +}
    27.9 +</script>
   27.10 +
   27.11 +<table>
   27.12 +
   27.13 +<tr>
   27.14 +<th align="left">Permission(s)</th>
   27.15 +<td><input type="text" name="guard_permissions" value="&dtml-getPermissionsText;" /></td>
   27.16 +<th align="left">Role(s)</th>
   27.17 +<td><input type="text" name="guard_roles" value="&dtml-getRolesText;" /></td>
   27.18 +<th align="left">Group(s)</th>
   27.19 +<td><input type="text" name="guard_groups" value="&dtml-getGroupsText;" /></td>
   27.20 +</tr>
   27.21 +
   27.22 +<tr>
   27.23 +<th align="left">Expression</th>
   27.24 +<td colspan="3">
   27.25 +<input type="text" name="guard_expr" value="&dtml-getExprText;" size="50" />
   27.26 +<a href="#" onclick="guardExprDocs(); return false;">[?]</a>
   27.27 +</td>
   27.28 +</tr>
   27.29 +
   27.30 +</table>
    28.1 new file mode 100644
    28.2 --- /dev/null
    28.3 +++ b/dtml/state_groups.pt
    28.4 @@ -0,0 +1,53 @@
    28.5 +<h1 tal:replace="structure here/manage_page_header">Header</h1>
    28.6 +<h2 tal:define="manage_tabs_message request/manage_tabs_message | nothing"
    28.7 +    tal:replace="structure here/manage_tabs">Tabs</h2>
    28.8 +
    28.9 +<p class="form-help">
   28.10 +When objects are in this state, they will take on the group to role
   28.11 +mappings defined below.  Only the <a href="../manage_groups">groups
   28.12 +and roles managed by this workflow</a> are shown.
   28.13 +</p>
   28.14 +
   28.15 +<form action="setGroups" method="POST"
   28.16 +  tal:define="wf here/getWorkflow; roles wf/getRoles">
   28.17 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   28.18 +<tr class="list-header">
   28.19 +  <td align="left">
   28.20 +  <div class="form-label">
   28.21 +  <strong>Group</strong>
   28.22 +  </div>
   28.23 +  </td>
   28.24 +  <td align="left" tal:attributes="colspan python: len(roles)">
   28.25 +  <div class="form-label">
   28.26 +  <strong>Roles</strong>
   28.27 +  </div>
   28.28 +  </td>
   28.29 +</tr>
   28.30 +
   28.31 +<tr class="row-normal">
   28.32 +  <td></td>
   28.33 +  <td tal:repeat="role roles" tal:content="role" class="list-item">
   28.34 +    Authenticated
   28.35 +  </td>
   28.36 +</tr>
   28.37 +
   28.38 +<tr tal:repeat="group wf/getGroups" tal:attributes="class
   28.39 +  python: repeat['group'].odd and 'row-normal' or 'row-hilite'">
   28.40 +<td tal:content="group" class="list-item">
   28.41 +  (Group) Everyone
   28.42 +</td>
   28.43 +<tal:block tal:define="group_roles python: here.getGroupInfo(group)">
   28.44 +<td tal:repeat="role roles">
   28.45 +  <input type="checkbox"
   28.46 +    tal:attributes="name python: '%s|%s' % (group, role);
   28.47 +      checked python: role in group_roles" />
   28.48 +</td>
   28.49 +</tal:block>
   28.50 +</tr>
   28.51 +</table>
   28.52 +
   28.53 +<input class="form-element" type="submit" name="submit" value="Save Changes" />
   28.54 +
   28.55 +</form>
   28.56 +
   28.57 +<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
    29.1 new file mode 100644
    29.2 --- /dev/null
    29.3 +++ b/dtml/state_permissions.dtml
    29.4 @@ -0,0 +1,84 @@
    29.5 +<dtml-var manage_page_header>
    29.6 +<dtml-var manage_tabs>
    29.7 +
    29.8 +<p class="form-help">
    29.9 +When objects are in this state they will take on the role to permission
   29.10 +mappings defined below.  Only the <a href="../manage_permissions">permissions
   29.11 +managed by this workflow</a> are shown.
   29.12 +</p>
   29.13 +
   29.14 +<form action="setPermissions" method="POST">
   29.15 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   29.16 +<tr class="list-header">
   29.17 +  <td>&nbsp;</td>
   29.18 +  <td align="left">
   29.19 +  <div class="form-label">
   29.20 +  <strong>Permission</strong>
   29.21 +  </div>
   29.22 +  </td>
   29.23 +  <td align="left" colspan="<dtml-var expr="_.len(getAvailableRoles())">">
   29.24 +  <div class="form-label">
   29.25 +  <strong>Roles</strong>
   29.26 +  </div>
   29.27 +  </td>
   29.28 +</tr>
   29.29 +
   29.30 +<tr class="row-normal">
   29.31 +  <td align="left" valign="top">
   29.32 +  <div class="form-label">
   29.33 +  <strong>
   29.34 +  Acquire<BR>permission<BR>settings?
   29.35 +  </strong>
   29.36 +  </div>
   29.37 +  </td>
   29.38 +  <td></td>
   29.39 +  <dtml-in getAvailableRoles>
   29.40 +  <td align="left">
   29.41 +  <div class="list-item">
   29.42 +  <dtml-var sequence-item>
   29.43 +  </div>
   29.44 +  </td>
   29.45 +  </dtml-in>
   29.46 +</tr>
   29.47 +
   29.48 +<dtml-in getManagedPermissions sort>
   29.49 +<dtml-let permission=sequence-item>
   29.50 +<dtml-with expr="getPermissionInfo(permission)" mapping>
   29.51 +<dtml-if sequence-odd>
   29.52 +<tr class="row-normal">
   29.53 +<dtml-else>
   29.54 +<tr class="row-hilite">
   29.55 +</dtml-if>
   29.56 +  <td align="left" valign="top">
   29.57 +  <dtml-let checked="acquired and 'checked' or ' '">
   29.58 +   <input type="checkbox" name="acquire_&dtml-permission;" &dtml-checked; />
   29.59 +  </dtml-let>
   29.60 +  </td>
   29.61 +  <td align="left" nowrap>
   29.62 +  <div class="list-item">
   29.63 +  &dtml-permission;
   29.64 +  </div>
   29.65 +  </td>
   29.66 +  <dtml-in getAvailableRoles sort>
   29.67 +  <td align="center">
   29.68 +  <dtml-let checked="_['sequence-item'] in roles and 'checked' or ' '">
   29.69 +   <input type="checkbox" name="&dtml-permission;|&dtml-sequence-item;" &dtml-checked; />
   29.70 +  </dtml-let>
   29.71 +  </td>
   29.72 +  </dtml-in>
   29.73 +</tr>
   29.74 +</dtml-with>
   29.75 +</dtml-let>
   29.76 +</dtml-in>
   29.77 +
   29.78 +<tr>
   29.79 +<td colspan="<dtml-var expr="_.len(getAvailableRoles())+2">" align="left">
   29.80 +<div class="form-element">
   29.81 +<input class="form-element" type="submit" name="submit" value="Save Changes" />
   29.82 +</div>
   29.83 +</td>
   29.84 +</tr>
   29.85 +</table>
   29.86 +</form>
   29.87 +
   29.88 +<dtml-var manage_page_footer>
    30.1 new file mode 100644
    30.2 --- /dev/null
    30.3 +++ b/dtml/state_properties.dtml
    30.4 @@ -0,0 +1,44 @@
    30.5 +<dtml-var manage_page_header>
    30.6 +<dtml-var manage_tabs>
    30.7 +
    30.8 +<form action="setProperties" method="POST">
    30.9 +<table>
   30.10 +
   30.11 +<tr>
   30.12 +<th align="left">Id</th>
   30.13 +<td>&dtml-id;</td>
   30.14 +</tr>
   30.15 +
   30.16 +<tr>
   30.17 +<th align="left">Title</th>
   30.18 +<td><input type="text" name="title" value="&dtml-title;" size="50" /></td>
   30.19 +</tr>
   30.20 +
   30.21 +<tr>
   30.22 +<th align="left" valign="top">Description</th>
   30.23 +<td><textarea name="description" rows="6" cols="35">&dtml-description;</textarea></td>
   30.24 +</tr>
   30.25 +
   30.26 +<tr>
   30.27 +<th align="left" valign="top">Possible Transitions</th>
   30.28 +<td>
   30.29 + <dtml-in getAvailableTransitionIds sort>
   30.30 +  <dtml-let checked="_['sequence-item'] in transitions and 'checked' or ' '">
   30.31 +   <input type="checkbox" name="transitions:list"
   30.32 +    value="&dtml-sequence-item;" &dtml-checked; /> &dtml-sequence-item;
   30.33 +   <dtml-let t_title="getTransitionTitle(_['sequence-item'])">
   30.34 +    <dtml-if t_title>(&dtml-t_title;)</dtml-if>
   30.35 +   </dtml-let>
   30.36 +  </dtml-let>
   30.37 +  <br />
   30.38 + <dtml-else>
   30.39 +  <em>No transitions defined.</em>
   30.40 + </dtml-in>
   30.41 + </select>
   30.42 +</td>
   30.43 +</tr>
   30.44 +
   30.45 +</table>
   30.46 +<input type="submit" name="submit" value="Save changes" />
   30.47 +</form>
   30.48 +<dtml-var manage_page_footer>
    31.1 new file mode 100644
    31.2 --- /dev/null
    31.3 +++ b/dtml/state_variables.dtml
    31.4 @@ -0,0 +1,84 @@
    31.5 +<dtml-var manage_page_header>
    31.6 +<dtml-var manage_tabs>
    31.7 +
    31.8 +<p class="form-help">
    31.9 +When objects move to this state the workflow variables will be assigned
   31.10 +the values below.
   31.11 +</p>
   31.12 +
   31.13 +<dtml-if getVariableValues>
   31.14 +
   31.15 +<form action="&dtml-absolute_url;" method="POST">
   31.16 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   31.17 +<tr class="list-header">
   31.18 +  <td>&nbsp;</td>
   31.19 +  <td align="left" valign="top">
   31.20 +  <div class="form-label">
   31.21 +  <strong>Variable</strong>
   31.22 +  </div>
   31.23 +  </td>
   31.24 +  <td align="left">
   31.25 +  <div class="form-label">
   31.26 +  <strong>Value</strong>
   31.27 +  </div>
   31.28 +  </td>
   31.29 +</tr>
   31.30 +
   31.31 +<dtml-in getVariableValues sort>
   31.32 +<dtml-if sequence-odd>
   31.33 +<tr class="row-normal">
   31.34 +<dtml-else>
   31.35 +<tr class="row-hilite">
   31.36 +</dtml-if>
   31.37 +  <td align="left" valign="top">
   31.38 +   <input type="checkbox" name="ids:list" value="&dtml-sequence-key;"/>
   31.39 +  </td>
   31.40 +  <td align="left" nowrap>
   31.41 +  <div class="list-item">
   31.42 +  &dtml-sequence-key;
   31.43 +  </div>
   31.44 +  </td>
   31.45 +  <td align="left">
   31.46 +   <input type="text" name="varval_&dtml-sequence-key;" value="&dtml-sequence-item;" size="50" />
   31.47 +  </td>
   31.48 +</tr>
   31.49 +</dtml-in>
   31.50 +
   31.51 +<tr>
   31.52 +<td colspan="3" align="left">
   31.53 +<div class="form-element">
   31.54 +<input class="form-element" type="submit" name="setVariables:method" value="Save Changes" />
   31.55 +<input class="form-element" type="submit" name="deleteVariables:method" value="Delete" />
   31.56 +</div>
   31.57 +</td>
   31.58 +</tr>
   31.59 +</table>
   31.60 +</form>
   31.61 +
   31.62 +</dtml-if>
   31.63 +
   31.64 +
   31.65 +<form action="addVariable" method="POST">
   31.66 + <table>
   31.67 +  <tr>
   31.68 +   <td>Add a variable value</td>
   31.69 +  </tr>
   31.70 +  <tr>
   31.71 +   <td>Variable</td>
   31.72 +   <td><select name="id">
   31.73 +    <dtml-in getWorkflowVariables>
   31.74 +     <option value="&dtml-sequence-item;">
   31.75 +      <dtml-var sequence-item>
   31.76 +     </option>
   31.77 +    </dtml-in>
   31.78 +   </select>
   31.79 +  </td>
   31.80 + </tr>
   31.81 + <tr>
   31.82 +  <td>Value</td><td><input type="text" name="value" value="" /></td>
   31.83 + </tr>
   31.84 + <tr><td><input type="submit" name="submit" value="Add" /></td></tr>
   31.85 +</table>
   31.86 +</form>
   31.87 +
   31.88 +<dtml-var manage_page_footer>
    32.1 new file mode 100644
    32.2 --- /dev/null
    32.3 +++ b/dtml/states.dtml
    32.4 @@ -0,0 +1,66 @@
    32.5 +<dtml-var manage_page_header>
    32.6 +<dtml-var manage_tabs>
    32.7 +<form action="&dtml-absolute_url;" method="POST">
    32.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    32.9 +<dtml-in values sort=id>
   32.10 + <tr bgcolor="#eeeeee">
   32.11 +  <th align="left" colspan="2">
   32.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   32.13 +   <dtml-if expr="id == initial_state">*</dtml-if>
   32.14 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   32.15 +   &nbsp;
   32.16 +   &dtml-title;
   32.17 +  </th>
   32.18 + </tr>
   32.19 + <dtml-let state_id=id>
   32.20 + <dtml-in getTransitions>
   32.21 +  <tr>
   32.22 +   <td width="10%"></td>
   32.23 +   <td>
   32.24 +    <a href="../transitions/&dtml-sequence-item;/manage_properties"
   32.25 +     >&dtml-sequence-item;</a>
   32.26 +    <dtml-let t_title="getTransitionTitle(_['sequence-item'])">
   32.27 +     <dtml-if t_title>(&dtml-t_title;)</dtml-if>
   32.28 +    </dtml-let>
   32.29 +   </td>
   32.30 +  </tr>
   32.31 + <dtml-else>
   32.32 +  <tr>
   32.33 +   <td></td>
   32.34 +   <td><em>No transitions.</em></td>
   32.35 +  </tr>
   32.36 + </dtml-in>
   32.37 + </dtml-let>
   32.38 + <dtml-if getVariableValues>
   32.39 + <tr>
   32.40 +  <th align="right">Variables</th>
   32.41 +  <th></th>
   32.42 + </tr>
   32.43 + <dtml-in getVariableValues sort>
   32.44 +  <tr>
   32.45 +   <td></td>
   32.46 +   <td>
   32.47 +    &dtml-sequence-key; = &dtml-sequence-item;
   32.48 +   </td>
   32.49 +  </tr>
   32.50 + </dtml-in>
   32.51 + </dtml-if>
   32.52 +<dtml-else>
   32.53 + <tr><td><em>No states defined.</em></td></tr>
   32.54 +</dtml-in>
   32.55 +</table>
   32.56 +<dtml-if values>
   32.57 +<p>
   32.58 +  <b>Note:</b> Renaming a state will not affect any items in that state. You
   32.59 +  will need to fix them manually.
   32.60 +</p>
   32.61 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   32.62 +<input type="submit" name="deleteStates:method" value="Delete" />
   32.63 +<input type="submit" name="setInitialState:method" value="Set Initial State" />
   32.64 +</dtml-if>
   32.65 +<hr />
   32.66 +<h3>Add a state</h3>
   32.67 +<p>Id <input type="text" name="id" value="" />
   32.68 +<input type="submit" name="addState:method" value="Add" /></p>
   32.69 +</form>
   32.70 +<dtml-var manage_page_footer>
    33.1 new file mode 100644
    33.2 --- /dev/null
    33.3 +++ b/dtml/transition_properties.dtml
    33.4 @@ -0,0 +1,137 @@
    33.5 +<dtml-var manage_page_header>
    33.6 +<dtml-var manage_tabs>
    33.7 +
    33.8 +<form action="setProperties" method="POST">
    33.9 +<table>
   33.10 +
   33.11 +<tr>
   33.12 +<th align="left">Id</th>
   33.13 +<td>&dtml-id;</td>
   33.14 +</tr>
   33.15 +
   33.16 +<tr>
   33.17 +<th align="left">Title</th>
   33.18 +<td><input type="text" name="title" value="&dtml-title;" size="50" /></td>
   33.19 +</tr>
   33.20 +
   33.21 +<tr>
   33.22 +<th align="left" valign="top">Description</th>
   33.23 +<td><textarea name="description" rows="6" cols="35">&dtml-description;</textarea></td>
   33.24 +</tr>
   33.25 +
   33.26 +<tr>
   33.27 +<th align="left">Destination state</th>
   33.28 +<td>
   33.29 + <select name="new_state_id" size="1">
   33.30 +  <dtml-let selected="not new_state_id and 'selected' or ' '">
   33.31 +   <option value="" &dtml-selected;>(Remain in state)</option>
   33.32 +  </dtml-let>
   33.33 +  <dtml-in getAvailableStateIds sort>
   33.34 +   <dtml-let selected="new_state_id == _['sequence-item'] and 'selected' or ' '">
   33.35 +    <option value="&dtml-sequence-item;" &dtml-selected;>&dtml-sequence-item;</option>
   33.36 +   </dtml-let>
   33.37 +  </dtml-in>
   33.38 + </select>
   33.39 +</td>
   33.40 +</tr>
   33.41 +
   33.42 +<tr>
   33.43 +<th align="left">Trigger type</th>
   33.44 +<td>
   33.45 +<dtml-let checked="trigger_type==0 and 'checked' or ' '">
   33.46 +<input type="radio" name="trigger_type" value="0" &dtml-checked; />
   33.47 +Automatic
   33.48 +</dtml-let>
   33.49 +</td>
   33.50 +</tr>
   33.51 +
   33.52 +<tr>
   33.53 +<th></th>
   33.54 +<td>
   33.55 +<dtml-let checked="trigger_type==1 and 'checked' or ' '">
   33.56 +<input type="radio" name="trigger_type" value="1" &dtml-checked; />
   33.57 +Initiated by user action
   33.58 +</dtml-let>
   33.59 +</td>
   33.60 +</tr>
   33.61 +
   33.62 +<tr>
   33.63 +<th></th>
   33.64 +<td>
   33.65 +<dtml-let checked="trigger_type==2 and 'checked' or ' '">
   33.66 +<input type="radio" name="trigger_type" value="2" &dtml-checked; />
   33.67 +Initiated by WorkflowMethod
   33.68 +</dtml-let>
   33.69 +</td>
   33.70 +</tr>
   33.71 +
   33.72 +<tr>
   33.73 +<th align="left">Script (before)</th>
   33.74 +<td>
   33.75 +<select name="script_name">
   33.76 +<option value="">(None)</option>
   33.77 +<dtml-in getAvailableScriptIds sort>
   33.78 + <dtml-let selected="script_name == _['sequence-item'] and 'selected' or ' '">
   33.79 +  <option value="&dtml-sequence-item;" &dtml-selected;>&dtml-sequence-item;</option>
   33.80 + </dtml-let>
   33.81 +</dtml-in>
   33.82 +</select>
   33.83 +</td>
   33.84 +</tr>
   33.85 +
   33.86 +<tr>
   33.87 +<th align="left">Script (after)</th>
   33.88 +<td>
   33.89 +<select name="after_script_name">
   33.90 +<option value="">(None)</option>
   33.91 +<dtml-in getAvailableScriptIds sort>
   33.92 + <dtml-let selected="after_script_name == _['sequence-item'] and 'selected' or ' '">
   33.93 +  <option value="&dtml-sequence-item;" &dtml-selected;>&dtml-sequence-item;</option>
   33.94 + </dtml-let>
   33.95 +</dtml-in>
   33.96 +</select>
   33.97 +</td>
   33.98 +</tr>
   33.99 +
  33.100 +<tr>
  33.101 +<th align="left" valign="top">Guard</th>
  33.102 +<td>
  33.103 + <dtml-with getGuard>
  33.104 +  <dtml-var guardForm>
  33.105 + </dtml-with>
  33.106 +</td>
  33.107 +</tr>
  33.108 +
  33.109 +<tr>
  33.110 +<th align="left" valign="top">Display in actions box</th>
  33.111 +<td>
  33.112 + <table>
  33.113 +  <tr>
  33.114 +   <th align="left">Name (formatted)</th>
  33.115 +   <td>
  33.116 +    <input type="text" name="actbox_name"
  33.117 +     value="&dtml-actbox_name;" size="50" />
  33.118 +   </td>
  33.119 +  </tr>
  33.120 +  <tr>
  33.121 +   <th align="left">URL (formatted)</th>
  33.122 +   <td>
  33.123 +    <input type="text" name="actbox_url"
  33.124 +     value="&dtml-actbox_url;" size="50" />
  33.125 +   </td>
  33.126 +  </tr>
  33.127 +  <tr>
  33.128 +   <th align="left">Category</th>
  33.129 +   <td>
  33.130 +    <input type="text" name="actbox_category"
  33.131 +     value="&dtml-actbox_category;" />
  33.132 +   </td>
  33.133 +  </tr>
  33.134 + </table>
  33.135 +</td>
  33.136 +</tr>
  33.137 +
  33.138 +</table>
  33.139 +<input type="submit" name="submit" value="Save changes" />
  33.140 +</form>
  33.141 +<dtml-var manage_page_footer>
    34.1 new file mode 100644
    34.2 --- /dev/null
    34.3 +++ b/dtml/transition_variables.dtml
    34.4 @@ -0,0 +1,84 @@
    34.5 +<dtml-var manage_page_header>
    34.6 +<dtml-var manage_tabs>
    34.7 +
    34.8 +<p class="form-help">
    34.9 +When the transition is executed, the workflow variables are
   34.10 +updated according to the expressions below.
   34.11 +</p>
   34.12 +
   34.13 +<dtml-if getVariableExprs>
   34.14 +
   34.15 +<form action="&dtml-absolute_url;" method="POST">
   34.16 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   34.17 +<tr class="list-header">
   34.18 +  <td>&nbsp;</td>
   34.19 +  <td align="left" valign="top">
   34.20 +  <div class="form-label">
   34.21 +  <strong>Variable</strong>
   34.22 +  </div>
   34.23 +  </td>
   34.24 +  <td align="left">
   34.25 +  <div class="form-label">
   34.26 +  <strong>Expression</strong>
   34.27 +  </div>
   34.28 +  </td>
   34.29 +</tr>
   34.30 +
   34.31 +<dtml-in getVariableExprs sort>
   34.32 +<dtml-if sequence-odd>
   34.33 +<tr class="row-normal">
   34.34 +<dtml-else>
   34.35 +<tr class="row-hilite">
   34.36 +</dtml-if>
   34.37 +  <td align="left" valign="top">
   34.38 +   <input type="checkbox" name="ids:list" value="&dtml-sequence-key;"/>
   34.39 +  </td>
   34.40 +  <td align="left" nowrap>
   34.41 +  <div class="list-item">
   34.42 +  &dtml-sequence-key;
   34.43 +  </div>
   34.44 +  </td>
   34.45 +  <td align="left">
   34.46 +   <input type="text" name="varexpr_&dtml-sequence-key;" value="&dtml-sequence-item;" size="50" />
   34.47 +  </td>
   34.48 +</tr>
   34.49 +</dtml-in>
   34.50 +
   34.51 +<tr>
   34.52 +<td colspan="3" align="left">
   34.53 +<div class="form-element">
   34.54 +<input class="form-element" type="submit" name="setVariables:method" value="Save Changes" />
   34.55 +<input class="form-element" type="submit" name="deleteVariables:method" value="Delete" />
   34.56 +</div>
   34.57 +</td>
   34.58 +</tr>
   34.59 +</table>
   34.60 +</form>
   34.61 +
   34.62 +</dtml-if>
   34.63 +
   34.64 +<form action="addVariable" method="POST">
   34.65 + <table>
   34.66 +  <tr>
   34.67 +   <td>Add a variable expression</td>
   34.68 +  </tr>
   34.69 +  <tr>
   34.70 +   <td>Variable</td>
   34.71 +   <td><select name="id">
   34.72 +    <dtml-in getWorkflowVariables>
   34.73 +     <option value="&dtml-sequence-item;">
   34.74 +      <dtml-var sequence-item>
   34.75 +     </option>
   34.76 +    </dtml-in>
   34.77 +   </select>
   34.78 +  </td>
   34.79 + </tr>
   34.80 + <tr>
   34.81 +  <td>Expression</td>
   34.82 +  <td><input type="text" name="text" size="50" value="" /></td>
   34.83 + </tr>
   34.84 + <tr><td><input type="submit" name="submit" value="Add" /></td></tr>
   34.85 +</table>
   34.86 +</form>
   34.87 +
   34.88 +<dtml-var manage_page_footer>
    35.1 new file mode 100644
    35.2 --- /dev/null
    35.3 +++ b/dtml/transitions.dtml
    35.4 @@ -0,0 +1,66 @@
    35.5 +<dtml-var manage_page_header>
    35.6 +<dtml-var manage_tabs>
    35.7 +<form action="&dtml-absolute_url;" method="POST">
    35.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    35.9 +<dtml-in values sort=id>
   35.10 + <tr bgcolor="#eeeeee">
   35.11 +  <th align="left" colspan="2">
   35.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   35.13 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   35.14 +   &nbsp;
   35.15 +   &dtml-title;
   35.16 +  </th>
   35.17 + </tr>
   35.18 +
   35.19 + <tr>
   35.20 +  <th width="10%"></th>
   35.21 +  <td>
   35.22 +   Destination state: <code><dtml-if new_state_id>&dtml-new_state_id;<dtml-else>(Remain in state)</dtml-if></code> <br />
   35.23 +   Trigger: <dtml-var expr="(trigger_type == 0 and 'Automatic') or
   35.24 +                            (trigger_type == 1 and 'User action') or
   35.25 +                            (trigger_type == 2 and 'WorkflowMethod')">
   35.26 +   <br />
   35.27 +   <dtml-if script_name>
   35.28 +     Script (before): &dtml-script_name;
   35.29 +     <br />
   35.30 +   </dtml-if>
   35.31 +   <dtml-if after_script_name>
   35.32 +     Script (after): &dtml-after_script_name;
   35.33 +     <br />
   35.34 +   </dtml-if>
   35.35 +   <dtml-if getGuardSummary><dtml-var getGuardSummary><br /></dtml-if>
   35.36 +   <dtml-if actbox_name>Adds to actions box: <code>&dtml-actbox_name;</code></dtml-if>
   35.37 +  </td>
   35.38 + </tr>
   35.39 + <dtml-if var_exprs>
   35.40 + <tr>
   35.41 +  <th align="right">Variables</th>
   35.42 +  <th></th>
   35.43 + </tr>
   35.44 + <dtml-in getVariableExprs sort>
   35.45 +  <tr>
   35.46 +   <td></td>
   35.47 +   <td>
   35.48 +    &dtml-sequence-key; = &dtml-sequence-item;
   35.49 +   </td>
   35.50 +  </tr>
   35.51 + </dtml-in>
   35.52 + </dtml-if>
   35.53 +<dtml-else>
   35.54 + <tr><td><em>No transitions defined.</em></td></tr>
   35.55 +</dtml-in>
   35.56 +</table>
   35.57 +<dtml-if values>
   35.58 +<p>
   35.59 +  <b>Note:</b> Renaming a transition will not automatically update all
   35.60 +  items in the workflow affected by it. You will need to fix them manually.
   35.61 +</p>  
   35.62 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   35.63 +<input type="submit" name="deleteTransitions:method" value="Delete" />
   35.64 +</dtml-if>
   35.65 +<hr />
   35.66 +<h3>Add a transition</h3>
   35.67 +<p>Id <input type="text" name="id" value="" />
   35.68 +<input type="submit" name="addTransition:method" value="Add" /></p>
   35.69 +</form>
   35.70 +<dtml-var manage_page_footer>
    36.1 new file mode 100644
    36.2 --- /dev/null
    36.3 +++ b/dtml/variable_properties.dtml
    36.4 @@ -0,0 +1,110 @@
    36.5 +<dtml-var manage_page_header>
    36.6 +<dtml-var manage_tabs>
    36.7 +
    36.8 +<form action="setProperties" method="POST">
    36.9 +<table>
   36.10 +
   36.11 +<tr>
   36.12 +<th align="left">Id</th>
   36.13 +<td>&dtml-id;</td>
   36.14 +</tr>
   36.15 +
   36.16 +<tr>
   36.17 +<th align="left">Description</th>
   36.18 +<td><input type="text" name="description" value="&dtml-description;"
   36.19 +     size="50" /></td>
   36.20 +</tr>
   36.21 +
   36.22 +<tr>
   36.23 +<th align="left">
   36.24 +  <div class="form-label">
   36.25 +  Make available to catalog
   36.26 +  </div>
   36.27 +</th>
   36.28 +<td>
   36.29 +  <div class="form-element">
   36.30 +   <dtml-let checked="for_catalog and 'checked' or ' '">
   36.31 +    <input type="checkbox" name="for_catalog" value="1" &dtml-checked; />
   36.32 +   </dtml-let>
   36.33 +  </div>
   36.34 +</td>
   36.35 +</tr>
   36.36 +
   36.37 +<tr>
   36.38 +<th align="left">
   36.39 +  <div class="form-label">
   36.40 +  Store in workflow status
   36.41 +  </div>
   36.42 +</th>
   36.43 +<td>
   36.44 +  <div class="form-element">
   36.45 +   <dtml-let checked="for_status and 'checked' or ' '">
   36.46 +    <input type="checkbox" name="for_status" value="1" &dtml-checked; />
   36.47 +   </dtml-let>
   36.48 +  </div>
   36.49 +</td>
   36.50 +</tr>
   36.51 +
   36.52 +<tr>
   36.53 +<th align="left">
   36.54 +  <div class="form-label">
   36.55 +  Variable update mode
   36.56 +  </div>
   36.57 +</th>
   36.58 +<td>
   36.59 +  <div class="form-element">
   36.60 +   <select name="update_always">
   36.61 +   <dtml-let checked="update_always and ' ' or 'selected'">
   36.62 +    <option value="" &dtml-checked;>Update only when the transition or
   36.63 +     new state specifies a new value</option>
   36.64 +   </dtml-let>
   36.65 +   <dtml-let checked="update_always and 'selected' or ' '">
   36.66 +    <option value="1" &dtml-checked;>Update on every transition</option>
   36.67 +   </dtml-let>
   36.68 +  </div>
   36.69 +</td>
   36.70 +</tr>
   36.71 +
   36.72 +<tr>
   36.73 +<th align="left">
   36.74 +  <div class="form-label">
   36.75 +  Default value
   36.76 +  </div>
   36.77 +</th>
   36.78 +<td>
   36.79 +  <div class="form-element">
   36.80 +  <input type="text" name="default_value" value="&dtml-default_value;" />
   36.81 +  </div>
   36.82 +</td>
   36.83 +</tr>
   36.84 +
   36.85 +<tr>
   36.86 +<th align="left">
   36.87 +  <div class="form-label">
   36.88 +  Default expression<br />(overrides default value)
   36.89 +  </div>
   36.90 +</th>
   36.91 +<td>
   36.92 +  <div class="form-element">
   36.93 +  <input type="text" name="default_expr" value="&dtml-getDefaultExprText;" size="50" />
   36.94 +  </div>
   36.95 +</td>
   36.96 +</tr>
   36.97 +
   36.98 +<tr>
   36.99 +<th align="left" valign="top">
  36.100 +  <div class="form-label">
  36.101 +  Info guard
  36.102 +  </div>
  36.103 +</th>
  36.104 +<td>
  36.105 + <dtml-with getInfoGuard>
  36.106 +  <dtml-var guardForm>
  36.107 + </dtml-with>
  36.108 +</td>
  36.109 +</tr>
  36.110 +
  36.111 +</table>
  36.112 +<input type="submit" name="submit" value="Save changes" />
  36.113 +</form>
  36.114 +<dtml-var manage_page_footer>
    37.1 new file mode 100644
    37.2 --- /dev/null
    37.3 +++ b/dtml/variables.dtml
    37.4 @@ -0,0 +1,57 @@
    37.5 +<dtml-var manage_page_header>
    37.6 +<dtml-var manage_tabs>
    37.7 +<form action="&dtml-absolute_url;" method="POST">
    37.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    37.9 +<dtml-in values sort=id>
   37.10 + <tr bgcolor="#eeeeee">
   37.11 +  <th align="left" colspan="2">
   37.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   37.13 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   37.14 +   &nbsp;
   37.15 +   &dtml-description;
   37.16 +  </th>
   37.17 + </tr>
   37.18 +
   37.19 + <tr>
   37.20 +  <th width="10%"></th>
   37.21 +  <td>
   37.22 +   Available to catalog:
   37.23 +   <code><dtml-if for_catalog>Yes<dtml-else>No</dtml-if></code><br />
   37.24 +   Stored in status:
   37.25 +   <code><dtml-if for_status>Yes<dtml-else>No</dtml-if></code><br />
   37.26 +   <dtml-if default_value>
   37.27 +    Default value: <code>&dtml-default_value;</code><br />
   37.28 +   </dtml-if>
   37.29 +   <dtml-if getDefaultExprText>
   37.30 +    Default expr: <code>&dtml-getDefaultExprText;</code><br />
   37.31 +   </dtml-if>
   37.32 +   <dtml-if getInfoGuardSummary>
   37.33 +    <dtml-var getInfoGuardSummary><br />
   37.34 +   </dtml-if>
   37.35 +  </td>
   37.36 + </tr>
   37.37 +<dtml-else>
   37.38 + <tr><td><em>No variables defined.</em></td></tr>
   37.39 +</dtml-in>
   37.40 +</table>
   37.41 +<dtml-if values>
   37.42 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   37.43 +<input type="submit" name="deleteVariables:method" value="Delete" />
   37.44 +</dtml-if>
   37.45 +</form>
   37.46 +
   37.47 +<hr />
   37.48 +<form action="addVariable" method="POST">
   37.49 +<h3>Add a variable</h3>
   37.50 +<p>Id <input type="text" name="id" value="" />
   37.51 +<input type="submit" name="submit" value="Add" /></p>
   37.52 +</form>
   37.53 +
   37.54 +<hr />
   37.55 +<form action="setStateVar" method="POST">
   37.56 +State variable name: <input type="text" name="id" value="&dtml-getStateVar;" />
   37.57 +<input type="submit" name="submit" value="Change" />
   37.58 +<i class="form-help">(Be careful!)</i>
   37.59 +</form>
   37.60 +
   37.61 +<dtml-var manage_page_footer>
    38.1 new file mode 100644
    38.2 --- /dev/null
    38.3 +++ b/dtml/workflow_groups.pt
    38.4 @@ -0,0 +1,60 @@
    38.5 +<h1 tal:replace="structure here/manage_page_header">Header</h1>
    38.6 +<h2 tal:define="manage_tabs_message request/manage_tabs_message | nothing"
    38.7 +    tal:replace="structure here/manage_tabs">Tabs</h2>
    38.8 +
    38.9 +<table>
   38.10 +<tr>
   38.11 +<td width="50%" valign="top">
   38.12 +
   38.13 +<form action="." method="POST" tal:attributes="action here/absolute_url"
   38.14 +  tal:define="groups here/getGroups">
   38.15 +<h3>Managed Groups</h3>
   38.16 +<div class="form-help">
   38.17 +This workflow controls access by the selected groups.  The mappings
   38.18 +from group to role depend on the workflow state.
   38.19 +</div>
   38.20 +<div tal:repeat="group groups">
   38.21 +<input type="checkbox" name="groups:list" tal:attributes="value group" />
   38.22 +<span tal:replace="group">Everyone</span>
   38.23 +</div>
   38.24 +<div tal:condition="not:groups">
   38.25 +<em>No groups are managed by this workflow.</em>
   38.26 +</div>
   38.27 +
   38.28 +<div tal:condition="groups">
   38.29 +<input type="submit" name="delGroups:method" value="Remove" />
   38.30 +</div>
   38.31 +
   38.32 +<hr />
   38.33 +
   38.34 +<h3>Add a managed group</h3>
   38.35 +<select name="group">
   38.36 + <option tal:repeat="group here/getAvailableGroups"
   38.37 +   tal:attributes="value group" tal:content="group" />
   38.38 +</select>
   38.39 +<input type="submit" name="addGroup:method" value="Add" />
   38.40 +</form>
   38.41 +
   38.42 +</td>
   38.43 +<td width="50%" style="border-left: 1px solid black; padding-left: 1em;"
   38.44 +  valign="top">
   38.45 +
   38.46 +<form method="POST" tal:attributes="action here/absolute_url">
   38.47 +<h3>Roles Mapped to Groups</h3>
   38.48 +<div class="form-help">
   38.49 +This workflow maps the following roles to groups.  Roles not selected
   38.50 +are managed outside this workflow.
   38.51 +<div tal:define="roles here/getRoles"
   38.52 +  tal:repeat="role here/getAvailableRoles">
   38.53 +<input type="checkbox" name="roles:list" tal:attributes="value role;
   38.54 +  checked python:role in roles" /><span tal:content="role" />
   38.55 +</div>
   38.56 +</div>
   38.57 +<input type="submit" name="setRoles:method" value="Save Changes" />
   38.58 +</form>
   38.59 +
   38.60 +</td>
   38.61 +</tr>
   38.62 +</table>
   38.63 +
   38.64 +<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
    39.1 new file mode 100644
    39.2 --- /dev/null
    39.3 +++ b/dtml/workflow_permissions.dtml
    39.4 @@ -0,0 +1,43 @@
    39.5 +<dtml-var manage_page_header>
    39.6 +<dtml-var manage_tabs>
    39.7 +
    39.8 +<form action="&dtml-absolute_url;" method="POST">
    39.9 +<table>
   39.10 +
   39.11 +<tr>
   39.12 +<td class="form-help">
   39.13 +The selected permissions are managed by this workflow.  The role to permission
   39.14 +mappings for an object in this workflow depend on its state.
   39.15 +</td>
   39.16 +</tr>
   39.17 +
   39.18 +<dtml-in permissions sort>
   39.19 +<tr>
   39.20 +<td>
   39.21 +<input type="checkbox" name="ps:list" value="&dtml-sequence-item;">
   39.22 +&dtml-sequence-item;
   39.23 +</td>
   39.24 +</tr>
   39.25 +<dtml-else>
   39.26 +<tr>
   39.27 +<td>
   39.28 +<em>No permissions are managed by this workflow.</em>
   39.29 +</td>
   39.30 +</tr>
   39.31 +</dtml-in>
   39.32 +
   39.33 +</table>
   39.34 +<dtml-if permissions>
   39.35 +<input type="submit" name="delManagedPermissions:method" value="Remove selected" />
   39.36 +</dtml-if>
   39.37 +<hr />
   39.38 +<h3>Add a managed permission</h3>
   39.39 +<select name="p">
   39.40 + <dtml-in getPossiblePermissions><dtml-if
   39.41 +   expr="_['sequence-item'] not in permissions">
   39.42 +  <option value="&dtml-sequence-item;">&dtml-sequence-item;</option>
   39.43 + </dtml-if></dtml-in>
   39.44 +</select>
   39.45 +<input type="submit" name="addManagedPermission:method" value="Add" />
   39.46 +</form>
   39.47 +<dtml-var manage_page_footer>
    40.1 new file mode 100644
    40.2 --- /dev/null
    40.3 +++ b/dtml/workflow_properties.dtml
    40.4 @@ -0,0 +1,39 @@
    40.5 +<dtml-var manage_page_header>
    40.6 +<dtml-var manage_tabs>
    40.7 +
    40.8 +<form action="setProperties" method="POST">
    40.9 +<table>
   40.10 +
   40.11 +<tr>
   40.12 +<th align="left">Id</th>
   40.13 +<td>&dtml-id;</td>
   40.14 +</tr>
   40.15 +
   40.16 +<tr>
   40.17 +<th align="left">Title</th>
   40.18 +<td><input type="text" name="title" value="&dtml-title;" size="40" /></td>
   40.19 +</tr>
   40.20 +
   40.21 +<tr>
   40.22 +<th align="left">'Manager' role bypasses guards</th>
   40.23 +<td>
   40.24 +<dtml-let cb="manager_bypass and 'checked=\'checked\'' or ''">
   40.25 +<input type="checkbox" name="manager_bypass" &dtml-cb; />
   40.26 +</dtml-let>
   40.27 +</td>
   40.28 +</tr>
   40.29 +
   40.30 +<tr>
   40.31 +<th align="left" valign="top">Instance creation conditions</th>
   40.32 +<td>
   40.33 + <dtml-with getGuard>
   40.34 +  <dtml-var guardForm>
   40.35 + </dtml-with>
   40.36 +</td>
   40.37 +</tr>
   40.38 +
   40.39 +</table>
   40.40 +
   40.41 +<input type="submit" name="submit" value="Save changes" />
   40.42 +</form>
   40.43 +<dtml-var manage_page_footer>
    41.1 new file mode 100644
    41.2 --- /dev/null
    41.3 +++ b/dtml/worklist_properties.dtml
    41.4 @@ -0,0 +1,87 @@
    41.5 +<dtml-var manage_page_header>
    41.6 +<dtml-var manage_tabs>
    41.7 +
    41.8 +<form action="setProperties" method="POST">
    41.9 +<table>
   41.10 +
   41.11 +<tr>
   41.12 +<th align="left">Id</th>
   41.13 +<td>&dtml-id;</td>
   41.14 +</tr>
   41.15 +
   41.16 +<tr>
   41.17 +<th align="left">Description</th>
   41.18 +<td>
   41.19 +<input type="text" name="description" value="&dtml-description;" size="50" />
   41.20 +</td>
   41.21 +</tr>
   41.22 +
   41.23 +<tr>
   41.24 +<th align="left" valign="top">
   41.25 +  <div class="form-label">
   41.26 +  Cataloged variable matches (formatted)
   41.27 +  </div>
   41.28 +</th>
   41.29 +<td>
   41.30 +  <table>
   41.31 +   <dtml-in getAvailableCatalogVars>
   41.32 +    <tr>
   41.33 +     <th align="left">&dtml-sequence-item; =</th>
   41.34 +     <td>
   41.35 +      <dtml-let value="getVarMatchText(_['sequence-item'])">
   41.36 +       <input type="text" name="var_match_&dtml-sequence-item;"
   41.37 +        value="&dtml-value;" />
   41.38 +      </dtml-let>
   41.39 +     </td>
   41.40 +    </tr>
   41.41 +   </dtml-in>
   41.42 +  </table>
   41.43 +</td>
   41.44 +</tr>
   41.45 +
   41.46 +<tr>
   41.47 +<th align="left" valign="top">Display in actions box</th>
   41.48 +<td>
   41.49 + <table>
   41.50 +  <tr>
   41.51 +   <th align="left">Name (formatted)</th>
   41.52 +   <td>
   41.53 +    <input type="text" name="actbox_name"
   41.54 +     value="&dtml-actbox_name;" size="50" />
   41.55 +   </td>
   41.56 +  </tr>
   41.57 +  <tr>
   41.58 +   <th align="left">URL (formatted)</th>
   41.59 +   <td>
   41.60 +    <input type="text" name="actbox_url"
   41.61 +     value="&dtml-actbox_url;" size="50" />
   41.62 +   </td>
   41.63 +  </tr>
   41.64 +  <tr>
   41.65 +   <th align="left">Category</th>
   41.66 +   <td>
   41.67 +    <input type="text" name="actbox_category"
   41.68 +     value="&dtml-actbox_category;" />
   41.69 +   </td>
   41.70 +  </tr>
   41.71 + </table>
   41.72 +</td>
   41.73 +</tr>
   41.74 +
   41.75 +<tr>
   41.76 +<th align="left" valign="top">
   41.77 +  <div class="form-label">
   41.78 +  Guard
   41.79 +  </div>
   41.80 +</th>
   41.81 +<td>
   41.82 + <dtml-with getGuard>
   41.83 +  <dtml-var guardForm>
   41.84 + </dtml-with>
   41.85 +</td>
   41.86 +</tr>
   41.87 +
   41.88 +</table>
   41.89 +<input type="submit" name="submit" value="Save changes" />
   41.90 +</form>
   41.91 +<dtml-var manage_page_footer>
    42.1 new file mode 100644
    42.2 --- /dev/null
    42.3 +++ b/dtml/worklists.dtml
    42.4 @@ -0,0 +1,57 @@
    42.5 +<dtml-var manage_page_header>
    42.6 +<dtml-var manage_tabs>
    42.7 +<form action="&dtml-absolute_url;" method="POST">
    42.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    42.9 +<dtml-in values sort=id>
   42.10 + <tr bgcolor="#eeeeee">
   42.11 +  <th align="left" colspan="2">
   42.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   42.13 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   42.14 +   &nbsp;
   42.15 +   &dtml-description;
   42.16 +  </th>
   42.17 + </tr>
   42.18 +
   42.19 + <tr>
   42.20 +  <th width="10%"></th>
   42.21 +  <td>
   42.22 +   <dtml-if name_fmt>
   42.23 +    Name format: <code>&dtml-name_fmt;</code><br />
   42.24 +   </dtml-if>
   42.25 +   <dtml-if getVarMatchKeys>
   42.26 +    Catalog matches:
   42.27 +    <dtml-in getVarMatchKeys sort>
   42.28 +    <dtml-let key=sequence-item value="getVarMatch(key)">
   42.29 +     <code>&dtml-key;</code> =
   42.30 +     <dtml-in value>
   42.31 +       <code>&dtml-sequence-item;</code>
   42.32 +       <dtml-unless sequence-end>or</dtml-unless>
   42.33 +     </dtml-in>
   42.34 +     <dtml-unless sequence-end>;</dtml-unless>
   42.35 +    </dtml-let>
   42.36 +    </dtml-in>
   42.37 +    <br />
   42.38 +   </dtml-if>
   42.39 +   <dtml-if getGuardSummary>
   42.40 +    <dtml-var getGuardSummary><br />
   42.41 +   </dtml-if>
   42.42 +  </td>
   42.43 + </tr>
   42.44 +<dtml-else>
   42.45 + <tr><td><em>No worklists defined.</em></td></tr>
   42.46 +</dtml-in>
   42.47 +</table>
   42.48 +<dtml-if values>
   42.49 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   42.50 +<input type="submit" name="deleteWorklists:method" value="Delete" />
   42.51 +</dtml-if>
   42.52 +</form>
   42.53 +
   42.54 +<hr />
   42.55 +<form action="addWorklist" method="POST">
   42.56 +<h3>Add a worklist</h3>
   42.57 +<p>Id <input type="text" name="id" value="" />
   42.58 +<input type="submit" name="submit" value="Add" /></p>
   42.59 +</form>
   42.60 +
   42.61 +<dtml-var manage_page_footer>
    43.1 new file mode 100644
    43.2 --- /dev/null
    43.3 +++ b/help/001-overview.stx
    43.4 @@ -0,0 +1,82 @@
    43.5 +Overview
    43.6 +
    43.7 + DCWorkflows provide workflow objects that are fully customizable via
    43.8 + the Zope Management Interface. You can specify the states, and the
    43.9 + permissions they set on content that is in that state, the transitions 
   43.10 + between those states, and other things like variables for things that
   43.11 + aren't well represented by states, work lists for reviewers, and
   43.12 + scripts to embody complex guards and to extend pre and post transition
   43.13 + behaviour.
   43.14 +
   43.15 + The process for creating a workflow runs something 
   43.16 + like this:
   43.17 +  
   43.18 +  - Draw a state diagram with the nodes (bubbles) as states and the
   43.19 +  arcs (arrows) as transitions. Remember to consider all the states
   43.20 +  your content can be in, and for each state, consider who should
   43.21 +  have permission to access and change the content. Consider what
   43.22 +  actions the users will perform to make the transitions between states,
   43.23 +  and not only who will be allowed to perform them, but who will be
   43.24 +  *required* to perform them.
   43.25 +
   43.26 +    It's often a good idea to start on paper, then transfer
   43.27 +    the diagram to a digital copy using a diagram/flowchart tool, such as
   43.28 +    'dia', so that you have an image to go with later documentation. This
   43.29 +    process tends to make it easier to spot corner cases before you
   43.30 +    actually create the workflow object.
   43.31 +
   43.32 +  - Start by creating an example DCworkflow, rather than a new one, as it's 
   43.33 +  faster to delete all the states and transitions than it is to create all the 
   43.34 +  standard variables that tend to be used by the CMF. [**Note:**
   43.35 +  Perhaps we should have a bare dcworkflow, a workflow with standard
   43.36 +  variables, and the couple of standard examples.]
   43.37 +
   43.38 +  - In the permissions tab, select all the permissions that you want the 
   43.39 +  workflow to govern. These will be dependent on the types of content
   43.40 +  you'll be using with the workflow; 'Access contents information',
   43.41 +  'Modify portal content', and 'View' are the standard permissions for 
   43.42 +  the default portal content types.
   43.43 +
   43.44 +  - Define any extra variables that you need for information that isn't
   43.45 +  well represented by states. [**Note:** generic examples? I can think of
   43.46 +  a few that could appear in some use cases, but they're all
   43.47 +  idiosyncratic of particular publishing needs]
   43.48 +
   43.49 +  - Set up the states for your workflow, one for each node in your state 
   43.50 +  diagram. Try to stick to the standard names for a publication workflow, as 
   43.51 +  some badly behaved products have states like 'published' hardcoded into
   43.52 +  their searches (ie CalendarTool, last I looked) [**Note**: Maybe I
   43.53 +  should just file some bug reports rather than casting aspersions :-)].
   43.54 +  Set up the permissions on the states now, as well, though see the 
   43.55 +  "State Tab" section for advice.
   43.56 +
   43.57 +  - Set up any scripts that you will be using in your transitions, such
   43.58 +  as pre and post transition scripts and to handle complex guard
   43.59 +  conditions. Just set up skeletons for now, if you haven't though
   43.60 +  through all the details.
   43.61 +
   43.62 +  - Create your transitions from all the arcs on your state diagram. You
   43.63 +  should be able to pick the right destination state as all your states
   43.64 +  are already defined, and set up the right scripts to run, as you've
   43.65 +  defined those as well. It's worth noting that the guard behaviour is
   43.66 +  such that if any guard matches, the transition can occur. You can
   43.67 +  specify more than one permission, role or expression by separating
   43.68 +  them with a semicolon.
   43.69 +
   43.70 +  - Go back to the states tab and, for each state, set the possible
   43.71 +  transitions from the list of all available transitions in each state.
   43.72 +
   43.73 +  - Finally, in the work lists tab, create any work lists for any states
   43.74 +  that need them. Work lists are actions that indicate how many objects
   43.75 +  of a given state are present, and usually link to some search page
   43.76 +  that lists the actual object instances. You typically use them to list
   43.77 +  all the pending content waiting for review. Work lists have several
   43.78 +  unusual behaviours, however, so check the specific notes in the
   43.79 +  "Worklists" section.
   43.80 +
   43.81 + By working in this order, you will tend to step through the 
   43.82 + creation process one tab at a time, rather than switching back and
   43.83 + forth between them, which tends to be slower and somewhat confusing.
   43.84 +
   43.85 +
   43.86 +
    44.1 new file mode 100644
    44.2 --- /dev/null
    44.3 +++ b/help/002-expressions.stx
    44.4 @@ -0,0 +1,64 @@
    44.5 +Expressions
    44.6 +
    44.7 + Expressions in DCWorkflow are TALES expressions. See 
    44.8 + "TALES Overview":/Control_Panel/Products/PageTemplates/Help/tales.stx
    44.9 + for general TALES information. They are used as access guards and for
   44.10 + the setting variable values. 
   44.11 + 
   44.12 + [**Note:** I haven't figured out what all these contexts actually are
   44.13 + and what you can use them for. Explanations are is welcome!]
   44.14 +
   44.15 + Some of the contexts have slightly different meanings from what is provided
   44.16 + for expressions in page templates:
   44.17 +
   44.18 +  'here' -- The content object (rather than the workflow object)
   44.19 +  'container' -- The content object's container
   44.20 +
   44.21 + Several other contexts are also 
   44.22 + provided:
   44.23 +
   44.24 +  'state_change' -- A special object containing information about the
   44.25 +  state change (see below)
   44.26 +  'transition' -- The transition object being executed
   44.27 +  'status' -- The former status
   44.28 +  'workflow' -- The workflow definition object
   44.29 +  'scripts' -- The scripts in the workflow definition object
   44.30 +
   44.31 + 'state_change' objects provide the following attributes, some of which
   44.32 + are duplicates of the above information:
   44.33 +
   44.34 +   - 'status' is a mapping containing the workflow status. This
   44.35 +   includes all the variables defined in the variable tab with "store
   44.36 +   in state" checked.
   44.37 +
   44.38 +   - 'object' is the object being modified by workflow.
   44.39 +     (Same as the 'here' variable above.)
   44.40 +
   44.41 +   - 'workflow' is the workflow definition object.  (Same as the
   44.42 +   'workflow' variable above.)
   44.43 +  
   44.44 +   - 'transition' is the transition object being executed.  (Same
   44.45 +   as the 'transition' variable above.)
   44.46 +
   44.47 +   - 'old_state' is the former state object.  The name of the former state,
   44.48 +   for example "published", is available as 'old_state.getId()'.  (Note
   44.49 +   that DCWorkflow defines 'state' and 'status' as different entities;
   44.50 +   the name of the current 'state' is stored in the 'status'.  The word
   44.51 +   clash is unfortunate; patches welcome.)
   44.52 +
   44.53 +   - 'new_state' is the destination state object.  Use 'new_state.getId()'
   44.54 +   to access the new state name.
   44.55 +
   44.56 +   - 'kwargs' is the keyword arguments passed to the doActionFor() method.
   44.57 +
   44.58 +   - 'getHistory()', a method that returns a copy of the object's workflow
   44.59 +   history.
   44.60 +
   44.61 +   - 'getPortal()', which returns the root of the portal.
   44.62 +
   44.63 +   - 'ObjectDeleted' and 'ObjectMoved', exceptions that can be raised by
   44.64 +   scripts to indicate to the workflow that an object has been moved or
   44.65 +   deleted.
   44.66 +
   44.67 +   - 'getDateTime' is a method that returns the DateTime of the transition.
   44.68 +
    45.1 new file mode 100644
    45.2 --- /dev/null
    45.3 +++ b/help/003-guards.stx
    45.4 @@ -0,0 +1,26 @@
    45.5 +Guards
    45.6 +
    45.7 +  Guard settings control access to the transitions, variables or work
    45.8 +  lists. If a user possesses any of the permissions or roles
    45.9 +  specified, or if one of the expressions evaluates as true, then
   45.10 +  whatever is being guarded is accessible. If no permissions, roles
   45.11 +  or expressions are specified, access is automatically granted.
   45.12 + 
   45.13 +  You can supply several options in each field by separating them with
   45.14 +  a semicolon.
   45.15 +
   45.16 +  The context in which the guards evaluate permissions and roles is
   45.17 +  obviously important. In the case of transitions and work lists, it 
   45.18 +  depends on the category. If it's 'worklist', then the context is 
   45.19 +  that of the content object, and local roles will behave just as
   45.20 +  you'd expect. If the category is 'global', then the context will be
   45.21 +  the site root, so the local roles between the site root and the
   45.22 +  content won't be considered.
   45.23 +
   45.24 +  [**Note:** What about variables?]
   45.25 +
   45.26 +  
   45.27 +
   45.28 +
   45.29 +
   45.30 +
    46.1 new file mode 100644
    46.2 --- /dev/null
    46.3 +++ b/help/004-actionbox.stx
    46.4 @@ -0,0 +1,39 @@
    46.5 +Action Boxes
    46.6 +
    46.7 + Action box settings are required for work lists and any transition that
    46.8 + is intended to be a user initiated action. They define how the action 
    46.9 + will appear in the action box, what section it will appear in and
   46.10 + what it will link to.
   46.11 +  
   46.12 + Names and URLs for the actions box can be formatted using standard Python
   46.13 + string formatting.  An example::
   46.14 +
   46.15 +   %(content_url)s/content_submit_form
   46.16 +
   46.17 + The string '%(content_url)s' will be replaced by the value of content_url.
   46.18 + The following names are available:
   46.19 +
   46.20 +   - portal_url
   46.21 +
   46.22 +   - folder_url
   46.23 +
   46.24 +   - content_url
   46.25 +
   46.26 +   - count (Available in work lists only. Represents the number of items in
   46.27 +   the work list.)
   46.28 +
   46.29 + The following names are also available, in case there is any use for them.
   46.30 + They are not strings.
   46.31 +
   46.32 +  - portal
   46.33 +
   46.34 +  - folder
   46.35 +
   46.36 +  - content
   46.37 +
   46.38 +  - isAnonymous
   46.39 +
   46.40 +  Note that this formatting is done using standard Python string formatting
   46.41 +  rather than TALES.  It might be more appropriate to use TALES instead.
   46.42 +  As always, patches welcome.
   46.43 +
    47.1 new file mode 100644
    47.2 --- /dev/null
    47.3 +++ b/help/011-states.stx
    47.4 @@ -0,0 +1,45 @@
    47.5 +States Tab
    47.6 +
    47.7 + From the states tab it's possible to add new states, and rename and
    47.8 + delete existing states. It is also possible to set a particular state
    47.9 + to be the initial state that new content is set to when created.
   47.10 +
   47.11 + The list of existing states also displays each state's title and all
   47.12 + the possible transitions from that state (and their titles). You can go
   47.13 + straight to the details of each state and transition from here.
   47.14 +
   47.15 + Within a state's properties tab you can set the title, description, 
   47.16 + and the transitions that are possible from this state from a list of
   47.17 + all the available transitions created in the workflow's 
   47.18 + transitions tab.
   47.19 +
   47.20 + In the state's permissions tab, you can set up the roles to
   47.21 + permissions mappings that will apply to roles when content 
   47.22 + managed by this workflow is in this state. It uses the usual cookie
   47.23 + cutter approach as do all other permissions tabs, except that the
   47.24 + only permissions listed are those that have been selected to be
   47.25 + managed by the workflow from the workflow's permissions tab.
   47.26 + 
   47.27 + A good strategy for managing permissions on each state is to rely on
   47.28 + acquisition for the "published" states, and to drop acquisition and
   47.29 + use explicit permissions on states that are private or interim
   47.30 + publishing states. This way, you can modify the access policy to 
   47.31 + "published" content at the site root or for specific folders without
   47.32 + having to modify each workflow's set of "published" states.
   47.33 + 
   47.34 + [**Note**: The available roles in the permissions tab will be
   47.35 + whatever is acquired from the site root, so I guess creating 
   47.36 + roles under sub-folders ought to be discouraged if people want
   47.37 + to use them in workflows]
   47.38 +
   47.39 + Reviewer roles should either have view permissions on every
   47.40 + state or you should change the appropriate skins to take them
   47.41 + somewhere sensible after a transition or they'll end up with an ugly
   47.42 + access denied page after sending content back to private state.
   47.43 +
   47.44 + In the state's variables tab, you can add, change and delete variables
   47.45 + that you want to assign a value to when objects move into 
   47.46 + this state. The available variables are set in the workflow's
   47.47 + variables tab, and the value is a TALES expression (see Expressions
   47.48 + for more details).
   47.49 +
    48.1 new file mode 100644
    48.2 --- /dev/null
    48.3 +++ b/help/021-transition.stx
    48.4 @@ -0,0 +1,57 @@
    48.5 +Transitions Tab
    48.6 +
    48.7 + Transitions are the actions that move content from one state in the
    48.8 + workflow to another. From the transitions tab it's possible to add new
    48.9 + transitions, and rename and delete existing transitions.
   48.10 +
   48.11 + The list of existing transitions also displays a summary of each
   48.12 + transition's title, description, destination state, trigger, guards, 
   48.13 + and action box entry. You can click through each transition to access 
   48.14 + their details.
   48.15 +
   48.16 + Within a transition's properties tab you can set the title, and 
   48.17 + a collection of properties the define the transtion's behaviour, as
   48.18 + follows:
   48.19 +
   48.20 +  Destination state -- selected from all the states defined in the 
   48.21 +  states tab. A transition can remain in state, which is useful for a
   48.22 +  reviewer adding comments to the review history, but not taking any
   48.23 +  action, updating some variable, or invoking scripts.
   48.24 +
   48.25 +  Trigger type - There are three types:
   48.26 +
   48.27 +    - User actions are the familiar user initiated transitions activated
   48.28 +    by actions in the action box.
   48.29 + 
   48.30 +    - Automatic transitions are executed any time other workflow
   48.31 +    events occur; so if a user action results in the content moving
   48.32 +    to a state that has automatic transitions, they will be executed.
   48.33 +    (You should use mutually exclusive guards to prevent indeterminate
   48.34 +    behavior.)
   48.35 +
   48.36 +    - WorkflowMethod initiate actions occur as a side effect when
   48.37 +    a method of the content object with the same name as the
   48.38 +    transtion, and that has been wrapped in the WorkflowMethod class
   48.39 +    is called.  DCWorkflow executes the body of the method just before
   48.40 +    the workflow transition.
   48.41 +
   48.42 +
   48.43 +  Scripts - Perform complicated behaviours either before or after the
   48.44 +  transition takes place. Scripts of all kinds are defined in the
   48.45 +  workflow's scripts tab. Scripts called from here must accept only
   48.46 +  one argument; a 'status_change' object. See Expressions for more
   48.47 +  details.
   48.48 +  
   48.49 +  Guards and Action boxes -- See the "Guards" and "Action Boxes"
   48.50 +  sections for specific details about those fields. Note that
   48.51 +  automatic and WorkflowMethod transitions don't need the action box
   48.52 +  fields to be filled out.
   48.53 +
   48.54 +  What the action should link to.
   48.55 +
   48.56 +
   48.57 + In the transition's variables tab, you can add, change and delete
   48.58 + variables that you want to assign a value to, when the transition is
   48.59 + executed. The available variables are set in the workflow's variables
   48.60 + tab, and the value is a TALES expression (see Expressions for more
   48.61 + details).
    49.1 new file mode 100644
    49.2 --- /dev/null
    49.3 +++ b/help/031-variables.stx
    49.4 @@ -0,0 +1,48 @@
    49.5 +Variables Tab
    49.6 +
    49.7 + Variables are used to handle the state of various workflow related
    49.8 + information that doesn't justify a state of it's own. The default 
    49.9 + CMF workflows use variables to track status history comments, and 
   49.10 + store the the last transition, who initiated it and when, for example.
   49.11 + From the variables tab it's possible to add new variables, and rename
   49.12 + and delete existing variables.
   49.13 +
   49.14 + The list of existing variables also displays a summary of each
   49.15 + variable's description, catalog availability, workflow status, default
   49.16 + value or expression and any access guards. You can click through to
   49.17 + each variable to configure it's details.
   49.18 +
   49.19 + In each variable's property tab you can set the variable's
   49.20 + description and a collection of properties the define the variable's
   49.21 + behaviour, as follows:
   49.22 + 
   49.23 +  Make available to catalog -- Just as it says, it makes this variable
   49.24 +  available to the catalog for indexing, however it doesn't
   49.25 +  automatically create an index for it - you have to create one by
   49.26 +  hand that reflects the content of the variable. Once indexed, you can
   49.27 +  query the catalog for content that has a particular value in is
   49.28 +  variable, and update the variable by workflow actions.
   49.29 + 
   49.30 +  Store in workflow status -- The workflow status is a mapping that
   49.31 +  exists in the state_change object that is passed to scripts and
   49.32 +  available to expressions. 
   49.33 + 
   49.34 +  Variable update mode -- Select whether the variable is updated on
   49.35 +  every transition (in which case, you should set a default
   49.36 +  expression), or whether it should only update if a transition or
   49.37 +  state sets a value.
   49.38 +
   49.39 +  Default value -- Set the default value to some string.
   49.40 +
   49.41 +  Default expression -- This is a TALES expression (as described in
   49.42 +  Expressions) and overrides the default value.
   49.43 +
   49.44 +  Guards -- See the "Guards" section. 
   49.45 +
   49.46 +
   49.47 + State variable - stores the name of the variable the current state of
   49.48 +the content is stored in. CMF uses 'review_state' by default, and will
   49.49 +have already created a FieldIndex for it. The state variable is
   49.50 +effectively a variable with "Make available to catalog" set, a default
   49.51 +value of whatever the initial state is and a default expression that
   49.52 +sets to the new state on every transition.
    50.1 new file mode 100644
    50.2 --- /dev/null
    50.3 +++ b/help/041-worklists.stx
    50.4 @@ -0,0 +1,128 @@
    50.5 +Worklists Tab
    50.6 +
    50.7 + Work lists are a way to make people aware of tasks they are required
    50.8 + to perform, usually reviewing content in the publishing context.
    50.9 + Work lists are implemented as a catalog query that puts an action in
   50.10 + the actions box when there are some tasks the member needs to perform.  
   50.11 +
   50.12 + From the work lists tab it's possible to add new work lists, and rename and
   50.13 + delete existing work lists. The list of existing work lists also
   50.14 + displays a short summary of each work list's description, the catalog query
   50.15 + it uses, and any guard conditions. You can access the details of each
   50.16 + work list by clicking on them.
   50.17 +
   50.18 + In each work list's properties tab, you can set the description of 
   50.19 + the work list, and it's various behaviour defining properties. The
   50.20 + "Catalog variable matches" field sets the state that is work list
   50.21 + matches. The "variable_name = " text to the left of the text box is
   50.22 + the name of the state variable defined at the bottom of the variables
   50.23 + tab. The values can be set to a number of possible matches separated 
   50.24 + by semicolons. [**Note:** CVS feature. There's more in
   50.25 + doc/worklists.stx, but I'm not sure I understand the implications]
   50.26 +
   50.27 + The action box fields are discussed in more detail in the Action Box
   50.28 + section. In this case, the url that the work list links to should
   50.29 + probably implement a search page with a catalog query similar to the
   50.30 + "Catalog variable matches", otherwise the difference between the
   50.31 + number of items waiting and the items reported in the search will be
   50.32 + confusing.
   50.33 +
   50.34 + [**Note:** What we *really* need from the work list is a way to define
   50.35 +  full catalog queries for the action, and a new action box variable
   50.36 +  that urlquotes that query so it can be passed straight to the search
   50.37 +  page. This way, the work list count and the number of items on the 
   50.38 +  search page will be the same as they are derived from the same
   50.39 +  query string, defined in one place.
   50.40 +
   50.41 +  Reply from Shane: work lists already exercise the catalog too heavily,
   50.42 +  and most people don't understand their purpose.  Expanding their
   50.43 +  capabilities further could impact performance.  I think perhaps
   50.44 +  the UI should instead display work lists on a user's home page rather
   50.45 +  than the actions box, which would open up new possibilities.]
   50.46 + 
   50.47 + The guard fields are described in detail in the "Guards"
   50.48 + section. It's probably better to avoid using permission and role
   50.49 + guards, as they're not really necessary - a user will see a
   50.50 + work list action only if they can see content in that particular
   50.51 + state, so the state guards are usually sufficient. In addition, as
   50.52 + the work lists are in the 'global' actions category by default, and
   50.53 + global actions are evaluated in the context of the site root, local
   50.54 + roles like Owner or locally set Reviewer roles, and the permissions
   50.55 + they grant, will not apply. [*Note:* Does anyone know a good reason
   50.56 + why work lists appear in the global box rather than in the workflow
   50.57 + box? This particular problem should vanish if they are moved there.]
   50.58 +
   50.59 + Whether a work list action appears in the action box, and the 
   50.60 + number of items in the work list depends on several factors:
   50.61 +
   50.62 +  - The state that the work list is generated for
   50.63 +
   50.64 +  - The name of the state variable used to indicate the 
   50.65 +  current state of an object
   50.66 + 
   50.67 +  - Whether the user can view content which is in that
   50.68 +  state
   50.69 +
   50.70 + This has some unexpected consequences:
   50.71 +
   50.72 +  - If you have several workflow that use the same state variable,
   50.73 +  and similar state names, and each has a work list on, say, the 
   50.74 +  'pending' state, then both work lists will appear in the action box,
   50.75 +  and the number of items in each will be the total of all the content
   50.76 +  in a 'pending' state, regardless of which workflow manages that
   50.77 +  content (except that if the work list action entries are exactly the 
   50.78 +  same text, the action tool will filter out the duplicate).
   50.79 +
   50.80 +  - If each workflow manages the permissions on content in the
   50.81 +  'pending' state differently, by, say, using two different reviewer
   50.82 +  roles, then users who have one role and not the other will
   50.83 +  see a single work list entry with the right number of items, but 
   50.84 +  users with both roles will see the same as above.
   50.85 +
   50.86 + So there are a few tricks to getting the work lists to do the kinds of
   50.87 + things we want. 
   50.88 +
   50.89 + If you have several similar workflows, such as a standard one, and a 
   50.90 + couple of specialized ones for particular content, and you want to
   50.91 + have one reviewer role for the lot, then you should set up just one
   50.92 + work list in the standard workflow for the states that need them, and 
   50.93 + leave the other workflow to rely on that work list. 
   50.94 +
   50.95 + If you have a workflow that uses a different reviewer role than
   50.96 + other workflows, and consequently, you want it to have it's own
   50.97 + separate work lists, you have two choices. One is to use state names
   50.98 + that are unique to each workflow, while the second is 
   50.99 + to use state variable name that is unique each workflow. The
  50.100 + second option is obviously a lot easier, however, if you change the
  50.101 + name of the state variable when there exists content that is using 
  50.102 + this workflow, they will immediately loose there workflow state
  50.103 + and default to the initial state. In addition, you'll need to add
  50.104 + a field index for the new state variable name in the portal_catalog
  50.105 + tool, by hand.
  50.106 +
  50.107 + [Note: In the first instance, we could add an action box name field
  50.108 +  to each state so that nicely formated names appear in the action
  50.109 +  box for things like "Published (yet to be effective)" rather than
  50.110 +  "published_not_yet_effective", and so we can lie about the names
  50.111 +  to make them unique, so that "foo_workflow_pending" looks like
  50.112 +  "Pending". In the second instance, I see no reason why the state
  50.113 +  variable name change action shouldn't migrate the value of the old
  50.114 +  state variable to the new for all the content managed by this
  50.115 +  workflow, and it could probably automatically add indexes for 
  50.116 +  new state variable names if they don't already exists (and 
  50.117 +  perhaps remove indexes for state variable names not used elsewhere).
  50.118 +
  50.119 +  While we're thinking about ways to make sweeping workflow changes 
  50.120 +  less painful, there are a couple of changes that could be made 
  50.121 +  to the code that changes content type to workflow mappings: if a
  50.122 +  content to workflow mapping has changed, then, for each instance of
  50.123 +  that content type, attempt to keep the state variable the same
  50.124 +  unless that state doesn't exist in the new workflow, then evaluate
  50.125 +  any automatic transitions on that state. This way it's possible 
  50.126 +  to migrate between workflows by ensuring that states with the same
  50.127 +  name have the same semantics, or if they don't exists in the new 
  50.128 +  workflow, we can create placeholder states with an automatic
  50.129 +  transition to the state we want to be in.
  50.130 + ]
  50.131 +
  50.132 +
    51.1 new file mode 100644
    51.2 --- /dev/null
    51.3 +++ b/help/051-scripts.stx
    51.4 @@ -0,0 +1,20 @@
    51.5 +Scripts Tab
    51.6 +
    51.7 + Scripts are used to extend the workflow in various ways.  Scripts can
    51.8 + be External Methods, Python Scripts, DTML methods, or any other callable
    51.9 + Zope object.  They are accessible by name in expressions, eg::
   51.10 +
   51.11 +   scripts/myScript
   51.12 +
   51.13 + or::
   51.14 +
   51.15 +   python:scripts.myScript(arg1, arg2...) 
   51.16 +
   51.17 + From transitions, as before and after scripts, they are invoked with a
   51.18 + 'state_change' object as the first argument; see the Expressions section
   51.19 + for more details on the 'state_change' object.
   51.20 +
   51.21 + Objects under the scripts are managed in the usual ZMI fashion.
   51.22 +
   51.23 +
   51.24 +
    52.1 new file mode 100644
    52.2 --- /dev/null
    52.3 +++ b/help/061-permissions.stx
    52.4 @@ -0,0 +1,10 @@
    52.5 +Permissions Tab
    52.6 +
    52.7 + You can manage all of the actions a user can perform on an object by
    52.8 + setting up permissions to be managed by the workflow under the
    52.9 + permissions tab. Here, you can select which permissions should be
   52.10 + state-dependent from a list of all available permissions, and you
   52.11 + can delete previously selected permissions. In each state, use
   52.12 + it's permissions tab to set up the role to permission mappings
   52.13 + appropriate for that state.
   52.14 +
    53.1 new file mode 100644
    53.2 index 0000000000000000000000000000000000000000..5c7e1719d3a14b3f7586bd41c9b3d5aab30012ec
    53.3 GIT binary patch
    53.4 literal 107
    53.5 zc${<hbhEHb6krfwSjYed^$?0d@t>%3QEFmIYKlU6W=V!ZNJgrHyQgmegW^vXMlJ>>
    53.6 z1|5(pkVXb3Ws#PZ)vs^PV-8%ECAe4QH5*G(d+DPYt;>(End_SN$>;jEPzDBT03DJZ
    53.7 AQUCw|
    53.8 
    54.1 new file mode 100644
    54.2 index 0000000000000000000000000000000000000000..c795aa6a673395f91ae85a31eb0b634259a1233d
    54.3 GIT binary patch
    54.4 literal 118
    54.5 zc${<hbhEHb6krfwSjYeZ|Ns97(+rCLM4gLL6H8K46v{J8G895GQWe}ieFGR2f3h%g
    54.6 zF)%UcfK-7rGBD|hw5-fNTPe`-`a<T$C0s4?DRUT`7Kcn*KD{pcwNXy>q|Dw~=bB@G
    54.7 OUp^SB=8(Y1U=0A2cqRn^
    54.8 
    55.1 new file mode 100644
    55.2 index 0000000000000000000000000000000000000000..3dc9144a69150093245587b89e4ac49835b1c922
    55.3 GIT binary patch
    55.4 literal 111
    55.5 zc${<hbhEHb6krfwSjYeZ|Ns97(+rCLM4gLL6H8K46v{J8G895GQWe}ieFGR2f3h%g
    55.6 zF)%UcfK-7rGBBx6VOX|XV4_4Z|EaLIp(c6r6z;3aDW}hKvs7ABRq<ANa-{9uboS}2
    55.7 G4AuZd9v?FR
    55.8 
    56.1 new file mode 100644
    56.2 index 0000000000000000000000000000000000000000..65863a50682cf1b24ec16fa01d953c585c6710cd
    56.3 GIT binary patch
    56.4 literal 99
    56.5 zc${<hbhEHb6krfwXkY+=|Ns9h{u6aBN=+<DO;IS%EXhy^$w*aj_w)^5Q2fcl$i=|O
    56.6 xpaW6}(!#)`*wdd`u;ye8!xXKb0Xd@Hb6Ou+m_FWK65zQhbL+MQ@5`(V)&Pq#A1D9-
    56.7 
    57.1 new file mode 100644
    57.2 index 0000000000000000000000000000000000000000..da28011a44ad3948afa5a713f5fbeb05b97c8ca3
    57.3 GIT binary patch
    57.4 literal 97
    57.5 zc${<hbhEHb6krfwSjfQemx+m?p5Z?jDE?$&<YHiA&|v@qkURsE(G-48_0wG8P8)Zb
    57.6 w$(wXN?5(`^d}>UZlw-rP><LxtKU_2Nx&FiOx$S<YS8WdS{f=#@<YKS}000Ri=l}o!
    57.7 
    58.1 new file mode 100644
    58.2 index 0000000000000000000000000000000000000000..2c2d228f0a513947109d04318652b8008f835b18
    58.3 GIT binary patch
    58.4 literal 114
    58.5 zc${<hbhEHb6krfwSjYeZ|NsAIsAm8o#ebsCMX8A;sVNHOnI#ztAsML(?w-B@42nNl
    58.6 z7`Ygj8FWC(K^hsDwC4189(XIYXwIofhUM$F&s~<;r@)ZNGJz+u?9HiTMb_7I+qHk4
    58.7 M`~J^fgMq;s0C0LG?*IS*
    58.8 
    59.1 new file mode 100644
    59.2 --- /dev/null
    59.3 +++ b/permissions.py
    59.4 @@ -0,0 +1,25 @@
    59.5 +""" DCWorkflow product permissions
    59.6 +
    59.7 +$Id: permissions.py,v 1.2 2004/04/29 16:13:23 tseaver Exp $
    59.8 +"""
    59.9 +from AccessControl import ModuleSecurityInfo
   59.10 +
   59.11 +security = ModuleSecurityInfo('Products.DCWorkflow.permissions')
   59.12 +
   59.13 +security.declarePublic('AccessContentsInformation')
   59.14 +from Products.CMFCore.permissions import AccessContentsInformation
   59.15 +
   59.16 +security.declarePublic('ManagePortal')
   59.17 +from Products.CMFCore.permissions import ManagePortal
   59.18 +
   59.19 +security.declarePublic('ModifyPortalContent')
   59.20 +from Products.CMFCore.permissions import ModifyPortalContent
   59.21 +
   59.22 +security.declarePublic('RequestReview')
   59.23 +from Products.CMFCore.permissions import RequestReview
   59.24 +
   59.25 +security.declarePublic('ReviewPortalContent')
   59.26 +from Products.CMFCore.permissions import ReviewPortalContent
   59.27 +
   59.28 +security.declarePublic('View')
   59.29 +from Products.CMFCore.permissions import View
    60.1 new file mode 100644
    60.2 --- /dev/null
    60.3 +++ b/tests/__init__.py
    60.4 @@ -0,0 +1,6 @@
    60.5 +"""\
    60.6 +Unit test package for DCWorkflow.
    60.7 +
    60.8 +As test suites are added, they should be added to the
    60.9 +mega-test-suite in Products.DCWorkflow.tests.test_all.py
   60.10 +"""
    61.1 new file mode 100644
    61.2 --- /dev/null
    61.3 +++ b/tests/test_DCWorkflow.py
    61.4 @@ -0,0 +1,143 @@
    61.5 +##############################################################################
    61.6 +#
    61.7 +# Copyright (c) 2003 Zope Corporation and Contributors. All Rights Reserved.
    61.8 +#
    61.9 +# This software is subject to the provisions of the Zope Public License,
   61.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   61.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   61.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   61.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   61.14 +# FOR A PARTICULAR PURPOSE.
   61.15 +#
   61.16 +##############################################################################
   61.17 +""" Unit tests for DCWorkflow module.
   61.18 +
   61.19 +$Id: test_DCWorkflow.py,v 1.4.2.4 2005/07/08 13:24:33 tseaver Exp $
   61.20 +"""
   61.21 +
   61.22 +from unittest import TestCase, TestSuite, makeSuite, main
   61.23 +import Testing
   61.24 +try:
   61.25 +    import Zope2
   61.26 +except ImportError: # BBB: for Zope 2.7
   61.27 +    import Zope as Zope2
   61.28 +Zope2.startup()
   61.29 +
   61.30 +from Products.CMFCore.tests.base.dummy import DummyContent
   61.31 +from Products.CMFCore.tests.base.dummy import DummySite
   61.32 +from Products.CMFCore.tests.base.dummy import DummyTool
   61.33 +from Products.CMFCore.WorkflowTool import addWorkflowFactory
   61.34 +from Products.CMFCore.WorkflowTool import WorkflowTool
   61.35 +
   61.36 +
   61.37 +class DCWorkflowDefinitionTests(TestCase):
   61.38 +
   61.39 +    def setUp(self):
   61.40 +        from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   61.41 +
   61.42 +        self.site = DummySite('site')
   61.43 +        self.site._setObject( 'portal_types', DummyTool() )
   61.44 +        self.site._setObject( 'portal_workflow', WorkflowTool() )
   61.45 +        addWorkflowFactory(DCWorkflowDefinition)
   61.46 +        self._constructDummyWorkflow()
   61.47 +
   61.48 +    def test_z2interfaces(self):
   61.49 +        from Interface.Verify import verifyClass
   61.50 +        from Products.CMFCore.interfaces.portal_workflow \
   61.51 +             import WorkflowDefinition as IWorkflowDefinition
   61.52 +        from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   61.53 +
   61.54 +        verifyClass(IWorkflowDefinition, DCWorkflowDefinition)
   61.55 +
   61.56 +    def test_z3interfaces(self):
   61.57 +        try:
   61.58 +            from zope.interface.verify import verifyClass
   61.59 +        except ImportError:
   61.60 +            # BBB: for Zope 2.7
   61.61 +            return
   61.62 +        from Products.CMFCore.interfaces import IWorkflowDefinition
   61.63 +        from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   61.64 +
   61.65 +        verifyClass(IWorkflowDefinition, DCWorkflowDefinition)
   61.66 +
   61.67 +    def _constructDummyWorkflow(self):
   61.68 +
   61.69 +        wftool = self.site.portal_workflow
   61.70 +        wftool.manage_addWorkflow('Workflow (DC Workflow Definition)', 'wf')
   61.71 +        wftool.setDefaultChain('wf')
   61.72 +        wf = wftool.wf
   61.73 +
   61.74 +        wf.states.addState('private')
   61.75 +        sdef = wf.states['private']
   61.76 +        sdef.setProperties( transitions=('publish',) )
   61.77 +
   61.78 +        wf.states.addState('published')
   61.79 +        wf.states.setInitialState('private')
   61.80 +
   61.81 +        wf.transitions.addTransition('publish')
   61.82 +        tdef = wf.transitions['publish']
   61.83 +        tdef.setProperties(title='', new_state_id='published', actbox_name='')
   61.84 +
   61.85 +        wf.variables.addVariable('comments')
   61.86 +        vdef = wf.variables['comments']
   61.87 +        vdef.setProperties(description='',
   61.88 +                 default_expr="python:state_change.kwargs.get('comment', '')",
   61.89 +                 for_status=1, update_always=1)
   61.90 +
   61.91 +    def _getDummyWorkflow(self):
   61.92 +        wftool = self.site.portal_workflow
   61.93 +        return wftool.wf
   61.94 +
   61.95 +    def test_doActionFor(self):
   61.96 +
   61.97 +        wftool = self.site.portal_workflow
   61.98 +        wf = self._getDummyWorkflow()
   61.99 +
  61.100 +        dummy = self.site._setObject( 'dummy', DummyContent() )
  61.101 +        wftool.notifyCreated(dummy)
  61.102 +        self.assertEqual( wf._getStatusOf(dummy),
  61.103 +                          {'state': 'private', 'comments': ''} )
  61.104 +        wf.doActionFor(dummy, 'publish', comment='foo' )
  61.105 +        self.assertEqual( wf._getStatusOf(dummy),
  61.106 +                          {'state': 'published', 'comments': 'foo'} )
  61.107 +
  61.108 +        # XXX more
  61.109 +
  61.110 +    def test_checkTransitionGuard(self):
  61.111 +
  61.112 +        wftool = self.site.portal_workflow
  61.113 +        wf = self._getDummyWorkflow()
  61.114 +        dummy = self.site._setObject( 'dummy', DummyContent() )
  61.115 +        wftool.notifyCreated(dummy)
  61.116 +        self.assertEqual( wf._getStatusOf(dummy),
  61.117 +                          {'state': 'private', 'comments': ''} )
  61.118 +
  61.119 +        # Check
  61.120 +        self.assert_(wf._checkTransitionGuard(wf.transitions['publish'],
  61.121 +                                              dummy))
  61.122 +
  61.123 +        # Check with kwargs propagation
  61.124 +        self.assert_(wf._checkTransitionGuard(wf.transitions['publish'],
  61.125 +                                              dummy, arg1=1, arg2=2))
  61.126 +
  61.127 +    def test_isActionSupported(self):
  61.128 +
  61.129 +        wf = self._getDummyWorkflow()
  61.130 +        dummy = self.site._setObject( 'dummy', DummyContent() )
  61.131 +
  61.132 +        # check publish
  61.133 +        self.assert_(wf.isActionSupported(dummy, 'publish'))
  61.134 +
  61.135 +        # Check with kwargs.
  61.136 +        self.assert_(wf.isActionSupported(dummy, 'publish', arg1=1, arg2=2))
  61.137 +
  61.138 +    # XXX more tests...
  61.139 +
  61.140 +
  61.141 +def test_suite():
  61.142 +    return TestSuite((
  61.143 +        makeSuite(DCWorkflowDefinitionTests),
  61.144 +        ))
  61.145 +
  61.146 +if __name__ == '__main__':
  61.147 +    main(defaultTest='test_suite')
    62.1 new file mode 100644
    62.2 --- /dev/null
    62.3 +++ b/tests/test_all.py
    62.4 @@ -0,0 +1,40 @@
    62.5 +##############################################################################
    62.6 +#
    62.7 +# Copyright (c) 2003 Zope Corporation and Contributors. All Rights Reserved.
    62.8 +#
    62.9 +# This software is subject to the provisions of the Zope Public License,
   62.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   62.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   62.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   62.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   62.14 +# FOR A PARTICULAR PURPOSE.
   62.15 +#
   62.16 +##############################################################################
   62.17 +""" DCWorkflow tests.
   62.18 +
   62.19 +$Id: test_all.py,v 1.9.2.2 2005/07/08 13:24:33 tseaver Exp $
   62.20 +"""
   62.21 +
   62.22 +from unittest import main
   62.23 +import Testing
   62.24 +try:
   62.25 +    import Zope2
   62.26 +except ImportError: # BBB: for Zope 2.7
   62.27 +    import Zope as Zope2
   62.28 +Zope2.startup()
   62.29 +
   62.30 +from Products.CMFCore.tests.base.utils import build_test_suite
   62.31 +
   62.32 +
   62.33 +def suite():
   62.34 +    return build_test_suite('Products.DCWorkflow.tests',[
   62.35 +        'test_DCWorkflow',
   62.36 +        'test_roles',
   62.37 +        ])
   62.38 +
   62.39 +def test_suite():
   62.40 +    # Just to silence the top-level test.py
   62.41 +    return None
   62.42 +
   62.43 +if __name__ == '__main__':
   62.44 +    main(defaultTest='suite')
    63.1 new file mode 100644
    63.2 --- /dev/null
    63.3 +++ b/tests/test_guard.py
    63.4 @@ -0,0 +1,270 @@
    63.5 +##############################################################################
    63.6 +#
    63.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    63.8 +#
    63.9 +# This software is subject to the provisions of the Zope Public License,
   63.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   63.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   63.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   63.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   63.14 +# FOR A PARTICULAR PURPOSE.
   63.15 +#
   63.16 +##############################################################################
   63.17 +""" Guard tests.
   63.18 +
   63.19 +$Id: test_guard.py,v 1.1.2.1 2005/04/26 16:00:39 anguenot Exp $
   63.20 +"""
   63.21 +
   63.22 +from unittest import TestCase, TestSuite, makeSuite, main
   63.23 +
   63.24 +from AccessControl import getSecurityManager
   63.25 +from Products.PageTemplates.TALES import CompilerError
   63.26 +
   63.27 +from Products.CMFCore.WorkflowTool import WorkflowTool
   63.28 +from Products.CMFCore.WorkflowTool import addWorkflowFactory
   63.29 +
   63.30 +from Products.DCWorkflow.Guard import Guard
   63.31 +from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   63.32 +
   63.33 +from Products.CMFCore.tests.base.dummy import DummyContent
   63.34 +from Products.CMFCore.tests.base.dummy import DummySite
   63.35 +from Products.CMFCore.tests.base.dummy import DummyTool
   63.36 +
   63.37 +class TestGuard(TestCase):
   63.38 +
   63.39 +    def setUp(self):
   63.40 +        self.site = DummySite('site')
   63.41 +        self.site._setObject( 'portal_types', DummyTool() )
   63.42 +        self.site._setObject( 'portal_workflow', WorkflowTool() )
   63.43 +        addWorkflowFactory(DCWorkflowDefinition)
   63.44 +
   63.45 +        # Construct a workflow
   63.46 +        wftool = self.site.portal_workflow
   63.47 +        wftool.manage_addWorkflow('Workflow (DC Workflow Definition)', 'wf')
   63.48 +        wftool.setDefaultChain('wf')
   63.49 +
   63.50 +    def _getDummyWorkflow(self):
   63.51 +        return self.site.portal_workflow['wf']
   63.52 +
   63.53 +    def test_BaseGuardAPI(self):
   63.54 +
   63.55 +        #
   63.56 +        # Test guard basic API
   63.57 +        #
   63.58 +
   63.59 +        guard = Guard()
   63.60 +        self.assertNotEqual(guard, None)
   63.61 +
   63.62 +        # Test default values
   63.63 +        self.assertEqual(guard.getPermissionsText(), '')
   63.64 +        self.assertEqual(guard.getRolesText(), '')
   63.65 +        self.assertEqual(guard.getExprText(), '')
   63.66 +
   63.67 +        # Initialize the guard with empty values
   63.68 +        # not initialization
   63.69 +        guard_props = {'guard_permissions':'',
   63.70 +                       'guard_roles':'',
   63.71 +                       'guard_expr' :''}
   63.72 +        res = guard.changeFromProperties(guard_props)
   63.73 +        self.assert_(res==0)
   63.74 +
   63.75 +        # Test default values
   63.76 +        self.assertEqual(guard.getPermissionsText(), '')
   63.77 +        self.assertEqual(guard.getRolesText(), '')
   63.78 +        self.assertEqual(guard.getExprText(), '')
   63.79 +
   63.80 +        # Change guard
   63.81 +        guard_props = {'guard_roles':'Manager',
   63.82 +                       'guard_permissions':'',
   63.83 +                       'guard_expr' :''}
   63.84 +        res = guard.changeFromProperties(guard_props)
   63.85 +        self.assert_(res==1)
   63.86 +        self.assertEqual(guard.getRolesText(), 'Manager')
   63.87 +        self.assertEqual(guard.getPermissionsText(), '')
   63.88 +        self.assertEqual(guard.getExprText(), '')
   63.89 +
   63.90 +        # Change guard
   63.91 +        guard_props = {'guard_roles':'Manager;',
   63.92 +                       'guard_permissions':'',
   63.93 +                       'guard_expr' :''}
   63.94 +        res = guard.changeFromProperties(guard_props)
   63.95 +        self.assert_(res==1)
   63.96 +        # With one space after the ';'
   63.97 +        self.assertEqual(guard.getRolesText(), 'Manager; ')
   63.98 +        self.assertEqual(guard.getPermissionsText(), '')
   63.99 +        self.assertEqual(guard.getExprText(), '')
  63.100 +
  63.101 +        # Change guard
  63.102 +        guard_props = {'guard_roles':'Manager;Member',
  63.103 +                       'guard_permissions':'',
  63.104 +                       'guard_expr' :''}
  63.105 +        res = guard.changeFromProperties(guard_props)
  63.106 +        self.assert_(res==1)
  63.107 +        # With one space after the ';'
  63.108 +        self.assertEqual(guard.getRolesText(), 'Manager; Member')
  63.109 +        self.assertEqual(guard.getPermissionsText(), '')
  63.110 +        self.assertEqual(guard.getExprText(), '')
  63.111 +
  63.112 +        # Change guard
  63.113 +        guard_props = {'guard_roles':'Manager;Member',
  63.114 +                       'guard_permissions':'',
  63.115 +                       'guard_expr' :''}
  63.116 +        res = guard.changeFromProperties(guard_props)
  63.117 +        self.assert_(res==1)
  63.118 +        # With one space after the ';'
  63.119 +        self.assertEqual(guard.getRolesText(), 'Manager; Member')
  63.120 +        self.assertEqual(guard.getPermissionsText(), '')
  63.121 +        self.assertEqual(guard.getExprText(), '')
  63.122 +
  63.123 +        # Change guard
  63.124 +        guard_props = {'guard_roles':'Manager',
  63.125 +                       'guard_permissions':'',
  63.126 +                       'guard_expr' :''}
  63.127 +        res = guard.changeFromProperties(guard_props)
  63.128 +        self.assert_(res==1)
  63.129 +        self.assertEqual(guard.getRolesText(), 'Manager')
  63.130 +        self.assertEqual(guard.getPermissionsText(), '')
  63.131 +        self.assertEqual(guard.getExprText(), '')
  63.132 +
  63.133 +        # Change guard
  63.134 +        guard_props = {'guard_roles':'Manager',
  63.135 +                       'guard_permissions':'ManagePortal;',
  63.136 +                       'guard_expr' :''}
  63.137 +        res = guard.changeFromProperties(guard_props)
  63.138 +        self.assert_(res==1)
  63.139 +        self.assertEqual(guard.getRolesText(), 'Manager')
  63.140 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal; ')
  63.141 +        self.assertEqual(guard.getExprText(), '')
  63.142 +
  63.143 +        # Change guard
  63.144 +        guard_props = {'guard_roles':'Manager',
  63.145 +                       'guard_permissions':'ManagePortal',
  63.146 +                       'guard_expr' :''}
  63.147 +        res = guard.changeFromProperties(guard_props)
  63.148 +        self.assert_(res==1)
  63.149 +        self.assertEqual(guard.getRolesText(), 'Manager')
  63.150 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  63.151 +        self.assertEqual(guard.getExprText(), '')
  63.152 +
  63.153 +        # Change guard
  63.154 +        guard_props = {'guard_roles':'Manager',
  63.155 +                       'guard_permissions':'ManagePortal',
  63.156 +                       'guard_expr' :'python:1'}
  63.157 +        res = guard.changeFromProperties(guard_props)
  63.158 +        self.assert_(res==1)
  63.159 +        self.assertEqual(guard.getRolesText(), 'Manager')
  63.160 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  63.161 +        self.assertEqual(guard.getExprText(), 'python:1')
  63.162 +
  63.163 +        # Change guard
  63.164 +        guard_props = {'guard_roles':'Manager',
  63.165 +                       'guard_permissions':'ManagePortal',
  63.166 +                       'guard_expr' :'string:'}
  63.167 +        res = guard.changeFromProperties(guard_props)
  63.168 +        self.assert_(res==1)
  63.169 +        self.assertEqual(guard.getRolesText(), 'Manager')
  63.170 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  63.171 +        self.assertEqual(guard.getExprText(), 'string:')
  63.172 +
  63.173 +        # Change guard with wrong TALES
  63.174 +        guard_props = {'guard_roles':'Manager',
  63.175 +                       'guard_permissions':'ManagePortal',
  63.176 +                       'guard_expr' :'python:'}
  63.177 +        self.assertRaises(CompilerError,
  63.178 +                          guard.changeFromProperties, guard_props)
  63.179 +
  63.180 +        self.assertEqual(guard.getRolesText(), 'Manager')
  63.181 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  63.182 +        self.assertEqual(guard.getExprText(), 'string:')
  63.183 +
  63.184 +        # reinit the guard
  63.185 +        guard_props = {'guard_permissions':'',
  63.186 +                       'guard_roles':'',
  63.187 +                       'guard_expr' :''}
  63.188 +        res = guard.changeFromProperties(guard_props)
  63.189 +        self.assert_(res==0)
  63.190 +
  63.191 +        # No API on DCWorkflow guard to reset properly....
  63.192 +        guard.permissions = ''
  63.193 +        guard.roles = ''
  63.194 +        guard.expr = None
  63.195 +
  63.196 +        # Test default values
  63.197 +        self.assertEqual(guard.getPermissionsText(), '')
  63.198 +        self.assertEqual(guard.getRolesText(), '')
  63.199 +        self.assertEqual(guard.getExprText(), '')
  63.200 +
  63.201 +        # XXX more tests with permissions and roles
  63.202 +
  63.203 +    def test_checkGuardExpr(self):
  63.204 +
  63.205 +        #
  63.206 +        # Basic checks.
  63.207 +        #
  63.208 +
  63.209 +        guard = Guard()
  63.210 +
  63.211 +        # Create compulsory context elements
  63.212 +        sm = getSecurityManager()
  63.213 +        ob = DummyContent('dummy')
  63.214 +        wf_def = self._getDummyWorkflow()
  63.215 +
  63.216 +        # Initialize the guard with an ok guard
  63.217 +        guard_props = {'guard_permissions':'',
  63.218 +                       'guard_roles':'',
  63.219 +                       'guard_expr' :'python:1'}
  63.220 +
  63.221 +        res = guard.changeFromProperties(guard_props)
  63.222 +        self.assert_(res)
  63.223 +        self.assert_(guard.check(sm, wf_def, ob))
  63.224 +
  63.225 +        # Initialize the guard with a not ok guard
  63.226 +        guard_props = {'guard_permissions':'',
  63.227 +                       'guard_roles':'',
  63.228 +                       'guard_expr' :'python:0'}
  63.229 +        res = guard.changeFromProperties(guard_props)
  63.230 +        self.assert_(res)
  63.231 +        self.assert_(not guard.check(sm, wf_def, ob))
  63.232 +
  63.233 +        # XXX more tests with permissions and roles
  63.234 +
  63.235 +    def test_checkWithKwargs(self):
  63.236 +
  63.237 +        #
  63.238 +        # Checks with kwargs
  63.239 +        #
  63.240 +
  63.241 +        guard = Guard()
  63.242 +
  63.243 +        # Create compulsory context elements
  63.244 +        sm = getSecurityManager()
  63.245 +        ob = DummyContent('dummy')
  63.246 +        wf_def = self._getDummyWorkflow()
  63.247 +
  63.248 +        # Initialize the guard with an ok guard
  63.249 +        guard_props = {'guard_permissions':'',
  63.250 +                       'guard_roles':'',
  63.251 +                       'guard_expr' :'python:1'}
  63.252 +
  63.253 +        res = guard.changeFromProperties(guard_props)
  63.254 +        self.assert_(res)
  63.255 +        self.assert_(guard.check(sm, wf_def, ob, arg1=1, arg2=2))
  63.256 +
  63.257 +        # Initialize the guard with a not ok guard
  63.258 +        guard_props = {'guard_permissions':'',
  63.259 +                       'guard_roles':'',
  63.260 +                       'guard_expr' :'python:0'}
  63.261 +        res = guard.changeFromProperties(guard_props)
  63.262 +        self.assert_(res)
  63.263 +        self.assert_(not guard.check(sm, wf_def, ob, arg1=1, arg2=2))
  63.264 +
  63.265 +        # XXX more tests with permissions and roles
  63.266 +
  63.267 +def test_suite():
  63.268 +    return TestSuite((
  63.269 +        makeSuite(TestGuard),
  63.270 +        ))
  63.271 +
  63.272 +if __name__ == '__main__':
  63.273 +    main(defaultTest='test_suite')
  63.274 +
    64.1 new file mode 100644
    64.2 --- /dev/null
    64.3 +++ b/tests/test_roles.py
    64.4 @@ -0,0 +1,75 @@
    64.5 +##############################################################################
    64.6 +#
    64.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    64.8 +#
    64.9 +# This software is subject to the provisions of the Zope Public License,
   64.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   64.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   64.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   64.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   64.14 +# FOR A PARTICULAR PURPOSE.
   64.15 +#
   64.16 +##############################################################################
   64.17 +""" Unit tests of role-mapping machinery.
   64.18 +
   64.19 +$Id: test_roles.py,v 1.5.2.2 2005/07/08 13:24:33 tseaver Exp $
   64.20 +"""
   64.21 +
   64.22 +import unittest
   64.23 +import Testing
   64.24 +try:
   64.25 +    import Zope2
   64.26 +except ImportError: # BBB: for Zope 2.7
   64.27 +    import Zope as Zope2
   64.28 +Zope2.startup()
   64.29 +
   64.30 +from OFS.Folder import Folder
   64.31 +from OFS.Application import Application
   64.32 +from Products.DCWorkflow.utils \
   64.33 +     import modifyRolesForPermission, modifyRolesForGroup
   64.34 +
   64.35 +
   64.36 +class RoleMapTests(unittest.TestCase):
   64.37 +
   64.38 +    def setUp(self):
   64.39 +        self.app = Application()
   64.40 +        self.app.ob = Folder()
   64.41 +        self.ob = self.app.ob
   64.42 +        self.ob.__ac_local_roles__ = {
   64.43 +            '(Group) Administrators': ['Manager', 'Member'],
   64.44 +            '(Group) Users': ['Member'],
   64.45 +            }
   64.46 +        self.ob._View_Permission = ('Member', 'Manager')
   64.47 +        self.ob._View_management_screens_Permission = ('Manager',)
   64.48 +
   64.49 +    def testModifyRolesForGroup(self):
   64.50 +        modifyRolesForGroup(
   64.51 +            self.ob, '(Group) Administrators', ['Owner'], ['Member', 'Owner'])
   64.52 +        modifyRolesForGroup(
   64.53 +            self.ob, '(Group) Users', [], ['Member'])
   64.54 +        self.assertEqual(self.ob.__ac_local_roles__, {
   64.55 +            '(Group) Administrators': ['Manager', 'Owner'],
   64.56 +            })
   64.57 +        modifyRolesForGroup(
   64.58 +            self.ob, '(Group) Administrators', ['Member'], ['Member', 'Owner'])
   64.59 +        modifyRolesForGroup(
   64.60 +            self.ob, '(Group) Users', ['Member'], ['Member'])
   64.61 +        self.assertEqual(self.ob.__ac_local_roles__, {
   64.62 +            '(Group) Administrators': ['Manager', 'Member'],
   64.63 +            '(Group) Users': ['Member'],
   64.64 +            })
   64.65 +
   64.66 +    def testModifyRolesForPermission(self):
   64.67 +        modifyRolesForPermission(self.ob, 'View', ['Manager'])
   64.68 +        modifyRolesForPermission(
   64.69 +            self.ob, 'View management screens', ['Member'])
   64.70 +        self.assertEqual(self.ob._View_Permission, ['Manager'])
   64.71 +        self.assertEqual(
   64.72 +            self.ob._View_management_screens_Permission, ['Member'])
   64.73 +
   64.74 +
   64.75 +def test_suite():
   64.76 +    return unittest.makeSuite(RoleMapTests)
   64.77 +
   64.78 +if __name__ == '__main__':
   64.79 +    unittest.main(defaultTest='test_suite')
    65.1 new file mode 100644
    65.2 --- /dev/null
    65.3 +++ b/utils.py
    65.4 @@ -0,0 +1,98 @@
    65.5 +##############################################################################
    65.6 +#
    65.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    65.8 +# 
    65.9 +# This software is subject to the provisions of the Zope Public License,
   65.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   65.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   65.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   65.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   65.14 +# FOR A PARTICULAR PURPOSE.
   65.15 +# 
   65.16 +##############################################################################
   65.17 +""" Some common utilities.
   65.18 +
   65.19 +$Id: utils.py,v 1.10 2004/08/12 15:07:44 jens Exp $
   65.20 +"""
   65.21 +
   65.22 +import os
   65.23 +from App.Common import package_home
   65.24 +
   65.25 +_dtmldir = os.path.join( package_home( globals() ), 'dtml' )
   65.26 +
   65.27 +from AccessControl.Role import gather_permissions
   65.28 +from AccessControl.Permission import Permission
   65.29 +from Acquisition import aq_base
   65.30 +
   65.31 +
   65.32 +def ac_inherited_permissions(ob, all=0):
   65.33 +    # Get all permissions not defined in ourself that are inherited
   65.34 +    # This will be a sequence of tuples with a name as the first item and
   65.35 +    # an empty tuple as the second.
   65.36 +    d = {}
   65.37 +    perms = getattr(ob, '__ac_permissions__', ())
   65.38 +    for p in perms: d[p[0]] = None
   65.39 +    r = gather_permissions(ob.__class__, [], d)
   65.40 +    if all:
   65.41 +       if hasattr(ob, '_subobject_permissions'):
   65.42 +           for p in ob._subobject_permissions():
   65.43 +               pname=p[0]
   65.44 +               if not d.has_key(pname):
   65.45 +                   d[pname]=1
   65.46 +                   r.append(p)
   65.47 +       r = list(perms) + r
   65.48 +    return r
   65.49 +
   65.50 +def modifyRolesForPermission(ob, pname, roles):
   65.51 +    '''
   65.52 +    Modifies multiple role to permission mappings.  roles is a list to
   65.53 +    acquire, a tuple to not acquire.
   65.54 +    '''
   65.55 +    # This mimics what AccessControl/Role.py does.
   65.56 +    data = ()
   65.57 +    for perm in ac_inherited_permissions(ob, 1):
   65.58 +        name, value = perm[:2]
   65.59 +        if name == pname:
   65.60 +            data = value
   65.61 +            break
   65.62 +    p = Permission(pname, data, ob)
   65.63 +    if p.getRoles() != roles:
   65.64 +        p.setRoles(roles)
   65.65 +        return 1
   65.66 +    return 0
   65.67 +
   65.68 +def modifyRolesForGroup(ob, group, grant_roles, managed_roles):
   65.69 +    """Modifies local roles for one group.
   65.70 +    """
   65.71 +    local_roles = getattr(ob, '__ac_local_roles__', None)
   65.72 +    if local_roles is None:
   65.73 +        local_roles = {}
   65.74 +    roles = local_roles.get(group)
   65.75 +    if not roles:
   65.76 +        if not grant_roles:
   65.77 +            # No roles exist and no grants requested.  Leave unchanged.
   65.78 +            return 0
   65.79 +        else:
   65.80 +            # Add new roles for this group.
   65.81 +            local_roles[group] = list(grant_roles)
   65.82 +            ob.__ac_local_roles__ = local_roles
   65.83 +            return 1
   65.84 +    # Edit the roles.
   65.85 +    roles = list(roles)
   65.86 +    changed = 0
   65.87 +    for role in managed_roles:
   65.88 +        if role in grant_roles and role not in roles:
   65.89 +            # Add one role for this group.
   65.90 +            roles.append(role)
   65.91 +            changed = 1
   65.92 +        elif role not in grant_roles and role in roles:
   65.93 +            # Remove one role for this group.
   65.94 +            roles.remove(role)
   65.95 +            changed = 1
   65.96 +    if changed:
   65.97 +        if not roles and local_roles.has_key(group):
   65.98 +            del local_roles[group]
   65.99 +        else:
  65.100 +            local_roles[group] = roles
  65.101 +        ob.__ac_local_roles__ = local_roles
  65.102 +    return changed
    66.1 new file mode 100644
    66.2 --- /dev/null
    66.3 +++ b/version.txt
    66.4 @@ -0,0 +1,1 @@
    66.5 +CMF-1.5.2