vendor/CMF/1.6-r40908/DCWorkflow

changeset 0:33593d612079 DCWorkflow tip

Vendor import of CMF 1.6 branch r40908
author fguillaume
date Tue, 20 Dec 2005 15:51:52 +0000
parents
children
files CHANGES.txt ContainerTab.py DCWorkflow.py DEPENDENCIES.txt Default.py Expression.py Extensions/test_method.py Guard.py README.txt Scripts.py States.py Transitions.py Variables.py WorkflowUIMixin.py Worklists.py __init__.py browser/__init__.py browser/configure.zcml browser/workflow.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 exportimport.py 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 interfaces.py permissions.py profiles/revision2/workflows.xml profiles/revision2/workflows/default_workflow/definition.xml tests/__init__.py tests/test_DCWorkflow.py tests/test_exportimport.py tests/test_guard.py tests/test_roles.py utils.py version.txt xml/wtcWorkflowExport.xml
diffstat 75 files changed, 9323 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 36457 2004-08-12 15:07:44Z jens $
    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,591 @@
     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 40346 2005-11-23 17:15:03Z yuppie $
    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 +from zope.interface import implements
    3.34 +
    3.35 +# CMFCore
    3.36 +from Products.CMFCore.interfaces import IWorkflowDefinition
    3.37 +from Products.CMFCore.interfaces.portal_workflow \
    3.38 +        import WorkflowDefinition as z2IWorkflowDefinition
    3.39 +from Products.CMFCore.utils import getToolByName
    3.40 +from Products.CMFCore.WorkflowCore import ObjectDeleted
    3.41 +from Products.CMFCore.WorkflowCore import ObjectMoved
    3.42 +from Products.CMFCore.WorkflowCore import WorkflowException
    3.43 +from Products.CMFCore.WorkflowTool import addWorkflowFactory
    3.44 +
    3.45 +# DCWorkflow
    3.46 +from interfaces import IDCWorkflowDefinition
    3.47 +from permissions import ManagePortal
    3.48 +from utils import _dtmldir
    3.49 +from utils import modifyRolesForPermission
    3.50 +from utils import modifyRolesForGroup
    3.51 +from WorkflowUIMixin import WorkflowUIMixin
    3.52 +from Transitions import TRIGGER_AUTOMATIC
    3.53 +from Transitions import TRIGGER_USER_ACTION
    3.54 +from Transitions import TRIGGER_WORKFLOW_METHOD
    3.55 +from Expression import StateChangeInfo
    3.56 +from Expression import createExprContext
    3.57 +
    3.58 +
    3.59 +def checkId(id):
    3.60 +    res = bad_id(id)
    3.61 +    if res != -1 and res is not None:
    3.62 +        raise ValueError, 'Illegal ID'
    3.63 +    return 1
    3.64 +
    3.65 +
    3.66 +class DCWorkflowDefinition (WorkflowUIMixin, Folder):
    3.67 +    '''
    3.68 +    This class is the workflow engine and the container for the
    3.69 +    workflow definition.
    3.70 +    UI methods are in WorkflowUIMixin.
    3.71 +    '''
    3.72 +
    3.73 +    implements(IDCWorkflowDefinition, IWorkflowDefinition)
    3.74 +    __implements__ = z2IWorkflowDefinition
    3.75 +
    3.76 +    title = 'DC Workflow Definition'
    3.77 +    _isAWorkflow = 1
    3.78 +
    3.79 +    state_var = 'state'
    3.80 +    initial_state = None
    3.81 +
    3.82 +    states = None
    3.83 +    transitions = None
    3.84 +    variables = None
    3.85 +    worklists = None
    3.86 +    scripts = None
    3.87 +
    3.88 +    permissions = ()
    3.89 +    groups = ()     # Names of groups managed by this workflow.
    3.90 +    roles = None  # The role names managed by this workflow.
    3.91 +    # If roles is None, listRoles() provides a default.
    3.92 +
    3.93 +    creation_guard = None  # The guard that can veto object creation.
    3.94 +
    3.95 +    manager_bypass = 0  # Boolean: 'Manager' role bypasses guards
    3.96 +
    3.97 +    manage_options = (
    3.98 +        {'label': 'Properties', 'action': 'manage_properties'},
    3.99 +        {'label': 'States', 'action': 'states/manage_main'},
   3.100 +        {'label': 'Transitions', 'action': 'transitions/manage_main'},
   3.101 +        {'label': 'Variables', 'action': 'variables/manage_main'},
   3.102 +        {'label': 'Worklists', 'action': 'worklists/manage_main'},
   3.103 +        {'label': 'Scripts', 'action': 'scripts/manage_main'},
   3.104 +        {'label': 'Permissions', 'action': 'manage_permissions'},
   3.105 +        {'label': 'Groups', 'action': 'manage_groups'},
   3.106 +        )
   3.107 +
   3.108 +    security = ClassSecurityInfo()
   3.109 +    security.declareObjectProtected(ManagePortal)
   3.110 +
   3.111 +    def __init__(self, id):
   3.112 +        self.id = id
   3.113 +        from States import States
   3.114 +        self._addObject(States('states'))
   3.115 +        from Transitions import Transitions
   3.116 +        self._addObject(Transitions('transitions'))
   3.117 +        from Variables import Variables
   3.118 +        self._addObject(Variables('variables'))
   3.119 +        from Worklists import Worklists
   3.120 +        self._addObject(Worklists('worklists'))
   3.121 +        from Scripts import Scripts
   3.122 +        self._addObject(Scripts('scripts'))
   3.123 +
   3.124 +    def _addObject(self, ob):
   3.125 +        id = ob.getId()
   3.126 +        setattr(self, id, ob)
   3.127 +        self._objects = self._objects + (
   3.128 +            {'id': id, 'meta_type': ob.meta_type},)
   3.129 +
   3.130 +    #
   3.131 +    # Workflow engine.
   3.132 +    #
   3.133 +
   3.134 +    def _getStatusOf(self, ob):
   3.135 +        tool = aq_parent(aq_inner(self))
   3.136 +        status = tool.getStatusOf(self.id, ob)
   3.137 +        if status is None:
   3.138 +            return {}
   3.139 +        else:
   3.140 +            return status
   3.141 +
   3.142 +    def _getWorkflowStateOf(self, ob, id_only=0):
   3.143 +        tool = aq_parent(aq_inner(self))
   3.144 +        status = tool.getStatusOf(self.id, ob)
   3.145 +        if status is None:
   3.146 +            state = self.initial_state
   3.147 +        else:
   3.148 +            state = status.get(self.state_var, None)
   3.149 +            if state is None:
   3.150 +                state = self.initial_state
   3.151 +        if id_only:
   3.152 +            return state
   3.153 +        else:
   3.154 +            return self.states.get(state, None)
   3.155 +
   3.156 +    def _getPortalRoot(self):
   3.157 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   3.158 +
   3.159 +    security.declarePrivate('getCatalogVariablesFor')
   3.160 +    def getCatalogVariablesFor(self, ob):
   3.161 +        '''
   3.162 +        Allows this workflow to make workflow-specific variables
   3.163 +        available to the catalog, making it possible to implement
   3.164 +        worklists in a simple way.
   3.165 +        Returns a mapping containing the catalog variables
   3.166 +        that apply to ob.
   3.167 +        '''
   3.168 +        res = {}
   3.169 +        status = self._getStatusOf(ob)
   3.170 +        for id, vdef in self.variables.items():
   3.171 +            if vdef.for_catalog:
   3.172 +                if status.has_key(id):
   3.173 +                    value = status[id]
   3.174 +
   3.175 +                # Not set yet.  Use a default.
   3.176 +                elif vdef.default_expr is not None:
   3.177 +                    ec = createExprContext(StateChangeInfo(ob, self, status))
   3.178 +                    value = vdef.default_expr(ec)
   3.179 +                else:
   3.180 +                    value = vdef.default_value
   3.181 +
   3.182 +                res[id] = value
   3.183 +        # Always provide the state variable.
   3.184 +        state_var = self.state_var
   3.185 +        res[state_var] = status.get(state_var, self.initial_state)
   3.186 +        return res
   3.187 +
   3.188 +    security.declarePrivate('listObjectActions')
   3.189 +    def listObjectActions(self, info):
   3.190 +        '''
   3.191 +        Allows this workflow to
   3.192 +        include actions to be displayed in the actions box.
   3.193 +        Called only when this workflow is applicable to
   3.194 +        info.object.
   3.195 +        Returns the actions to be displayed to the user.
   3.196 +        '''
   3.197 +        ob = info.object
   3.198 +        sdef = self._getWorkflowStateOf(ob)
   3.199 +        if sdef is None:
   3.200 +            return None
   3.201 +        res = []
   3.202 +        for tid in sdef.transitions:
   3.203 +            tdef = self.transitions.get(tid, None)
   3.204 +            if tdef is not None and tdef.trigger_type == TRIGGER_USER_ACTION:
   3.205 +                if tdef.actbox_name:
   3.206 +                    if self._checkTransitionGuard(tdef, ob):
   3.207 +                        res.append((tid, {
   3.208 +                            'id': tid,
   3.209 +                            'name': tdef.actbox_name % info,
   3.210 +                            'url': tdef.actbox_url % info,
   3.211 +                            'permissions': (),  # Predetermined.
   3.212 +                            'category': tdef.actbox_category,
   3.213 +                            'transition': tdef}))
   3.214 +        res.sort()
   3.215 +        return [ result[1] for result in res ]
   3.216 +
   3.217 +    security.declarePrivate('listGlobalActions')
   3.218 +    def listGlobalActions(self, info):
   3.219 +        '''
   3.220 +        Allows this workflow to
   3.221 +        include actions to be displayed in the actions box.
   3.222 +        Called on every request.
   3.223 +        Returns the actions to be displayed to the user.
   3.224 +        '''
   3.225 +        if not self.worklists:
   3.226 +            return None  # Optimization
   3.227 +        sm = getSecurityManager()
   3.228 +        portal = self._getPortalRoot()
   3.229 +        res = []
   3.230 +        fmt_data = None
   3.231 +        for id, qdef in self.worklists.items():
   3.232 +            if qdef.actbox_name:
   3.233 +                guard = qdef.guard
   3.234 +                if guard is None or guard.check(sm, self, portal):
   3.235 +                    searchres = None
   3.236 +                    var_match_keys = qdef.getVarMatchKeys()
   3.237 +                    if var_match_keys:
   3.238 +                        # Check the catalog for items in the worklist.
   3.239 +                        catalog = getToolByName(self, 'portal_catalog')
   3.240 +                        kw = {}
   3.241 +                        for k in var_match_keys:
   3.242 +                            v = qdef.getVarMatch(k)
   3.243 +                            kw[k] = [ x % info for x in v ]
   3.244 +                        searchres = catalog.searchResults(**kw)
   3.245 +                        if not searchres:
   3.246 +                            continue
   3.247 +                    if fmt_data is None:
   3.248 +                        fmt_data = TemplateDict()
   3.249 +                        fmt_data._push(info)
   3.250 +                    fmt_data._push({'count': len(searchres)})
   3.251 +                    res.append((id, {'name': qdef.actbox_name % fmt_data,
   3.252 +                                     'url': qdef.actbox_url % fmt_data,
   3.253 +                                     'permissions': (),  # Predetermined.
   3.254 +                                     'category': qdef.actbox_category}))
   3.255 +                    fmt_data._pop()
   3.256 +        res.sort()
   3.257 +        return [ result[1] for result in res ]
   3.258 +
   3.259 +    security.declarePrivate('isActionSupported')
   3.260 +    def isActionSupported(self, ob, action, **kw):
   3.261 +        '''
   3.262 +        Returns a true value if the given action name
   3.263 +        is possible in the current state.
   3.264 +        '''
   3.265 +        sdef = self._getWorkflowStateOf(ob)
   3.266 +        if sdef is None:
   3.267 +            return 0
   3.268 +        if action in sdef.transitions:
   3.269 +            tdef = self.transitions.get(action, None)
   3.270 +            if (tdef is not None and
   3.271 +                tdef.trigger_type == TRIGGER_USER_ACTION and
   3.272 +                self._checkTransitionGuard(tdef, ob, **kw)):
   3.273 +                return 1
   3.274 +        return 0
   3.275 +
   3.276 +    security.declarePrivate('doActionFor')
   3.277 +    def doActionFor(self, ob, action, comment='', **kw):
   3.278 +        '''
   3.279 +        Allows the user to request a workflow action.  This method
   3.280 +        must perform its own security checks.
   3.281 +        '''
   3.282 +        kw['comment'] = comment
   3.283 +        sdef = self._getWorkflowStateOf(ob)
   3.284 +        if sdef is None:
   3.285 +            raise WorkflowException, 'Object is in an undefined state'
   3.286 +        if action not in sdef.transitions:
   3.287 +            raise Unauthorized(action)
   3.288 +        tdef = self.transitions.get(action, None)
   3.289 +        if tdef is None or tdef.trigger_type != TRIGGER_USER_ACTION:
   3.290 +            raise WorkflowException, (
   3.291 +                'Transition %s is not triggered by a user action' % action)
   3.292 +        if not self._checkTransitionGuard(tdef, ob, **kw):
   3.293 +            raise Unauthorized(action)
   3.294 +        self._changeStateOf(ob, tdef, kw)
   3.295 +
   3.296 +    security.declarePrivate('isWorkflowMethodSupported')
   3.297 +    def isWorkflowMethodSupported(self, ob, method_id):
   3.298 +        '''
   3.299 +        Returns a true value if the given workflow method
   3.300 +        is supported in the current state.
   3.301 +        '''
   3.302 +        sdef = self._getWorkflowStateOf(ob)
   3.303 +        if sdef is None:
   3.304 +            return 0
   3.305 +        if method_id in sdef.transitions:
   3.306 +            tdef = self.transitions.get(method_id, None)
   3.307 +            if (tdef is not None and
   3.308 +                tdef.trigger_type == TRIGGER_WORKFLOW_METHOD and
   3.309 +                self._checkTransitionGuard(tdef, ob)):
   3.310 +                return 1
   3.311 +        return 0
   3.312 +
   3.313 +    security.declarePrivate('wrapWorkflowMethod')
   3.314 +    def wrapWorkflowMethod(self, ob, method_id, func, args, kw):
   3.315 +        '''
   3.316 +        Allows the user to request a workflow action.  This method
   3.317 +        must perform its own security checks.
   3.318 +        '''
   3.319 +        sdef = self._getWorkflowStateOf(ob)
   3.320 +        if sdef is None:
   3.321 +            raise WorkflowException, 'Object is in an undefined state'
   3.322 +        if method_id not in sdef.transitions:
   3.323 +            raise Unauthorized(method_id)
   3.324 +        tdef = self.transitions.get(method_id, None)
   3.325 +        if tdef is None or tdef.trigger_type != TRIGGER_WORKFLOW_METHOD:
   3.326 +            raise WorkflowException, (
   3.327 +                'Transition %s is not triggered by a workflow method'
   3.328 +                % method_id)
   3.329 +        if not self._checkTransitionGuard(tdef, ob):
   3.330 +            raise Unauthorized(method_id)
   3.331 +        res = func(*args, **kw)
   3.332 +        try:
   3.333 +            self._changeStateOf(ob, tdef)
   3.334 +        except ObjectDeleted:
   3.335 +            # Re-raise with a different result.
   3.336 +            raise ObjectDeleted(res)
   3.337 +        except ObjectMoved, ex:
   3.338 +            # Re-raise with a different result.
   3.339 +            raise ObjectMoved(ex.getNewObject(), res)
   3.340 +        return res
   3.341 +
   3.342 +    security.declarePrivate('isInfoSupported')
   3.343 +    def isInfoSupported(self, ob, name):
   3.344 +        '''
   3.345 +        Returns a true value if the given info name is supported.
   3.346 +        '''
   3.347 +        if name == self.state_var:
   3.348 +            return 1
   3.349 +        vdef = self.variables.get(name, None)
   3.350 +        if vdef is None:
   3.351 +            return 0
   3.352 +        return 1
   3.353 +
   3.354 +    security.declarePrivate('getInfoFor')
   3.355 +    def getInfoFor(self, ob, name, default):
   3.356 +        '''
   3.357 +        Allows the user to request information provided by the
   3.358 +        workflow.  This method must perform its own security checks.
   3.359 +        '''
   3.360 +        if name == self.state_var:
   3.361 +            return self._getWorkflowStateOf(ob, 1)
   3.362 +        vdef = self.variables[name]
   3.363 +        if vdef.info_guard is not None and not vdef.info_guard.check(
   3.364 +            getSecurityManager(), self, ob):
   3.365 +            return default
   3.366 +        status = self._getStatusOf(ob)
   3.367 +        if status is not None and status.has_key(name):
   3.368 +            value = status[name]
   3.369 +
   3.370 +        # Not set yet.  Use a default.
   3.371 +        elif vdef.default_expr is not None:
   3.372 +            ec = createExprContext(StateChangeInfo(ob, self, status))
   3.373 +            value = vdef.default_expr(ec)
   3.374 +        else:
   3.375 +            value = vdef.default_value
   3.376 +
   3.377 +        return value
   3.378 +
   3.379 +    security.declarePrivate('allowCreate')
   3.380 +    def allowCreate(self, container, type_name):
   3.381 +        """Returns true if the user is allowed to create a workflow instance.
   3.382 +
   3.383 +        The object passed to the guard is the prospective container.
   3.384 +        """
   3.385 +        if self.creation_guard is not None:
   3.386 +            return self.creation_guard.check(
   3.387 +                getSecurityManager(), self, container)
   3.388 +        return 1
   3.389 +
   3.390 +    security.declarePrivate('notifyCreated')
   3.391 +    def notifyCreated(self, ob):
   3.392 +        """Notifies this workflow after an object has been created and added.
   3.393 +        """
   3.394 +        try:
   3.395 +            self._changeStateOf(ob, None)
   3.396 +        except ( ObjectDeleted, ObjectMoved ):
   3.397 +            # Swallow.
   3.398 +            pass
   3.399 +
   3.400 +    security.declarePrivate('notifyBefore')
   3.401 +    def notifyBefore(self, ob, action):
   3.402 +        '''
   3.403 +        Notifies this workflow of an action before it happens,
   3.404 +        allowing veto by exception.  Unless an exception is thrown, either
   3.405 +        a notifySuccess() or notifyException() can be expected later on.
   3.406 +        The action usually corresponds to a method name.
   3.407 +        '''
   3.408 +        pass
   3.409 +
   3.410 +    security.declarePrivate('notifySuccess')
   3.411 +    def notifySuccess(self, ob, action, result):
   3.412 +        '''
   3.413 +        Notifies this workflow that an action has taken place.
   3.414 +        '''
   3.415 +        pass
   3.416 +
   3.417 +    security.declarePrivate('notifyException')
   3.418 +    def notifyException(self, ob, action, exc):
   3.419 +        '''
   3.420 +        Notifies this workflow that an action failed.
   3.421 +        '''
   3.422 +        pass
   3.423 +
   3.424 +    security.declarePrivate('updateRoleMappingsFor')
   3.425 +    def updateRoleMappingsFor(self, ob):
   3.426 +        """Changes the object permissions according to the current state.
   3.427 +        """
   3.428 +        changed = 0
   3.429 +        sdef = self._getWorkflowStateOf(ob)
   3.430 +        if sdef is None:
   3.431 +            return 0
   3.432 +        # Update the role -> permission map.
   3.433 +        if self.permissions:
   3.434 +            for p in self.permissions:
   3.435 +                roles = []
   3.436 +                if sdef.permission_roles is not None:
   3.437 +                    roles = sdef.permission_roles.get(p, roles)
   3.438 +                if modifyRolesForPermission(ob, p, roles):
   3.439 +                    changed = 1
   3.440 +        # Update the group -> role map.
   3.441 +        groups = self.getGroups()
   3.442 +        managed_roles = self.getRoles()
   3.443 +        if groups and managed_roles:
   3.444 +            for group in groups:
   3.445 +                roles = ()
   3.446 +                if sdef.group_roles is not None:
   3.447 +                    roles = sdef.group_roles.get(group, ())
   3.448 +                if modifyRolesForGroup(ob, group, roles, managed_roles):
   3.449 +                    changed = 1
   3.450 +        return changed
   3.451 +
   3.452 +    def _checkTransitionGuard(self, t, ob, **kw):
   3.453 +        guard = t.guard
   3.454 +        if guard is None:
   3.455 +            return 1
   3.456 +        if guard.check(getSecurityManager(), self, ob, **kw):
   3.457 +            return 1
   3.458 +        return 0
   3.459 +
   3.460 +    def _findAutomaticTransition(self, ob, sdef):
   3.461 +        tdef = None
   3.462 +        for tid in sdef.transitions:
   3.463 +            t = self.transitions.get(tid, None)
   3.464 +            if t is not None and t.trigger_type == TRIGGER_AUTOMATIC:
   3.465 +                if self._checkTransitionGuard(t, ob):
   3.466 +                    tdef = t
   3.467 +                    break
   3.468 +        return tdef
   3.469 +        
   3.470 +    def _changeStateOf(self, ob, tdef=None, kwargs=None):
   3.471 +        '''
   3.472 +        Changes state.  Can execute multiple transitions if there are
   3.473 +        automatic transitions.  tdef set to None means the object
   3.474 +        was just created.
   3.475 +        '''
   3.476 +        moved_exc = None
   3.477 +        while 1:
   3.478 +            try:
   3.479 +                sdef = self._executeTransition(ob, tdef, kwargs)
   3.480 +            except ObjectMoved, moved_exc:
   3.481 +                ob = moved_exc.getNewObject()
   3.482 +                sdef = self._getWorkflowStateOf(ob)
   3.483 +                # Re-raise after all transitions.
   3.484 +            if sdef is None:
   3.485 +                break
   3.486 +            tdef = self._findAutomaticTransition(ob, sdef)
   3.487 +            if tdef is None:
   3.488 +                # No more automatic transitions.
   3.489 +                break
   3.490 +            # Else continue.
   3.491 +        if moved_exc is not None:
   3.492 +            # Re-raise.
   3.493 +            raise moved_exc
   3.494 +
   3.495 +    def _executeTransition(self, ob, tdef=None, kwargs=None):
   3.496 +        '''
   3.497 +        Private method.
   3.498 +        Puts object in a new state.
   3.499 +        '''
   3.500 +        sci = None
   3.501 +        econtext = None
   3.502 +        moved_exc = None
   3.503 +
   3.504 +        # Figure out the old and new states.
   3.505 +        old_sdef = self._getWorkflowStateOf(ob)
   3.506 +        old_state = old_sdef.getId()
   3.507 +        if tdef is None:
   3.508 +            new_state = self.initial_state
   3.509 +            former_status = {}
   3.510 +        else:
   3.511 +            new_state = tdef.new_state_id
   3.512 +            if not new_state:
   3.513 +                # Stay in same state.
   3.514 +                new_state = old_state
   3.515 +            former_status = self._getStatusOf(ob)
   3.516 +        new_sdef = self.states.get(new_state, None)
   3.517 +        if new_sdef is None:
   3.518 +            raise WorkflowException, (
   3.519 +                'Destination state undefined: ' + new_state)
   3.520 +
   3.521 +        # Execute the "before" script.
   3.522 +        if tdef is not None and tdef.script_name:
   3.523 +            script = self.scripts[tdef.script_name]
   3.524 +            # Pass lots of info to the script in a single parameter.
   3.525 +            sci = StateChangeInfo(
   3.526 +                ob, self, former_status, tdef, old_sdef, new_sdef, kwargs)
   3.527 +            try:
   3.528 +                script(sci)  # May throw an exception.
   3.529 +            except ObjectMoved, moved_exc:
   3.530 +                ob = moved_exc.getNewObject()
   3.531 +                # Re-raise after transition
   3.532 +
   3.533 +        # Update variables.
   3.534 +        state_values = new_sdef.var_values
   3.535 +        if state_values is None: state_values = {}
   3.536 +        tdef_exprs = None
   3.537 +        if tdef is not None: tdef_exprs = tdef.var_exprs
   3.538 +        if tdef_exprs is None: tdef_exprs = {}
   3.539 +        status = {}
   3.540 +        for id, vdef in self.variables.items():
   3.541 +            if not vdef.for_status:
   3.542 +                continue
   3.543 +            expr = None
   3.544 +            if state_values.has_key(id):
   3.545 +                value = state_values[id]
   3.546 +            elif tdef_exprs.has_key(id):
   3.547 +                expr = tdef_exprs[id]
   3.548 +            elif not vdef.update_always and former_status.has_key(id):
   3.549 +                # Preserve former value
   3.550 +                value = former_status[id]
   3.551 +            else:
   3.552 +                if vdef.default_expr is not None:
   3.553 +                    expr = vdef.default_expr
   3.554 +                else:
   3.555 +                    value = vdef.default_value
   3.556 +            if expr is not None:
   3.557 +                # Evaluate an expression.
   3.558 +                if econtext is None:
   3.559 +                    # Lazily create the expression context.
   3.560 +                    if sci is None:
   3.561 +                        sci = StateChangeInfo(
   3.562 +                            ob, self, former_status, tdef,
   3.563 +                            old_sdef, new_sdef, kwargs)
   3.564 +                    econtext = createExprContext(sci)
   3.565 +                value = expr(econtext)
   3.566 +            status[id] = value
   3.567 +
   3.568 +        # Update state.
   3.569 +        status[self.state_var] = new_state
   3.570 +        tool = aq_parent(aq_inner(self))
   3.571 +        tool.setStatusOf(self.id, ob, status)
   3.572 +
   3.573 +        # Update role to permission assignments.
   3.574 +        self.updateRoleMappingsFor(ob)
   3.575 +
   3.576 +        # Execute the "after" script.
   3.577 +        if tdef is not None and tdef.after_script_name:
   3.578 +            script = self.scripts[tdef.after_script_name]
   3.579 +            # Pass lots of info to the script in a single parameter.
   3.580 +            sci = StateChangeInfo(
   3.581 +                ob, self, status, tdef, old_sdef, new_sdef, kwargs)
   3.582 +            script(sci)  # May throw an exception.
   3.583 +
   3.584 +        # Return the new state object.
   3.585 +        if moved_exc is not None:
   3.586 +            # Propagate the notification that the object has moved.
   3.587 +            raise moved_exc
   3.588 +        else:
   3.589 +            return new_sdef
   3.590 +
   3.591 +InitializeClass(DCWorkflowDefinition)
   3.592 +
   3.593 +
   3.594 +addWorkflowFactory(DCWorkflowDefinition, id='dc_workflow',
   3.595 +                   title='Web-configurable workflow')
     4.1 new file mode 100644
     4.2 --- /dev/null
     4.3 +++ b/DEPENDENCIES.txt
     4.4 @@ -0,0 +1,4 @@
     4.5 +Zope >= 2.8.5
     4.6 +Five >= 1.2
     4.7 +CMFCore
     4.8 +GenericSetup
     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 36457 2004-08-12 15:07:44Z jens $
    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 36930 2005-04-15 09:00:50Z jens $
    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/Extensions/test_method.py
     7.4 @@ -0,0 +1,19 @@
     7.5 +##############################################################################
     7.6 +#
     7.7 +# Copyright (c) 2005 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 +""" External method used for unit tests - do not remove
    7.18 +
    7.19 +$Id: test_method.py 37115 2005-07-07 15:50:07Z jens $
    7.20 +""" 
    7.21 +
    7.22 +def test(self):
    7.23 +    return None
     8.1 new file mode 100644
     8.2 --- /dev/null
     8.3 +++ b/Guard.py
     8.4 @@ -0,0 +1,180 @@
     8.5 +##############################################################################
     8.6 +#
     8.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
     8.8 +#
     8.9 +# This software is subject to the provisions of the Zope Public License,
    8.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
    8.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
    8.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    8.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    8.14 +# FOR A PARTICULAR PURPOSE.
    8.15 +#
    8.16 +##############################################################################
    8.17 +""" Guard conditions in a web-configurable workflow.
    8.18 +
    8.19 +$Id: Guard.py 36985 2005-04-26 14:59:28Z anguenot $
    8.20 +"""
    8.21 +
    8.22 +from cgi import escape
    8.23 +
    8.24 +from Globals import DTMLFile
    8.25 +from Globals import InitializeClass
    8.26 +from Globals import Persistent
    8.27 +from AccessControl import ClassSecurityInfo
    8.28 +from Acquisition import Explicit
    8.29 +from Acquisition import aq_base
    8.30 +
    8.31 +from Products.CMFCore.utils import _checkPermission
    8.32 +
    8.33 +from Expression import Expression
    8.34 +from Expression import StateChangeInfo
    8.35 +from Expression import createExprContext
    8.36 +from permissions import ManagePortal
    8.37 +from utils import _dtmldir
    8.38 +
    8.39 +
    8.40 +class Guard (Persistent, Explicit):
    8.41 +    permissions = ()
    8.42 +    roles = ()
    8.43 +    groups = ()
    8.44 +    expr = None
    8.45 +
    8.46 +    security = ClassSecurityInfo()
    8.47 +    security.declareObjectProtected(ManagePortal)
    8.48 +
    8.49 +    guardForm = DTMLFile('guard', _dtmldir)
    8.50 +
    8.51 +    def check(self, sm, wf_def, ob, **kw):
    8.52 +        """Checks conditions in this guard.
    8.53 +        """
    8.54 +        u_roles = None
    8.55 +        if wf_def.manager_bypass:
    8.56 +            # Possibly bypass.
    8.57 +            u_roles = sm.getUser().getRolesInContext(ob)
    8.58 +            if 'Manager' in u_roles:
    8.59 +                return 1
    8.60 +        if self.permissions:
    8.61 +            for p in self.permissions:
    8.62 +                if _checkPermission(p, ob):
    8.63 +                    break
    8.64 +            else:
    8.65 +                return 0
    8.66 +        if self.roles:
    8.67 +            # Require at least one of the given roles.
    8.68 +            if u_roles is None:
    8.69 +                u_roles = sm.getUser().getRolesInContext(ob)
    8.70 +            for role in self.roles:
    8.71 +                if role in u_roles:
    8.72 +                    break
    8.73 +            else:
    8.74 +                return 0
    8.75 +        if self.groups:
    8.76 +            # Require at least one of the specified groups.
    8.77 +            u = sm.getUser()
    8.78 +            b = aq_base( u )
    8.79 +            if hasattr( b, 'getGroupsInContext' ):
    8.80 +                u_groups = u.getGroupsInContext( ob )
    8.81 +            elif hasattr( b, 'getGroups' ):
    8.82 +                u_groups = u.getGroups()
    8.83 +            else:
    8.84 +                u_groups = ()
    8.85 +            for group in self.groups:
    8.86 +                if group in u_groups:
    8.87 +                    break
    8.88 +            else:
    8.89 +                return 0
    8.90 +        expr = self.expr
    8.91 +        if expr is not None:
    8.92 +            econtext = createExprContext(
    8.93 +                StateChangeInfo(ob, wf_def, kwargs=kw))
    8.94 +            res = expr(econtext)
    8.95 +            if not res:
    8.96 +                return 0
    8.97 +        return 1
    8.98 +
    8.99 +    security.declareProtected(ManagePortal, 'getSummary')
   8.100 +    def getSummary(self):
   8.101 +        # Perhaps ought to be in DTML.
   8.102 +        res = []
   8.103 +        if self.permissions:
   8.104 +            res.append('Requires permission:')
   8.105 +            res.append(formatNameUnion(self.permissions))
   8.106 +        if self.roles:
   8.107 +            if res:
   8.108 +                res.append('<br/>')
   8.109 +            res.append('Requires role:')
   8.110 +            res.append(formatNameUnion(self.roles))
   8.111 +        if self.groups:
   8.112 +            if res:
   8.113 +                res.append('<br/>')
   8.114 +            res.append('Requires group:')
   8.115 +            res.append(formatNameUnion(self.groups))
   8.116 +        if self.expr is not None:
   8.117 +            if res:
   8.118 +                res.append('<br/>')
   8.119 +            res.append('Requires expr:')
   8.120 +            res.append('<code>' + escape(self.expr.text) + '</code>')
   8.121 +        return ' '.join(res)
   8.122 +
   8.123 +    def changeFromProperties(self, props):
   8.124 +        '''
   8.125 +        Returns 1 if changes were specified.
   8.126 +        '''
   8.127 +        if props is None:
   8.128 +            return 0
   8.129 +        res = 0
   8.130 +        s = props.get('guard_permissions', None)
   8.131 +        if s:
   8.132 +            res = 1
   8.133 +            p = [ permission.strip() for permission in s.split(';') ]
   8.134 +            self.permissions = tuple(p)
   8.135 +        s = props.get('guard_roles', None)
   8.136 +        if s:
   8.137 +            res = 1
   8.138 +            r = [ role.strip() for role in s.split(';') ]
   8.139 +            self.roles = tuple(r)
   8.140 +        s = props.get('guard_groups', None)
   8.141 +        if s:
   8.142 +            res = 1
   8.143 +            g = [ group.strip() for group in s.split(';') ]
   8.144 +            self.groups = tuple(g)
   8.145 +        s = props.get('guard_expr', None)
   8.146 +        if s:
   8.147 +            res = 1
   8.148 +            self.expr = Expression(s)
   8.149 +        return res
   8.150 +
   8.151 +    security.declareProtected(ManagePortal, 'getPermissionsText')
   8.152 +    def getPermissionsText(self):
   8.153 +        if not self.permissions:
   8.154 +            return ''
   8.155 +        return '; '.join(self.permissions)
   8.156 +
   8.157 +    security.declareProtected(ManagePortal, 'getRolesText')
   8.158 +    def getRolesText(self):
   8.159 +        if not self.roles:
   8.160 +            return ''
   8.161 +        return '; '.join(self.roles)
   8.162 +
   8.163 +    security.declareProtected(ManagePortal, 'getGroupsText')
   8.164 +    def getGroupsText(self):
   8.165 +        if not self.groups:
   8.166 +            return ''
   8.167 +        return '; '.join(self.groups)
   8.168 +
   8.169 +    security.declareProtected(ManagePortal, 'getExprText')
   8.170 +    def getExprText(self):
   8.171 +        if not self.expr:
   8.172 +            return ''
   8.173 +        return str(self.expr.text)
   8.174 +
   8.175 +InitializeClass(Guard)
   8.176 +
   8.177 +
   8.178 +def formatNameUnion(names):
   8.179 +    escaped = ['<code>' + escape(name) + '</code>' for name in names]
   8.180 +    if len(escaped) == 2:
   8.181 +        return ' or '.join(escaped)
   8.182 +    elif len(escaped) > 2:
   8.183 +        escaped[-1] = ' or ' + escaped[-1]
   8.184 +    return '; '.join(escaped)
     9.1 new file mode 100644
     9.2 --- /dev/null
     9.3 +++ b/README.txt
     9.4 @@ -0,0 +1,10 @@
     9.5 +
     9.6 +There is some documentation in the 'doc' directory.
     9.7 +
     9.8 +I encourage you to experiment with what's available and invite you to
     9.9 +ask questions about the tool on zope-cmf@zope.org.  Future development
    9.10 +depends entirely on people's needs, so speak up!
    9.11 +
    9.12 +Shane Hathaway
    9.13 +Zope Corporation
    9.14 +shane@zope.com
    10.1 new file mode 100644
    10.2 --- /dev/null
    10.3 +++ b/Scripts.py
    10.4 @@ -0,0 +1,41 @@
    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 +""" Scripts in a web-configurable workflow.
   10.18 +
   10.19 +$Id: Scripts.py 37044 2005-06-14 18:08:03Z sidnei $
   10.20 +"""
   10.21 +
   10.22 +from OFS.Folder import Folder
   10.23 +from Globals import InitializeClass
   10.24 +from AccessControl import ClassSecurityInfo
   10.25 +
   10.26 +from ContainerTab import ContainerTab
   10.27 +from permissions import ManagePortal
   10.28 +
   10.29 +
   10.30 +class Scripts (ContainerTab):
   10.31 +    """A container for workflow scripts"""
   10.32 +
   10.33 +    meta_type = 'Workflow Scripts'
   10.34 +
   10.35 +    security = ClassSecurityInfo()
   10.36 +    security.declareObjectProtected(ManagePortal)
   10.37 +
   10.38 +    def manage_main(self, client=None, REQUEST=None, **kw):
   10.39 +        '''
   10.40 +        '''
   10.41 +        kw['management_view'] = 'Scripts'
   10.42 +        m = Folder.manage_main.__of__(self)
   10.43 +        return m(self, client, REQUEST, **kw)
   10.44 +
   10.45 +InitializeClass(Scripts)
    11.1 new file mode 100644
    11.2 --- /dev/null
    11.3 +++ b/States.py
    11.4 @@ -0,0 +1,304 @@
    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 +""" States in a web-configurable workflow.
   11.18 +
   11.19 +$Id: States.py 36911 2005-04-07 16:38:47Z yuppie $
   11.20 +"""
   11.21 +
   11.22 +from AccessControl import ClassSecurityInfo
   11.23 +from Acquisition import aq_inner
   11.24 +from Acquisition import aq_parent
   11.25 +from Globals import DTMLFile
   11.26 +from Globals import InitializeClass
   11.27 +from Globals import PersistentMapping
   11.28 +from OFS.SimpleItem import SimpleItem
   11.29 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   11.30 +
   11.31 +from ContainerTab import ContainerTab
   11.32 +from permissions import ManagePortal
   11.33 +from utils import _dtmldir
   11.34 +
   11.35 +
   11.36 +class StateDefinition(SimpleItem):
   11.37 +    """State definition"""
   11.38 +
   11.39 +    meta_type = 'Workflow State'
   11.40 +
   11.41 +    manage_options = (
   11.42 +        {'label': 'Properties', 'action': 'manage_properties'},
   11.43 +        {'label': 'Permissions', 'action': 'manage_permissions'},
   11.44 +        {'label': 'Groups', 'action': 'manage_groups'},
   11.45 +        {'label': 'Variables', 'action': 'manage_variables'},
   11.46 +        )
   11.47 +
   11.48 +    title = ''
   11.49 +    description = ''
   11.50 +    transitions = ()  # The ids of possible transitions.
   11.51 +    permission_roles = None  # { permission: [role] or (role,) }
   11.52 +    group_roles = None  # { group name : (role,) }
   11.53 +    var_values = None  # PersistentMapping if set.  Overrides transition exprs.
   11.54 +
   11.55 +    security = ClassSecurityInfo()
   11.56 +    security.declareObjectProtected(ManagePortal)
   11.57 +
   11.58 +    def __init__(self, id):
   11.59 +        self.id = id
   11.60 +
   11.61 +    def getId(self):
   11.62 +        return self.id
   11.63 +
   11.64 +    def getWorkflow(self):
   11.65 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   11.66 +
   11.67 +    def getTransitions(self):
   11.68 +        return filter(self.getWorkflow().transitions.has_key,
   11.69 +                      self.transitions)
   11.70 +
   11.71 +    def getTransitionTitle(self, tid):
   11.72 +        t = self.getWorkflow().transitions.get(tid, None)
   11.73 +        if t is not None:
   11.74 +            return t.title
   11.75 +        return ''
   11.76 +
   11.77 +    def getAvailableTransitionIds(self):
   11.78 +        return self.getWorkflow().transitions.keys()
   11.79 +
   11.80 +    def getAvailableVarIds(self):
   11.81 +        return self.getWorkflow().variables.keys()
   11.82 +
   11.83 +    def getManagedPermissions(self):
   11.84 +        return list(self.getWorkflow().permissions)
   11.85 +
   11.86 +    def getAvailableRoles(self):
   11.87 +        return self.getWorkflow().getAvailableRoles()
   11.88 +
   11.89 +    def getPermissionInfo(self, p):
   11.90 +        """Returns the list of roles to be assigned to a permission.
   11.91 +        """
   11.92 +        roles = None
   11.93 +        if self.permission_roles:
   11.94 +            roles = self.permission_roles.get(p, None)
   11.95 +        if roles is None:
   11.96 +            return {'acquired':1, 'roles':[]}
   11.97 +        else:
   11.98 +            if isinstance(roles, tuple):
   11.99 +                acq = 0
  11.100 +            else:
  11.101 +                acq = 1
  11.102 +            return {'acquired':acq, 'roles':list(roles)}
  11.103 +
  11.104 +    def getGroupInfo(self, group):
  11.105 +        """Returns the list of roles to be assigned to a group.
  11.106 +        """
  11.107 +        if self.group_roles:
  11.108 +            return self.group_roles.get(group, ())
  11.109 +        return ()
  11.110 +
  11.111 +    _properties_form = DTMLFile('state_properties', _dtmldir)
  11.112 +
  11.113 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
  11.114 +        """Show state properties ZMI form."""
  11.115 +        return self._properties_form(REQUEST,
  11.116 +                                     management_view='Properties',
  11.117 +                                     manage_tabs_message=manage_tabs_message,
  11.118 +                                     )
  11.119 +
  11.120 +    def setProperties(self, title='', transitions=(), REQUEST=None, description=''):
  11.121 +        """Set the properties for this State."""
  11.122 +        self.title = str(title)
  11.123 +        self.description = str(description)
  11.124 +        self.transitions = tuple(map(str, transitions))
  11.125 +        if REQUEST is not None:
  11.126 +            return self.manage_properties(REQUEST, 'Properties changed.')
  11.127 +
  11.128 +
  11.129 +    _variables_form = DTMLFile('state_variables', _dtmldir)
  11.130 +
  11.131 +    def manage_variables(self, REQUEST, manage_tabs_message=None):
  11.132 +        """Show State variables ZMI form."""
  11.133 +        return self._variables_form(REQUEST,
  11.134 +                                     management_view='Variables',
  11.135 +                                     manage_tabs_message=manage_tabs_message,
  11.136 +                                     )
  11.137 +
  11.138 +    def getVariableValues(self):
  11.139 +        """Get VariableValues for management UI."""
  11.140 +        vv = self.var_values
  11.141 +        if vv is None:
  11.142 +            return []
  11.143 +        else:
  11.144 +            return vv.items()
  11.145 +
  11.146 +    def getWorkflowVariables(self):
  11.147 +        """Get all variables that are available from the workflow and
  11.148 +        not handled yet.
  11.149 +        """
  11.150 +        wf_vars = self.getAvailableVarIds()
  11.151 +        if self.var_values is None:
  11.152 +            return wf_vars
  11.153 +        ret = []
  11.154 +        for vid in wf_vars:
  11.155 +            if not self.var_values.has_key(vid):
  11.156 +                ret.append(vid)
  11.157 +        return ret
  11.158 +
  11.159 +    def addVariable(self,id,value,REQUEST=None):
  11.160 +        """Add a WorkflowVariable to State."""
  11.161 +        if self.var_values is None:
  11.162 +            self.var_values = PersistentMapping()
  11.163 +
  11.164 +        self.var_values[id] = value
  11.165 +
  11.166 +        if REQUEST is not None:
  11.167 +            return self.manage_variables(REQUEST, 'Variable added.')
  11.168 +
  11.169 +    def deleteVariables(self,ids=[],REQUEST=None):
  11.170 +        """Delete a WorkflowVariable from State."""
  11.171 +        vv = self.var_values
  11.172 +        for id in ids:
  11.173 +            if vv.has_key(id):
  11.174 +                del vv[id]
  11.175 +
  11.176 +        if REQUEST is not None:
  11.177 +            return self.manage_variables(REQUEST, 'Variables deleted.')
  11.178 +
  11.179 +    def setVariables(self, ids=[], REQUEST=None):
  11.180 +        """Set values for Variables set by this State."""
  11.181 +        if self.var_values is None:
  11.182 +            self.var_values = PersistentMapping()
  11.183 +
  11.184 +        vv = self.var_values
  11.185 +
  11.186 +        if REQUEST is not None:
  11.187 +            for id in vv.keys():
  11.188 +                fname = 'varval_%s' % id
  11.189 +                vv[id] = str(REQUEST[fname])
  11.190 +            return self.manage_variables(REQUEST, 'Variables changed.')
  11.191 +
  11.192 +
  11.193 +
  11.194 +    _permissions_form = DTMLFile('state_permissions', _dtmldir)
  11.195 +
  11.196 +    def manage_permissions(self, REQUEST, manage_tabs_message=None):
  11.197 +        """Present TTW UI for managing this State's permissions."""
  11.198 +        return self._permissions_form(REQUEST,
  11.199 +                                     management_view='Permissions',
  11.200 +                                     manage_tabs_message=manage_tabs_message,
  11.201 +                                     )
  11.202 +
  11.203 +    def setPermissions(self, REQUEST):
  11.204 +        """Set the permissions in REQUEST for this State."""
  11.205 +        pr = self.permission_roles
  11.206 +        if pr is None:
  11.207 +            self.permission_roles = pr = PersistentMapping()
  11.208 +        pr.clear()
  11.209 +        for p in self.getManagedPermissions():
  11.210 +            roles = []
  11.211 +            acquired = REQUEST.get('acquire_' + p, 0)
  11.212 +            for r in self.getAvailableRoles():
  11.213 +                if REQUEST.get('%s|%s' % (p, r), 0):
  11.214 +                    roles.append(r)
  11.215 +            roles.sort()
  11.216 +            if not acquired:
  11.217 +                roles = tuple(roles)
  11.218 +            pr[p] = roles
  11.219 +        return self.manage_permissions(REQUEST, 'Permissions changed.')
  11.220 +
  11.221 +    def setPermission(self, permission, acquired, roles):
  11.222 +        """Set a permission for this State."""
  11.223 +        pr = self.permission_roles
  11.224 +        if pr is None:
  11.225 +            self.permission_roles = pr = PersistentMapping()
  11.226 +        if acquired:
  11.227 +            roles = list(roles)
  11.228 +        else:
  11.229 +            roles = tuple(roles)
  11.230 +        pr[permission] = roles
  11.231 +
  11.232 +    manage_groups = PageTemplateFile('state_groups.pt', _dtmldir)
  11.233 +
  11.234 +    def setGroups(self, REQUEST, RESPONSE=None):
  11.235 +        """Set the group to role mappings in REQUEST for this State.
  11.236 +        """
  11.237 +        map = self.group_roles
  11.238 +        if map is None:
  11.239 +            self.group_roles = map = PersistentMapping()
  11.240 +        map.clear()
  11.241 +        all_roles = self.getWorkflow().getRoles()
  11.242 +        for group in self.getWorkflow().getGroups():
  11.243 +            roles = []
  11.244 +            for role in all_roles:
  11.245 +                if REQUEST.get('%s|%s' % (group, role), 0):
  11.246 +                    roles.append(role)
  11.247 +            roles.sort()
  11.248 +            roles = tuple(roles)
  11.249 +            map[group] = roles
  11.250 +        if RESPONSE is not None:
  11.251 +            RESPONSE.redirect(
  11.252 +                "%s/manage_groups?manage_tabs_message=Groups+changed."
  11.253 +                % self.absolute_url())
  11.254 +
  11.255 +InitializeClass(StateDefinition)
  11.256 +
  11.257 +
  11.258 +class States(ContainerTab):
  11.259 +    """A container for state definitions"""
  11.260 +
  11.261 +    meta_type = 'Workflow States'
  11.262 +
  11.263 +    security = ClassSecurityInfo()
  11.264 +    security.declareObjectProtected(ManagePortal)
  11.265 +
  11.266 +    all_meta_types = ({'name':StateDefinition.meta_type,
  11.267 +                       'action':'addState',
  11.268 +                       },)
  11.269 +
  11.270 +    _manage_states = DTMLFile('states', _dtmldir)
  11.271 +
  11.272 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  11.273 +        '''
  11.274 +        '''
  11.275 +        return self._manage_states(REQUEST,
  11.276 +                                   management_view='States',
  11.277 +                                   manage_tabs_message=manage_tabs_message,
  11.278 +                                   )
  11.279 +
  11.280 +    def addState(self, id, REQUEST=None):
  11.281 +        '''
  11.282 +        '''
  11.283 +        sdef = StateDefinition(id)
  11.284 +        self._setObject(id, sdef)
  11.285 +        if REQUEST is not None:
  11.286 +            return self.manage_main(REQUEST, 'State added.')
  11.287 +
  11.288 +    def deleteStates(self, ids, REQUEST=None):
  11.289 +        '''
  11.290 +        '''
  11.291 +        for id in ids:
  11.292 +            self._delObject(id)
  11.293 +        if REQUEST is not None:
  11.294 +            return self.manage_main(REQUEST, 'State(s) removed.')
  11.295 +
  11.296 +    def setInitialState(self, id=None, ids=None, REQUEST=None):
  11.297 +        '''
  11.298 +        '''
  11.299 +        if not id:
  11.300 +            if len(ids) != 1:
  11.301 +                raise ValueError, 'One and only one state must be selected'
  11.302 +            id = ids[0]
  11.303 +        id = str(id)
  11.304 +        aq_parent(aq_inner(self)).initial_state = id
  11.305 +        if REQUEST is not None:
  11.306 +            return self.manage_main(REQUEST, 'Initial state selected.')
  11.307 +
  11.308 +InitializeClass(States)
    12.1 new file mode 100644
    12.2 --- /dev/null
    12.3 +++ b/Transitions.py
    12.4 @@ -0,0 +1,260 @@
    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 +""" Transitions in a web-configurable workflow.
   12.18 +
   12.19 +$Id: Transitions.py 36457 2004-08-12 15:07:44Z jens $
   12.20 +"""
   12.21 +
   12.22 +from OFS.SimpleItem import SimpleItem
   12.23 +from Globals import DTMLFile
   12.24 +from Globals import PersistentMapping
   12.25 +from Globals import InitializeClass
   12.26 +from Acquisition import aq_inner
   12.27 +from Acquisition import aq_parent
   12.28 +from AccessControl import ClassSecurityInfo
   12.29 +
   12.30 +from ContainerTab import ContainerTab
   12.31 +from Guard import Guard
   12.32 +from permissions import ManagePortal
   12.33 +from utils import _dtmldir
   12.34 +from Expression import Expression
   12.35 +
   12.36 +TRIGGER_AUTOMATIC = 0
   12.37 +TRIGGER_USER_ACTION = 1
   12.38 +TRIGGER_WORKFLOW_METHOD = 2
   12.39 +
   12.40 +
   12.41 +class TransitionDefinition (SimpleItem):
   12.42 +    """Transition definition"""
   12.43 +
   12.44 +    meta_type = 'Workflow Transition'
   12.45 +
   12.46 +    security = ClassSecurityInfo()
   12.47 +    security.declareObjectProtected(ManagePortal)
   12.48 +
   12.49 +    title = ''
   12.50 +    description = ''
   12.51 +    new_state_id = ''
   12.52 +    trigger_type = TRIGGER_USER_ACTION
   12.53 +    guard = None
   12.54 +    actbox_name = ''
   12.55 +    actbox_url = ''
   12.56 +    actbox_category = 'workflow'
   12.57 +    var_exprs = None  # A mapping.
   12.58 +    script_name = None  # Executed before transition
   12.59 +    after_script_name = None  # Executed after transition
   12.60 +
   12.61 +    manage_options = (
   12.62 +        {'label': 'Properties', 'action': 'manage_properties'},
   12.63 +        {'label': 'Variables', 'action': 'manage_variables'},
   12.64 +        )
   12.65 +
   12.66 +    def __init__(self, id):
   12.67 +        self.id = id
   12.68 +
   12.69 +    def getId(self):
   12.70 +        return self.id
   12.71 +
   12.72 +    def getGuardSummary(self):
   12.73 +        res = None
   12.74 +        if self.guard is not None:
   12.75 +            res = self.guard.getSummary()
   12.76 +        return res
   12.77 +
   12.78 +    def getGuard(self):
   12.79 +        if self.guard is not None:
   12.80 +            return self.guard
   12.81 +        else:
   12.82 +            return Guard().__of__(self)  # Create a temporary guard.
   12.83 +
   12.84 +    def getVarExprText(self, id):
   12.85 +        if not self.var_exprs:
   12.86 +            return ''
   12.87 +        else:
   12.88 +            expr = self.var_exprs.get(id, None)
   12.89 +            if expr is not None:
   12.90 +                return expr.text
   12.91 +            else:
   12.92 +                return ''
   12.93 +
   12.94 +    def getWorkflow(self):
   12.95 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   12.96 +
   12.97 +    def getAvailableStateIds(self):
   12.98 +        return self.getWorkflow().states.keys()
   12.99 +
  12.100 +    def getAvailableScriptIds(self):
  12.101 +        return self.getWorkflow().scripts.keys()
  12.102 +
  12.103 +    def getAvailableVarIds(self):
  12.104 +        return self.getWorkflow().variables.keys()
  12.105 +
  12.106 +    _properties_form = DTMLFile('transition_properties', _dtmldir)
  12.107 +
  12.108 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
  12.109 +        '''
  12.110 +        '''
  12.111 +        return self._properties_form(REQUEST,
  12.112 +                                     management_view='Properties',
  12.113 +                                     manage_tabs_message=manage_tabs_message,
  12.114 +                                     )
  12.115 +
  12.116 +    def setProperties(self, title, new_state_id,
  12.117 +                      trigger_type=TRIGGER_USER_ACTION, script_name='',
  12.118 +                      after_script_name='',
  12.119 +                      actbox_name='', actbox_url='',
  12.120 +                      actbox_category='workflow',
  12.121 +                      props=None, REQUEST=None, description=''):
  12.122 +        '''
  12.123 +        '''
  12.124 +        self.title = str(title)
  12.125 +        self.description = str(description)
  12.126 +        self.new_state_id = str(new_state_id)
  12.127 +        self.trigger_type = int(trigger_type)
  12.128 +        self.script_name = str(script_name)
  12.129 +        self.after_script_name = str(after_script_name)
  12.130 +        g = Guard()
  12.131 +        if g.changeFromProperties(props or REQUEST):
  12.132 +            self.guard = g
  12.133 +        else:
  12.134 +            self.guard = None
  12.135 +        self.actbox_name = str(actbox_name)
  12.136 +        self.actbox_url = str(actbox_url)
  12.137 +        self.actbox_category = str(actbox_category)
  12.138 +        if REQUEST is not None:
  12.139 +            return self.manage_properties(REQUEST, 'Properties changed.')
  12.140 +
  12.141 +    _variables_form = DTMLFile('transition_variables', _dtmldir)
  12.142 +
  12.143 +    def manage_variables(self, REQUEST, manage_tabs_message=None):
  12.144 +        '''
  12.145 +        '''
  12.146 +        return self._variables_form(REQUEST,
  12.147 +                                     management_view='Variables',
  12.148 +                                     manage_tabs_message=manage_tabs_message,
  12.149 +                                     )
  12.150 +
  12.151 +    def getVariableExprs(self):
  12.152 +        ''' get variable exprs for management UI
  12.153 +        '''
  12.154 +        ve = self.var_exprs
  12.155 +        if ve is None:
  12.156 +            return []
  12.157 +        else:
  12.158 +            ret = []
  12.159 +            for key in ve.keys():
  12.160 +                ret.append((key,self.getVarExprText(key)))
  12.161 +            return ret
  12.162 +
  12.163 +    def getWorkflowVariables(self):
  12.164 +        ''' get all variables that are available form
  12.165 +            workflow and not handled yet.
  12.166 +        '''
  12.167 +        wf_vars = self.getAvailableVarIds()
  12.168 +        if self.var_exprs is None:
  12.169 +                return wf_vars
  12.170 +        ret = []
  12.171 +        for vid in wf_vars:
  12.172 +            if not self.var_exprs.has_key(vid):
  12.173 +                ret.append(vid)
  12.174 +        return ret
  12.175 +
  12.176 +    def addVariable(self, id, text, REQUEST=None):
  12.177 +        '''
  12.178 +        Add a variable expression.
  12.179 +        '''
  12.180 +        if self.var_exprs is None:
  12.181 +            self.var_exprs = PersistentMapping()
  12.182 +
  12.183 +        expr = None
  12.184 +        if text:
  12.185 +          expr = Expression(str(text))
  12.186 +        self.var_exprs[id] = expr
  12.187 +
  12.188 +        if REQUEST is not None:
  12.189 +            return self.manage_variables(REQUEST, 'Variable added.')
  12.190 +
  12.191 +    def deleteVariables(self,ids=[],REQUEST=None):
  12.192 +        ''' delete a WorkflowVariable from State
  12.193 +        '''
  12.194 +        ve = self.var_exprs
  12.195 +        for id in ids:
  12.196 +            if ve.has_key(id):
  12.197 +                del ve[id]
  12.198 +
  12.199 +        if REQUEST is not None:
  12.200 +            return self.manage_variables(REQUEST, 'Variables deleted.')
  12.201 +
  12.202 +    def setVariables(self, ids=[], REQUEST=None):
  12.203 +        ''' set values for Variables set by this state
  12.204 +        '''
  12.205 +        if self.var_exprs is None:
  12.206 +            self.var_exprs = PersistentMapping()
  12.207 +
  12.208 +        ve = self.var_exprs
  12.209 +
  12.210 +        if REQUEST is not None:
  12.211 +            for id in ve.keys():
  12.212 +                fname = 'varexpr_%s' % id
  12.213 +
  12.214 +                val = REQUEST[fname]
  12.215 +                expr = None
  12.216 +                if val:
  12.217 +                    expr = Expression(str(REQUEST[fname]))
  12.218 +                ve[id] = expr
  12.219 +
  12.220 +            return self.manage_variables(REQUEST, 'Variables changed.')
  12.221 +
  12.222 +InitializeClass(TransitionDefinition)
  12.223 +
  12.224 +
  12.225 +class Transitions (ContainerTab):
  12.226 +    """A container for transition definitions"""
  12.227 +
  12.228 +    meta_type = 'Workflow Transitions'
  12.229 +
  12.230 +    security = ClassSecurityInfo()
  12.231 +    security.declareObjectProtected(ManagePortal)
  12.232 +
  12.233 +    all_meta_types = ({'name':TransitionDefinition.meta_type,
  12.234 +                       'action':'addTransition',
  12.235 +                       },)
  12.236 +
  12.237 +    _manage_transitions = DTMLFile('transitions', _dtmldir)
  12.238 +
  12.239 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  12.240 +        '''
  12.241 +        '''
  12.242 +        return self._manage_transitions(
  12.243 +            REQUEST,
  12.244 +            management_view='Transitions',
  12.245 +            manage_tabs_message=manage_tabs_message,
  12.246 +            )
  12.247 +
  12.248 +    def addTransition(self, id, REQUEST=None):
  12.249 +        '''
  12.250 +        '''
  12.251 +        tdef = TransitionDefinition(id)
  12.252 +        self._setObject(id, tdef)
  12.253 +        if REQUEST is not None:
  12.254 +            return self.manage_main(REQUEST, 'Transition added.')
  12.255 +
  12.256 +    def deleteTransitions(self, ids, REQUEST=None):
  12.257 +        '''
  12.258 +        '''
  12.259 +        for id in ids:
  12.260 +            self._delObject(id)
  12.261 +        if REQUEST is not None:
  12.262 +            return self.manage_main(REQUEST, 'Transition(s) removed.')
  12.263 +
  12.264 +InitializeClass(Transitions)
    13.1 new file mode 100644
    13.2 --- /dev/null
    13.3 +++ b/Variables.py
    13.4 @@ -0,0 +1,167 @@
    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 +""" Variables in a web-configurable workflow.
   13.18 +
   13.19 +$Id: Variables.py 36911 2005-04-07 16:38:47Z yuppie $
   13.20 +"""
   13.21 +
   13.22 +from AccessControl import ClassSecurityInfo
   13.23 +from Acquisition import aq_inner
   13.24 +from Acquisition import aq_parent
   13.25 +from Globals import DTMLFile
   13.26 +from Globals import InitializeClass
   13.27 +from OFS.SimpleItem import SimpleItem
   13.28 +
   13.29 +from ContainerTab import ContainerTab
   13.30 +from Expression import Expression
   13.31 +from Guard import Guard
   13.32 +from permissions import ManagePortal
   13.33 +from utils import _dtmldir
   13.34 +
   13.35 +
   13.36 +class VariableDefinition(SimpleItem):
   13.37 +    """Variable definition"""
   13.38 +
   13.39 +    meta_type = 'Workflow Variable'
   13.40 +
   13.41 +    security = ClassSecurityInfo()
   13.42 +    security.declareObjectProtected(ManagePortal)
   13.43 +
   13.44 +    description = ''
   13.45 +    for_catalog = 1
   13.46 +    for_status = 1
   13.47 +    default_value = ''
   13.48 +    default_expr = None  # Overrides default_value if set
   13.49 +    info_guard = None
   13.50 +    update_always = 1
   13.51 +
   13.52 +    manage_options = (
   13.53 +        {'label': 'Properties', 'action': 'manage_properties'},
   13.54 +        )
   13.55 +
   13.56 +    def __init__(self, id):
   13.57 +        self.id = id
   13.58 +
   13.59 +    def getDefaultExprText(self):
   13.60 +        if not self.default_expr:
   13.61 +            return ''
   13.62 +        else:
   13.63 +            return self.default_expr.text
   13.64 +
   13.65 +    def getInfoGuard(self):
   13.66 +        if self.info_guard is not None:
   13.67 +            return self.info_guard
   13.68 +        else:
   13.69 +            return Guard().__of__(self)  # Create a temporary guard.
   13.70 +
   13.71 +    def getInfoGuardSummary(self):
   13.72 +        res = None
   13.73 +        if self.info_guard is not None:
   13.74 +            res = self.info_guard.getSummary()
   13.75 +        return res
   13.76 +
   13.77 +    _properties_form = DTMLFile('variable_properties', _dtmldir)
   13.78 +
   13.79 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
   13.80 +        '''
   13.81 +        '''
   13.82 +        return self._properties_form(REQUEST,
   13.83 +                                     management_view='Properties',
   13.84 +                                     manage_tabs_message=manage_tabs_message,
   13.85 +                                     )
   13.86 +
   13.87 +    def setProperties(self, description,
   13.88 +                      default_value='', default_expr='',
   13.89 +                      for_catalog=0, for_status=0,
   13.90 +                      update_always=0,
   13.91 +                      props=None, REQUEST=None):
   13.92 +        '''
   13.93 +        '''
   13.94 +        self.description = str(description)
   13.95 +        self.default_value = str(default_value)
   13.96 +        if default_expr:
   13.97 +            self.default_expr = Expression(default_expr)
   13.98 +        else:
   13.99 +            self.default_expr = None
  13.100 +
  13.101 +        g = Guard()
  13.102 +        if g.changeFromProperties(props or REQUEST):
  13.103 +            self.info_guard = g
  13.104 +        else:
  13.105 +            self.info_guard = None
  13.106 +        self.for_catalog = bool(for_catalog)
  13.107 +        self.for_status = bool(for_status)
  13.108 +        self.update_always = bool(update_always)
  13.109 +        if REQUEST is not None:
  13.110 +            return self.manage_properties(REQUEST, 'Properties changed.')
  13.111 +
  13.112 +InitializeClass(VariableDefinition)
  13.113 +
  13.114 +
  13.115 +class Variables(ContainerTab):
  13.116 +    """A container for variable definitions"""
  13.117 +
  13.118 +    meta_type = 'Workflow Variables'
  13.119 +
  13.120 +    all_meta_types = ({'name':VariableDefinition.meta_type,
  13.121 +                       'action':'addVariable',
  13.122 +                       },)
  13.123 +
  13.124 +    _manage_variables = DTMLFile('variables', _dtmldir)
  13.125 +
  13.126 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  13.127 +        '''
  13.128 +        '''
  13.129 +        return self._manage_variables(
  13.130 +            REQUEST,
  13.131 +            management_view='Variables',
  13.132 +            manage_tabs_message=manage_tabs_message,
  13.133 +            )
  13.134 +
  13.135 +    def addVariable(self, id, REQUEST=None):
  13.136 +        '''
  13.137 +        '''
  13.138 +        vdef = VariableDefinition(id)
  13.139 +        self._setObject(id, vdef)
  13.140 +        if REQUEST is not None:
  13.141 +            return self.manage_main(REQUEST, 'Variable added.')
  13.142 +
  13.143 +    def deleteVariables(self, ids, REQUEST=None):
  13.144 +        '''
  13.145 +        '''
  13.146 +        for id in ids:
  13.147 +            self._delObject(id)
  13.148 +        if REQUEST is not None:
  13.149 +            return self.manage_main(REQUEST, 'Variable(s) removed.')
  13.150 +
  13.151 +    def _checkId(self, id, allow_dup=0):
  13.152 +        wf_def = aq_parent(aq_inner(self))
  13.153 +        if id == wf_def.state_var:
  13.154 +            raise 'Bad Request', '"%s" is used for keeping state.' % id
  13.155 +        return ContainerTab._checkId(self, id, allow_dup)
  13.156 +
  13.157 +    def getStateVar(self):
  13.158 +        wf_def = aq_parent(aq_inner(self))
  13.159 +        return wf_def.state_var
  13.160 +
  13.161 +    def setStateVar(self, id, REQUEST=None):
  13.162 +        '''
  13.163 +        '''
  13.164 +        wf_def = aq_parent(aq_inner(self))
  13.165 +        if id != wf_def.state_var:
  13.166 +            self._checkId(id)
  13.167 +            wf_def.state_var = str(id)
  13.168 +        if REQUEST is not None:
  13.169 +            return self.manage_main(REQUEST, 'Set state variable.')
  13.170 +
  13.171 +InitializeClass(Variables)
    14.1 new file mode 100644
    14.2 --- /dev/null
    14.3 +++ b/WorkflowUIMixin.py
    14.4 @@ -0,0 +1,220 @@
    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 +""" Web-configurable workflow UI.
   14.18 +
   14.19 +$Id: WorkflowUIMixin.py 36457 2004-08-12 15:07:44Z jens $
   14.20 +"""
   14.21 +
   14.22 +import os
   14.23 +
   14.24 +from Globals import DTMLFile
   14.25 +from Globals import InitializeClass
   14.26 +from AccessControl import ClassSecurityInfo
   14.27 +from Acquisition import aq_get
   14.28 +
   14.29 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   14.30 +
   14.31 +from permissions import ManagePortal
   14.32 +from Guard import Guard
   14.33 +from utils import _dtmldir
   14.34 +
   14.35 +try:
   14.36 +    #
   14.37 +    #   XXX: 2004/04/28  This factoring *has* to go;  if necessary,
   14.38 +    #         this module could have a hook function, which the dependent
   14.39 +    #         module could replace.
   14.40 +    #
   14.41 +
   14.42 +    # If base_cms exists, include the roles it defines.
   14.43 +    from Products.base_cms.permissions import getDefaultRolePermissionMap
   14.44 +except ImportError:
   14.45 +    def getDefaultRolePermissionMap():
   14.46 +        return {}
   14.47 +
   14.48 +
   14.49 +class WorkflowUIMixin:
   14.50 +    '''
   14.51 +    '''
   14.52 +
   14.53 +    security = ClassSecurityInfo()
   14.54 +
   14.55 +    security.declareProtected(ManagePortal, 'manage_properties')
   14.56 +    manage_properties = DTMLFile('workflow_properties', _dtmldir)
   14.57 +    manage_groups = PageTemplateFile('workflow_groups.pt', _dtmldir)
   14.58 +
   14.59 +    security.declareProtected(ManagePortal, 'setProperties')
   14.60 +    def setProperties(self, title, manager_bypass=0, props=None, REQUEST=None):
   14.61 +        """Sets basic properties.
   14.62 +        """
   14.63 +        self.title = str(title)
   14.64 +        self.manager_bypass = manager_bypass and 1 or 0
   14.65 +        g = Guard()
   14.66 +        if g.changeFromProperties(props or REQUEST):
   14.67 +            self.creation_guard = g
   14.68 +        else:
   14.69 +            self.creation_guard = None
   14.70 +        if REQUEST is not None:
   14.71 +            return self.manage_properties(
   14.72 +                REQUEST, manage_tabs_message='Properties changed.')
   14.73 +
   14.74 +    _permissions_form = DTMLFile('workflow_permissions', _dtmldir)
   14.75 +
   14.76 +    security.declareProtected(ManagePortal, 'manage_permissions')
   14.77 +    def manage_permissions(self, REQUEST, manage_tabs_message=None):
   14.78 +        """Displays the form for choosing which permissions to manage.
   14.79 +        """
   14.80 +        return self._permissions_form(REQUEST,
   14.81 +                                      management_view='Permissions',
   14.82 +                                      manage_tabs_message=manage_tabs_message,
   14.83 +                                      )
   14.84 +
   14.85 +    security.declareProtected(ManagePortal, 'addManagedPermission')
   14.86 +    def addManagedPermission(self, p, REQUEST=None):
   14.87 +        """Adds to the list of permissions to manage.
   14.88 +        """
   14.89 +        if p in self.permissions:
   14.90 +            raise ValueError, 'Already a managed permission: ' + p
   14.91 +        if REQUEST is not None and p not in self.getPossiblePermissions():
   14.92 +            raise ValueError, 'Not a valid permission name:' + p
   14.93 +        self.permissions = self.permissions + (p,)
   14.94 +        if REQUEST is not None:
   14.95 +            return self.manage_permissions(
   14.96 +                REQUEST, manage_tabs_message='Permission added.')
   14.97 +
   14.98 +    security.declareProtected(ManagePortal, 'delManagedPermissions')
   14.99 +    def delManagedPermissions(self, ps, REQUEST=None):
  14.100 +        """Removes from the list of permissions to manage.
  14.101 +        """
  14.102 +        if ps:
  14.103 +            l = list(self.permissions)
  14.104 +            for p in ps:
  14.105 +                l.remove(p)
  14.106 +            self.permissions = tuple(l)
  14.107 +        if REQUEST is not None:
  14.108 +            return self.manage_permissions(
  14.109 +                REQUEST, manage_tabs_message='Permission(s) removed.')
  14.110 +
  14.111 +    security.declareProtected(ManagePortal, 'getPossiblePermissions')
  14.112 +    def getPossiblePermissions(self):
  14.113 +        """Returns the list of all permissions that can be managed.
  14.114 +        """
  14.115 +        # possible_permissions is in AccessControl.Role.RoleManager.
  14.116 +        return list(self.possible_permissions())
  14.117 +
  14.118 +    security.declareProtected(ManagePortal, 'getGroups')
  14.119 +    def getGroups(self):
  14.120 +        """Returns the names of groups managed by this workflow.
  14.121 +        """
  14.122 +        return tuple(self.groups)
  14.123 +
  14.124 +    security.declareProtected(ManagePortal, 'getAvailableGroups')
  14.125 +    def getAvailableGroups(self):
  14.126 +        """Returns a list of available group names.
  14.127 +        """
  14.128 +        gf = aq_get( self, '__allow_groups__', None, 1 )
  14.129 +        if gf is None:
  14.130 +            return ()
  14.131 +        try:
  14.132 +            groups = gf.searchGroups()
  14.133 +        except AttributeError:
  14.134 +            return ()
  14.135 +        else:
  14.136 +            return [g['id'] for g in groups]
  14.137 +
  14.138 +    security.declareProtected(ManagePortal, 'addGroup')
  14.139 +    def addGroup(self, group, RESPONSE=None):
  14.140 +        """Adds a group by name.
  14.141 +        """
  14.142 +        if group not in self.getAvailableGroups():
  14.143 +            raise ValueError(group)
  14.144 +        self.groups = self.groups + (group,)
  14.145 +        if RESPONSE is not None:
  14.146 +            RESPONSE.redirect(
  14.147 +                "%s/manage_groups?manage_tabs_message=Added+group."
  14.148 +                % self.absolute_url())
  14.149 +
  14.150 +    security.declareProtected(ManagePortal, 'delGroups')
  14.151 +    def delGroups(self, groups, RESPONSE=None):
  14.152 +        """Removes groups by name.
  14.153 +        """
  14.154 +        self.groups = tuple([g for g in self.groups if g not in groups])
  14.155 +        if RESPONSE is not None:
  14.156 +            RESPONSE.redirect(
  14.157 +                "%s/manage_groups?manage_tabs_message=Groups+removed."
  14.158 +                % self.absolute_url())
  14.159 +
  14.160 +    security.declareProtected(ManagePortal, 'getAvailableRoles')
  14.161 +    def getAvailableRoles(self):
  14.162 +        """Returns the acquired roles mixed with base_cms roles.
  14.163 +        """
  14.164 +        roles = list(self.valid_roles())
  14.165 +        for role in getDefaultRolePermissionMap().keys():
  14.166 +            if role not in roles:
  14.167 +                roles.append(role)
  14.168 +        roles.sort()
  14.169 +        return roles
  14.170 +
  14.171 +    security.declareProtected(ManagePortal, 'getRoles')
  14.172 +    def getRoles(self):
  14.173 +        """Returns the list of roles managed by this workflow.
  14.174 +        """
  14.175 +        roles = self.roles
  14.176 +        if roles is not None:
  14.177 +            return roles
  14.178 +        roles = getDefaultRolePermissionMap().keys()
  14.179 +        if roles:
  14.180 +            # Map the base_cms roles by default.
  14.181 +            roles.sort()
  14.182 +            return roles
  14.183 +        return self.valid_roles()
  14.184 +
  14.185 +    security.declareProtected(ManagePortal, 'setRoles')
  14.186 +    def setRoles(self, roles, RESPONSE=None):
  14.187 +        """Changes the list of roles mapped to groups by this workflow.
  14.188 +        """
  14.189 +        avail = self.getAvailableRoles()
  14.190 +        for role in roles:
  14.191 +            if role not in avail:
  14.192 +                raise ValueError(role)
  14.193 +        self.roles = tuple(roles)
  14.194 +        if RESPONSE is not None:
  14.195 +            RESPONSE.redirect(
  14.196 +                "%s/manage_groups?manage_tabs_message=Roles+changed."
  14.197 +                % self.absolute_url())
  14.198 +
  14.199 +    security.declareProtected(ManagePortal, 'getGuard')
  14.200 +    def getGuard(self):
  14.201 +        """Returns the initiation guard.
  14.202 +
  14.203 +        If no init guard has been created, returns a temporary object.
  14.204 +        """
  14.205 +        if self.creation_guard is not None:
  14.206 +            return self.creation_guard
  14.207 +        else:
  14.208 +            return Guard().__of__(self)  # Create a temporary guard.
  14.209 +
  14.210 +    security.declarePublic('guardExprDocs')
  14.211 +    def guardExprDocs(self):
  14.212 +        """Returns documentation on guard expressions.
  14.213 +        """
  14.214 +        here = os.path.dirname(__file__)
  14.215 +        fn = os.path.join(here, 'doc', 'expressions.stx')
  14.216 +        f = open(fn, 'rt')
  14.217 +        try:
  14.218 +            text = f.read()
  14.219 +        finally:
  14.220 +            f.close()
  14.221 +        from DocumentTemplate.DT_Var import structured_text
  14.222 +        return structured_text(text)
  14.223 +
  14.224 +InitializeClass(WorkflowUIMixin)
    15.1 new file mode 100644
    15.2 --- /dev/null
    15.3 +++ b/Worklists.py
    15.4 @@ -0,0 +1,182 @@
    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 +""" Worklists in a web-configurable workflow.
   15.18 +
   15.19 +$Id: Worklists.py 36911 2005-04-07 16:38:47Z yuppie $
   15.20 +"""
   15.21 +
   15.22 +from AccessControl import ClassSecurityInfo
   15.23 +from Acquisition import aq_inner
   15.24 +from Acquisition import aq_parent
   15.25 +from Globals import DTMLFile
   15.26 +from Globals import InitializeClass
   15.27 +from Globals import PersistentMapping
   15.28 +from OFS.SimpleItem import SimpleItem
   15.29 +
   15.30 +from ContainerTab import ContainerTab
   15.31 +from Guard import Guard
   15.32 +from permissions import ManagePortal
   15.33 +from utils import _dtmldir
   15.34 +
   15.35 +
   15.36 +class WorklistDefinition(SimpleItem):
   15.37 +    """Worklist definiton"""
   15.38 +
   15.39 +    meta_type = 'Worklist'
   15.40 +
   15.41 +    security = ClassSecurityInfo()
   15.42 +    security.declareObjectProtected(ManagePortal)
   15.43 +
   15.44 +    description = ''
   15.45 +    var_matches = None  # Compared with catalog when set.
   15.46 +    actbox_name = ''
   15.47 +    actbox_url = ''
   15.48 +    actbox_category = 'global'
   15.49 +    guard = None
   15.50 +
   15.51 +    manage_options = (
   15.52 +        {'label': 'Properties', 'action': 'manage_properties'},
   15.53 +        )
   15.54 +
   15.55 +    def __init__(self, id):
   15.56 +        self.id = id
   15.57 +
   15.58 +    def getGuard(self):
   15.59 +        if self.guard is not None:
   15.60 +            return self.guard
   15.61 +        else:
   15.62 +            return Guard().__of__(self)  # Create a temporary guard.
   15.63 +
   15.64 +    def getGuardSummary(self):
   15.65 +        res = None
   15.66 +        if self.guard is not None:
   15.67 +            res = self.guard.getSummary()
   15.68 +        return res
   15.69 +
   15.70 +    def getWorkflow(self):
   15.71 +        return aq_parent(aq_inner(aq_parent(aq_inner(self))))
   15.72 +
   15.73 +    def getAvailableCatalogVars(self):
   15.74 +        res = []
   15.75 +        res.append(self.getWorkflow().state_var)
   15.76 +        for id, vdef in self.getWorkflow().variables.items():
   15.77 +            if vdef.for_catalog:
   15.78 +                res.append(id)
   15.79 +        res.sort()
   15.80 +        return res
   15.81 +
   15.82 +    def getVarMatchKeys(self):
   15.83 +        if self.var_matches:
   15.84 +            return self.var_matches.keys()
   15.85 +        else:
   15.86 +            return []
   15.87 +
   15.88 +    def getVarMatch(self, id):
   15.89 +        if self.var_matches:
   15.90 +            matches = self.var_matches.get(id, ())
   15.91 +            if not isinstance(matches, tuple):
   15.92 +                # Old version, convert it.
   15.93 +                matches = (matches,)
   15.94 +                self.var_matches[id] = matches
   15.95 +            return matches
   15.96 +        else:
   15.97 +            return ()
   15.98 +
   15.99 +    def getVarMatchText(self, id):
  15.100 +        values = self.getVarMatch(id)
  15.101 +        return '; '.join(values)
  15.102 +
  15.103 +    _properties_form = DTMLFile('worklist_properties', _dtmldir)
  15.104 +
  15.105 +    def manage_properties(self, REQUEST, manage_tabs_message=None):
  15.106 +        '''
  15.107 +        '''
  15.108 +        return self._properties_form(REQUEST,
  15.109 +                                     management_view='Properties',
  15.110 +                                     manage_tabs_message=manage_tabs_message,
  15.111 +                                     )
  15.112 +
  15.113 +    def setProperties(self, description,
  15.114 +                      actbox_name='', actbox_url='', actbox_category='global',
  15.115 +                      props=None, REQUEST=None):
  15.116 +        '''
  15.117 +        '''
  15.118 +        if props is None:
  15.119 +            props = REQUEST
  15.120 +        self.description = str(description)
  15.121 +        for key in self.getAvailableCatalogVars():
  15.122 +            # Populate var_matches.
  15.123 +            fieldname = 'var_match_%s' % key
  15.124 +            v = props.get(fieldname, '')
  15.125 +            if v:
  15.126 +                if not self.var_matches:
  15.127 +                    self.var_matches = PersistentMapping()
  15.128 +                v = [ var.strip() for var in v.split(';') ]
  15.129 +                self.var_matches[key] = tuple(v)
  15.130 +            else:
  15.131 +                if self.var_matches and self.var_matches.has_key(key):
  15.132 +                    del self.var_matches[key]
  15.133 +        self.actbox_name = str(actbox_name)
  15.134 +        self.actbox_url = str(actbox_url)
  15.135 +        self.actbox_category = str(actbox_category)
  15.136 +        g = Guard()
  15.137 +        if g.changeFromProperties(props or REQUEST):
  15.138 +            self.guard = g
  15.139 +        else:
  15.140 +            self.guard = None
  15.141 +        if REQUEST is not None:
  15.142 +            return self.manage_properties(REQUEST, 'Properties changed.')
  15.143 +
  15.144 +InitializeClass(WorklistDefinition)
  15.145 +
  15.146 +
  15.147 +class Worklists(ContainerTab):
  15.148 +    """A container for worklist definitions"""
  15.149 +
  15.150 +    meta_type = 'Worklists'
  15.151 +
  15.152 +    security = ClassSecurityInfo()
  15.153 +    security.declareObjectProtected(ManagePortal)
  15.154 +
  15.155 +    all_meta_types = ({'name':WorklistDefinition.meta_type,
  15.156 +                       'action':'addWorklist',
  15.157 +                       },)
  15.158 +
  15.159 +    _manage_worklists = DTMLFile('worklists', _dtmldir)
  15.160 +
  15.161 +    def manage_main(self, REQUEST, manage_tabs_message=None):
  15.162 +        '''
  15.163 +        '''
  15.164 +        return self._manage_worklists(
  15.165 +            REQUEST,
  15.166 +            management_view='Worklists',
  15.167 +            manage_tabs_message=manage_tabs_message,
  15.168 +            )
  15.169 +
  15.170 +    def addWorklist(self, id, REQUEST=None):
  15.171 +        '''
  15.172 +        '''
  15.173 +        qdef = WorklistDefinition(id)
  15.174 +        self._setObject(id, qdef)
  15.175 +        if REQUEST is not None:
  15.176 +            return self.manage_main(REQUEST, 'Worklist added.')
  15.177 +
  15.178 +    def deleteWorklists(self, ids, REQUEST=None):
  15.179 +        '''
  15.180 +        '''
  15.181 +        for id in ids:
  15.182 +            self._delObject(id)
  15.183 +        if REQUEST is not None:
  15.184 +            return self.manage_main(REQUEST, 'Worklist(s) removed.')
  15.185 +
  15.186 +InitializeClass(Worklists)
    16.1 new file mode 100644
    16.2 --- /dev/null
    16.3 +++ b/__init__.py
    16.4 @@ -0,0 +1,55 @@
    16.5 +##############################################################################
    16.6 +#
    16.7 +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    16.8 +# 
    16.9 +# This software is subject to the provisions of the Zope Public License,
   16.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   16.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   16.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   16.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   16.14 +# FOR A PARTICULAR PURPOSE.
   16.15 +# 
   16.16 +##############################################################################
   16.17 +""" Web-configurable workflow.
   16.18 +
   16.19 +$Id: __init__.py 40346 2005-11-23 17:15:03Z yuppie $
   16.20 +"""
   16.21 +
   16.22 +from Products.CMFCore.interfaces import ISiteRoot
   16.23 +from Products.CMFCore.utils import registerIcon
   16.24 +from Products.GenericSetup import EXTENSION
   16.25 +from Products.GenericSetup import profile_registry
   16.26 +
   16.27 +import DCWorkflow, States, Transitions, Variables, Worklists, Scripts
   16.28 +import Default
   16.29 +
   16.30 +
   16.31 +def initialize(context):
   16.32 +    
   16.33 +    context.registerHelp(directory='help')
   16.34 +    context.registerHelpTitle('DCWorkflow')
   16.35 +    
   16.36 +    registerIcon(DCWorkflow.DCWorkflowDefinition,
   16.37 +                 'images/workflow.gif', globals())
   16.38 +    registerIcon(States.States,
   16.39 +                 'images/state.gif', globals())
   16.40 +    States.StateDefinition.icon = States.States.icon
   16.41 +    registerIcon(Transitions.Transitions,
   16.42 +                 'images/transition.gif', globals())
   16.43 +    Transitions.TransitionDefinition.icon = Transitions.Transitions.icon
   16.44 +    registerIcon(Variables.Variables,
   16.45 +                 'images/variable.gif', globals())
   16.46 +    Variables.VariableDefinition.icon = Variables.Variables.icon
   16.47 +    registerIcon(Worklists.Worklists,
   16.48 +                 'images/worklist.gif', globals())
   16.49 +    Worklists.WorklistDefinition.icon = Worklists.Worklists.icon
   16.50 +    registerIcon(Scripts.Scripts,
   16.51 +                 'images/script.gif', globals())
   16.52 +
   16.53 +    profile_registry.registerProfile('revision2',
   16.54 +                                     'CMF Default Workflow [Revision 2]',
   16.55 +                                     'Adds revision 2 of default workflow.',
   16.56 +                                     'profiles/revision2',
   16.57 +                                     'DCWorkflow',
   16.58 +                                     EXTENSION,
   16.59 +                                     for_=ISiteRoot)
    17.1 new file mode 100644
    17.2 --- /dev/null
    17.3 +++ b/browser/__init__.py
    17.4 @@ -0,0 +1,16 @@
    17.5 +##############################################################################
    17.6 +#
    17.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    17.8 +#
    17.9 +# This software is subject to the provisions of the Zope Public License,
   17.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   17.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   17.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   17.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   17.14 +# FOR A PARTICULAR PURPOSE.
   17.15 +#
   17.16 +##############################################################################
   17.17 +"""DCWorkflow browser views.
   17.18 +
   17.19 +$Id: __init__.py 40304 2005-11-21 18:57:42Z yuppie $
   17.20 +"""
    18.1 new file mode 100644
    18.2 --- /dev/null
    18.3 +++ b/browser/configure.zcml
    18.4 @@ -0,0 +1,14 @@
    18.5 +<configure
    18.6 +    xmlns:browser="http://namespaces.zope.org/browser"
    18.7 +    package="Products.GenericSetup.browser"
    18.8 +    >
    18.9 +
   18.10 +  <browser:page
   18.11 +      for="zope.app.container.interfaces.IAdding"
   18.12 +      name="addDCWorkflowDefinition.html"
   18.13 +      template="addWithPresettings.pt"
   18.14 +      class="Products.DCWorkflow.browser.workflow.DCWorkflowDefinitionAddView"
   18.15 +      permission="cmf.ManagePortal"
   18.16 +      />
   18.17 +
   18.18 +</configure>
    19.1 new file mode 100644
    19.2 --- /dev/null
    19.3 +++ b/browser/workflow.py
    19.4 @@ -0,0 +1,84 @@
    19.5 +##############################################################################
    19.6 +#
    19.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    19.8 +#
    19.9 +# This software is subject to the provisions of the Zope Public License,
   19.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   19.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   19.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   19.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   19.14 +# FOR A PARTICULAR PURPOSE.
   19.15 +#
   19.16 +##############################################################################
   19.17 +"""DCWorkflowDefinition browser views.
   19.18 +
   19.19 +$Id: workflow.py 40716 2005-12-12 11:02:25Z yuppie $
   19.20 +"""
   19.21 +
   19.22 +from xml.dom.minidom import parseString
   19.23 +
   19.24 +from zope.app import zapi
   19.25 +
   19.26 +from Products.CMFCore.utils import getToolByName
   19.27 +from Products.GenericSetup.browser.utils import AddWithPresettingsViewBase
   19.28 +from Products.GenericSetup.interfaces import IBody
   19.29 +
   19.30 +from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   19.31 +
   19.32 +
   19.33 +class DCWorkflowDefinitionAddView(AddWithPresettingsViewBase):
   19.34 +
   19.35 +    """Add view for DCWorkflowDefinition.
   19.36 +    """
   19.37 +
   19.38 +    klass = DCWorkflowDefinition
   19.39 +
   19.40 +    description = u'Add a web-configurable workflow.'
   19.41 +
   19.42 +    def getProfileInfos(self):
   19.43 +        profiles = []
   19.44 +        stool = getToolByName(self, 'portal_setup', None)
   19.45 +        if stool:
   19.46 +            for info in stool.listContextInfos():
   19.47 +                obj_ids = []
   19.48 +                context = stool._getImportContext(info['id'])
   19.49 +                file_ids = context.listDirectory('workflows')
   19.50 +                for file_id in file_ids or ():
   19.51 +                    filename = 'workflows/%s/definition.xml' % file_id
   19.52 +                    body = context.readDataFile(filename)
   19.53 +                    if body is None:
   19.54 +                        continue
   19.55 +                    root = parseString(body).documentElement
   19.56 +                    obj_id = root.getAttribute('workflow_id')
   19.57 +                    obj_ids.append(obj_id)
   19.58 +                if not obj_ids:
   19.59 +                    continue
   19.60 +                obj_ids.sort()
   19.61 +                profiles.append({'id': info['id'],
   19.62 +                                 'title': info['title'],
   19.63 +                                 'obj_ids': tuple(obj_ids)})
   19.64 +        return tuple(profiles)
   19.65 +
   19.66 +    def _initSettings(self, obj, profile_id, obj_path):
   19.67 +        stool = getToolByName(self, 'portal_setup', None)
   19.68 +        if stool is None:
   19.69 +            return
   19.70 +
   19.71 +        context = stool._getImportContext(profile_id)
   19.72 +        file_ids = context.listDirectory('workflows')
   19.73 +        for file_id in file_ids or ():
   19.74 +            filename = 'workflows/%s/definition.xml' % file_id
   19.75 +            body = context.readDataFile(filename)
   19.76 +            if body is None:
   19.77 +                continue
   19.78 +
   19.79 +            root = parseString(body).documentElement
   19.80 +            if not root.getAttribute('workflow_id') == obj_path[0]:
   19.81 +                continue
   19.82 +
   19.83 +            importer = zapi.queryMultiAdapter((obj, context), IBody)
   19.84 +            if importer is None:
   19.85 +                continue
   19.86 +
   19.87 +            importer.body = body
   19.88 +            return
    20.1 new file mode 100644
    20.2 --- /dev/null
    20.3 +++ b/configure.zcml
    20.4 @@ -0,0 +1,28 @@
    20.5 +<configure
    20.6 +    xmlns="http://namespaces.zope.org/zope"
    20.7 +    xmlns:five="http://namespaces.zope.org/five"
    20.8 +    >
    20.9 +
   20.10 +  <include package=".browser"/>
   20.11 +
   20.12 +  <five:registerClass
   20.13 +      class=".DCWorkflow.DCWorkflowDefinition"
   20.14 +      meta_type="Workflow"
   20.15 +      addview="addDCWorkflowDefinition.html"
   20.16 +      permission="cmf.ManagePortal"
   20.17 +      global="False"
   20.18 +      />
   20.19 +
   20.20 +  <adapter
   20.21 +      factory=".exportimport.DCWorkflowDefinitionBodyAdapter"
   20.22 +      provides="Products.GenericSetup.interfaces.IBody"
   20.23 +      for=".interfaces.IDCWorkflowDefinition
   20.24 +           Products.GenericSetup.interfaces.ISetupEnviron"
   20.25 +      />
   20.26 +
   20.27 +  <five:implements
   20.28 +      class=".DCWorkflow.DCWorkflowDefinition"
   20.29 +      interface="Products.CMFCore.interfaces.IWorkflowDefinition"
   20.30 +      />
   20.31 +
   20.32 +</configure>
    21.1 new file mode 100644
    21.2 --- /dev/null
    21.3 +++ b/doc/actbox.stx
    21.4 @@ -0,0 +1,30 @@
    21.5 +
    21.6 +Names and URLs for the actions box can be formatted using standard Python
    21.7 +string formatting.  An example::
    21.8 +
    21.9 +  %(content_url)s/content_submit_form
   21.10 +
   21.11 +The string '%(content_url)s' will be replaced by the value of content_url.
   21.12 +The following names are available:
   21.13 +
   21.14 +- portal_url
   21.15 +
   21.16 +- folder_url
   21.17 +
   21.18 +- content_url
   21.19 +
   21.20 +- user_id
   21.21 +
   21.22 +- count (Available in work lists only. Represents the number of items in
   21.23 +  the work list.)
   21.24 +
   21.25 +The following names are also available, in case there is any use for them.
   21.26 +They are not strings.
   21.27 +
   21.28 +- portal
   21.29 +
   21.30 +- folder
   21.31 +
   21.32 +- content
   21.33 +
   21.34 +- isAnonymous
    22.1 new file mode 100644
    22.2 --- /dev/null
    22.3 +++ b/doc/basics.stx
    22.4 @@ -0,0 +1,59 @@
    22.5 +
    22.6 +This product can be added to the portal_workflow tool of CMF site.
    22.7 +It provides fully customizable workflow.
    22.8 +
    22.9 +To see an example, after installing DCWorkflow, using the "Contents"
   22.10 +tab of your portal_workflow instance add a "CMF default workflow (rev 2)"
   22.11 +instance.  The other way to create it is to add a "Workflow" object,
   22.12 +selecting "dc_workflow" as the type.  The second way will create a
   22.13 +blank workflow.
   22.14 +
   22.15 +This tool is easiest to use if you draw a state diagram first.  Your
   22.16 +diagram should have:
   22.17 +
   22.18 +- States (bubbles)
   22.19 +
   22.20 +- Transitions (arrows)
   22.21 +
   22.22 +- Variables (both in states and transitions)
   22.23 +
   22.24 +Remember to consider all the states your content can be in.  Consider
   22.25 +the actions users will perform to make the transitions between states.
   22.26 +And consider not only who will be allowed to perform what functions,
   22.27 +but also who will be *required* to perform certain functions.
   22.28 +
   22.29 +On the "States" tab, add a state with a simple ID for each state on
   22.30 +your diagram.  On the "Transitions" tab, add a transition with a simple
   22.31 +ID for each group of arrows that point to the same state and have
   22.32 +similar characteristics.  Then for each state choose which transitions
   22.33 +are allowed to leave that state.
   22.34 +
   22.35 +Variables are useful for keeping track of things that aren't very well
   22.36 +represented as separate states, such as counters or information about
   22.37 +the action that was last performed.  You can create variables that get
   22.38 +stored alongside the workflow state and you can make those variables
   22.39 +available in catalog searches.  Some variables, such as the review
   22.40 +history, should not be stored at all.  Those variables are accessible
   22.41 +through the getInfoFor() interface.
   22.42 +
   22.43 +Worklists are a way to make people aware of tasks they are required
   22.44 +to perform.  Worklists are implemented as a catalog query that puts
   22.45 +actions in the actions box when there is some task the user needs to
   22.46 +perform.  Most of the time you just need to enter a state ID,
   22.47 +a role name, and the information to put in the actions box.
   22.48 +
   22.49 +You can manage all of the actions a user can perform on an object by
   22.50 +setting up permissions to be managed by the workflow.  Using the
   22.51 +"Permissions" tab, select which permissions should be state-dependent.
   22.52 +Then in each state, using the "permissions" tab, set up the
   22.53 +role to permission mappings appropriate for that state.
   22.54 +
   22.55 +Finally, you can extend the workflow with scripts.  Scripts can be
   22.56 +External Methods, Python Scripts, DTML methods, or any other callable
   22.57 +Zope object.  They are accessible by name in expressions.  Scripts
   22.58 +are invoked with a state_change object as the first argument; see
   22.59 +expressions.stx.
   22.60 +
   22.61 +Once you've crafted your workflow, you hook it up with a content type
   22.62 +by using the portal_workflow top-level "Workflows" tab.  Specify the
   22.63 +workflow name in the target content type's box.
    23.1 new file mode 100644
    23.2 --- /dev/null
    23.3 +++ b/doc/examples/staging/checkin.py
    23.4 @@ -0,0 +1,7 @@
    23.5 +## Script (Python) "checkin"
    23.6 +##parameters=sci
    23.7 +# Check in the object to a Zope version repository, disallowing changes.
    23.8 +object = sci.object
    23.9 +vt = object.portal_versions
   23.10 +if vt.isCheckedOut(object):
   23.11 +  vt.checkin(object, sci.kwargs.get('comment', ''))
    24.1 new file mode 100644
    24.2 --- /dev/null
    24.3 +++ b/doc/examples/staging/checkout.py
    24.4 @@ -0,0 +1,10 @@
    24.5 +## Script (Python) "checkout"
    24.6 +##parameters=sci
    24.7 +# Check out the object from a repository, allowing changes.
    24.8 +#
    24.9 +# For workflows that control staging, it makes sense to call this script
   24.10 +# before all transitions.
   24.11 +object = sci.object
   24.12 +vt = object.portal_versions
   24.13 +if not vt.isCheckedOut(object):
   24.14 +  vt.checkout(object)
    25.1 new file mode 100644
    25.2 --- /dev/null
    25.3 +++ b/doc/examples/staging/retractStages.py
    25.4 @@ -0,0 +1,6 @@
    25.5 +## Script (Python) "retractStages"
    25.6 +##parameters=sci
    25.7 +# Remove the object from the given stages.
    25.8 +object = sci.object
    25.9 +st = object.portal_staging
   25.10 +st.removeStages(object, ['review', 'prod'])
    26.1 new file mode 100644
    26.2 --- /dev/null
    26.3 +++ b/doc/examples/staging/updateProductionStage.py
    26.4 @@ -0,0 +1,7 @@
    26.5 +## Script (Python) "updateProductionStage"
    26.6 +##parameters=sci
    26.7 +# Copy the object in development to review and production.
    26.8 +object = sci.object
    26.9 +st = object.portal_staging
   26.10 +st.updateStages(object, 'dev', ['review', 'prod'],
   26.11 +                sci.kwargs.get('comment', ''))
    27.1 new file mode 100644
    27.2 --- /dev/null
    27.3 +++ b/doc/examples/staging/updateReviewStage.py
    27.4 @@ -0,0 +1,7 @@
    27.5 +## Script (Python) "updateReviewStage"
    27.6 +##parameters=sci
    27.7 +# Copy the object in development to review.
    27.8 +object = sci.object
    27.9 +st = object.portal_staging
   27.10 +st.updateStages(object, 'dev', ['review'],
   27.11 +                sci.kwargs.get('comment', ''))
    28.1 new file mode 100644
    28.2 --- /dev/null
    28.3 +++ b/doc/expressions.stx
    28.4 @@ -0,0 +1,52 @@
    28.5 +
    28.6 +DCWorkflow Expressions
    28.7 +
    28.8 +  Expressions in DCWorkflow are TALES expressions.
    28.9 +(See the <a href="http://zope.org/Documentation/Books/ZopeBook">Zope Book</a>
   28.10 +for background on page templates and TALES.)
   28.11 +Some of the contexts have slightly different meanings from what is provided
   28.12 +for expressions in page templates.
   28.13 +
   28.14 +- 'here' -- The content object
   28.15 +
   28.16 +- 'container' -- The content object's container
   28.17 +
   28.18 +Several other contexts are also provided.
   28.19 +
   28.20 +- 'state_change' -- A special object containing info about the state change
   28.21 +
   28.22 +- 'transition' -- The transition object being executed
   28.23 +
   28.24 +- 'status' -- The former status
   28.25 +
   28.26 +- 'workflow' -- The workflow definition object
   28.27 +
   28.28 +- 'scripts' -- The scripts in the workflow definition object
   28.29 +
   28.30 +'state_change' objects provide the following attributes:
   28.31 +
   28.32 +- 'state_change.status' -- a mapping containing the workflow status.
   28.33 +
   28.34 +- 'state_change.object' -- the object being modified by workflow.
   28.35 +
   28.36 +- 'state_change.workflow' -- the workflow definition object.
   28.37 +
   28.38 +- 'state_change.transition' -- the transition object being executed.
   28.39 +
   28.40 +- 'state_change.old_state' -- the former state object.
   28.41 +
   28.42 +- 'state_change.new_state' -- the destination state object.
   28.43 +
   28.44 +- 'state_change.kwargs' -- the keyword arguments passed to the
   28.45 +  doActionFor() method.
   28.46 +
   28.47 +- 'state_change.getHistory()' -- returns a copy of the object's workflow
   28.48 +   history.
   28.49 +
   28.50 +- 'state_change.getPortal()' -- returns the root of the portal.
   28.51 +
   28.52 +- 'state_change.ObjectDeleted' and 'ObjectMoved' -- exceptions that
   28.53 +  can be raised by scripts to indicate to the workflow that an object
   28.54 +  has been moved or deleted.
   28.55 +
   28.56 +- 'state_change.getDateTime()' -- returns the DateTime of the transition.
    29.1 new file mode 100644
    29.2 --- /dev/null
    29.3 +++ b/doc/howto.stx
    29.4 @@ -0,0 +1,46 @@
    29.5 +From: John Morton <jwm@plain.co.nz>
    29.6 +
    29.7 +Here's how I generally go about putting together a new workflow:
    29.8 +
    29.9 +1. Draw up a state diagram with the nodes as states and the arcs as
   29.10 +   transitions. I usually do this on paper, then do a good copy in dia
   29.11 +   or some other diagram tool, so I have an image to go with the
   29.12 +   documentation. I usually spot several corner cases in the process.
   29.13 +
   29.14 +2. Start by creating an example DCworkflow, rather than a new one, as
   29.15 +   it's faster to delete all the states and transitions than it is to
   29.16 +   create all the standard review_state variables.
   29.17 +
   29.18 +3. In the permissions tab, select all the permissions that you want the
   29.19 +   workflow to govern.
   29.20 +
   29.21 +4. Define any extra variables that you need.
   29.22 +
   29.23 +5. Set up the states for your workflow, one for each node in your state
   29.24 +   diagram. Try to stick to the standard names for a publication
   29.25 +   workflow, as some badly behaved products have states like 'published'
   29.26 +   hardcoded into their searches (ie CalendarTool, last I looked). Set
   29.27 +   up the permissions on the states now, as well. I find that using
   29.28 +   acquisition for the site visible states, and using explicit
   29.29 +   permissions for the private and interim states works well. Reviewer
   29.30 +   roles should either have view permissions on every state or you
   29.31 +   should change the appropriate skins to take them somewhere sensible
   29.32 +   after a transition or they'll end up with an ugly access denied page
   29.33 +   after sending some content back to private state.
   29.34 +
   29.35 +6. Set up any scripts that you'll need for transitions - pre and post
   29.36 +   transition scripts and ones to handle complex guard conditions. Just
   29.37 +   set up skeletons for now, if you haven't though through all the
   29.38 +   details.
   29.39 +
   29.40 +7. Create your transitions from all the arcs on your state diagram. You
   29.41 +   should be able to pick the right destination state as all your states
   29.42 +   are already defined, and set up the right scripts to run, as you've
   29.43 +   defined those as well. It's worth noting that the guards are or'ed -
   29.44 +   if any guard matches, the transition can occur. You can specify more
   29.45 +   than one permission, role or expression by separating them with a
   29.46 +   semicolon.
   29.47 +
   29.48 +That about covers it. By working in this order, you tend to step through
   29.49 +the creation process one tab at a time, rather than switching back and
   29.50 +forth. I find it tends to be faster and less confusing that way.
    30.1 new file mode 100644
    30.2 --- /dev/null
    30.3 +++ b/doc/worklists.stx
    30.4 @@ -0,0 +1,7 @@
    30.5 +
    30.6 +Worklists are predefined catalog queries.
    30.7 +
    30.8 +When defining a variable match, you can enter a list of possible matches
    30.9 +separated by semicolons. In addition, the matches will be formatted
   30.10 +according to the rules described in "actbox.stx":actbox.stx.
   30.11 +This way you can enter "%(user_id)s" to match only the current user.
    31.1 new file mode 100644
    31.2 --- /dev/null
    31.3 +++ b/dtml/guard.dtml
    31.4 @@ -0,0 +1,26 @@
    31.5 +<script type="text/javascript">
    31.6 +function guardExprDocs() {
    31.7 +  window.open('guardExprDocs', '', 'width=640, height=480, resizable, scrollbars, status');
    31.8 +}
    31.9 +</script>
   31.10 +
   31.11 +<table>
   31.12 +
   31.13 +<tr>
   31.14 +<th align="left">Permission(s)</th>
   31.15 +<td><input type="text" name="guard_permissions" value="&dtml-getPermissionsText;" /></td>
   31.16 +<th align="left">Role(s)</th>
   31.17 +<td><input type="text" name="guard_roles" value="&dtml-getRolesText;" /></td>
   31.18 +<th align="left">Group(s)</th>
   31.19 +<td><input type="text" name="guard_groups" value="&dtml-getGroupsText;" /></td>
   31.20 +</tr>
   31.21 +
   31.22 +<tr>
   31.23 +<th align="left">Expression</th>
   31.24 +<td colspan="3">
   31.25 +<input type="text" name="guard_expr" value="&dtml-getExprText;" size="50" />
   31.26 +<a href="#" onclick="guardExprDocs(); return false;">[?]</a>
   31.27 +</td>
   31.28 +</tr>
   31.29 +
   31.30 +</table>
    32.1 new file mode 100644
    32.2 --- /dev/null
    32.3 +++ b/dtml/state_groups.pt
    32.4 @@ -0,0 +1,53 @@
    32.5 +<h1 tal:replace="structure here/manage_page_header">Header</h1>
    32.6 +<h2 tal:define="manage_tabs_message request/manage_tabs_message | nothing"
    32.7 +    tal:replace="structure here/manage_tabs">Tabs</h2>
    32.8 +
    32.9 +<p class="form-help">
   32.10 +When objects are in this state, they will take on the group to role
   32.11 +mappings defined below.  Only the <a href="../manage_groups">groups
   32.12 +and roles managed by this workflow</a> are shown.
   32.13 +</p>
   32.14 +
   32.15 +<form action="setGroups" method="POST"
   32.16 +  tal:define="wf here/getWorkflow; roles wf/getRoles">
   32.17 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   32.18 +<tr class="list-header">
   32.19 +  <td align="left">
   32.20 +  <div class="form-label">
   32.21 +  <strong>Group</strong>
   32.22 +  </div>
   32.23 +  </td>
   32.24 +  <td align="left" tal:attributes="colspan python: len(roles)">
   32.25 +  <div class="form-label">
   32.26 +  <strong>Roles</strong>
   32.27 +  </div>
   32.28 +  </td>
   32.29 +</tr>
   32.30 +
   32.31 +<tr class="row-normal">
   32.32 +  <td></td>
   32.33 +  <td tal:repeat="role roles" tal:content="role" class="list-item">
   32.34 +    Authenticated
   32.35 +  </td>
   32.36 +</tr>
   32.37 +
   32.38 +<tr tal:repeat="group wf/getGroups" tal:attributes="class
   32.39 +  python: repeat['group'].odd and 'row-normal' or 'row-hilite'">
   32.40 +<td tal:content="group" class="list-item">
   32.41 +  (Group) Everyone
   32.42 +</td>
   32.43 +<tal:block tal:define="group_roles python: here.getGroupInfo(group)">
   32.44 +<td tal:repeat="role roles">
   32.45 +  <input type="checkbox"
   32.46 +    tal:attributes="name python: '%s|%s' % (group, role);
   32.47 +      checked python: role in group_roles" />
   32.48 +</td>
   32.49 +</tal:block>
   32.50 +</tr>
   32.51 +</table>
   32.52 +
   32.53 +<input class="form-element" type="submit" name="submit" value="Save Changes" />
   32.54 +
   32.55 +</form>
   32.56 +
   32.57 +<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
    33.1 new file mode 100644
    33.2 --- /dev/null
    33.3 +++ b/dtml/state_permissions.dtml
    33.4 @@ -0,0 +1,84 @@
    33.5 +<dtml-var manage_page_header>
    33.6 +<dtml-var manage_tabs>
    33.7 +
    33.8 +<p class="form-help">
    33.9 +When objects are in this state they will take on the role to permission
   33.10 +mappings defined below.  Only the <a href="../manage_permissions">permissions
   33.11 +managed by this workflow</a> are shown.
   33.12 +</p>
   33.13 +
   33.14 +<form action="setPermissions" method="POST">
   33.15 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   33.16 +<tr class="list-header">
   33.17 +  <td>&nbsp;</td>
   33.18 +  <td align="left">
   33.19 +  <div class="form-label">
   33.20 +  <strong>Permission</strong>
   33.21 +  </div>
   33.22 +  </td>
   33.23 +  <td align="left" colspan="<dtml-var expr="_.len(getAvailableRoles())">">
   33.24 +  <div class="form-label">
   33.25 +  <strong>Roles</strong>
   33.26 +  </div>
   33.27 +  </td>
   33.28 +</tr>
   33.29 +
   33.30 +<tr class="row-normal">
   33.31 +  <td align="left" valign="top">
   33.32 +  <div class="form-label">
   33.33 +  <strong>
   33.34 +  Acquire<BR>permission<BR>settings?
   33.35 +  </strong>
   33.36 +  </div>
   33.37 +  </td>
   33.38 +  <td></td>
   33.39 +  <dtml-in getAvailableRoles>
   33.40 +  <td align="left">
   33.41 +  <div class="list-item">
   33.42 +  <dtml-var sequence-item>
   33.43 +  </div>
   33.44 +  </td>
   33.45 +  </dtml-in>
   33.46 +</tr>
   33.47 +
   33.48 +<dtml-in getManagedPermissions sort>
   33.49 +<dtml-let permission=sequence-item>
   33.50 +<dtml-with expr="getPermissionInfo(permission)" mapping>
   33.51 +<dtml-if sequence-odd>
   33.52 +<tr class="row-normal">
   33.53 +<dtml-else>
   33.54 +<tr class="row-hilite">
   33.55 +</dtml-if>
   33.56 +  <td align="left" valign="top">
   33.57 +  <dtml-let checked="acquired and 'checked' or ' '">
   33.58 +   <input type="checkbox" name="acquire_&dtml-permission;" &dtml-checked; />
   33.59 +  </dtml-let>
   33.60 +  </td>
   33.61 +  <td align="left" nowrap>
   33.62 +  <div class="list-item">
   33.63 +  &dtml-permission;
   33.64 +  </div>
   33.65 +  </td>
   33.66 +  <dtml-in getAvailableRoles sort>
   33.67 +  <td align="center">
   33.68 +  <dtml-let checked="_['sequence-item'] in roles and 'checked' or ' '">
   33.69 +   <input type="checkbox" name="&dtml-permission;|&dtml-sequence-item;" &dtml-checked; />
   33.70 +  </dtml-let>
   33.71 +  </td>
   33.72 +  </dtml-in>
   33.73 +</tr>
   33.74 +</dtml-with>
   33.75 +</dtml-let>
   33.76 +</dtml-in>
   33.77 +
   33.78 +<tr>
   33.79 +<td colspan="<dtml-var expr="_.len(getAvailableRoles())+2">" align="left">
   33.80 +<div class="form-element">
   33.81 +<input class="form-element" type="submit" name="submit" value="Save Changes" />
   33.82 +</div>
   33.83 +</td>
   33.84 +</tr>
   33.85 +</table>
   33.86 +</form>
   33.87 +
   33.88 +<dtml-var manage_page_footer>
    34.1 new file mode 100644
    34.2 --- /dev/null
    34.3 +++ b/dtml/state_properties.dtml
    34.4 @@ -0,0 +1,44 @@
    34.5 +<dtml-var manage_page_header>
    34.6 +<dtml-var manage_tabs>
    34.7 +
    34.8 +<form action="setProperties" method="POST">
    34.9 +<table>
   34.10 +
   34.11 +<tr>
   34.12 +<th align="left">Id</th>
   34.13 +<td>&dtml-id;</td>
   34.14 +</tr>
   34.15 +
   34.16 +<tr>
   34.17 +<th align="left">Title</th>
   34.18 +<td><input type="text" name="title" value="&dtml-title;" size="50" /></td>
   34.19 +</tr>
   34.20 +
   34.21 +<tr>
   34.22 +<th align="left" valign="top">Description</th>
   34.23 +<td><textarea name="description" rows="6" cols="35">&dtml-description;</textarea></td>
   34.24 +</tr>
   34.25 +
   34.26 +<tr>
   34.27 +<th align="left" valign="top">Possible Transitions</th>
   34.28 +<td>
   34.29 + <dtml-in getAvailableTransitionIds sort>
   34.30 +  <dtml-let checked="_['sequence-item'] in transitions and 'checked' or ' '">
   34.31 +   <input type="checkbox" name="transitions:list"
   34.32 +    value="&dtml-sequence-item;" &dtml-checked; /> &dtml-sequence-item;
   34.33 +   <dtml-let t_title="getTransitionTitle(_['sequence-item'])">
   34.34 +    <dtml-if t_title>(&dtml-t_title;)</dtml-if>
   34.35 +   </dtml-let>
   34.36 +  </dtml-let>
   34.37 +  <br />
   34.38 + <dtml-else>
   34.39 +  <em>No transitions defined.</em>
   34.40 + </dtml-in>
   34.41 + </select>
   34.42 +</td>
   34.43 +</tr>
   34.44 +
   34.45 +</table>
   34.46 +<input type="submit" name="submit" value="Save changes" />
   34.47 +</form>
   34.48 +<dtml-var manage_page_footer>
    35.1 new file mode 100644
    35.2 --- /dev/null
    35.3 +++ b/dtml/state_variables.dtml
    35.4 @@ -0,0 +1,84 @@
    35.5 +<dtml-var manage_page_header>
    35.6 +<dtml-var manage_tabs>
    35.7 +
    35.8 +<p class="form-help">
    35.9 +When objects move to this state the workflow variables will be assigned
   35.10 +the values below.
   35.11 +</p>
   35.12 +
   35.13 +<dtml-if getVariableValues>
   35.14 +
   35.15 +<form action="&dtml-absolute_url;" method="POST">
   35.16 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   35.17 +<tr class="list-header">
   35.18 +  <td>&nbsp;</td>
   35.19 +  <td align="left" valign="top">
   35.20 +  <div class="form-label">
   35.21 +  <strong>Variable</strong>
   35.22 +  </div>
   35.23 +  </td>
   35.24 +  <td align="left">
   35.25 +  <div class="form-label">
   35.26 +  <strong>Value</strong>
   35.27 +  </div>
   35.28 +  </td>
   35.29 +</tr>
   35.30 +
   35.31 +<dtml-in getVariableValues sort>
   35.32 +<dtml-if sequence-odd>
   35.33 +<tr class="row-normal">
   35.34 +<dtml-else>
   35.35 +<tr class="row-hilite">
   35.36 +</dtml-if>
   35.37 +  <td align="left" valign="top">
   35.38 +   <input type="checkbox" name="ids:list" value="&dtml-sequence-key;"/>
   35.39 +  </td>
   35.40 +  <td align="left" nowrap>
   35.41 +  <div class="list-item">
   35.42 +  &dtml-sequence-key;
   35.43 +  </div>
   35.44 +  </td>
   35.45 +  <td align="left">
   35.46 +   <input type="text" name="varval_&dtml-sequence-key;" value="&dtml-sequence-item;" size="50" />
   35.47 +  </td>
   35.48 +</tr>
   35.49 +</dtml-in>
   35.50 +
   35.51 +<tr>
   35.52 +<td colspan="3" align="left">
   35.53 +<div class="form-element">
   35.54 +<input class="form-element" type="submit" name="setVariables:method" value="Save Changes" />
   35.55 +<input class="form-element" type="submit" name="deleteVariables:method" value="Delete" />
   35.56 +</div>
   35.57 +</td>
   35.58 +</tr>
   35.59 +</table>
   35.60 +</form>
   35.61 +
   35.62 +</dtml-if>
   35.63 +
   35.64 +
   35.65 +<form action="addVariable" method="POST">
   35.66 + <table>
   35.67 +  <tr>
   35.68 +   <td>Add a variable value</td>
   35.69 +  </tr>
   35.70 +  <tr>
   35.71 +   <td>Variable</td>
   35.72 +   <td><select name="id">
   35.73 +    <dtml-in getWorkflowVariables>
   35.74 +     <option value="&dtml-sequence-item;">
   35.75 +      <dtml-var sequence-item>
   35.76 +     </option>
   35.77 +    </dtml-in>
   35.78 +   </select>
   35.79 +  </td>
   35.80 + </tr>
   35.81 + <tr>
   35.82 +  <td>Value</td><td><input type="text" name="value" value="" /></td>
   35.83 + </tr>
   35.84 + <tr><td><input type="submit" name="submit" value="Add" /></td></tr>
   35.85 +</table>
   35.86 +</form>
   35.87 +
   35.88 +<dtml-var manage_page_footer>
    36.1 new file mode 100644
    36.2 --- /dev/null
    36.3 +++ b/dtml/states.dtml
    36.4 @@ -0,0 +1,66 @@
    36.5 +<dtml-var manage_page_header>
    36.6 +<dtml-var manage_tabs>
    36.7 +<form action="&dtml-absolute_url;" method="POST">
    36.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    36.9 +<dtml-in values sort=id>
   36.10 + <tr bgcolor="#eeeeee">
   36.11 +  <th align="left" colspan="2">
   36.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   36.13 +   <dtml-if expr="id == initial_state">*</dtml-if>
   36.14 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   36.15 +   &nbsp;
   36.16 +   &dtml-title;
   36.17 +  </th>
   36.18 + </tr>
   36.19 + <dtml-let state_id=id>
   36.20 + <dtml-in getTransitions>
   36.21 +  <tr>
   36.22 +   <td width="10%"></td>
   36.23 +   <td>
   36.24 +    <a href="../transitions/&dtml-sequence-item;/manage_properties"
   36.25 +     >&dtml-sequence-item;</a>
   36.26 +    <dtml-let t_title="getTransitionTitle(_['sequence-item'])">
   36.27 +     <dtml-if t_title>(&dtml-t_title;)</dtml-if>
   36.28 +    </dtml-let>
   36.29 +   </td>
   36.30 +  </tr>
   36.31 + <dtml-else>
   36.32 +  <tr>
   36.33 +   <td></td>
   36.34 +   <td><em>No transitions.</em></td>
   36.35 +  </tr>
   36.36 + </dtml-in>
   36.37 + </dtml-let>
   36.38 + <dtml-if getVariableValues>
   36.39 + <tr>
   36.40 +  <th align="right">Variables</th>
   36.41 +  <th></th>
   36.42 + </tr>
   36.43 + <dtml-in getVariableValues sort>
   36.44 +  <tr>
   36.45 +   <td></td>
   36.46 +   <td>
   36.47 +    &dtml-sequence-key; = &dtml-sequence-item;
   36.48 +   </td>
   36.49 +  </tr>
   36.50 + </dtml-in>
   36.51 + </dtml-if>
   36.52 +<dtml-else>
   36.53 + <tr><td><em>No states defined.</em></td></tr>
   36.54 +</dtml-in>
   36.55 +</table>
   36.56 +<dtml-if values>
   36.57 +<p>
   36.58 +  <b>Note:</b> Renaming a state will not affect any items in that state. You
   36.59 +  will need to fix them manually.
   36.60 +</p>
   36.61 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   36.62 +<input type="submit" name="deleteStates:method" value="Delete" />
   36.63 +<input type="submit" name="setInitialState:method" value="Set Initial State" />
   36.64 +</dtml-if>
   36.65 +<hr />
   36.66 +<h3>Add a state</h3>
   36.67 +<p>Id <input type="text" name="id" value="" />
   36.68 +<input type="submit" name="addState:method" value="Add" /></p>
   36.69 +</form>
   36.70 +<dtml-var manage_page_footer>
    37.1 new file mode 100644
    37.2 --- /dev/null
    37.3 +++ b/dtml/transition_properties.dtml
    37.4 @@ -0,0 +1,137 @@
    37.5 +<dtml-var manage_page_header>
    37.6 +<dtml-var manage_tabs>
    37.7 +
    37.8 +<form action="setProperties" method="POST">
    37.9 +<table>
   37.10 +
   37.11 +<tr>
   37.12 +<th align="left">Id</th>
   37.13 +<td>&dtml-id;</td>
   37.14 +</tr>
   37.15 +
   37.16 +<tr>
   37.17 +<th align="left">Title</th>
   37.18 +<td><input type="text" name="title" value="&dtml-title;" size="50" /></td>
   37.19 +</tr>
   37.20 +
   37.21 +<tr>
   37.22 +<th align="left" valign="top">Description</th>
   37.23 +<td><textarea name="description" rows="6" cols="35">&dtml-description;</textarea></td>
   37.24 +</tr>
   37.25 +
   37.26 +<tr>
   37.27 +<th align="left">Destination state</th>
   37.28 +<td>
   37.29 + <select name="new_state_id" size="1">
   37.30 +  <dtml-let selected="not new_state_id and 'selected' or ' '">
   37.31 +   <option value="" &dtml-selected;>(Remain in state)</option>
   37.32 +  </dtml-let>
   37.33 +  <dtml-in getAvailableStateIds sort>
   37.34 +   <dtml-let selected="new_state_id == _['sequence-item'] and 'selected' or ' '">
   37.35 +    <option value="&dtml-sequence-item;" &dtml-selected;>&dtml-sequence-item;</option>
   37.36 +   </dtml-let>
   37.37 +  </dtml-in>
   37.38 + </select>
   37.39 +</td>
   37.40 +</tr>
   37.41 +
   37.42 +<tr>
   37.43 +<th align="left">Trigger type</th>
   37.44 +<td>
   37.45 +<dtml-let checked="trigger_type==0 and 'checked' or ' '">
   37.46 +<input type="radio" name="trigger_type" value="0" &dtml-checked; />
   37.47 +Automatic
   37.48 +</dtml-let>
   37.49 +</td>
   37.50 +</tr>
   37.51 +
   37.52 +<tr>
   37.53 +<th></th>
   37.54 +<td>
   37.55 +<dtml-let checked="trigger_type==1 and 'checked' or ' '">
   37.56 +<input type="radio" name="trigger_type" value="1" &dtml-checked; />
   37.57 +Initiated by user action
   37.58 +</dtml-let>
   37.59 +</td>
   37.60 +</tr>
   37.61 +
   37.62 +<tr>
   37.63 +<th></th>
   37.64 +<td>
   37.65 +<dtml-let checked="trigger_type==2 and 'checked' or ' '">
   37.66 +<input type="radio" name="trigger_type" value="2" &dtml-checked; />
   37.67 +Initiated by WorkflowMethod
   37.68 +</dtml-let>
   37.69 +</td>
   37.70 +</tr>
   37.71 +
   37.72 +<tr>
   37.73 +<th align="left">Script (before)</th>
   37.74 +<td>
   37.75 +<select name="script_name">
   37.76 +<option value="">(None)</option>
   37.77 +<dtml-in getAvailableScriptIds sort>
   37.78 + <dtml-let selected="script_name == _['sequence-item'] and 'selected' or ' '">
   37.79 +  <option value="&dtml-sequence-item;" &dtml-selected;>&dtml-sequence-item;</option>
   37.80 + </dtml-let>
   37.81 +</dtml-in>
   37.82 +</select>
   37.83 +</td>
   37.84 +</tr>
   37.85 +
   37.86 +<tr>
   37.87 +<th align="left">Script (after)</th>
   37.88 +<td>
   37.89 +<select name="after_script_name">
   37.90 +<option value="">(None)</option>
   37.91 +<dtml-in getAvailableScriptIds sort>
   37.92 + <dtml-let selected="after_script_name == _['sequence-item'] and 'selected' or ' '">
   37.93 +  <option value="&dtml-sequence-item;" &dtml-selected;>&dtml-sequence-item;</option>
   37.94 + </dtml-let>
   37.95 +</dtml-in>
   37.96 +</select>
   37.97 +</td>
   37.98 +</tr>
   37.99 +
  37.100 +<tr>
  37.101 +<th align="left" valign="top">Guard</th>
  37.102 +<td>
  37.103 + <dtml-with getGuard>
  37.104 +  <dtml-var guardForm>
  37.105 + </dtml-with>
  37.106 +</td>
  37.107 +</tr>
  37.108 +
  37.109 +<tr>
  37.110 +<th align="left" valign="top">Display in actions box</th>
  37.111 +<td>
  37.112 + <table>
  37.113 +  <tr>
  37.114 +   <th align="left">Name (formatted)</th>
  37.115 +   <td>
  37.116 +    <input type="text" name="actbox_name"
  37.117 +     value="&dtml-actbox_name;" size="50" />
  37.118 +   </td>
  37.119 +  </tr>
  37.120 +  <tr>
  37.121 +   <th align="left">URL (formatted)</th>
  37.122 +   <td>
  37.123 +    <input type="text" name="actbox_url"
  37.124 +     value="&dtml-actbox_url;" size="50" />
  37.125 +   </td>
  37.126 +  </tr>
  37.127 +  <tr>
  37.128 +   <th align="left">Category</th>
  37.129 +   <td>
  37.130 +    <input type="text" name="actbox_category"
  37.131 +     value="&dtml-actbox_category;" />
  37.132 +   </td>
  37.133 +  </tr>
  37.134 + </table>
  37.135 +</td>
  37.136 +</tr>
  37.137 +
  37.138 +</table>
  37.139 +<input type="submit" name="submit" value="Save changes" />
  37.140 +</form>
  37.141 +<dtml-var manage_page_footer>
    38.1 new file mode 100644
    38.2 --- /dev/null
    38.3 +++ b/dtml/transition_variables.dtml
    38.4 @@ -0,0 +1,84 @@
    38.5 +<dtml-var manage_page_header>
    38.6 +<dtml-var manage_tabs>
    38.7 +
    38.8 +<p class="form-help">
    38.9 +When the transition is executed, the workflow variables are
   38.10 +updated according to the expressions below.
   38.11 +</p>
   38.12 +
   38.13 +<dtml-if getVariableExprs>
   38.14 +
   38.15 +<form action="&dtml-absolute_url;" method="POST">
   38.16 +<table width="100%" cellspacing="0" cellpadding="2" border="0" nowrap>
   38.17 +<tr class="list-header">
   38.18 +  <td>&nbsp;</td>
   38.19 +  <td align="left" valign="top">
   38.20 +  <div class="form-label">
   38.21 +  <strong>Variable</strong>
   38.22 +  </div>
   38.23 +  </td>
   38.24 +  <td align="left">
   38.25 +  <div class="form-label">
   38.26 +  <strong>Expression</strong>
   38.27 +  </div>
   38.28 +  </td>
   38.29 +</tr>
   38.30 +
   38.31 +<dtml-in getVariableExprs sort>
   38.32 +<dtml-if sequence-odd>
   38.33 +<tr class="row-normal">
   38.34 +<dtml-else>
   38.35 +<tr class="row-hilite">
   38.36 +</dtml-if>
   38.37 +  <td align="left" valign="top">
   38.38 +   <input type="checkbox" name="ids:list" value="&dtml-sequence-key;"/>
   38.39 +  </td>
   38.40 +  <td align="left" nowrap>
   38.41 +  <div class="list-item">
   38.42 +  &dtml-sequence-key;
   38.43 +  </div>
   38.44 +  </td>
   38.45 +  <td align="left">
   38.46 +   <input type="text" name="varexpr_&dtml-sequence-key;" value="&dtml-sequence-item;" size="50" />
   38.47 +  </td>
   38.48 +</tr>
   38.49 +</dtml-in>
   38.50 +
   38.51 +<tr>
   38.52 +<td colspan="3" align="left">
   38.53 +<div class="form-element">
   38.54 +<input class="form-element" type="submit" name="setVariables:method" value="Save Changes" />
   38.55 +<input class="form-element" type="submit" name="deleteVariables:method" value="Delete" />
   38.56 +</div>
   38.57 +</td>
   38.58 +</tr>
   38.59 +</table>
   38.60 +</form>
   38.61 +
   38.62 +</dtml-if>
   38.63 +
   38.64 +<form action="addVariable" method="POST">
   38.65 + <table>
   38.66 +  <tr>
   38.67 +   <td>Add a variable expression</td>
   38.68 +  </tr>
   38.69 +  <tr>
   38.70 +   <td>Variable</td>
   38.71 +   <td><select name="id">
   38.72 +    <dtml-in getWorkflowVariables>
   38.73 +     <option value="&dtml-sequence-item;">
   38.74 +      <dtml-var sequence-item>
   38.75 +     </option>
   38.76 +    </dtml-in>
   38.77 +   </select>
   38.78 +  </td>
   38.79 + </tr>
   38.80 + <tr>
   38.81 +  <td>Expression</td>
   38.82 +  <td><input type="text" name="text" size="50" value="" /></td>
   38.83 + </tr>
   38.84 + <tr><td><input type="submit" name="submit" value="Add" /></td></tr>
   38.85 +</table>
   38.86 +</form>
   38.87 +
   38.88 +<dtml-var manage_page_footer>
    39.1 new file mode 100644
    39.2 --- /dev/null
    39.3 +++ b/dtml/transitions.dtml
    39.4 @@ -0,0 +1,66 @@
    39.5 +<dtml-var manage_page_header>
    39.6 +<dtml-var manage_tabs>
    39.7 +<form action="&dtml-absolute_url;" method="POST">
    39.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    39.9 +<dtml-in values sort=id>
   39.10 + <tr bgcolor="#eeeeee">
   39.11 +  <th align="left" colspan="2">
   39.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   39.13 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   39.14 +   &nbsp;
   39.15 +   &dtml-title;
   39.16 +  </th>
   39.17 + </tr>
   39.18 +
   39.19 + <tr>
   39.20 +  <th width="10%"></th>
   39.21 +  <td>
   39.22 +   Destination state: <code><dtml-if new_state_id>&dtml-new_state_id;<dtml-else>(Remain in state)</dtml-if></code> <br />
   39.23 +   Trigger: <dtml-var expr="(trigger_type == 0 and 'Automatic') or
   39.24 +                            (trigger_type == 1 and 'User action') or
   39.25 +                            (trigger_type == 2 and 'WorkflowMethod')">
   39.26 +   <br />
   39.27 +   <dtml-if script_name>
   39.28 +     Script (before): &dtml-script_name;
   39.29 +     <br />
   39.30 +   </dtml-if>
   39.31 +   <dtml-if after_script_name>
   39.32 +     Script (after): &dtml-after_script_name;
   39.33 +     <br />
   39.34 +   </dtml-if>
   39.35 +   <dtml-if getGuardSummary><dtml-var getGuardSummary><br /></dtml-if>
   39.36 +   <dtml-if actbox_name>Adds to actions box: <code>&dtml-actbox_name;</code></dtml-if>
   39.37 +  </td>
   39.38 + </tr>
   39.39 + <dtml-if var_exprs>
   39.40 + <tr>
   39.41 +  <th align="right">Variables</th>
   39.42 +  <th></th>
   39.43 + </tr>
   39.44 + <dtml-in getVariableExprs sort>
   39.45 +  <tr>
   39.46 +   <td></td>
   39.47 +   <td>
   39.48 +    &dtml-sequence-key; = &dtml-sequence-item;
   39.49 +   </td>
   39.50 +  </tr>
   39.51 + </dtml-in>
   39.52 + </dtml-if>
   39.53 +<dtml-else>
   39.54 + <tr><td><em>No transitions defined.</em></td></tr>
   39.55 +</dtml-in>
   39.56 +</table>
   39.57 +<dtml-if values>
   39.58 +<p>
   39.59 +  <b>Note:</b> Renaming a transition will not automatically update all
   39.60 +  items in the workflow affected by it. You will need to fix them manually.
   39.61 +</p>  
   39.62 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   39.63 +<input type="submit" name="deleteTransitions:method" value="Delete" />
   39.64 +</dtml-if>
   39.65 +<hr />
   39.66 +<h3>Add a transition</h3>
   39.67 +<p>Id <input type="text" name="id" value="" />
   39.68 +<input type="submit" name="addTransition:method" value="Add" /></p>
   39.69 +</form>
   39.70 +<dtml-var manage_page_footer>
    40.1 new file mode 100644
    40.2 --- /dev/null
    40.3 +++ b/dtml/variable_properties.dtml
    40.4 @@ -0,0 +1,110 @@
    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">Description</th>
   40.18 +<td><input type="text" name="description" value="&dtml-description;"
   40.19 +     size="50" /></td>
   40.20 +</tr>
   40.21 +
   40.22 +<tr>
   40.23 +<th align="left">
   40.24 +  <div class="form-label">
   40.25 +  Make available to catalog
   40.26 +  </div>
   40.27 +</th>
   40.28 +<td>
   40.29 +  <div class="form-element">
   40.30 +   <dtml-let checked="for_catalog and 'checked' or ' '">
   40.31 +    <input type="checkbox" name="for_catalog" value="1" &dtml-checked; />
   40.32 +   </dtml-let>
   40.33 +  </div>
   40.34 +</td>
   40.35 +</tr>
   40.36 +
   40.37 +<tr>
   40.38 +<th align="left">
   40.39 +  <div class="form-label">
   40.40 +  Store in workflow status
   40.41 +  </div>
   40.42 +</th>
   40.43 +<td>
   40.44 +  <div class="form-element">
   40.45 +   <dtml-let checked="for_status and 'checked' or ' '">
   40.46 +    <input type="checkbox" name="for_status" value="1" &dtml-checked; />
   40.47 +   </dtml-let>
   40.48 +  </div>
   40.49 +</td>
   40.50 +</tr>
   40.51 +
   40.52 +<tr>
   40.53 +<th align="left">
   40.54 +  <div class="form-label">
   40.55 +  Variable update mode
   40.56 +  </div>
   40.57 +</th>
   40.58 +<td>
   40.59 +  <div class="form-element">
   40.60 +   <select name="update_always">
   40.61 +   <dtml-let checked="update_always and ' ' or 'selected'">
   40.62 +    <option value="" &dtml-checked;>Update only when the transition or
   40.63 +     new state specifies a new value</option>
   40.64 +   </dtml-let>
   40.65 +   <dtml-let checked="update_always and 'selected' or ' '">
   40.66 +    <option value="1" &dtml-checked;>Update on every transition</option>
   40.67 +   </dtml-let>
   40.68 +  </div>
   40.69 +</td>
   40.70 +</tr>
   40.71 +
   40.72 +<tr>
   40.73 +<th align="left">
   40.74 +  <div class="form-label">
   40.75 +  Default value
   40.76 +  </div>
   40.77 +</th>
   40.78 +<td>
   40.79 +  <div class="form-element">
   40.80 +  <input type="text" name="default_value" value="&dtml-default_value;" />
   40.81 +  </div>
   40.82 +</td>
   40.83 +</tr>
   40.84 +
   40.85 +<tr>
   40.86 +<th align="left">
   40.87 +  <div class="form-label">
   40.88 +  Default expression<br />(overrides default value)
   40.89 +  </div>
   40.90 +</th>
   40.91 +<td>
   40.92 +  <div class="form-element">
   40.93 +  <input type="text" name="default_expr" value="&dtml-getDefaultExprText;" size="50" />
   40.94 +  </div>
   40.95 +</td>
   40.96 +</tr>
   40.97 +
   40.98 +<tr>
   40.99 +<th align="left" valign="top">
  40.100 +  <div class="form-label">
  40.101 +  Info guard
  40.102 +  </div>
  40.103 +</th>
  40.104 +<td>
  40.105 + <dtml-with getInfoGuard>
  40.106 +  <dtml-var guardForm>
  40.107 + </dtml-with>
  40.108 +</td>
  40.109 +</tr>
  40.110 +
  40.111 +</table>
  40.112 +<input type="submit" name="submit" value="Save changes" />
  40.113 +</form>
  40.114 +<dtml-var manage_page_footer>
    41.1 new file mode 100644
    41.2 --- /dev/null
    41.3 +++ b/dtml/variables.dtml
    41.4 @@ -0,0 +1,57 @@
    41.5 +<dtml-var manage_page_header>
    41.6 +<dtml-var manage_tabs>
    41.7 +<form action="&dtml-absolute_url;" method="POST">
    41.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    41.9 +<dtml-in values sort=id>
   41.10 + <tr bgcolor="#eeeeee">
   41.11 +  <th align="left" colspan="2">
   41.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   41.13 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   41.14 +   &nbsp;
   41.15 +   &dtml-description;
   41.16 +  </th>
   41.17 + </tr>
   41.18 +
   41.19 + <tr>
   41.20 +  <th width="10%"></th>
   41.21 +  <td>
   41.22 +   Available to catalog:
   41.23 +   <code><dtml-if for_catalog>Yes<dtml-else>No</dtml-if></code><br />
   41.24 +   Stored in status:
   41.25 +   <code><dtml-if for_status>Yes<dtml-else>No</dtml-if></code><br />
   41.26 +   <dtml-if default_value>
   41.27 +    Default value: <code>&dtml-default_value;</code><br />
   41.28 +   </dtml-if>
   41.29 +   <dtml-if getDefaultExprText>
   41.30 +    Default expr: <code>&dtml-getDefaultExprText;</code><br />
   41.31 +   </dtml-if>
   41.32 +   <dtml-if getInfoGuardSummary>
   41.33 +    <dtml-var getInfoGuardSummary><br />
   41.34 +   </dtml-if>
   41.35 +  </td>
   41.36 + </tr>
   41.37 +<dtml-else>
   41.38 + <tr><td><em>No variables defined.</em></td></tr>
   41.39 +</dtml-in>
   41.40 +</table>
   41.41 +<dtml-if values>
   41.42 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   41.43 +<input type="submit" name="deleteVariables:method" value="Delete" />
   41.44 +</dtml-if>
   41.45 +</form>
   41.46 +
   41.47 +<hr />
   41.48 +<form action="addVariable" method="POST">
   41.49 +<h3>Add a variable</h3>
   41.50 +<p>Id <input type="text" name="id" value="" />
   41.51 +<input type="submit" name="submit" value="Add" /></p>
   41.52 +</form>
   41.53 +
   41.54 +<hr />
   41.55 +<form action="setStateVar" method="POST">
   41.56 +State variable name: <input type="text" name="id" value="&dtml-getStateVar;" />
   41.57 +<input type="submit" name="submit" value="Change" />
   41.58 +<i class="form-help">(Be careful!)</i>
   41.59 +</form>
   41.60 +
   41.61 +<dtml-var manage_page_footer>
    42.1 new file mode 100644
    42.2 --- /dev/null
    42.3 +++ b/dtml/workflow_groups.pt
    42.4 @@ -0,0 +1,60 @@
    42.5 +<h1 tal:replace="structure here/manage_page_header">Header</h1>
    42.6 +<h2 tal:define="manage_tabs_message request/manage_tabs_message | nothing"
    42.7 +    tal:replace="structure here/manage_tabs">Tabs</h2>
    42.8 +
    42.9 +<table>
   42.10 +<tr>
   42.11 +<td width="50%" valign="top">
   42.12 +
   42.13 +<form action="." method="POST" tal:attributes="action here/absolute_url"
   42.14 +  tal:define="groups here/getGroups">
   42.15 +<h3>Managed Groups</h3>
   42.16 +<div class="form-help">
   42.17 +This workflow controls access by the selected groups.  The mappings
   42.18 +from group to role depend on the workflow state.
   42.19 +</div>
   42.20 +<div tal:repeat="group groups">
   42.21 +<input type="checkbox" name="groups:list" tal:attributes="value group" />
   42.22 +<span tal:replace="group">Everyone</span>
   42.23 +</div>
   42.24 +<div tal:condition="not:groups">
   42.25 +<em>No groups are managed by this workflow.</em>
   42.26 +</div>
   42.27 +
   42.28 +<div tal:condition="groups">
   42.29 +<input type="submit" name="delGroups:method" value="Remove" />
   42.30 +</div>
   42.31 +
   42.32 +<hr />
   42.33 +
   42.34 +<h3>Add a managed group</h3>
   42.35 +<select name="group">
   42.36 + <option tal:repeat="group here/getAvailableGroups"
   42.37 +   tal:attributes="value group" tal:content="group" />
   42.38 +</select>
   42.39 +<input type="submit" name="addGroup:method" value="Add" />
   42.40 +</form>
   42.41 +
   42.42 +</td>
   42.43 +<td width="50%" style="border-left: 1px solid black; padding-left: 1em;"
   42.44 +  valign="top">
   42.45 +
   42.46 +<form method="POST" tal:attributes="action here/absolute_url">
   42.47 +<h3>Roles Mapped to Groups</h3>
   42.48 +<div class="form-help">
   42.49 +This workflow maps the following roles to groups.  Roles not selected
   42.50 +are managed outside this workflow.
   42.51 +<div tal:define="roles here/getRoles"
   42.52 +  tal:repeat="role here/getAvailableRoles">
   42.53 +<input type="checkbox" name="roles:list" tal:attributes="value role;
   42.54 +  checked python:role in roles" /><span tal:content="role" />
   42.55 +</div>
   42.56 +</div>
   42.57 +<input type="submit" name="setRoles:method" value="Save Changes" />
   42.58 +</form>
   42.59 +
   42.60 +</td>
   42.61 +</tr>
   42.62 +</table>
   42.63 +
   42.64 +<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
    43.1 new file mode 100644
    43.2 --- /dev/null
    43.3 +++ b/dtml/workflow_permissions.dtml
    43.4 @@ -0,0 +1,43 @@
    43.5 +<dtml-var manage_page_header>
    43.6 +<dtml-var manage_tabs>
    43.7 +
    43.8 +<form action="&dtml-absolute_url;" method="POST">
    43.9 +<table>
   43.10 +
   43.11 +<tr>
   43.12 +<td class="form-help">
   43.13 +The selected permissions are managed by this workflow.  The role to permission
   43.14 +mappings for an object in this workflow depend on its state.
   43.15 +</td>
   43.16 +</tr>
   43.17 +
   43.18 +<dtml-in permissions sort>
   43.19 +<tr>
   43.20 +<td>
   43.21 +<input type="checkbox" name="ps:list" value="&dtml-sequence-item;">
   43.22 +&dtml-sequence-item;
   43.23 +</td>
   43.24 +</tr>
   43.25 +<dtml-else>
   43.26 +<tr>
   43.27 +<td>
   43.28 +<em>No permissions are managed by this workflow.</em>
   43.29 +</td>
   43.30 +</tr>
   43.31 +</dtml-in>
   43.32 +
   43.33 +</table>
   43.34 +<dtml-if permissions>
   43.35 +<input type="submit" name="delManagedPermissions:method" value="Remove selected" />
   43.36 +</dtml-if>
   43.37 +<hr />
   43.38 +<h3>Add a managed permission</h3>
   43.39 +<select name="p">
   43.40 + <dtml-in getPossiblePermissions><dtml-if
   43.41 +   expr="_['sequence-item'] not in permissions">
   43.42 +  <option value="&dtml-sequence-item;">&dtml-sequence-item;</option>
   43.43 + </dtml-if></dtml-in>
   43.44 +</select>
   43.45 +<input type="submit" name="addManagedPermission:method" value="Add" />
   43.46 +</form>
   43.47 +<dtml-var manage_page_footer>
    44.1 new file mode 100644
    44.2 --- /dev/null
    44.3 +++ b/dtml/workflow_properties.dtml
    44.4 @@ -0,0 +1,39 @@
    44.5 +<dtml-var manage_page_header>
    44.6 +<dtml-var manage_tabs>
    44.7 +
    44.8 +<form action="setProperties" method="POST">
    44.9 +<table>
   44.10 +
   44.11 +<tr>
   44.12 +<th align="left">Id</th>
   44.13 +<td>&dtml-id;</td>
   44.14 +</tr>
   44.15 +
   44.16 +<tr>
   44.17 +<th align="left">Title</th>
   44.18 +<td><input type="text" name="title" value="&dtml-title;" size="40" /></td>
   44.19 +</tr>
   44.20 +
   44.21 +<tr>
   44.22 +<th align="left">'Manager' role bypasses guards</th>
   44.23 +<td>
   44.24 +<dtml-let cb="manager_bypass and 'checked=\'checked\'' or ''">
   44.25 +<input type="checkbox" name="manager_bypass" &dtml-cb; />
   44.26 +</dtml-let>
   44.27 +</td>
   44.28 +</tr>
   44.29 +
   44.30 +<tr>
   44.31 +<th align="left" valign="top">Instance creation conditions</th>
   44.32 +<td>
   44.33 + <dtml-with getGuard>
   44.34 +  <dtml-var guardForm>
   44.35 + </dtml-with>
   44.36 +</td>
   44.37 +</tr>
   44.38 +
   44.39 +</table>
   44.40 +
   44.41 +<input type="submit" name="submit" value="Save changes" />
   44.42 +</form>
   44.43 +<dtml-var manage_page_footer>
    45.1 new file mode 100644
    45.2 --- /dev/null
    45.3 +++ b/dtml/worklist_properties.dtml
    45.4 @@ -0,0 +1,87 @@
    45.5 +<dtml-var manage_page_header>
    45.6 +<dtml-var manage_tabs>
    45.7 +
    45.8 +<form action="setProperties" method="POST">
    45.9 +<table>
   45.10 +
   45.11 +<tr>
   45.12 +<th align="left">Id</th>
   45.13 +<td>&dtml-id;</td>
   45.14 +</tr>
   45.15 +
   45.16 +<tr>
   45.17 +<th align="left">Description</th>
   45.18 +<td>
   45.19 +<input type="text" name="description" value="&dtml-description;" size="50" />
   45.20 +</td>
   45.21 +</tr>
   45.22 +
   45.23 +<tr>
   45.24 +<th align="left" valign="top">
   45.25 +  <div class="form-label">
   45.26 +  Cataloged variable matches (formatted)
   45.27 +  </div>
   45.28 +</th>
   45.29 +<td>
   45.30 +  <table>
   45.31 +   <dtml-in getAvailableCatalogVars>
   45.32 +    <tr>
   45.33 +     <th align="left">&dtml-sequence-item; =</th>
   45.34 +     <td>
   45.35 +      <dtml-let value="getVarMatchText(_['sequence-item'])">
   45.36 +       <input type="text" name="var_match_&dtml-sequence-item;"
   45.37 +        value="&dtml-value;" />
   45.38 +      </dtml-let>
   45.39 +     </td>
   45.40 +    </tr>
   45.41 +   </dtml-in>
   45.42 +  </table>
   45.43 +</td>
   45.44 +</tr>
   45.45 +
   45.46 +<tr>
   45.47 +<th align="left" valign="top">Display in actions box</th>
   45.48 +<td>
   45.49 + <table>
   45.50 +  <tr>
   45.51 +   <th align="left">Name (formatted)</th>
   45.52 +   <td>
   45.53 +    <input type="text" name="actbox_name"
   45.54 +     value="&dtml-actbox_name;" size="50" />
   45.55 +   </td>
   45.56 +  </tr>
   45.57 +  <tr>
   45.58 +   <th align="left">URL (formatted)</th>
   45.59 +   <td>
   45.60 +    <input type="text" name="actbox_url"
   45.61 +     value="&dtml-actbox_url;" size="50" />
   45.62 +   </td>
   45.63 +  </tr>
   45.64 +  <tr>
   45.65 +   <th align="left">Category</th>
   45.66 +   <td>
   45.67 +    <input type="text" name="actbox_category"
   45.68 +     value="&dtml-actbox_category;" />
   45.69 +   </td>
   45.70 +  </tr>
   45.71 + </table>
   45.72 +</td>
   45.73 +</tr>
   45.74 +
   45.75 +<tr>
   45.76 +<th align="left" valign="top">
   45.77 +  <div class="form-label">
   45.78 +  Guard
   45.79 +  </div>
   45.80 +</th>
   45.81 +<td>
   45.82 + <dtml-with getGuard>
   45.83 +  <dtml-var guardForm>
   45.84 + </dtml-with>
   45.85 +</td>
   45.86 +</tr>
   45.87 +
   45.88 +</table>
   45.89 +<input type="submit" name="submit" value="Save changes" />
   45.90 +</form>
   45.91 +<dtml-var manage_page_footer>
    46.1 new file mode 100644
    46.2 --- /dev/null
    46.3 +++ b/dtml/worklists.dtml
    46.4 @@ -0,0 +1,57 @@
    46.5 +<dtml-var manage_page_header>
    46.6 +<dtml-var manage_tabs>
    46.7 +<form action="&dtml-absolute_url;" method="POST">
    46.8 +<table border="0" cellspacing="0" cellpadding="2" width="100%">
    46.9 +<dtml-in values sort=id>
   46.10 + <tr bgcolor="#eeeeee">
   46.11 +  <th align="left" colspan="2">
   46.12 +   <input type="checkbox" name="ids:list" value="&dtml-id;" />
   46.13 +   <a href="&dtml.url_quote-id;/manage_properties">&dtml-id;</a>
   46.14 +   &nbsp;
   46.15 +   &dtml-description;
   46.16 +  </th>
   46.17 + </tr>
   46.18 +
   46.19 + <tr>
   46.20 +  <th width="10%"></th>
   46.21 +  <td>
   46.22 +   <dtml-if name_fmt>
   46.23 +    Name format: <code>&dtml-name_fmt;</code><br />
   46.24 +   </dtml-if>
   46.25 +   <dtml-if getVarMatchKeys>
   46.26 +    Catalog matches:
   46.27 +    <dtml-in getVarMatchKeys sort>
   46.28 +    <dtml-let key=sequence-item value="getVarMatch(key)">
   46.29 +     <code>&dtml-key;</code> =
   46.30 +     <dtml-in value>
   46.31 +       <code>&dtml-sequence-item;</code>
   46.32 +       <dtml-unless sequence-end>or</dtml-unless>
   46.33 +     </dtml-in>
   46.34 +     <dtml-unless sequence-end>;</dtml-unless>
   46.35 +    </dtml-let>
   46.36 +    </dtml-in>
   46.37 +    <br />
   46.38 +   </dtml-if>
   46.39 +   <dtml-if getGuardSummary>
   46.40 +    <dtml-var getGuardSummary><br />
   46.41 +   </dtml-if>
   46.42 +  </td>
   46.43 + </tr>
   46.44 +<dtml-else>
   46.45 + <tr><td><em>No worklists defined.</em></td></tr>
   46.46 +</dtml-in>
   46.47 +</table>
   46.48 +<dtml-if values>
   46.49 +<input type="submit" name="manage_renameForm:method" value="Rename" />
   46.50 +<input type="submit" name="deleteWorklists:method" value="Delete" />
   46.51 +</dtml-if>
   46.52 +</form>
   46.53 +
   46.54 +<hr />
   46.55 +<form action="addWorklist" method="POST">
   46.56 +<h3>Add a worklist</h3>
   46.57 +<p>Id <input type="text" name="id" value="" />
   46.58 +<input type="submit" name="submit" value="Add" /></p>
   46.59 +</form>
   46.60 +
   46.61 +<dtml-var manage_page_footer>
    47.1 new file mode 100644
    47.2 --- /dev/null
    47.3 +++ b/exportimport.py
    47.4 @@ -0,0 +1,1225 @@
    47.5 +##############################################################################
    47.6 +#
    47.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    47.8 +#
    47.9 +# This software is subject to the provisions of the Zope Public License,
   47.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   47.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   47.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   47.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   47.14 +# FOR A PARTICULAR PURPOSE.
   47.15 +#
   47.16 +##############################################################################
   47.17 +"""DCWorkflow export / import support.
   47.18 +
   47.19 +$Id: exportimport.py 40636 2005-12-07 21:20:58Z tseaver $
   47.20 +"""
   47.21 +
   47.22 +import re
   47.23 +from xml.dom.minidom import parseString
   47.24 +
   47.25 +from AccessControl import ClassSecurityInfo
   47.26 +from Acquisition import Implicit
   47.27 +from Globals import InitializeClass
   47.28 +from Products.PageTemplates.PageTemplateFile import PageTemplateFile
   47.29 +
   47.30 +from Products.GenericSetup.utils import BodyAdapterBase
   47.31 +
   47.32 +from utils import _xmldir
   47.33 +from DCWorkflow import DCWorkflowDefinition
   47.34 +from interfaces import IDCWorkflowDefinition
   47.35 +from permissions import ManagePortal
   47.36 +
   47.37 +
   47.38 +TRIGGER_TYPES = ( 'AUTOMATIC', 'USER' )
   47.39 +_FILENAME = 'workflows.xml'
   47.40 +
   47.41 +
   47.42 +class DCWorkflowDefinitionBodyAdapter(BodyAdapterBase):
   47.43 +
   47.44 +    """Body im- and exporter for DCWorkflowDefinition.
   47.45 +    """
   47.46 +
   47.47 +    __used_for__ = IDCWorkflowDefinition
   47.48 +
   47.49 +    def _exportBody(self):
   47.50 +        """Export the object as a file body.
   47.51 +        """
   47.52 +        wfdc = WorkflowDefinitionConfigurator(self.context)
   47.53 +        return wfdc.__of__(self.context).generateWorkflowXML()
   47.54 +
   47.55 +    def _importBody(self, body):
   47.56 +        """Import the object from the file body.
   47.57 +        """
   47.58 +        encoding = 'utf-8'
   47.59 +        wfdc = WorkflowDefinitionConfigurator(self.context)
   47.60 +
   47.61 +        ( workflow_id
   47.62 +        , title
   47.63 +        , state_variable
   47.64 +        , initial_state
   47.65 +        , states
   47.66 +        , transitions
   47.67 +        , variables
   47.68 +        , worklists
   47.69 +        , permissions
   47.70 +        , scripts
   47.71 +        ) = wfdc.parseWorkflowXML(body, encoding)
   47.72 +
   47.73 +        _initDCWorkflow( self.context
   47.74 +                       , title
   47.75 +                       , state_variable
   47.76 +                       , initial_state
   47.77 +                       , states
   47.78 +                       , transitions
   47.79 +                       , variables
   47.80 +                       , worklists
   47.81 +                       , permissions
   47.82 +                       , scripts
   47.83 +                       , self.environ
   47.84 +                       )
   47.85 +
   47.86 +    body = property(_exportBody, _importBody)
   47.87 +
   47.88 +    mime_type = 'text/xml'
   47.89 +
   47.90 +    suffix = '/definition.xml'
   47.91 +
   47.92 +
   47.93 +class WorkflowDefinitionConfigurator( Implicit ):
   47.94 +    """ Synthesize XML description of site's workflows.
   47.95 +    """
   47.96 +    security = ClassSecurityInfo()
   47.97 +
   47.98 +    def __init__(self, obj):
   47.99 +        self._obj = obj
  47.100 +
  47.101 +    security.declareProtected( ManagePortal, 'getWorkflowInfo' )
  47.102 +    def getWorkflowInfo( self, workflow_id ):
  47.103 +
  47.104 +        """ Return a mapping describing a given workflow.
  47.105 +
  47.106 +        o Keys in the mappings:
  47.107 +
  47.108 +          'id' -- the ID of the workflow within the tool
  47.109 +
  47.110 +          'meta_type' -- the workflow's meta_type
  47.111 +
  47.112 +          'title' -- the workflow's title property
  47.113 +
  47.114 +        o See '_extractDCWorkflowInfo' below for keys present only for
  47.115 +          DCWorkflow definitions.
  47.116 +
  47.117 +        """
  47.118 +        workflow = self._obj
  47.119 +
  47.120 +        workflow_info = { 'id'          : workflow_id
  47.121 +                        , 'meta_type'   : workflow.meta_type
  47.122 +                        , 'title'       : workflow.title_or_id()
  47.123 +                        }
  47.124 +
  47.125 +        if workflow.meta_type == DCWorkflowDefinition.meta_type:
  47.126 +            self._extractDCWorkflowInfo( workflow, workflow_info )
  47.127 +
  47.128 +        return workflow_info
  47.129 +
  47.130 +    security.declareProtected( ManagePortal, 'generateWorkflowXML' )
  47.131 +    def generateWorkflowXML(self):
  47.132 +        """ Pseudo API.
  47.133 +        """
  47.134 +        return self._workflowConfig(workflow_id=self._obj.getId())
  47.135 +
  47.136 +    security.declareProtected( ManagePortal, 'getWorkflowScripts' )
  47.137 +    def getWorkflowScripts(self):
  47.138 +        """ Get workflow scripts information
  47.139 +        """
  47.140 +        return self._extractScripts(self._obj)
  47.141 +
  47.142 +    security.declareProtected( ManagePortal, 'parseWorkflowXML' )
  47.143 +    def parseWorkflowXML( self, xml, encoding=None ):
  47.144 +        """ Pseudo API.
  47.145 +        """
  47.146 +        dom = parseString( xml )
  47.147 +
  47.148 +        root = dom.getElementsByTagName( 'dc-workflow' )[ 0 ]
  47.149 +
  47.150 +        workflow_id = _getNodeAttribute( root, 'workflow_id', encoding )
  47.151 +        title = _getNodeAttribute( root, 'title', encoding )
  47.152 +        state_variable = _getNodeAttribute( root, 'state_variable', encoding )
  47.153 +        initial_state = _getNodeAttribute( root, 'initial_state', encoding )
  47.154 +
  47.155 +        states = _extractStateNodes( root, encoding )
  47.156 +        transitions = _extractTransitionNodes( root, encoding )
  47.157 +        variables = _extractVariableNodes( root, encoding )
  47.158 +        worklists = _extractWorklistNodes( root, encoding )
  47.159 +        permissions = _extractPermissionNodes( root, encoding )
  47.160 +        scripts = _extractScriptNodes( root, encoding )
  47.161 +
  47.162 +        return ( workflow_id
  47.163 +               , title
  47.164 +               , state_variable
  47.165 +               , initial_state
  47.166 +               , states
  47.167 +               , transitions
  47.168 +               , variables
  47.169 +               , worklists
  47.170 +               , permissions
  47.171 +               , scripts
  47.172 +               )
  47.173 +
  47.174 +    security.declarePrivate( '_workflowConfig' )
  47.175 +    _workflowConfig = PageTemplateFile( 'wtcWorkflowExport.xml'
  47.176 +                                      , _xmldir
  47.177 +                                      , __name__='workflowConfig'
  47.178 +                                      )
  47.179 +
  47.180 +    security.declarePrivate( '_extractDCWorkflowInfo' )
  47.181 +    def _extractDCWorkflowInfo( self, workflow, workflow_info ):
  47.182 +
  47.183 +        """ Append the information for a 'workflow' into 'workflow_info'
  47.184 +
  47.185 +        o 'workflow' must be a DCWorkflowDefinition instance.
  47.186 +
  47.187 +        o 'workflow_info' must be a dictionary.
  47.188 +
  47.189 +        o The following keys will be added to 'workflow_info':
  47.190 +
  47.191 +          'permissions' -- a list of names of permissions managed
  47.192 +            by the workflow
  47.193 +
  47.194 +          'state_variable' -- the name of the workflow's "main"
  47.195 +            state variable
  47.196 +
  47.197 +          'initial_state' -- the name of the state in the workflow
  47.198 +            in which objects start their lifecycle.
  47.199 +
  47.200 +          'variable_info' -- a list of mappings describing the
  47.201 +            variables tracked by the workflow (see '_extractVariables').
  47.202 +
  47.203 +          'state_info' -- a list of mappings describing the
  47.204 +            states tracked by the workflow (see '_extractStates').
  47.205 +
  47.206 +          'transition_info' -- a list of mappings describing the
  47.207 +            transitions tracked by the workflow (see '_extractTransitions').
  47.208 +
  47.209 +          'worklist_info' -- a list of mappings describing the
  47.210 +            worklists tracked by the workflow (see '_extractWorklists').
  47.211 +
  47.212 +          'script_info' -- a list of mappings describing the scripts which
  47.213 +            provide added business logic (see '_extractScripts').
  47.214 +        """
  47.215 +        workflow_info[ 'state_variable' ] = workflow.state_var
  47.216 +        workflow_info[ 'initial_state' ] = workflow.initial_state
  47.217 +        workflow_info[ 'permissions' ] = workflow.permissions
  47.218 +        workflow_info[ 'variable_info' ] = self._extractVariables( workflow )
  47.219 +        workflow_info[ 'state_info' ] = self._extractStates( workflow )
  47.220 +        workflow_info[ 'transition_info' ] = self._extractTransitions(
  47.221 +                                                                   workflow )
  47.222 +        workflow_info[ 'worklist_info' ] = self._extractWorklists( workflow )
  47.223 +        workflow_info[ 'script_info' ] = self._extractScripts( workflow )
  47.224 +
  47.225 +    security.declarePrivate( '_extractVariables' )
  47.226 +    def _extractVariables( self, workflow ):
  47.227 +
  47.228 +        """ Return a sequence of mappings describing DCWorkflow variables.
  47.229 +
  47.230 +        o Keys for each mapping will include:
  47.231 +
  47.232 +          'id' -- the variable's ID
  47.233 +
  47.234 +          'description' -- a textual description of the variable
  47.235 +
  47.236 +          'for_catalog' -- whether to catalog this variable
  47.237 +
  47.238 +          'for_status' -- whether to ??? this variable (XXX)
  47.239 +
  47.240 +          'update_always' -- whether to update this variable whenever
  47.241 +            executing a transition (xxX)
  47.242 +
  47.243 +          'default_value' -- a default value for the variable (XXX)
  47.244 +
  47.245 +          'default_expression' -- a TALES expression for the default value
  47.246 +
  47.247 +          'guard_permissions' -- a list of permissions guarding access
  47.248 +            to the variable
  47.249 +
  47.250 +          'guard_roles' -- a list of roles guarding access
  47.251 +            to the variable
  47.252 +
  47.253 +          'guard_groups' -- a list of groups guarding the transition
  47.254 +
  47.255 +          'guard_expr' -- an expression guarding access to the variable
  47.256 +        """
  47.257 +        result = []
  47.258 +
  47.259 +        items = workflow.variables.objectItems()
  47.260 +        items.sort()
  47.261 +
  47.262 +        for k, v in items:
  47.263 +
  47.264 +            guard = v.getInfoGuard()
  47.265 +
  47.266 +            default_type = _guessVariableType( v.default_value )
  47.267 +
  47.268 +            info = { 'id'                   : k
  47.269 +                   , 'description'          : v.description
  47.270 +                   , 'for_catalog'          : bool( v.for_catalog )
  47.271 +                   , 'for_status'           : bool( v.for_status )
  47.272 +                   , 'update_always'        : bool( v.update_always )
  47.273 +                   , 'default_value'        : v.default_value
  47.274 +                   , 'default_type'         : default_type
  47.275 +                   , 'default_expr'         : v.getDefaultExprText()
  47.276 +                   , 'guard_permissions'    : guard.permissions
  47.277 +                   , 'guard_roles'          : guard.roles
  47.278 +                   , 'guard_groups'         : guard.groups
  47.279 +                   , 'guard_expr'           : guard.getExprText()
  47.280 +                   }
  47.281 +
  47.282 +            result.append( info )
  47.283 +
  47.284 +        return result
  47.285 +
  47.286 +    security.declarePrivate( '_extractStates' )
  47.287 +    def _extractStates( self, workflow ):
  47.288 +
  47.289 +        """ Return a sequence of mappings describing DCWorkflow states.
  47.290 +
  47.291 +        o Within the workflow mapping, each 'state_info' mapping has keys:
  47.292 +
  47.293 +          'id' -- the state's ID
  47.294 +
  47.295 +          'title' -- the state's title
  47.296 +
  47.297 +          'description' -- the state's description
  47.298 +
  47.299 +          'transitions' -- a list of IDs of transitions out of the state
  47.300 +
  47.301 +          'permissions' -- a list of mappings describing the permission
  47.302 +            map for the state
  47.303 +
  47.304 +          'groups' -- a list of ( group_id, (roles,) ) tuples describing the
  47.305 +            group-role assignments for the state
  47.306 +
  47.307 +          'variables' -- a list of mapping for the variables
  47.308 +            to be set when entering the state.
  47.309 +
  47.310 +        o Within the state_info mappings, each 'permissions' mapping
  47.311 +          has the keys:
  47.312 +
  47.313 +          'name' -- the name of the permission
  47.314 +
  47.315 +          'roles' -- a sequence of role IDs which have the permission
  47.316 +
  47.317 +          'acquired' -- whether roles are acquired for the permission
  47.318 +
  47.319 +        o Within the state_info mappings, each 'variable' mapping
  47.320 +          has the keys:
  47.321 +
  47.322 +          'name' -- the name of the variable
  47.323 +
  47.324 +          'type' -- the type of the value (allowed values are:
  47.325 +                    'string', 'datetime', 'bool', 'int')
  47.326 +
  47.327 +          'value' -- the value to be set
  47.328 +        """
  47.329 +        result = []
  47.330 +
  47.331 +        items = workflow.states.objectItems()
  47.332 +        items.sort()
  47.333 +
  47.334 +        for k, v in items:
  47.335 +
  47.336 +            groups = v.group_roles and list( v.group_roles.items() ) or []
  47.337 +            groups = [ x for x in groups if x[1] ]
  47.338 +            groups.sort()
  47.339 +
  47.340 +            variables = list( v.getVariableValues() )
  47.341 +            variables.sort()
  47.342 +
  47.343 +            v_info = []
  47.344 +
  47.345 +            for v_name, value in variables:
  47.346 +                v_info.append( { 'name' : v_name
  47.347 +                               , 'type' :_guessVariableType( value )
  47.348 +                               , 'value' : value
  47.349 +                               } )
  47.350 +
  47.351 +            info = { 'id'           : k
  47.352 +                   , 'title'        : v.title
  47.353 +                   , 'description'  : v.description
  47.354 +                   , 'transitions'  : v.transitions
  47.355 +                   , 'permissions'  : self._extractStatePermissions( v )
  47.356 +                   , 'groups'       : groups
  47.357 +                   , 'variables'    : v_info
  47.358 +                   }
  47.359 +
  47.360 +            result.append( info )
  47.361 +
  47.362 +        return result
  47.363 +
  47.364 +    security.declarePrivate( '_extractStatePermissions' )
  47.365 +    def _extractStatePermissions( self, state ):
  47.366 +
  47.367 +        """ Return a sequence of mappings for the permissions in a state.
  47.368 +
  47.369 +        o Each mapping has the keys:
  47.370 +
  47.371 +          'name' -- the name of the permission
  47.372 +
  47.373 +          'roles' -- a sequence of role IDs which have the permission
  47.374 +
  47.375 +          'acquired' -- whether roles are acquired for the permission
  47.376 +        """
  47.377 +        result = []
  47.378 +
  47.379 +        items = state.permission_roles.items()
  47.380 +        items.sort()
  47.381 +
  47.382 +        for k, v in items:
  47.383 +
  47.384 +            result.append( { 'name' : k
  47.385 +                           , 'roles' : v
  47.386 +                           , 'acquired' : not isinstance( v, tuple )
  47.387 +                           } )
  47.388 +
  47.389 +        return result
  47.390 +
  47.391 +
  47.392 +    security.declarePrivate( '_extractTransitions' )
  47.393 +    def _extractTransitions( self, workflow ):
  47.394 +
  47.395 +        """ Return a sequence of mappings describing DCWorkflow transitions.
  47.396 +
  47.397 +        o Each mapping has the keys:
  47.398 +
  47.399 +          'id' -- the transition's ID
  47.400 +
  47.401 +          'title' -- the transition's ID
  47.402 +
  47.403 +          'description' -- the transition's description
  47.404 +
  47.405 +          'new_state_id' -- the ID of the state into which the transition
  47.406 +            moves an object
  47.407 +
  47.408 +          'trigger_type' -- one of the following values, indicating how the
  47.409 +            transition is fired:
  47.410 +
  47.411 +            - "AUTOMATIC" -> fired opportunistically whenever the workflow
  47.412 +               notices that its guard conditions permit
  47.413 +
  47.414 +            - "USER" -> fired in response to user request
  47.415 +
  47.416 +          'script_name' -- the ID of a script to be executed before
  47.417 +             the transition
  47.418 +
  47.419 +          'after_script_name' -- the ID of a script to be executed after
  47.420 +             the transition
  47.421 +
  47.422 +          'actbox_name' -- the name of the action by which the user
  47.423 +             triggers the transition
  47.424 +
  47.425 +          'actbox_url' -- the URL of the action by which the user
  47.426 +             triggers the transition
  47.427 +
  47.428 +          'actbox_category' -- the category of the action by which the user
  47.429 +             triggers the transition
  47.430 +
  47.431 +          'variables' -- a list of ( id, expr ) tuples defining how variables
  47.432 +            are to be set during the transition
  47.433 +
  47.434 +          'guard_permissions' -- a list of permissions guarding the transition
  47.435 +
  47.436 +          'guard_roles' -- a list of roles guarding the transition
  47.437 +
  47.438 +          'guard_groups' -- a list of groups guarding the transition
  47.439 +
  47.440 +          'guard_expr' -- an expression guarding the transition
  47.441 +
  47.442 +        """
  47.443 +        result = []
  47.444 +
  47.445 +        items = workflow.transitions.objectItems()
  47.446 +        items.sort()
  47.447 +
  47.448 +        for k, v in items:
  47.449 +
  47.450 +            guard = v.getGuard()
  47.451 +
  47.452 +            v_info = []
  47.453 +
  47.454 +            for v_name, expr in v.getVariableExprs():
  47.455 +                v_info.append( { 'name' : v_name, 'expr' : expr } )
  47.456 +
  47.457 +            info = { 'id'                   : k
  47.458 +                   , 'title'                : v.title
  47.459 +                   , 'description'          : v.description
  47.460 +                   , 'new_state_id'         : v.new_state_id
  47.461 +                   , 'trigger_type'         : TRIGGER_TYPES[ v.trigger_type ]
  47.462 +                   , 'script_name'          : v.script_name
  47.463 +                   , 'after_script_name'    : v.after_script_name
  47.464 +                   , 'actbox_name'          : v.actbox_name
  47.465 +                   , 'actbox_url'           : v.actbox_url
  47.466 +                   , 'actbox_category'      : v.actbox_category
  47.467 +                   , 'variables'            : v_info
  47.468 +                   , 'guard_permissions'    : guard.permissions
  47.469 +                   , 'guard_roles'          : guard.roles
  47.470 +                   , 'guard_groups'         : guard.groups
  47.471 +                   , 'guard_expr'           : guard.getExprText()
  47.472 +                   }
  47.473 +
  47.474 +            result.append( info )
  47.475 +
  47.476 +        return result
  47.477 +
  47.478 +    security.declarePrivate( '_extractWorklists' )
  47.479 +    def _extractWorklists( self, workflow ):
  47.480 +
  47.481 +        """ Return a sequence of mappings describing DCWorkflow transitions.
  47.482 +
  47.483 +        o Each mapping has the keys:
  47.484 +
  47.485 +          'id' -- the ID of the worklist
  47.486 +
  47.487 +          'title' -- the title of the worklist
  47.488 +
  47.489 +          'description' -- a textual description of the worklist
  47.490 +
  47.491 +          'var_match' -- a list of ( key, value ) tuples defining
  47.492 +            the variables used to "activate" the worklist.
  47.493 +
  47.494 +          'actbox_name' -- the name of the "action" corresponding to the
  47.495 +            worklist
  47.496 +
  47.497 +          'actbox_url' -- the URL of the "action" corresponding to the
  47.498 +            worklist
  47.499 +
  47.500 +          'actbox_category' -- the category of the "action" corresponding
  47.501 +            to the worklist
  47.502 +
  47.503 +          'guard_permissions' -- a list of permissions guarding access
  47.504 +            to the worklist
  47.505 +
  47.506 +          'guard_roles' -- a list of roles guarding access
  47.507 +            to the worklist
  47.508 +
  47.509 +          'guard_expr' -- an expression guarding access to the worklist
  47.510 +
  47.511 +        """
  47.512 +        result = []
  47.513 +
  47.514 +        items = workflow.worklists.objectItems()
  47.515 +        items.sort()
  47.516 +
  47.517 +        for k, v in items:
  47.518 +
  47.519 +            guard = v.getGuard()
  47.520 +
  47.521 +            var_match = [ ( id, v.getVarMatchText( id ) )
  47.522 +                            for id in v.getVarMatchKeys() ]
  47.523 +
  47.524 +            info = { 'id'                   : k
  47.525 +                   , 'title'                : v.title
  47.526 +                   , 'description'          : v.description
  47.527 +                   , 'var_match'            : var_match
  47.528 +                   , 'actbox_name'          : v.actbox_name
  47.529 +                   , 'actbox_url'           : v.actbox_url
  47.530 +                   , 'actbox_category'      : v.actbox_category
  47.531 +                   , 'guard_permissions'    : guard.permissions
  47.532 +                   , 'guard_roles'          : guard.roles
  47.533 +                   , 'guard_groups'         : guard.groups
  47.534 +                   , 'guard_expr'           : guard.getExprText()
  47.535 +                   }
  47.536 +
  47.537 +            result.append( info )
  47.538 +
  47.539 +        return result
  47.540 +
  47.541 +    security.declarePrivate( '_extractScripts' )
  47.542 +    def _extractScripts( self, workflow ):
  47.543 +
  47.544 +        """ Return a sequence of mappings describing DCWorkflow scripts.
  47.545 +
  47.546 +        o Each mapping has the keys:
  47.547 +
  47.548 +          'id' -- the ID of the script
  47.549 +
  47.550 +          'meta_type' -- the title of the worklist
  47.551 +
  47.552 +          'body' -- the text of the script (only applicable to scripts
  47.553 +            of type Script (Python))
  47.554 +
  47.555 +          'module' -- The module from where to load the function (only
  47.556 +            applicable to External Method scripts)
  47.557 +
  47.558 +          'function' -- The function to load from the 'module' given
  47.559 +            (Only applicable to External Method scripts)
  47.560 +
  47.561 +          'filename' -- the name of the file to / from which the script
  47.562 +            is stored / loaded (Script (Python) only)
  47.563 +        """
  47.564 +        result = []
  47.565 +
  47.566 +        items = workflow.scripts.objectItems()
  47.567 +        items.sort()
  47.568 +
  47.569 +        for k, v in items:
  47.570 +
  47.571 +            filename = _getScriptFilename( workflow.getId(), k, v.meta_type )
  47.572 +            module = ''
  47.573 +            function = ''
  47.574 +
  47.575 +            if v.meta_type == 'External Method':
  47.576 +                module = v.module()
  47.577 +                function = v.function()
  47.578 +
  47.579 +            info = { 'id'                   : k
  47.580 +                   , 'meta_type'            : v.meta_type
  47.581 +                   , 'module'               : module
  47.582 +                   , 'function'             : function
  47.583 +                   , 'filename'             : filename
  47.584 +                   }
  47.585 +
  47.586 +            result.append( info )
  47.587 +
  47.588 +        return result
  47.589 +
  47.590 +InitializeClass( WorkflowDefinitionConfigurator )
  47.591 +
  47.592 +
  47.593 +def _getScriptFilename( workflow_id, script_id, meta_type ):
  47.594 +
  47.595 +    """ Return the name of the file which holds the script.
  47.596 +    """
  47.597 +    wf_dir = workflow_id.replace( ' ', '_' )
  47.598 +    suffix = _METATYPE_SUFFIXES.get(meta_type, None)
  47.599 +
  47.600 +    if suffix is None:
  47.601 +        return ''
  47.602 +
  47.603 +    return 'workflows/%s/scripts/%s.%s' % ( wf_dir, script_id, suffix )
  47.604 +
  47.605 +def _extractStateNodes( root, encoding=None ):
  47.606 +
  47.607 +    result = []
  47.608 +
  47.609 +    for s_node in root.getElementsByTagName( 'state' ):
  47.610 +
  47.611 +        info = { 'state_id' : _getNodeAttribute( s_node, 'state_id', encoding )
  47.612 +               , 'title' : _getNodeAttribute( s_node, 'title', encoding )
  47.613 +               , 'description' : _extractDescriptionNode( s_node, encoding )
  47.614 +               }
  47.615 +
  47.616 +        info[ 'transitions' ] = [ _getNodeAttribute( x, 'transition_id'
  47.617 +                                                   , encoding )
  47.618 +                                  for x in s_node.getElementsByTagName(
  47.619 +                                                        'exit-transition' ) ]
  47.620 +
  47.621 +        info[ 'permissions' ] = permission_map = {}
  47.622 +
  47.623 +        for p_map in s_node.getElementsByTagName( 'permission-map' ):
  47.624 +
  47.625 +            name = _getNodeAttribute( p_map, 'name', encoding )
  47.626 +            acquired = _getNodeAttributeBoolean( p_map, 'acquired' )
  47.627 +
  47.628 +            roles = [ _coalesceTextNodeChildren( x, encoding )
  47.629 +                        for x in p_map.getElementsByTagName(
  47.630 +                                            'permission-role' ) ]
  47.631 +
  47.632 +            if not acquired:
  47.633 +                roles = tuple( roles )
  47.634 +
  47.635 +            permission_map[ name ] = roles
  47.636 +
  47.637 +        info[ 'groups' ] = group_map = []
  47.638 +
  47.639 +        for g_map in s_node.getElementsByTagName( 'group-map' ):
  47.640 +
  47.641 +            name = _getNodeAttribute( g_map, 'name', encoding )
  47.642 +
  47.643 +            roles = [ _coalesceTextNodeChildren( x, encoding )
  47.644 +                        for x in g_map.getElementsByTagName(
  47.645 +                                            'group-role' ) ]
  47.646 +
  47.647 +            group_map.append( ( name, tuple( roles ) ) )
  47.648 +
  47.649 +        info[ 'variables' ] = var_map = {}
  47.650 +
  47.651 +        for assignment in s_node.getElementsByTagName( 'assignment' ):
  47.652 +
  47.653 +            name = _getNodeAttribute( assignment, 'name', encoding )
  47.654 +            type_id = _getNodeAttribute( assignment, 'type', encoding )
  47.655 +            value = _coalesceTextNodeChildren( assignment, encoding )
  47.656 +
  47.657 +            var_map[ name ] = { 'name'  : name
  47.658 +                              , 'type'  : type_id
  47.659 +                              , 'value' : value
  47.660 +                              }
  47.661 +
  47.662 +        result.append( info )
  47.663 +
  47.664 +    return result
  47.665 +
  47.666 +def _extractTransitionNodes( root, encoding=None ):
  47.667 +
  47.668 +    result = []
  47.669 +
  47.670 +    for t_node in root.getElementsByTagName( 'transition' ):
  47.671 +
  47.672 +        info = { 'transition_id' : _getNodeAttribute( t_node, 'transition_id'
  47.673 +                                                    , encoding )
  47.674 +               , 'title' : _getNodeAttribute( t_node, 'title', encoding )
  47.675 +               , 'description' : _extractDescriptionNode( t_node, encoding )
  47.676 +               , 'new_state' : _getNodeAttribute( t_node, 'new_state'
  47.677 +                                                , encoding )
  47.678 +               , 'trigger' : _getNodeAttribute( t_node, 'trigger', encoding )
  47.679 +               , 'before_script' : _getNodeAttribute( t_node, 'before_script'
  47.680 +                                                  , encoding )
  47.681 +               , 'after_script' : _getNodeAttribute( t_node, 'after_script'
  47.682 +                                                   , encoding )
  47.683 +               , 'action' : _extractActionNode( t_node, encoding )
  47.684 +               , 'guard' : _extractGuardNode( t_node, encoding )
  47.685 +               }
  47.686 +
  47.687 +        info[ 'variables' ] = var_map = {}
  47.688 +
  47.689 +        for assignment in t_node.getElementsByTagName( 'assignment' ):
  47.690 +
  47.691 +            name = _getNodeAttribute( assignment, 'name', encoding )
  47.692 +            expr = _coalesceTextNodeChildren( assignment, encoding )
  47.693 +            var_map[ name ] = expr
  47.694 +
  47.695 +        result.append( info )
  47.696 +
  47.697 +    return result
  47.698 +
  47.699 +def _extractVariableNodes( root, encoding=None ):
  47.700 +
  47.701 +    result = []
  47.702 +
  47.703 +    for v_node in root.getElementsByTagName( 'variable' ):
  47.704 +
  47.705 +        info = { 'variable_id' : _getNodeAttribute( v_node, 'variable_id'
  47.706 +                                                    , encoding )
  47.707 +               , 'description' : _extractDescriptionNode( v_node, encoding )
  47.708 +               , 'for_catalog' : _getNodeAttributeBoolean( v_node
  47.709 +                                                         , 'for_catalog'
  47.710 +                                                         )
  47.711 +               , 'for_status' : _getNodeAttributeBoolean( v_node
  47.712 +                                                        , 'for_status'
  47.713 +                                                        )
  47.714 +               , 'update_always' : _getNodeAttributeBoolean( v_node
  47.715 +                                                           , 'update_always'
  47.716 +                                                           )
  47.717 +               , 'default' : _extractDefaultNode( v_node, encoding )
  47.718 +               , 'guard' : _extractGuardNode( v_node, encoding )
  47.719 +               }
  47.720 +
  47.721 +        result.append( info )
  47.722 +
  47.723 +    return result
  47.724 +
  47.725 +def _extractWorklistNodes( root, encoding=None ):
  47.726 +
  47.727 +    result = []
  47.728 +
  47.729 +    for w_node in root.getElementsByTagName( 'worklist' ):
  47.730 +
  47.731 +        info = { 'worklist_id' : _getNodeAttribute( w_node, 'worklist_id'
  47.732 +                                                    , encoding )
  47.733 +               , 'title' : _getNodeAttribute( w_node, 'title' , encoding )
  47.734 +               , 'description' : _extractDescriptionNode( w_node, encoding )
  47.735 +               , 'match' : _extractMatchNode( w_node, encoding )
  47.736 +               , 'action' : _extractActionNode( w_node, encoding )
  47.737 +               , 'guard' : _extractGuardNode( w_node, encoding )
  47.738 +               }
  47.739 +
  47.740 +        result.append( info )
  47.741 +
  47.742 +    return result
  47.743 +
  47.744 +def _extractScriptNodes( root, encoding=None ):
  47.745 +
  47.746 +    result = []
  47.747 +
  47.748 +    for s_node in root.getElementsByTagName( 'script' ):
  47.749 +
  47.750 +        try:
  47.751 +            function = _getNodeAttribute( s_node, 'function' )
  47.752 +        except ValueError:
  47.753 +            function = ''
  47.754 +
  47.755 +        try:
  47.756 +            module = _getNodeAttribute( s_node, 'module' )
  47.757 +        except ValueError:
  47.758 +            module = ''
  47.759 +
  47.760 +        info = { 'script_id' : _getNodeAttribute( s_node, 'script_id' )
  47.761 +               , 'meta_type' : _getNodeAttribute( s_node, 'type' , encoding )
  47.762 +               , 'function'  : function
  47.763 +               , 'module'    : module
  47.764 +               }
  47.765 +
  47.766 +        filename = _queryNodeAttribute( s_node, 'filename' , None, encoding )
  47.767 +
  47.768 +        if filename is not None:
  47.769 +            info[ 'filename' ] = filename
  47.770 +
  47.771 +        result.append( info )
  47.772 +
  47.773 +    return result
  47.774 +
  47.775 +def _extractPermissionNodes( root, encoding=None ):
  47.776 +
  47.777 +    result = []
  47.778 +
  47.779 +    for p_node in root.getElementsByTagName( 'permission' ):
  47.780 +
  47.781 +        result.append( _coalesceTextNodeChildren( p_node, encoding ) )
  47.782 +
  47.783 +    return result
  47.784 +
  47.785 +def _extractActionNode( parent, encoding=None ):
  47.786 +
  47.787 +    nodes = parent.getElementsByTagName( 'action' )
  47.788 +    assert len( nodes ) <= 1, nodes
  47.789 +
  47.790 +    if len( nodes ) < 1:
  47.791 +        return { 'name' : '', 'url' : '', 'category' : '' }
  47.792 +
  47.793 +    node = nodes[ 0 ]
  47.794 +
  47.795 +    return { 'name' : _coalesceTextNodeChildren( node, encoding )
  47.796 +           , 'url' : _getNodeAttribute( node, 'url', encoding )
  47.797 +           , 'category' : _getNodeAttribute( node, 'category', encoding )
  47.798 +           }
  47.799 +
  47.800 +def _extractGuardNode( parent, encoding=None ):
  47.801 +
  47.802 +    nodes = parent.getElementsByTagName( 'guard' )
  47.803 +    assert len( nodes ) <= 1, nodes
  47.804 +
  47.805 +    if len( nodes ) < 1:
  47.806 +        return { 'permissions' : (), 'roles' : (), 'groups' : (), 'expr' : '' }
  47.807 +
  47.808 +    node = nodes[ 0 ]
  47.809 +
  47.810 +    expr_nodes = node.getElementsByTagName( 'guard-expression' )
  47.811 +    assert( len( expr_nodes ) <= 1 )
  47.812 +
  47.813 +    expr_text = expr_nodes and _coalesceTextNodeChildren( expr_nodes[ 0 ]
  47.814 +                                                        , encoding
  47.815 +                                                        ) or ''
  47.816 +
  47.817 +    return { 'permissions' : [ _coalesceTextNodeChildren( x, encoding )
  47.818 +                                for x in node.getElementsByTagName(
  47.819 +                                                    'guard-permission' ) ]
  47.820 +           , 'roles' : [ _coalesceTextNodeChildren( x, encoding )
  47.821 +                          for x in node.getElementsByTagName( 'guard-role' ) ]
  47.822 +           , 'groups' : [ _coalesceTextNodeChildren( x, encoding )
  47.823 +                          for x in node.getElementsByTagName( 'guard-group' ) ]
  47.824 +           , 'expression' : expr_text
  47.825 +           }
  47.826 +
  47.827 +def _extractDefaultNode( parent, encoding=None ):
  47.828 +
  47.829 +    nodes = parent.getElementsByTagName( 'default' )
  47.830 +    assert len( nodes ) <= 1, nodes
  47.831 +
  47.832 +    if len( nodes ) < 1:
  47.833 +        return { 'value' : '', 'expression' : '', 'type' : 'n/a' }
  47.834 +
  47.835 +    node = nodes[ 0 ]
  47.836 +
  47.837 +    value_nodes = node.getElementsByTagName( 'value' )
  47.838 +    assert( len( value_nodes ) <= 1 )
  47.839 +
  47.840 +    value_type = 'n/a'
  47.841 +    if value_nodes:
  47.842 +        value_type = value_nodes[ 0 ].getAttribute( 'type' ) or 'n/a'
  47.843 +
  47.844 +    value_text = value_nodes and _coalesceTextNodeChildren( value_nodes[ 0 ]
  47.845 +                                                          , encoding
  47.846 +                                                          ) or ''
  47.847 +
  47.848 +    expr_nodes = node.getElementsByTagName( 'expression' )
  47.849 +    assert( len( expr_nodes ) <= 1 )
  47.850 +
  47.851 +    expr_text = expr_nodes and _coalesceTextNodeChildren( expr_nodes[ 0 ]
  47.852 +                                                        , encoding
  47.853 +                                                        ) or ''
  47.854 +
  47.855 +    return { 'value' : value_text
  47.856 +           , 'type' : value_type
  47.857 +           , 'expression' : expr_text
  47.858 +           }
  47.859 +
  47.860 +_SEMICOLON_LIST_SPLITTER = re.compile( r';[ ]*' )
  47.861 +
  47.862 +def _extractMatchNode( parent, encoding=None ):
  47.863 +
  47.864 +    nodes = parent.getElementsByTagName( 'match' )
  47.865 +
  47.866 +    result = {}
  47.867 +
  47.868 +    for node in nodes:
  47.869 +
  47.870 +        name = _getNodeAttribute( node, 'name', encoding )
  47.871 +        values = _getNodeAttribute( node, 'values', encoding )
  47.872 +        result[ name ] = _SEMICOLON_LIST_SPLITTER.split( values )
  47.873 +
  47.874 +    return result
  47.875 +
  47.876 +def _guessVariableType( value ):
  47.877 +
  47.878 +    from DateTime.DateTime import DateTime
  47.879 +
  47.880 +    if value is None:
  47.881 +        return 'none'
  47.882 +
  47.883 +    if isinstance( value, DateTime ):
  47.884 +        return 'datetime'
  47.885 +
  47.886 +    if isinstance( value, bool ):
  47.887 +        return 'bool'
  47.888 +
  47.889 +    if isinstance( value, int ):
  47.890 +        return 'int'
  47.891 +
  47.892 +    if isinstance( value, float ):
  47.893 +        return 'float'
  47.894 +
  47.895 +    if isinstance( value, basestring ):
  47.896 +        return 'string'
  47.897 +
  47.898 +    return 'unknown'
  47.899 +
  47.900 +def _convertVariableValue( value, type_id ):
  47.901 +
  47.902 +    from DateTime.DateTime import DateTime
  47.903 +
  47.904 +    if type_id == 'none':
  47.905 +        return None
  47.906 +
  47.907 +    if type_id == 'datetime':
  47.908 +
  47.909 +        return DateTime( value )
  47.910 +
  47.911 +    if type_id == 'bool':
  47.912 +
  47.913 +        if isinstance( value, basestring ):
  47.914 +
  47.915 +            value = str( value ).lower()
  47.916 +
  47.917 +            return value in ( 'true', 'yes', '1' )
  47.918 +
  47.919 +        else:
  47.920 +            return bool( value )
  47.921 +
  47.922 +    if type_id == 'int':
  47.923 +        return int( value )
  47.924 +
  47.925 +    if type_id == 'float':
  47.926 +        return float( value )
  47.927 +
  47.928 +    return value
  47.929 +
  47.930 +from Products.PythonScripts.PythonScript import PythonScript
  47.931 +from Products.ExternalMethod.ExternalMethod import ExternalMethod
  47.932 +from OFS.DTMLMethod import DTMLMethod
  47.933 +
  47.934 +_METATYPE_SUFFIXES = \
  47.935 +{ PythonScript.meta_type : 'py'
  47.936 +, DTMLMethod.meta_type : 'dtml'
  47.937 +}
  47.938 +
  47.939 +def _initDCWorkflow( workflow
  47.940 +                   , title
  47.941 +                   , state_variable
  47.942 +                   , initial_state
  47.943 +                   , states
  47.944 +                   , transitions
  47.945 +                   , variables
  47.946 +                   , worklists
  47.947 +                   , permissions
  47.948 +                   , scripts
  47.949 +                   , context
  47.950 +                   ):
  47.951 +    """ Initialize a DC Workflow using values parsed from XML.
  47.952 +    """
  47.953 +    workflow.title = title
  47.954 +    workflow.state_var = state_variable
  47.955 +    workflow.initial_state = initial_state
  47.956 +
  47.957 +    permissions = permissions[:]
  47.958 +    permissions.sort()
  47.959 +    workflow.permissions = tuple(permissions)
  47.960 +
  47.961 +    _initDCWorkflowVariables( workflow, variables )
  47.962 +    _initDCWorkflowStates( workflow, states )
  47.963 +    _initDCWorkflowTransitions( workflow, transitions )
  47.964 +    _initDCWorkflowWorklists( workflow, worklists )
  47.965 +    _initDCWorkflowScripts( workflow, scripts, context )
  47.966 +
  47.967 +
  47.968 +def _initDCWorkflowVariables( workflow, variables ):
  47.969 +
  47.970 +    """ Initialize DCWorkflow variables
  47.971 +    """
  47.972 +    from Products.DCWorkflow.Variables import VariableDefinition
  47.973 +
  47.974 +    for v_info in variables:
  47.975 +
  47.976 +        id = str( v_info[ 'variable_id' ] ) # no unicode!
  47.977 +        v = VariableDefinition( id )
  47.978 +        workflow.variables._setObject( id, v )
  47.979 +        v = workflow.variables._getOb( id )
  47.980 +
  47.981 +        guard = v_info[ 'guard' ]
  47.982 +        props = { 'guard_roles' : ';'.join( guard[ 'roles' ] )
  47.983 +                , 'guard_permissions' : ';'.join( guard[ 'permissions' ] )
  47.984 +                , 'guard_groups' : ';'.join( guard[ 'groups' ] )
  47.985 +                , 'guard_expr' : guard[ 'expression' ]
  47.986 +                }
  47.987 +
  47.988 +        default = v_info[ 'default' ]
  47.989 +        default_value = _convertVariableValue( default[ 'value' ]
  47.990 +                                             , default[ 'type' ] )
  47.991 +
  47.992 +        v.setProperties( description = v_info[ 'description' ]
  47.993 +                       , default_value = default_value
  47.994 +                       , default_expr = default[ 'expression' ]
  47.995 +                       , for_catalog = v_info[ 'for_catalog' ]
  47.996 +                       , for_status = v_info[ 'for_status' ]
  47.997 +                       , update_always = v_info[ 'update_always' ]
  47.998 +                       , props = props
  47.999 +                       )
 47.1000 +
 47.1001 +
 47.1002 +def _initDCWorkflowStates( workflow, states ):
 47.1003 +
 47.1004 +    """ Initialize DCWorkflow states
 47.1005 +    """
 47.1006 +    from Globals import PersistentMapping
 47.1007 +    from Products.DCWorkflow.States import StateDefinition
 47.1008 +
 47.1009 +    for s_info in states:
 47.1010 +
 47.1011 +        id = str( s_info[ 'state_id' ] ) # no unicode!
 47.1012 +        s = StateDefinition( id )
 47.1013 +        workflow.states._setObject( id, s )
 47.1014 +        s = workflow.states._getOb( id )
 47.1015 +
 47.1016 +        s.setProperties( title = s_info[ 'title' ]
 47.1017 +                       , description = s_info[ 'description' ]
 47.1018 +                       , transitions = s_info[ 'transitions' ]
 47.1019 +                       )
 47.1020 +
 47.1021 +        for k, v in s_info[ 'permissions' ].items():
 47.1022 +            s.setPermission( k, isinstance(v, list), v )
 47.1023 +
 47.1024 +        gmap = s.group_roles = PersistentMapping()
 47.1025 +
 47.1026 +        for group_id, roles in s_info[ 'groups' ]:
 47.1027 +            gmap[ group_id ] = roles
 47.1028 +
 47.1029 +        vmap = s.var_values = PersistentMapping()
 47.1030 +
 47.1031 +        for name, v_info in s_info[ 'variables' ].items():
 47.1032 +
 47.1033 +            value = _convertVariableValue( v_info[ 'value' ]
 47.1034 +                                         , v_info[ 'type' ] )
 47.1035 +
 47.1036 +            vmap[ name ] = value
 47.1037 +
 47.1038 +
 47.1039 +def _initDCWorkflowTransitions( workflow, transitions ):
 47.1040 +
 47.1041 +    """ Initialize DCWorkflow transitions
 47.1042 +    """
 47.1043 +    from Globals import PersistentMapping
 47.1044 +    from Products.DCWorkflow.Transitions import TransitionDefinition
 47.1045 +
 47.1046 +    for t_info in transitions:
 47.1047 +
 47.1048 +        id = str( t_info[ 'transition_id' ] ) # no unicode!
 47.1049 +        t = TransitionDefinition( id )
 47.1050 +        workflow.transitions._setObject( id, t )
 47.1051 +        t = workflow.transitions._getOb( id )
 47.1052 +
 47.1053 +        trigger_type = list( TRIGGER_TYPES ).index( t_info[ 'trigger' ] )
 47.1054 +
 47.1055 +        action = t_info[ 'action' ]
 47.1056 +
 47.1057 +        guard = t_info[ 'guard' ]
 47.1058 +        props = { 'guard_roles' : ';'.join( guard[ 'roles' ] )
 47.1059 +                , 'guard_permissions' : ';'.join( guard[ 'permissions' ] )
 47.1060 +                , 'guard_groups' : ';'.join( guard[ 'groups' ] )
 47.1061 +                , 'guard_expr' : guard[ 'expression' ]
 47.1062 +                }
 47.1063 +
 47.1064 +        t.setProperties( title = t_info[ 'title' ]
 47.1065 +                       , description = t_info[ 'description' ]
 47.1066 +                       , new_state_id = t_info[ 'new_state' ]
 47.1067 +                       , trigger_type = trigger_type
 47.1068 +                       , script_name = t_info[ 'before_script' ]
 47.1069 +                       , after_script_name = t_info[ 'after_script' ]
 47.1070 +                       , actbox_name = action[ 'name' ]
 47.1071 +                       , actbox_url = action[ 'url' ]
 47.1072 +                       , actbox_category = action[ 'category' ]
 47.1073 +                       , props = props
 47.1074 +                       )
 47.1075 +
 47.1076 +        t.var_exprs = PersistentMapping( t_info[ 'variables' ].items() )
 47.1077 +
 47.1078 +def _initDCWorkflowWorklists( workflow, worklists ):
 47.1079 +
 47.1080 +    """ Initialize DCWorkflow worklists
 47.1081 +    """
 47.1082 +    from Globals import PersistentMapping
 47.1083 +    from Products.DCWorkflow.Worklists import WorklistDefinition
 47.1084 +
 47.1085 +    for w_info in worklists:
 47.1086 +
 47.1087 +        id = str( w_info[ 'worklist_id' ] ) # no unicode!
 47.1088 +        w = WorklistDefinition( id )
 47.1089 +        workflow.worklists._setObject( id, w )
 47.1090 +
 47.1091 +        w = workflow.worklists._getOb( id )
 47.1092 +
 47.1093 +        action = w_info[ 'action' ]
 47.1094 +
 47.1095 +        guard = w_info[ 'guard' ]
 47.1096 +        props = { 'guard_roles' : ';'.join( guard[ 'roles' ] )
 47.1097 +                , 'guard_permissions' : ';'.join( guard[ 'permissions' ] )
 47.1098 +                , 'guard_groups' : ';'.join( guard[ 'groups' ] )
 47.1099 +                , 'guard_expr' : guard[ 'expression' ]
 47.1100 +                }
 47.1101 +
 47.1102 +        w.setProperties( description = w_info[ 'description' ]
 47.1103 +                       , actbox_name = action[ 'name' ]
 47.1104 +                       , actbox_url = action[ 'url' ]
 47.1105 +                       , actbox_category = action[ 'category' ]
 47.1106 +                       , props = props
 47.1107 +                       )
 47.1108 +
 47.1109 +        w.var_matches = PersistentMapping()
 47.1110 +        for k, v in w_info[ 'match' ].items():
 47.1111 +            w.var_matches[ str( k ) ] = tuple( [ str(x) for x in v ] )
 47.1112 +
 47.1113 +def _initDCWorkflowScripts( workflow, scripts, context ):
 47.1114 +
 47.1115 +    """ Initialize DCWorkflow scripts
 47.1116 +    """
 47.1117 +    for s_info in scripts:
 47.1118 +
 47.1119 +        id = str( s_info[ 'script_id' ] ) # no unicode!
 47.1120 +        meta_type = s_info[ 'meta_type' ]
 47.1121 +        filename = s_info[ 'filename' ]
 47.1122 +        file = ''
 47.1123 +
 47.1124 +        if filename:
 47.1125 +            file = context.readDataFile( filename )
 47.1126 +
 47.1127 +        if meta_type == PythonScript.meta_type:
 47.1128 +            script = PythonScript( id )
 47.1129 +            script.write( file )
 47.1130 +
 47.1131 +        elif meta_type == ExternalMethod.meta_type:
 47.1132 +            script = ExternalMethod( id
 47.1133 +                                   , ''
 47.1134 +                                   , s_info['module']
 47.1135 +                                   , s_info['function']
 47.1136 +                                   )
 47.1137 +
 47.1138 +        elif meta_type == DTMLMethod.meta_type:
 47.1139 +            script = DTMLMethod( file, __name__=id )
 47.1140 +
 47.1141 +        workflow.scripts._setObject( id, script )
 47.1142 +
 47.1143 +#
 47.1144 +#   deprecated DOM parsing utilities
 47.1145 +#
 47.1146 +_marker = object()
 47.1147 +
 47.1148 +def _queryNodeAttribute( node, attr_name, default, encoding=None ):
 47.1149 +
 47.1150 +    """ Extract a string-valued attribute from node.
 47.1151 +
 47.1152 +    o Return 'default' if the attribute is not present.
 47.1153 +    """
 47.1154 +    attr_node = node.attributes.get( attr_name, _marker )
 47.1155 +
 47.1156 +    if attr_node is _marker:
 47.1157 +        return default
 47.1158 +
 47.1159 +    value = attr_node.nodeValue
 47.1160 +
 47.1161 +    if encoding is not None:
 47.1162 +        value = value.encode( encoding )
 47.1163 +
 47.1164 +    return value
 47.1165 +
 47.1166 +def _getNodeAttribute( node, attr_name, encoding=None ):
 47.1167 +
 47.1168 +    """ Extract a string-valued attribute from node.
 47.1169 +    """
 47.1170 +    value = _queryNodeAttribute( node, attr_name, _marker, encoding )
 47.1171 +
 47.1172 +    if value is _marker:
 47.1173 +        raise ValueError, 'Invalid attribute: %s' % attr_name
 47.1174 +
 47.1175 +    return value
 47.1176 +
 47.1177 +def _queryNodeAttributeBoolean( node, attr_name, default ):
 47.1178 +
 47.1179 +    """ Extract a string-valued attribute from node.
 47.1180 +
 47.1181 +    o Return 'default' if the attribute is not present.
 47.1182 +    """
 47.1183 +    attr_node = node.attributes.get( attr_name, _marker )
 47.1184 +
 47.1185 +    if attr_node is _marker:
 47.1186 +        return default
 47.1187 +
 47.1188 +    value = node.attributes[ attr_name ].nodeValue.lower()
 47.1189 +
 47.1190 +    return value in ( 'true', 'yes', '1' )
 47.1191 +
 47.1192 +def _getNodeAttributeBoolean( node, attr_name ):
 47.1193 +
 47.1194 +    """ Extract a string-valued attribute from node.
 47.1195 +    """
 47.1196 +    value = node.attributes[ attr_name ].nodeValue.lower()
 47.1197 +
 47.1198 +    return value in ( 'true', 'yes', '1' )
 47.1199 +
 47.1200 +def _coalesceTextNodeChildren( node, encoding=None ):
 47.1201 +
 47.1202 +    """ Concatenate all childe text nodes into a single string.
 47.1203 +    """
 47.1204 +    from xml.dom import Node
 47.1205 +    fragments = []
 47.1206 +    node.normalize()
 47.1207 +    child = node.firstChild
 47.1208 +
 47.1209 +    while child is not None:
 47.1210 +
 47.1211 +        if child.nodeType == Node.TEXT_NODE:
 47.1212 +            fragments.append( child.nodeValue )
 47.1213 +
 47.1214 +        child = child.nextSibling
 47.1215 +
 47.1216 +    joined = ''.join( fragments )
 47.1217 +
 47.1218 +    if encoding is not None:
 47.1219 +        joined = joined.encode( encoding )
 47.1220 +
 47.1221 +    return ''.join( [ line.lstrip() for line in joined.splitlines(True) ] )
 47.1222 +
 47.1223 +def _extractDescriptionNode(parent, encoding=None):
 47.1224 +
 47.1225 +    d_nodes = parent.getElementsByTagName('description')
 47.1226 +    if d_nodes:
 47.1227 +        return _coalesceTextNodeChildren(d_nodes[0], encoding)
 47.1228 +    else:
 47.1229 +        return ''
    48.1 new file mode 100644
    48.2 --- /dev/null
    48.3 +++ b/help/001-overview.stx
    48.4 @@ -0,0 +1,82 @@
    48.5 +Overview
    48.6 +
    48.7 + DCWorkflows provide workflow objects that are fully customizable via
    48.8 + the Zope Management Interface. You can specify the states, and the
    48.9 + permissions they set on content that is in that state, the transitions 
   48.10 + between those states, and other things like variables for things that
   48.11 + aren't well represented by states, work lists for reviewers, and
   48.12 + scripts to embody complex guards and to extend pre and post transition
   48.13 + behaviour.
   48.14 +
   48.15 + The process for creating a workflow runs something 
   48.16 + like this:
   48.17 +  
   48.18 +  - Draw a state diagram with the nodes (bubbles) as states and the
   48.19 +  arcs (arrows) as transitions. Remember to consider all the states
   48.20 +  your content can be in, and for each state, consider who should
   48.21 +  have permission to access and change the content. Consider what
   48.22 +  actions the users will perform to make the transitions between states,
   48.23 +  and not only who will be allowed to perform them, but who will be
   48.24 +  *required* to perform them.
   48.25 +
   48.26 +    It's often a good idea to start on paper, then transfer
   48.27 +    the diagram to a digital copy using a diagram/flowchart tool, such as
   48.28 +    'dia', so that you have an image to go with later documentation. This
   48.29 +    process tends to make it easier to spot corner cases before you
   48.30 +    actually create the workflow object.
   48.31 +
   48.32 +  - Start by creating an example DCworkflow, rather than a new one, as it's 
   48.33 +  faster to delete all the states and transitions than it is to create all the 
   48.34 +  standard variables that tend to be used by the CMF. [**Note:**
   48.35 +  Perhaps we should have a bare dcworkflow, a workflow with standard
   48.36 +  variables, and the couple of standard examples.]
   48.37 +
   48.38 +  - In the permissions tab, select all the permissions that you want the 
   48.39 +  workflow to govern. These will be dependent on the types of content
   48.40 +  you'll be using with the workflow; 'Access contents information',
   48.41 +  'Modify portal content', and 'View' are the standard permissions for 
   48.42 +  the default portal content types.
   48.43 +
   48.44 +  - Define any extra variables that you need for information that isn't
   48.45 +  well represented by states. [**Note:** generic examples? I can think of
   48.46 +  a few that could appear in some use cases, but they're all
   48.47 +  idiosyncratic of particular publishing needs]
   48.48 +
   48.49 +  - Set up the states for your workflow, one for each node in your state 
   48.50 +  diagram. Try to stick to the standard names for a publication workflow, as 
   48.51 +  some badly behaved products have states like 'published' hardcoded into
   48.52 +  their searches (ie CalendarTool, last I looked) [**Note**: Maybe I
   48.53 +  should just file some bug reports rather than casting aspersions :-)].
   48.54 +  Set up the permissions on the states now, as well, though see the 
   48.55 +  "State Tab" section for advice.
   48.56 +
   48.57 +  - Set up any scripts that you will be using in your transitions, such
   48.58 +  as pre and post transition scripts and to handle complex guard
   48.59 +  conditions. Just set up skeletons for now, if you haven't though
   48.60 +  through all the details.
   48.61 +
   48.62 +  - Create your transitions from all the arcs on your state diagram. You
   48.63 +  should be able to pick the right destination state as all your states
   48.64 +  are already defined, and set up the right scripts to run, as you've
   48.65 +  defined those as well. It's worth noting that the guard behaviour is
   48.66 +  such that if any guard matches, the transition can occur. You can
   48.67 +  specify more than one permission, role or expression by separating
   48.68 +  them with a semicolon.
   48.69 +
   48.70 +  - Go back to the states tab and, for each state, set the possible
   48.71 +  transitions from the list of all available transitions in each state.
   48.72 +
   48.73 +  - Finally, in the work lists tab, create any work lists for any states
   48.74 +  that need them. Work lists are actions that indicate how many objects
   48.75 +  of a given state are present, and usually link to some search page
   48.76 +  that lists the actual object instances. You typically use them to list
   48.77 +  all the pending content waiting for review. Work lists have several
   48.78 +  unusual behaviours, however, so check the specific notes in the
   48.79 +  "Worklists" section.
   48.80 +
   48.81 + By working in this order, you will tend to step through the 
   48.82 + creation process one tab at a time, rather than switching back and
   48.83 + forth between them, which tends to be slower and somewhat confusing.
   48.84 +
   48.85 +
   48.86 +
    49.1 new file mode 100644
    49.2 --- /dev/null
    49.3 +++ b/help/002-expressions.stx
    49.4 @@ -0,0 +1,64 @@
    49.5 +Expressions
    49.6 +
    49.7 + Expressions in DCWorkflow are TALES expressions. See 
    49.8 + "TALES Overview":/Control_Panel/Products/PageTemplates/Help/tales.stx
    49.9 + for general TALES information. They are used as access guards and for
   49.10 + the setting variable values. 
   49.11 + 
   49.12 + [**Note:** I haven't figured out what all these contexts actually are
   49.13 + and what you can use them for. Explanations are is welcome!]
   49.14 +
   49.15 + Some of the contexts have slightly different meanings from what is provided
   49.16 + for expressions in page templates:
   49.17 +
   49.18 +  'here' -- The content object (rather than the workflow object)
   49.19 +  'container' -- The content object's container
   49.20 +
   49.21 + Several other contexts are also 
   49.22 + provided:
   49.23 +
   49.24 +  'state_change' -- A special object containing information about the
   49.25 +  state change (see below)
   49.26 +  'transition' -- The transition object being executed
   49.27 +  'status' -- The former status
   49.28 +  'workflow' -- The workflow definition object
   49.29 +  'scripts' -- The scripts in the workflow definition object
   49.30 +
   49.31 + 'state_change' objects provide the following attributes, some of which
   49.32 + are duplicates of the above information:
   49.33 +
   49.34 +   - 'status' is a mapping containing the workflow status. This
   49.35 +   includes all the variables defined in the variable tab with "store
   49.36 +   in state" checked.
   49.37 +
   49.38 +   - 'object' is the object being modified by workflow.
   49.39 +     (Same as the 'here' variable above.)
   49.40 +
   49.41 +   - 'workflow' is the workflow definition object.  (Same as the
   49.42 +   'workflow' variable above.)
   49.43 +  
   49.44 +   - 'transition' is the transition object being executed.  (Same
   49.45 +   as the 'transition' variable above.)
   49.46 +
   49.47 +   - 'old_state' is the former state object.  The name of the former state,
   49.48 +   for example "published", is available as 'old_state.getId()'.  (Note
   49.49 +   that DCWorkflow defines 'state' and 'status' as different entities;
   49.50 +   the name of the current 'state' is stored in the 'status'.  The word
   49.51 +   clash is unfortunate; patches welcome.)
   49.52 +
   49.53 +   - 'new_state' is the destination state object.  Use 'new_state.getId()'
   49.54 +   to access the new state name.
   49.55 +
   49.56 +   - 'kwargs' is the keyword arguments passed to the doActionFor() method.
   49.57 +
   49.58 +   - 'getHistory()', a method that returns a copy of the object's workflow
   49.59 +   history.
   49.60 +
   49.61 +   - 'getPortal()', which returns the root of the portal.
   49.62 +
   49.63 +   - 'ObjectDeleted' and 'ObjectMoved', exceptions that can be raised by
   49.64 +   scripts to indicate to the workflow that an object has been moved or
   49.65 +   deleted.
   49.66 +
   49.67 +   - 'getDateTime' is a method that returns the DateTime of the transition.
   49.68 +
    50.1 new file mode 100644
    50.2 --- /dev/null
    50.3 +++ b/help/003-guards.stx
    50.4 @@ -0,0 +1,26 @@
    50.5 +Guards
    50.6 +
    50.7 +  Guard settings control access to the transitions, variables or work
    50.8 +  lists. If a user possesses any of the permissions or roles
    50.9 +  specified, or if one of the expressions evaluates as true, then
   50.10 +  whatever is being guarded is accessible. If no permissions, roles
   50.11 +  or expressions are specified, access is automatically granted.
   50.12 + 
   50.13 +  You can supply several options in each field by separating them with
   50.14 +  a semicolon.
   50.15 +
   50.16 +  The context in which the guards evaluate permissions and roles is
   50.17 +  obviously important. In the case of transitions and work lists, it 
   50.18 +  depends on the category. If it's 'worklist', then the context is 
   50.19 +  that of the content object, and local roles will behave just as
   50.20 +  you'd expect. If the category is 'global', then the context will be
   50.21 +  the site root, so the local roles between the site root and the
   50.22 +  content won't be considered.
   50.23 +
   50.24 +  [**Note:** What about variables?]
   50.25 +
   50.26 +  
   50.27 +
   50.28 +
   50.29 +
   50.30 +
    51.1 new file mode 100644
    51.2 --- /dev/null
    51.3 +++ b/help/004-actionbox.stx
    51.4 @@ -0,0 +1,39 @@
    51.5 +Action Boxes
    51.6 +
    51.7 + Action box settings are required for work lists and any transition that
    51.8 + is intended to be a user initiated action. They define how the action 
    51.9 + will appear in the action box, what section it will appear in and
   51.10 + what it will link to.
   51.11 +  
   51.12 + Names and URLs for the actions box can be formatted using standard Python
   51.13 + string formatting.  An example::
   51.14 +
   51.15 +   %(content_url)s/content_submit_form
   51.16 +
   51.17 + The string '%(content_url)s' will be replaced by the value of content_url.
   51.18 + The following names are available:
   51.19 +
   51.20 +   - portal_url
   51.21 +
   51.22 +   - folder_url
   51.23 +
   51.24 +   - content_url
   51.25 +
   51.26 +   - count (Available in work lists only. Represents the number of items in
   51.27 +   the work list.)
   51.28 +
   51.29 + The following names are also available, in case there is any use for them.
   51.30 + They are not strings.
   51.31 +
   51.32 +  - portal
   51.33 +
   51.34 +  - folder
   51.35 +
   51.36 +  - content
   51.37 +
   51.38 +  - isAnonymous
   51.39 +
   51.40 +  Note that this formatting is done using standard Python string formatting
   51.41 +  rather than TALES.  It might be more appropriate to use TALES instead.
   51.42 +  As always, patches welcome.
   51.43 +
    52.1 new file mode 100644
    52.2 --- /dev/null
    52.3 +++ b/help/011-states.stx
    52.4 @@ -0,0 +1,45 @@
    52.5 +States Tab
    52.6 +
    52.7 + From the states tab it's possible to add new states, and rename and
    52.8 + delete existing states. It is also possible to set a particular state
    52.9 + to be the initial state that new content is set to when created.
   52.10 +
   52.11 + The list of existing states also displays each state's title and all
   52.12 + the possible transitions from that state (and their titles). You can go
   52.13 + straight to the details of each state and transition from here.
   52.14 +
   52.15 + Within a state's properties tab you can set the title, description, 
   52.16 + and the transitions that are possible from this state from a list of
   52.17 + all the available transitions created in the workflow's 
   52.18 + transitions tab.
   52.19 +
   52.20 + In the state's permissions tab, you can set up the roles to
   52.21 + permissions mappings that will apply to roles when content 
   52.22 + managed by this workflow is in this state. It uses the usual cookie
   52.23 + cutter approach as do all other permissions tabs, except that the
   52.24 + only permissions listed are those that have been selected to be
   52.25 + managed by the workflow from the workflow's permissions tab.
   52.26 + 
   52.27 + A good strategy for managing permissions on each state is to rely on
   52.28 + acquisition for the "published" states, and to drop acquisition and
   52.29 + use explicit permissions on states that are private or interim
   52.30 + publishing states. This way, you can modify the access policy to 
   52.31 + "published" content at the site root or for specific folders without
   52.32 + having to modify each workflow's set of "published" states.
   52.33 + 
   52.34 + [**Note**: The available roles in the permissions tab will be
   52.35 + whatever is acquired from the site root, so I guess creating 
   52.36 + roles under sub-folders ought to be discouraged if people want
   52.37 + to use them in workflows]
   52.38 +
   52.39 + Reviewer roles should either have view permissions on every
   52.40 + state or you should change the appropriate skins to take them
   52.41 + somewhere sensible after a transition or they'll end up with an ugly
   52.42 + access denied page after sending content back to private state.
   52.43 +
   52.44 + In the state's variables tab, you can add, change and delete variables
   52.45 + that you want to assign a value to when objects move into 
   52.46 + this state. The available variables are set in the workflow's
   52.47 + variables tab, and the value is a TALES expression (see Expressions
   52.48 + for more details).
   52.49 +
    53.1 new file mode 100644
    53.2 --- /dev/null
    53.3 +++ b/help/021-transition.stx
    53.4 @@ -0,0 +1,57 @@
    53.5 +Transitions Tab
    53.6 +
    53.7 + Transitions are the actions that move content from one state in the
    53.8 + workflow to another. From the transitions tab it's possible to add new
    53.9 + transitions, and rename and delete existing transitions.
   53.10 +
   53.11 + The list of existing transitions also displays a summary of each
   53.12 + transition's title, description, destination state, trigger, guards, 
   53.13 + and action box entry. You can click through each transition to access 
   53.14 + their details.
   53.15 +
   53.16 + Within a transition's properties tab you can set the title, and 
   53.17 + a collection of properties the define the transtion's behaviour, as
   53.18 + follows:
   53.19 +
   53.20 +  Destination state -- selected from all the states defined in the 
   53.21 +  states tab. A transition can remain in state, which is useful for a
   53.22 +  reviewer adding comments to the review history, but not taking any
   53.23 +  action, updating some variable, or invoking scripts.
   53.24 +
   53.25 +  Trigger type - There are three types:
   53.26 +
   53.27 +    - User actions are the familiar user initiated transitions activated
   53.28 +    by actions in the action box.
   53.29 + 
   53.30 +    - Automatic transitions are executed any time other workflow
   53.31 +    events occur; so if a user action results in the content moving
   53.32 +    to a state that has automatic transitions, they will be executed.
   53.33 +    (You should use mutually exclusive guards to prevent indeterminate
   53.34 +    behavior.)
   53.35 +
   53.36 +    - WorkflowMethod initiate actions occur as a side effect when
   53.37 +    a method of the content object with the same name as the
   53.38 +    transtion, and that has been wrapped in the WorkflowMethod class
   53.39 +    is called.  DCWorkflow executes the body of the method just before
   53.40 +    the workflow transition.
   53.41 +
   53.42 +
   53.43 +  Scripts - Perform complicated behaviours either before or after the
   53.44 +  transition takes place. Scripts of all kinds are defined in the
   53.45 +  workflow's scripts tab. Scripts called from here must accept only
   53.46 +  one argument; a 'status_change' object. See Expressions for more
   53.47 +  details.
   53.48 +  
   53.49 +  Guards and Action boxes -- See the "Guards" and "Action Boxes"
   53.50 +  sections for specific details about those fields. Note that
   53.51 +  automatic and WorkflowMethod transitions don't need the action box
   53.52 +  fields to be filled out.
   53.53 +
   53.54 +  What the action should link to.
   53.55 +
   53.56 +
   53.57 + In the transition's variables tab, you can add, change and delete
   53.58 + variables that you want to assign a value to, when the transition is
   53.59 + executed. The available variables are set in the workflow's variables
   53.60 + tab, and the value is a TALES expression (see Expressions for more
   53.61 + details).
    54.1 new file mode 100644
    54.2 --- /dev/null
    54.3 +++ b/help/031-variables.stx
    54.4 @@ -0,0 +1,48 @@
    54.5 +Variables Tab
    54.6 +
    54.7 + Variables are used to handle the state of various workflow related
    54.8 + information that doesn't justify a state of it's own. The default 
    54.9 + CMF workflows use variables to track status history comments, and 
   54.10 + store the the last transition, who initiated it and when, for example.
   54.11 + From the variables tab it's possible to add new variables, and rename
   54.12 + and delete existing variables.
   54.13 +
   54.14 + The list of existing variables also displays a summary of each
   54.15 + variable's description, catalog availability, workflow status, default
   54.16 + value or expression and any access guards. You can click through to
   54.17 + each variable to configure it's details.
   54.18 +
   54.19 + In each variable's property tab you can set the variable's
   54.20 + description and a collection of properties the define the variable's
   54.21 + behaviour, as follows:
   54.22 + 
   54.23 +  Make available to catalog -- Just as it says, it makes this variable
   54.24 +  available to the catalog for indexing, however it doesn't
   54.25 +  automatically create an index for it - you have to create one by
   54.26 +  hand that reflects the content of the variable. Once indexed, you can
   54.27 +  query the catalog for content that has a particular value in is
   54.28 +  variable, and update the variable by workflow actions.
   54.29 + 
   54.30 +  Store in workflow status -- The workflow status is a mapping that
   54.31 +  exists in the state_change object that is passed to scripts and
   54.32 +  available to expressions. 
   54.33 + 
   54.34 +  Variable update mode -- Select whether the variable is updated on
   54.35 +  every transition (in which case, you should set a default
   54.36 +  expression), or whether it should only update if a transition or
   54.37 +  state sets a value.
   54.38 +
   54.39 +  Default value -- Set the default value to some string.
   54.40 +
   54.41 +  Default expression -- This is a TALES expression (as described in
   54.42 +  Expressions) and overrides the default value.
   54.43 +
   54.44 +  Guards -- See the "Guards" section. 
   54.45 +
   54.46 +
   54.47 + State variable - stores the name of the variable the current state of
   54.48 +the content is stored in. CMF uses 'review_state' by default, and will
   54.49 +have already created a FieldIndex for it. The state variable is
   54.50 +effectively a variable with "Make available to catalog" set, a default
   54.51 +value of whatever the initial state is and a default expression that
   54.52 +sets to the new state on every transition.
    55.1 new file mode 100644
    55.2 --- /dev/null
    55.3 +++ b/help/041-worklists.stx
    55.4 @@ -0,0 +1,128 @@
    55.5 +Worklists Tab
    55.6 +
    55.7 + Work lists are a way to make people aware of tasks they are required
    55.8 + to perform, usually reviewing content in the publishing context.
    55.9 + Work lists are implemented as a catalog query that puts an action in
   55.10 + the actions box when there are some tasks the member needs to perform.  
   55.11 +
   55.12 + From the work lists tab it's possible to add new work lists, and rename and
   55.13 + delete existing work lists. The list of existing work lists also
   55.14 + displays a short summary of each work list's description, the catalog query
   55.15 + it uses, and any guard conditions. You can access the details of each
   55.16 + work list by clicking on them.
   55.17 +
   55.18 + In each work list's properties tab, you can set the description of 
   55.19 + the work list, and it's various behaviour defining properties. The
   55.20 + "Catalog variable matches" field sets the state that is work list
   55.21 + matches. The "variable_name = " text to the left of the text box is
   55.22 + the name of the state variable defined at the bottom of the variables
   55.23 + tab. The values can be set to a number of possible matches separated 
   55.24 + by semicolons. [**Note:** CVS feature. There's more in
   55.25 + doc/worklists.stx, but I'm not sure I understand the implications]
   55.26 +
   55.27 + The action box fields are discussed in more detail in the Action Box
   55.28 + section. In this case, the url that the work list links to should
   55.29 + probably implement a search page with a catalog query similar to the
   55.30 + "Catalog variable matches", otherwise the difference between the
   55.31 + number of items waiting and the items reported in the search will be
   55.32 + confusing.
   55.33 +
   55.34 + [**Note:** What we *really* need from the work list is a way to define
   55.35 +  full catalog queries for the action, and a new action box variable
   55.36 +  that urlquotes that query so it can be passed straight to the search
   55.37 +  page. This way, the work list count and the number of items on the 
   55.38 +  search page will be the same as they are derived from the same
   55.39 +  query string, defined in one place.
   55.40 +
   55.41 +  Reply from Shane: work lists already exercise the catalog too heavily,
   55.42 +  and most people don't understand their purpose.  Expanding their
   55.43 +  capabilities further could impact performance.  I think perhaps
   55.44 +  the UI should instead display work lists on a user's home page rather
   55.45 +  than the actions box, which would open up new possibilities.]
   55.46 + 
   55.47 + The guard fields are described in detail in the "Guards"
   55.48 + section. It's probably better to avoid using permission and role
   55.49 + guards, as they're not really necessary - a user will see a
   55.50 + work list action only if they can see content in that particular
   55.51 + state, so the state guards are usually sufficient. In addition, as
   55.52 + the work lists are in the 'global' actions category by default, and
   55.53 + global actions are evaluated in the context of the site root, local
   55.54 + roles like Owner or locally set Reviewer roles, and the permissions
   55.55 + they grant, will not apply. [*Note:* Does anyone know a good reason
   55.56 + why work lists appear in the global box rather than in the workflow
   55.57 + box? This particular problem should vanish if they are moved there.]
   55.58 +
   55.59 + Whether a work list action appears in the action box, and the 
   55.60 + number of items in the work list depends on several factors:
   55.61 +
   55.62 +  - The state that the work list is generated for
   55.63 +
   55.64 +  - The name of the state variable used to indicate the 
   55.65 +  current state of an object
   55.66 + 
   55.67 +  - Whether the user can view content which is in that
   55.68 +  state
   55.69 +
   55.70 + This has some unexpected consequences:
   55.71 +
   55.72 +  - If you have several workflow that use the same state variable,
   55.73 +  and similar state names, and each has a work list on, say, the 
   55.74 +  'pending' state, then both work lists will appear in the action box,
   55.75 +  and the number of items in each will be the total of all the content
   55.76 +  in a 'pending' state, regardless of which workflow manages that
   55.77 +  content (except that if the work list action entries are exactly the 
   55.78 +  same text, the action tool will filter out the duplicate).
   55.79 +
   55.80 +  - If each workflow manages the permissions on content in the
   55.81 +  'pending' state differently, by, say, using two different reviewer
   55.82 +  roles, then users who have one role and not the other will
   55.83 +  see a single work list entry with the right number of items, but 
   55.84 +  users with both roles will see the same as above.
   55.85 +
   55.86 + So there are a few tricks to getting the work lists to do the kinds of
   55.87 + things we want. 
   55.88 +
   55.89 + If you have several similar workflows, such as a standard one, and a 
   55.90 + couple of specialized ones for particular content, and you want to
   55.91 + have one reviewer role for the lot, then you should set up just one
   55.92 + work list in the standard workflow for the states that need them, and 
   55.93 + leave the other workflow to rely on that work list. 
   55.94 +
   55.95 + If you have a workflow that uses a different reviewer role than
   55.96 + other workflows, and consequently, you want it to have it's own
   55.97 + separate work lists, you have two choices. One is to use state names
   55.98 + that are unique to each workflow, while the second is 
   55.99 + to use state variable name that is unique each workflow. The
  55.100 + second option is obviously a lot easier, however, if you change the
  55.101 + name of the state variable when there exists content that is using 
  55.102 + this workflow, they will immediately loose there workflow state
  55.103 + and default to the initial state. In addition, you'll need to add
  55.104 + a field index for the new state variable name in the portal_catalog
  55.105 + tool, by hand.
  55.106 +
  55.107 + [Note: In the first instance, we could add an action box name field
  55.108 +  to each state so that nicely formated names appear in the action
  55.109 +  box for things like "Published (yet to be effective)" rather than
  55.110 +  "published_not_yet_effective", and so we can lie about the names
  55.111 +  to make them unique, so that "foo_workflow_pending" looks like
  55.112 +  "Pending". In the second instance, I see no reason why the state
  55.113 +  variable name change action shouldn't migrate the value of the old
  55.114 +  state variable to the new for all the content managed by this
  55.115 +  workflow, and it could probably automatically add indexes for 
  55.116 +  new state variable names if they don't already exists (and 
  55.117 +  perhaps remove indexes for state variable names not used elsewhere).
  55.118 +
  55.119 +  While we're thinking about ways to make sweeping workflow changes 
  55.120 +  less painful, there are a couple of changes that could be made 
  55.121 +  to the code that changes content type to workflow mappings: if a
  55.122 +  content to workflow mapping has changed, then, for each instance of
  55.123 +  that content type, attempt to keep the state variable the same
  55.124 +  unless that state doesn't exist in the new workflow, then evaluate
  55.125 +  any automatic transitions on that state. This way it's possible 
  55.126 +  to migrate between workflows by ensuring that states with the same
  55.127 +  name have the same semantics, or if they don't exists in the new 
  55.128 +  workflow, we can create placeholder states with an automatic
  55.129 +  transition to the state we want to be in.
  55.130 + ]
  55.131 +
  55.132 +
    56.1 new file mode 100644
    56.2 --- /dev/null
    56.3 +++ b/help/051-scripts.stx
    56.4 @@ -0,0 +1,20 @@
    56.5 +Scripts Tab
    56.6 +
    56.7 + Scripts are used to extend the workflow in various ways.  Scripts can
    56.8 + be External Methods, Python Scripts, DTML methods, or any other callable
    56.9 + Zope object.  They are accessible by name in expressions, eg::
   56.10 +
   56.11 +   scripts/myScript
   56.12 +
   56.13 + or::
   56.14 +
   56.15 +   python:scripts.myScript(arg1, arg2...) 
   56.16 +
   56.17 + From transitions, as before and after scripts, they are invoked with a
   56.18 + 'state_change' object as the first argument; see the Expressions section
   56.19 + for more details on the 'state_change' object.
   56.20 +
   56.21 + Objects under the scripts are managed in the usual ZMI fashion.
   56.22 +
   56.23 +
   56.24 +
    57.1 new file mode 100644
    57.2 --- /dev/null
    57.3 +++ b/help/061-permissions.stx
    57.4 @@ -0,0 +1,10 @@
    57.5 +Permissions Tab
    57.6 +
    57.7 + You can manage all of the actions a user can perform on an object by
    57.8 + setting up permissions to be managed by the workflow under the
    57.9 + permissions tab. Here, you can select which permissions should be
   57.10 + state-dependent from a list of all available permissions, and you
   57.11 + can delete previously selected permissions. In each state, use
   57.12 + it's permissions tab to set up the role to permission mappings
   57.13 + appropriate for that state.
   57.14 +
    58.1 new file mode 100644
    58.2 index 0000000000000000000000000000000000000000..5c7e1719d3a14b3f7586bd41c9b3d5aab30012ec
    58.3 GIT binary patch
    58.4 literal 107
    58.5 zc${<hbhEHb6krfwSjYed^$?0d@t>%3QEFmIYKlU6W=V!ZNJgrHyQgmegW^vXMlJ>>
    58.6 z1|5(pkVXb3Ws#PZ)vs^PV-8%ECAe4QH5*G(d+DPYt;>(End_SN$>;jEPzDBT03DJZ
    58.7 AQUCw|
    58.8 
    59.1 new file mode 100644
    59.2 index 0000000000000000000000000000000000000000..c795aa6a673395f91ae85a31eb0b634259a1233d
    59.3 GIT binary patch
    59.4 literal 118
    59.5 zc${<hbhEHb6krfwSjYeZ|Ns97(+rCLM4gLL6H8K46v{J8G895GQWe}ieFGR2f3h%g
    59.6 zF)%UcfK-7rGBD|hw5-fNTPe`-`a<T$C0s4?DRUT`7Kcn*KD{pcwNXy>q|Dw~=bB@G
    59.7 OUp^SB=8(Y1U=0A2cqRn^
    59.8 
    60.1 new file mode 100644
    60.2 index 0000000000000000000000000000000000000000..3dc9144a69150093245587b89e4ac49835b1c922
    60.3 GIT binary patch
    60.4 literal 111
    60.5 zc${<hbhEHb6krfwSjYeZ|Ns97(+rCLM4gLL6H8K46v{J8G895GQWe}ieFGR2f3h%g
    60.6 zF)%UcfK-7rGBBx6VOX|XV4_4Z|EaLIp(c6r6z;3aDW}hKvs7ABRq<ANa-{9uboS}2
    60.7 G4AuZd9v?FR
    60.8 
    61.1 new file mode 100644
    61.2 index 0000000000000000000000000000000000000000..65863a50682cf1b24ec16fa01d953c585c6710cd
    61.3 GIT binary patch
    61.4 literal 99
    61.5 zc${<hbhEHb6krfwXkY+=|Ns9h{u6aBN=+<DO;IS%EXhy^$w*aj_w)^5Q2fcl$i=|O
    61.6 xpaW6}(!#)`*wdd`u;ye8!xXKb0Xd@Hb6Ou+m_FWK65zQhbL+MQ@5`(V)&Pq#A1D9-
    61.7 
    62.1 new file mode 100644
    62.2 index 0000000000000000000000000000000000000000..da28011a44ad3948afa5a713f5fbeb05b97c8ca3
    62.3 GIT binary patch
    62.4 literal 97
    62.5 zc${<hbhEHb6krfwSjfQemx+m?p5Z?jDE?$&<YHiA&|v@qkURsE(G-48_0wG8P8)Zb
    62.6 w$(wXN?5(`^d}>UZlw-rP><LxtKU_2Nx&FiOx$S<YS8WdS{f=#@<YKS}000Ri=l}o!
    62.7 
    63.1 new file mode 100644
    63.2 index 0000000000000000000000000000000000000000..2c2d228f0a513947109d04318652b8008f835b18
    63.3 GIT binary patch
    63.4 literal 114
    63.5 zc${<hbhEHb6krfwSjYeZ|NsAIsAm8o#ebsCMX8A;sVNHOnI#ztAsML(?w-B@42nNl
    63.6 z7`Ygj8FWC(K^hsDwC4189(XIYXwIofhUM$F&s~<;r@)ZNGJz+u?9HiTMb_7I+qHk4
    63.7 M`~J^fgMq;s0C0LG?*IS*
    63.8 
    64.1 new file mode 100644
    64.2 --- /dev/null
    64.3 +++ b/interfaces.py
    64.4 @@ -0,0 +1,24 @@
    64.5 +##############################################################################
    64.6 +#
    64.7 +# Copyright (c) 2005 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 +"""DCWorkflow interfaces.
   64.18 +
   64.19 +$Id: interfaces.py 40346 2005-11-23 17:15:03Z yuppie $
   64.20 +"""
   64.21 +
   64.22 +from zope.interface import Interface
   64.23 +
   64.24 +
   64.25 +class IDCWorkflowDefinition(Interface):
   64.26 +
   64.27 +    """Web-configurable workflow definition.
   64.28 +    """
    65.1 new file mode 100644
    65.2 --- /dev/null
    65.3 +++ b/permissions.py
    65.4 @@ -0,0 +1,25 @@
    65.5 +""" DCWorkflow product permissions
    65.6 +
    65.7 +$Id: permissions.py 36089 2004-04-29 16:13:23Z tseaver $
    65.8 +"""
    65.9 +from AccessControl import ModuleSecurityInfo
   65.10 +
   65.11 +security = ModuleSecurityInfo('Products.DCWorkflow.permissions')
   65.12 +
   65.13 +security.declarePublic('AccessContentsInformation')
   65.14 +from Products.CMFCore.permissions import AccessContentsInformation
   65.15 +
   65.16 +security.declarePublic('ManagePortal')
   65.17 +from Products.CMFCore.permissions import ManagePortal
   65.18 +
   65.19 +security.declarePublic('ModifyPortalContent')
   65.20 +from Products.CMFCore.permissions import ModifyPortalContent
   65.21 +
   65.22 +security.declarePublic('RequestReview')
   65.23 +from Products.CMFCore.permissions import RequestReview
   65.24 +
   65.25 +security.declarePublic('ReviewPortalContent')
   65.26 +from Products.CMFCore.permissions import ReviewPortalContent
   65.27 +
   65.28 +security.declarePublic('View')
   65.29 +from Products.CMFCore.permissions import View
    66.1 new file mode 100644
    66.2 --- /dev/null
    66.3 +++ b/profiles/revision2/workflows.xml
    66.4 @@ -0,0 +1,4 @@
    66.5 +<?xml version="1.0"?>
    66.6 +<object name="portal_workflow" meta_type="CMF Workflow Tool">
    66.7 + <object name="default_workflow" meta_type="Workflow"/>
    66.8 +</object>
    67.1 new file mode 100644
    67.2 --- /dev/null
    67.3 +++ b/profiles/revision2/workflows/default_workflow/definition.xml
    67.4 @@ -0,0 +1,223 @@
    67.5 +<?xml version="1.0"?>
    67.6 +<dc-workflow workflow_id="default_workflow"
    67.7 +             title="CMF default workflow [Revision 2]"
    67.8 +             state_variable="review_state"
    67.9 +             initial_state="visible">
   67.10 + <permission>Access contents information</permission>
   67.11 + <permission>Modify portal content</permission>
   67.12 + <permission>View</permission>
   67.13 + <state state_id="pending" title="Waiting for reviewer">
   67.14 +  <exit-transition transition_id="hide"/>
   67.15 +  <exit-transition transition_id="publish"/>
   67.16 +  <exit-transition transition_id="reject"/>
   67.17 +  <exit-transition transition_id="retract"/>
   67.18 +  <permission-map name="Access contents information"
   67.19 +                  acquired="True">
   67.20 +   <permission-role>Manager</permission-role>
   67.21 +   <permission-role>Owner</permission-role>
   67.22 +   <permission-role>Reviewer</permission-role>
   67.23 +  </permission-map>
   67.24 +  <permission-map name="Modify portal content"
   67.25 +                  acquired="False">
   67.26 +   <permission-role>Manager</permission-role>
   67.27 +   <permission-role>Reviewer</permission-role>
   67.28 +  </permission-map>
   67.29 +  <permission-map name="View" acquired="True">
   67.30 +   <permission-role>Manager</permission-role>
   67.31 +   <permission-role>Owner</permission-role>
   67.32 +   <permission-role>Reviewer</permission-role>
   67.33 +  </permission-map>
   67.34 +
   67.35 +
   67.36 + </state>
   67.37 + <state state_id="private"
   67.38 +        title="Visible and editable only by owner">
   67.39 +  <exit-transition transition_id="show"/>
   67.40 +  <permission-map name="Access contents information"
   67.41 +                  acquired="False">
   67.42 +   <permission-role>Manager</permission-role>
   67.43 +   <permission-role>Owner</permission-role>
   67.44 +  </permission-map>
   67.45 +  <permission-map name="Modify portal content"
   67.46 +                  acquired="False">
   67.47 +   <permission-role>Manager</permission-role>
   67.48 +   <permission-role>Owner</permission-role>
   67.49 +  </permission-map>
   67.50 +  <permission-map name="View" acquired="False">
   67.51 +   <permission-role>Manager</permission-role>
   67.52 +   <permission-role>Owner</permission-role>
   67.53 +  </permission-map>
   67.54 +
   67.55 +
   67.56 + </state>
   67.57 + <state state_id="published" title="Public">
   67.58 +  <exit-transition transition_id="reject"/>
   67.59 +  <exit-transition transition_id="retract"/>
   67.60 +  <permission-map name="Access contents information"
   67.61 +                  acquired="True">
   67.62 +   <permission-role>Anonymous</permission-role>
   67.63 +   <permission-role>Manager</permission-role>
   67.64 +  </permission-map>
   67.65 +  <permission-map name="Modify portal content"
   67.66 +                  acquired="False">
   67.67 +   <permission-role>Manager</permission-role>
   67.68 +  </permission-map>
   67.69 +  <permission-map name="View" acquired="True">
   67.70 +   <permission-role>Anonymous</permission-role>
   67.71 +   <permission-role>Manager</permission-role>
   67.72 +  </permission-map>
   67.73 +
   67.74 +
   67.75 + </state>
   67.76 + <state state_id="visible" title="Visible but not published">
   67.77 +  <exit-transition transition_id="hide"/>
   67.78 +  <exit-transition transition_id="publish"/>
   67.79 +  <exit-transition transition_id="submit"/>
   67.80 +  <permission-map name="Access contents information"
   67.81 +                  acquired="True">
   67.82 +   <permission-role>Anonymous</permission-role>
   67.83 +   <permission-role>Manager</permission-role>
   67.84 +   <permission-role>Reviewer</permission-role>
   67.85 +  </permission-map>
   67.86 +  <permission-map name="Modify portal content"
   67.87 +                  acquired="False">
   67.88 +   <permission-role>Manager</permission-role>
   67.89 +   <permission-role>Owner</permission-role>
   67.90 +  </permission-map>
   67.91 +  <permission-map name="View" acquired="True">
   67.92 +   <permission-role>Anonymous</permission-role>
   67.93 +   <permission-role>Manager</permission-role>
   67.94 +   <permission-role>Reviewer</permission-role>
   67.95 +  </permission-map>
   67.96 +
   67.97 +
   67.98 + </state>
   67.99 + <transition transition_id="hide"
  67.100 +             title="Member makes content private"
  67.101 +             new_state="private" trigger="USER"
  67.102 +             before_script="" after_script="">
  67.103 +  <action url="%(content_url)s/content_hide_form"
  67.104 +          category="workflow">Make private</action>
  67.105 +  <guard>
  67.106 +   <guard-role>Owner</guard-role>
  67.107 +  </guard>
  67.108 +
  67.109 + </transition>
  67.110 + <transition transition_id="publish"
  67.111 +             title="Reviewer publishes content"
  67.112 +             new_state="published" trigger="USER"
  67.113 +             before_script="" after_script="">
  67.114 +  <action url="%(object_url)s/content_publish_form"
  67.115 +          category="workflow">Publish</action>
  67.116 +  <guard>
  67.117 +   <guard-permission>Review portal content</guard-permission>
  67.118 +  </guard>
  67.119 +
  67.120 + </transition>
  67.121 + <transition transition_id="reject"
  67.122 +             title="Reviewer rejects submission"
  67.123 +             new_state="visible" trigger="USER"
  67.124 +             before_script="" after_script="">
  67.125 +  <action url="%(object_url)s/content_reject_form"
  67.126 +          category="workflow">Reject</action>
  67.127 +  <guard>
  67.128 +   <guard-permission>Review portal content</guard-permission>
  67.129 +  </guard>
  67.130 +
  67.131 + </transition>
  67.132 + <transition transition_id="retract"
  67.133 +             title="Member retracts submission"
  67.134 +             new_state="visible" trigger="USER"
  67.135 +             before_script="" after_script="">
  67.136 +  <action url="%(object_url)s/content_retract_form"
  67.137 +          category="workflow">Retract</action>
  67.138 +  <guard>
  67.139 +   <guard-permission>Request review</guard-permission>
  67.140 +  </guard>
  67.141 +
  67.142 + </transition>
  67.143 + <transition transition_id="show"
  67.144 +             title="Member makes content visible"
  67.145 +             new_state="visible" trigger="USER"
  67.146 +             before_script="" after_script="">
  67.147 +  <action url="%(content_url)s/content_show_form"
  67.148 +          category="workflow">Make visible</action>
  67.149 +  <guard>
  67.150 +   <guard-role>Owner</guard-role>
  67.151 +  </guard>
  67.152 +
  67.153 + </transition>
  67.154 + <transition transition_id="submit"
  67.155 +             title="Member requests publishing"
  67.156 +             new_state="pending" trigger="USER"
  67.157 +             before_script="" after_script="">
  67.158 +  <action url="%(object_url)s/content_submit_form"
  67.159 +          category="workflow">Submit</action>
  67.160 +  <guard>
  67.161 +   <guard-permission>Request review</guard-permission>
  67.162 +  </guard>
  67.163 +
  67.164 + </transition>
  67.165 + <worklist worklist_id="reviewer_queue" title="">
  67.166 +  <description>Reviewer tasks</description>
  67.167 +  <action url="%(portal_url)s/search?review_state=pending"
  67.168 +          category="global">Pending (%(count)d)</action>
  67.169 +  <guard>
  67.170 +   <guard-permission>Review portal content</guard-permission>
  67.171 +  </guard>
  67.172 +  <match name="review_state" values="pending"/>
  67.173 + </worklist>
  67.174 + <variable variable_id="action" for_catalog="False"
  67.175 +           for_status="True" update_always="True">
  67.176 +  <description>The last transition</description>
  67.177 +  <default>
  67.178 +   
  67.179 +   <expression>transition/getId|nothing</expression>
  67.180 +  </default>
  67.181 +  <guard>
  67.182 +  </guard>
  67.183 + </variable>
  67.184 + <variable variable_id="actor" for_catalog="False"
  67.185 +           for_status="True" update_always="True">
  67.186 +  <description>The ID of the user who performed the last transition</description>
  67.187 +  <default>
  67.188 +   
  67.189 +   <expression>user/getId</expression>
  67.190 +  </default>
  67.191 +  <guard>
  67.192 +  </guard>
  67.193 + </variable>
  67.194 + <variable variable_id="comments" for_catalog="False"
  67.195 +           for_status="True" update_always="True">
  67.196 +  <description>Comments about the last transition</description>
  67.197 +  <default>
  67.198 +   
  67.199 +   <expression>python:state_change.kwargs.get('comment', '')</expression>
  67.200 +  </default>
  67.201 +  <guard>
  67.202 +  </guard>
  67.203 + </variable>
  67.204 + <variable variable_id="review_history" for_catalog="False"
  67.205 +           for_status="False" update_always="False">
  67.206 +  <description>Provides access to workflow history</description>
  67.207 +  <default>
  67.208 +   
  67.209 +   <expression>state_change/getHistory</expression>
  67.210 +  </default>
  67.211 +  <guard>
  67.212 +   <guard-permission>Request review</guard-permission>
  67.213 +   <guard-permission>Review portal content</guard-permission>
  67.214 +  </guard>
  67.215 + </variable>
  67.216 + <variable variable_id="time" for_catalog="False"
  67.217 +           for_status="True" update_always="True">
  67.218 +  <description>Time of the last transition</description>
  67.219 +  <default>
  67.220 +   
  67.221 +   <expression>state_change/getDateTime</expression>
  67.222 +  </default>
  67.223 +  <guard>
  67.224 +  </guard>
  67.225 + </variable>
  67.226 +
  67.227 +</dc-workflow>
    68.1 new file mode 100644
    68.2 --- /dev/null
    68.3 +++ b/tests/__init__.py
    68.4 @@ -0,0 +1,6 @@
    68.5 +"""\
    68.6 +Unit test package for DCWorkflow.
    68.7 +
    68.8 +As test suites are added, they should be added to the
    68.9 +mega-test-suite in Products.DCWorkflow.tests.test_all.py
   68.10 +"""
    69.1 new file mode 100644
    69.2 --- /dev/null
    69.3 +++ b/tests/test_DCWorkflow.py
    69.4 @@ -0,0 +1,140 @@
    69.5 +##############################################################################
    69.6 +#
    69.7 +# Copyright (c) 2003 Zope Corporation and Contributors. All Rights Reserved.
    69.8 +#
    69.9 +# This software is subject to the provisions of the Zope Public License,
   69.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   69.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   69.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   69.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   69.14 +# FOR A PARTICULAR PURPOSE.
   69.15 +#
   69.16 +##############################################################################
   69.17 +""" Unit tests for DCWorkflow module.
   69.18 +
   69.19 +$Id: test_DCWorkflow.py 40346 2005-11-23 17:15:03Z yuppie $
   69.20 +"""
   69.21 +
   69.22 +from unittest import TestCase, TestSuite, makeSuite, main
   69.23 +import Testing
   69.24 +try:
   69.25 +    import Zope2
   69.26 +except ImportError: # BBB: for Zope 2.7
   69.27 +    import Zope as Zope2
   69.28 +Zope2.startup()
   69.29 +
   69.30 +from Products.CMFCore.tests.base.dummy import DummyContent
   69.31 +from Products.CMFCore.tests.base.dummy import DummySite
   69.32 +from Products.CMFCore.tests.base.dummy import DummyTool
   69.33 +from Products.CMFCore.WorkflowTool import WorkflowTool
   69.34 +
   69.35 +
   69.36 +class DCWorkflowDefinitionTests(TestCase):
   69.37 +
   69.38 +    def setUp(self):
   69.39 +        self.site = DummySite('site')
   69.40 +        self.site._setObject( 'portal_types', DummyTool() )
   69.41 +        self.site._setObject( 'portal_workflow', WorkflowTool() )
   69.42 +        self._constructDummyWorkflow()
   69.43 +
   69.44 +    def test_z2interfaces(self):
   69.45 +        from Interface.Verify import verifyClass
   69.46 +        from Products.CMFCore.interfaces.portal_workflow \
   69.47 +             import WorkflowDefinition as IWorkflowDefinition
   69.48 +        from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   69.49 +
   69.50 +        verifyClass(IWorkflowDefinition, DCWorkflowDefinition)
   69.51 +
   69.52 +    def test_z3interfaces(self):
   69.53 +        try:
   69.54 +            from zope.interface.verify import verifyClass
   69.55 +            from Products.CMFCore.interfaces import IWorkflowDefinition
   69.56 +        except ImportError:
   69.57 +            # BBB: for Zope 2.7
   69.58 +            return
   69.59 +        from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   69.60 +
   69.61 +        verifyClass(IWorkflowDefinition, DCWorkflowDefinition)
   69.62 +
   69.63 +    def _constructDummyWorkflow(self):
   69.64 +        from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   69.65 +
   69.66 +        wftool = self.site.portal_workflow
   69.67 +        wftool._setObject('wf', DCWorkflowDefinition('wf'))
   69.68 +        wftool.setDefaultChain('wf')
   69.69 +        wf = wftool.wf
   69.70 +
   69.71 +        wf.states.addState('private')
   69.72 +        sdef = wf.states['private']
   69.73 +        sdef.setProperties( transitions=('publish',) )
   69.74 +
   69.75 +        wf.states.addState('published')
   69.76 +        wf.states.setInitialState('private')
   69.77 +
   69.78 +        wf.transitions.addTransition('publish')
   69.79 +        tdef = wf.transitions['publish']
   69.80 +        tdef.setProperties(title='', new_state_id='published', actbox_name='')
   69.81 +
   69.82 +        wf.variables.addVariable('comments')
   69.83 +        vdef = wf.variables['comments']
   69.84 +        vdef.setProperties(description='',
   69.85 +                 default_expr="python:state_change.kwargs.get('comment', '')",
   69.86 +                 for_status=1, update_always=1)
   69.87 +
   69.88 +    def _getDummyWorkflow(self):
   69.89 +        wftool = self.site.portal_workflow
   69.90 +        return wftool.wf
   69.91 +
   69.92 +    def test_doActionFor(self):
   69.93 +
   69.94 +        wftool = self.site.portal_workflow
   69.95 +        wf = self._getDummyWorkflow()
   69.96 +
   69.97 +        dummy = self.site._setObject( 'dummy', DummyContent() )
   69.98 +        wftool.notifyCreated(dummy)
   69.99 +        self.assertEqual( wf._getStatusOf(dummy),
  69.100 +                          {'state': 'private', 'comments': ''} )
  69.101 +        wf.doActionFor(dummy, 'publish', comment='foo' )
  69.102 +        self.assertEqual( wf._getStatusOf(dummy),
  69.103 +                          {'state': 'published', 'comments': 'foo'} )
  69.104 +
  69.105 +        # XXX more
  69.106 +
  69.107 +    def test_checkTransitionGuard(self):
  69.108 +
  69.109 +        wftool = self.site.portal_workflow
  69.110 +        wf = self._getDummyWorkflow()
  69.111 +        dummy = self.site._setObject( 'dummy', DummyContent() )
  69.112 +        wftool.notifyCreated(dummy)
  69.113 +        self.assertEqual( wf._getStatusOf(dummy),
  69.114 +                          {'state': 'private', 'comments': ''} )
  69.115 +
  69.116 +        # Check
  69.117 +        self.assert_(wf._checkTransitionGuard(wf.transitions['publish'],
  69.118 +                                              dummy))
  69.119 +
  69.120 +        # Check with kwargs propagation
  69.121 +        self.assert_(wf._checkTransitionGuard(wf.transitions['publish'],
  69.122 +                                              dummy, arg1=1, arg2=2))
  69.123 +
  69.124 +    def test_isActionSupported(self):
  69.125 +
  69.126 +        wf = self._getDummyWorkflow()
  69.127 +        dummy = self.site._setObject( 'dummy', DummyContent() )
  69.128 +
  69.129 +        # check publish
  69.130 +        self.assert_(wf.isActionSupported(dummy, 'publish'))
  69.131 +
  69.132 +        # Check with kwargs.
  69.133 +        self.assert_(wf.isActionSupported(dummy, 'publish', arg1=1, arg2=2))
  69.134 +
  69.135 +    # XXX more tests...
  69.136 +
  69.137 +
  69.138 +def test_suite():
  69.139 +    return TestSuite((
  69.140 +        makeSuite(DCWorkflowDefinitionTests),
  69.141 +        ))
  69.142 +
  69.143 +if __name__ == '__main__':
  69.144 +    main(defaultTest='test_suite')
    70.1 new file mode 100644
    70.2 --- /dev/null
    70.3 +++ b/tests/test_exportimport.py
    70.4 @@ -0,0 +1,2336 @@
    70.5 +##############################################################################
    70.6 +#
    70.7 +# Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
    70.8 +#
    70.9 +# This software is subject to the provisions of the Zope Public License,
   70.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   70.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   70.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   70.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   70.14 +# FOR A PARTICULAR PURPOSE.
   70.15 +#
   70.16 +##############################################################################
   70.17 +"""DCWorkflow export / import unit tests.
   70.18 +
   70.19 +$Id: test_exportimport.py 40391 2005-11-28 16:21:21Z yuppie $
   70.20 +"""
   70.21 +
   70.22 +import unittest
   70.23 +import Testing
   70.24 +
   70.25 +import Products
   70.26 +from Products.PythonScripts.PythonScript import PythonScript
   70.27 +from Products.ExternalMethod.ExternalMethod import ExternalMethod
   70.28 +from Products.Five import zcml
   70.29 +
   70.30 +import Products.GenericSetup.PythonScripts
   70.31 +from Products.CMFCore.exportimport.tests.test_workflow \
   70.32 +        import _BINDINGS_TOOL_EXPORT
   70.33 +from Products.CMFCore.exportimport.tests.test_workflow import _DUMMY_ZCML
   70.34 +from Products.CMFCore.exportimport.tests.test_workflow \
   70.35 +        import _EMPTY_TOOL_EXPORT
   70.36 +from Products.CMFCore.exportimport.tests.test_workflow \
   70.37 +        import _WorkflowSetup as WorkflowSetupBase
   70.38 +from Products.CMFCore.exportimport.tests.test_workflow import DummyWorkflow
   70.39 +from Products.CMFCore.exportimport.tests.test_workflow \
   70.40 +        import DummyWorkflowTool
   70.41 +from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   70.42 +from Products.DCWorkflow.Transitions import TRIGGER_USER_ACTION
   70.43 +from Products.DCWorkflow.Transitions import TRIGGER_AUTOMATIC
   70.44 +from Products.GenericSetup.tests.common import DummyExportContext
   70.45 +from Products.GenericSetup.tests.common import DummyImportContext
   70.46 +
   70.47 +
   70.48 +class _GuardChecker:
   70.49 +
   70.50 +    def _genGuardProps( self, permissions, roles, groups, expr ):
   70.51 +
   70.52 +        return { 'guard_permissions'   : '; '.join( permissions )
   70.53 +               , 'guard_roles'         : '; '.join( roles )
   70.54 +               , 'guard_groups'        : '; '.join( groups )
   70.55 +               , 'guard_expr'          : expr
   70.56 +               }
   70.57 +
   70.58 +    def _assertGuard( self, info, permissions, roles, groups, expr ):
   70.59 +
   70.60 +        self.assertEqual( len( info[ 'guard_permissions' ] )
   70.61 +                        , len( permissions ) )
   70.62 +
   70.63 +        for expected in permissions:
   70.64 +            self.failUnless( expected in info[ 'guard_permissions' ] )
   70.65 +
   70.66 +        self.assertEqual( len( info[ 'guard_roles' ] )
   70.67 +                        , len( roles ) )
   70.68 +
   70.69 +        for expected in roles:
   70.70 +            self.failUnless( expected in info[ 'guard_roles' ] )
   70.71 +
   70.72 +        self.assertEqual( len( info[ 'guard_groups' ] )
   70.73 +                        , len( groups ) )
   70.74 +
   70.75 +        for expected in groups:
   70.76 +            self.failUnless( expected in info[ 'guard_groups' ] )
   70.77 +
   70.78 +        self.assertEqual( info[ 'guard_expr' ], expr )
   70.79 +
   70.80 +
   70.81 +class _WorkflowSetup(WorkflowSetupBase):
   70.82 +
   70.83 +    def setUp(self):
   70.84 +        WorkflowSetupBase.setUp(self)
   70.85 +        zcml.load_config('permissions.zcml', Products.Five)
   70.86 +        zcml.load_config('configure.zcml', Products.DCWorkflow)
   70.87 +        zcml.load_config('configure.zcml', Products.GenericSetup.PythonScripts)
   70.88 +
   70.89 +    def _initDCWorkflow( self, workflow_id ):
   70.90 +
   70.91 +        wf_tool = self.root.site.portal_workflow
   70.92 +        wf_tool._setObject( workflow_id, DCWorkflowDefinition( workflow_id ) )
   70.93 +
   70.94 +        return wf_tool._getOb( workflow_id )
   70.95 +
   70.96 +    def _initVariables( self, dcworkflow ):
   70.97 +
   70.98 +        for id, args in _WF_VARIABLES.items():
   70.99 +
  70.100 +            dcworkflow.variables.addVariable( id )
  70.101 +            variable = dcworkflow.variables._getOb( id )
  70.102 +
  70.103 +            ( descr, def_val, def_exp, for_cat, for_stat, upd_alw
  70.104 +            ) = args[ :-4 ]
  70.105 +
  70.106 +            variable.setProperties( description=args[0]
  70.107 +                                  , default_value=args[1]
  70.108 +                                  , default_expr=args[2]
  70.109 +                                  , for_catalog=args[3]
  70.110 +                                  , for_status=args[4]
  70.111 +                                  , update_always=args[5]
  70.112 +                                  , props=self._genGuardProps( *args[ -4: ] )
  70.113 +                                  )
  70.114 +
  70.115 +    def _initStates( self, dcworkflow ):
  70.116 +
  70.117 +        dcworkflow.groups = _WF_GROUPS
  70.118 +
  70.119 +        for k, v in _WF_STATES.items():
  70.120 +
  70.121 +            dcworkflow.states.addState( k )
  70.122 +            state = dcworkflow.states._getOb( k )
  70.123 +
  70.124 +            state.setProperties( title=v[ 0 ]
  70.125 +                               , description=v[ 1 ]
  70.126 +                               , transitions=v[ 2 ]
  70.127 +                               )
  70.128 +            if not v[ 3 ]:
  70.129 +                state.permission_roles = {}
  70.130 +
  70.131 +            for permission, roles in v[ 3 ].items():
  70.132 +                state.setPermission( permission
  70.133 +                                   , not isinstance( roles, tuple )
  70.134 +                                   , roles
  70.135 +                                   )
  70.136 +            faux_request = {}
  70.137 +
  70.138 +            for group_id, roles in v[ 4 ]:
  70.139 +                for role in roles:
  70.140 +                    faux_request[ '%s|%s' % ( group_id, role ) ] = True
  70.141 +
  70.142 +            state.setGroups( REQUEST=faux_request )
  70.143 +
  70.144 +            for k, v in v[ 5 ].items():
  70.145 +                state.addVariable( k, v )
  70.146 +
  70.147 +    def _initTransitions( self, dcworkflow ):
  70.148 +
  70.149 +        for k, v in _WF_TRANSITIONS.items():
  70.150 +
  70.151 +            dcworkflow.transitions.addTransition( k )
  70.152 +            transition = dcworkflow.transitions._getOb( k )
  70.153 +
  70.154 +            transition.setProperties( title=v[ 0 ]
  70.155 +                                    , description=v[ 1 ]
  70.156 +                                    , new_state_id=v[ 2 ]
  70.157 +                                    , trigger_type=v[ 3 ]
  70.158 +                                    , script_name=v[ 4 ]
  70.159 +                                    , after_script_name=v[ 5 ]
  70.160 +                                    , actbox_name=v[ 6 ]
  70.161 +                                    , actbox_url=v[ 7 ]
  70.162 +                                    , actbox_category=v[ 8 ]
  70.163 +                                    , props=self._genGuardProps( *v[ -4: ] )
  70.164 +                                    )
  70.165 +
  70.166 +            for k, v in v[ 9 ].items():
  70.167 +                transition.addVariable( k, v )
  70.168 +
  70.169 +    def _initWorklists( self, dcworkflow ):
  70.170 +
  70.171 +        for k, v in _WF_WORKLISTS.items():
  70.172 +
  70.173 +            dcworkflow.worklists.addWorklist( k )
  70.174 +            worklist = dcworkflow.worklists._getOb( k )
  70.175 +
  70.176 +            worklist.title = v[ 0 ]
  70.177 +
  70.178 +            props=self._genGuardProps( *v[ -4: ] )
  70.179 +
  70.180 +            for var_id, matches in v[ 2 ].items():
  70.181 +                props[ 'var_match_%s' % var_id ] = ';'.join( matches )
  70.182 +
  70.183 +            worklist.setProperties( description=v[ 1 ]
  70.184 +                                  , actbox_name=v[ 3 ]
  70.185 +                                  , actbox_url=v[ 4 ]
  70.186 +                                  , actbox_category=v[ 5 ]
  70.187 +                                  , props=props
  70.188 +                                  )
  70.189 +
  70.190 +    def _initScripts( self, dcworkflow ):
  70.191 +
  70.192 +        for k, v in _WF_SCRIPTS.items():
  70.193 +
  70.194 +            if v[ 0 ] == PythonScript.meta_type:
  70.195 +                script = PythonScript( k )
  70.196 +                script.write( v[ 1 ] )
  70.197 +
  70.198 +            elif v[ 0 ] == ExternalMethod.meta_type:
  70.199 +                script = ExternalMethod(k,'', v[3], v[4])
  70.200 +
  70.201 +            else:
  70.202 +                raise ValueError, 'Unknown script type: %s' % v[ 0 ]
  70.203 +
  70.204 +            dcworkflow.scripts._setObject( k, script )
  70.205 +
  70.206 +
  70.207 +class WorkflowDefinitionConfiguratorTests( _WorkflowSetup, _GuardChecker ):
  70.208 +
  70.209 +    def _getTargetClass( self ):
  70.210 +        from Products.DCWorkflow.exportimport \
  70.211 +                import WorkflowDefinitionConfigurator
  70.212 +
  70.213 +        return WorkflowDefinitionConfigurator
  70.214 +
  70.215 +    def test_getWorkflowInfo_dcworkflow_defaults( self ):
  70.216 +
  70.217 +        WF_ID = 'dcworkflow_defaults'
  70.218 +
  70.219 +        site = self._initSite()
  70.220 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.221 +
  70.222 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.223 +        info = configurator.getWorkflowInfo( WF_ID )
  70.224 +
  70.225 +        self.assertEqual( info[ 'id' ], WF_ID )
  70.226 +        self.assertEqual( info[ 'meta_type' ], DCWorkflowDefinition.meta_type )
  70.227 +        self.assertEqual( info[ 'title' ], dcworkflow.title )
  70.228 +
  70.229 +        self.assertEqual( info[ 'state_variable' ], dcworkflow.state_var )
  70.230 +
  70.231 +        self.assertEqual( len( info[ 'permissions' ] ), 0 )
  70.232 +        self.assertEqual( len( info[ 'variable_info' ] ), 0 )
  70.233 +        self.assertEqual( len( info[ 'state_info' ] ), 0 )
  70.234 +        self.assertEqual( len( info[ 'transition_info' ] ), 0 )
  70.235 +
  70.236 +    def test_getWorkflowInfo_dcworkflow_permissions( self ):
  70.237 +
  70.238 +        WF_ID = 'dcworkflow_permissions'
  70.239 +
  70.240 +        site = self._initSite()
  70.241 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.242 +        dcworkflow.permissions = _WF_PERMISSIONS
  70.243 +
  70.244 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.245 +        info = configurator.getWorkflowInfo( WF_ID )
  70.246 +
  70.247 +        permissions = info[ 'permissions' ]
  70.248 +        self.assertEqual( len( permissions ), len( _WF_PERMISSIONS ) )
  70.249 +
  70.250 +        for permission in _WF_PERMISSIONS:
  70.251 +            self.failUnless( permission in permissions )
  70.252 +
  70.253 +    def test_getWorkflowInfo_dcworkflow_variables( self ):
  70.254 +
  70.255 +        WF_ID = 'dcworkflow_variables'
  70.256 +
  70.257 +        site = self._initSite()
  70.258 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.259 +        self._initVariables( dcworkflow )
  70.260 +
  70.261 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.262 +        info = configurator.getWorkflowInfo( WF_ID )
  70.263 +
  70.264 +        variable_info = info[ 'variable_info' ]
  70.265 +        self.assertEqual( len( variable_info ), len( _WF_VARIABLES ) )
  70.266 +
  70.267 +        ids = [ x[ 'id' ] for x in variable_info ]
  70.268 +
  70.269 +        for k in _WF_VARIABLES.keys():
  70.270 +            self.failUnless( k in ids )
  70.271 +
  70.272 +        for info in variable_info:
  70.273 +
  70.274 +            expected = _WF_VARIABLES[ info[ 'id' ] ]
  70.275 +
  70.276 +            self.assertEqual( info[ 'description' ], expected[ 0 ] )
  70.277 +            self.assertEqual( info[ 'default_value' ], expected[ 1 ] )
  70.278 +            self.assertEqual( info[ 'default_expr' ], expected[ 2 ] )
  70.279 +            self.assertEqual( info[ 'for_catalog' ], expected[ 3 ] )
  70.280 +            self.assertEqual( info[ 'for_status' ], expected[ 4 ] )
  70.281 +            self.assertEqual( info[ 'update_always' ], expected[ 5 ] )
  70.282 +
  70.283 +            self._assertGuard( info, *expected[ -4: ] )
  70.284 +
  70.285 +    def test_getWorkflowInfo_dcworkflow_states( self ):
  70.286 +
  70.287 +        WF_ID = 'dcworkflow_states'
  70.288 +        WF_INITIAL_STATE = 'closed'
  70.289 +
  70.290 +        site = self._initSite()
  70.291 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.292 +        dcworkflow.initial_state = WF_INITIAL_STATE
  70.293 +        self._initStates( dcworkflow )
  70.294 +
  70.295 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.296 +        info = configurator.getWorkflowInfo( WF_ID )
  70.297 +
  70.298 +        self.assertEqual( info[ 'state_variable' ], dcworkflow.state_var )
  70.299 +        self.assertEqual( info[ 'initial_state' ], dcworkflow.initial_state )
  70.300 +
  70.301 +        state_info = info[ 'state_info' ]
  70.302 +        self.assertEqual( len( state_info ), len( _WF_STATES ) )
  70.303 +
  70.304 +        ids = [ x[ 'id' ] for x in state_info ]
  70.305 +
  70.306 +        for k in _WF_STATES.keys():
  70.307 +            self.failUnless( k in ids )
  70.308 +
  70.309 +        for info in state_info:
  70.310 +
  70.311 +            expected = _WF_STATES[ info[ 'id' ] ]
  70.312 +
  70.313 +            self.assertEqual( info[ 'title' ], expected[ 0 ] )
  70.314 +            self.assertEqual( info[ 'description' ], expected[ 1 ] )
  70.315 +            self.assertEqual( info[ 'transitions' ], expected[ 2 ] )
  70.316 +
  70.317 +            permissions = info[ 'permissions' ]
  70.318 +
  70.319 +            self.assertEqual( len( permissions ), len( expected[ 3 ] ) )
  70.320 +
  70.321 +            for ep_id, ep_roles in expected[ 3 ].items():
  70.322 +
  70.323 +                fp = [ x for x in permissions if x[ 'name' ] == ep_id ][ 0 ]
  70.324 +
  70.325 +                self.assertEqual( fp[ 'acquired' ]
  70.326 +                                , not isinstance( ep_roles, tuple ) )
  70.327 +
  70.328 +                self.assertEqual( len( fp[ 'roles' ] ), len( ep_roles ) )
  70.329 +
  70.330 +                for ep_role in ep_roles:
  70.331 +                    self.failUnless( ep_role in fp[ 'roles' ] )
  70.332 +
  70.333 +            groups = info[ 'groups' ]
  70.334 +            self.assertEqual( len( groups ), len( expected[ 4 ] ) )
  70.335 +
  70.336 +            for i in range( len( groups ) ):
  70.337 +                self.assertEqual( groups[ i ], expected[ 4 ][ i ] )
  70.338 +
  70.339 +            variables = info[ 'variables' ]
  70.340 +            self.assertEqual( len( variables ), len( expected[ 5 ] ) )
  70.341 +
  70.342 +            for v_info in variables:
  70.343 +
  70.344 +                name, type, value = ( v_info[ 'name' ]
  70.345 +                                    , v_info[ 'type' ], v_info[ 'value' ] )
  70.346 +
  70.347 +                self.assertEqual( value, expected[ 5 ][ name ] )
  70.348 +
  70.349 +                if isinstance( value, bool ):
  70.350 +                    self.assertEqual( type, 'bool' )
  70.351 +                elif isinstance( value, int ):
  70.352 +                    self.assertEqual( type, 'int' )
  70.353 +                elif isinstance( value, float ):
  70.354 +                    self.assertEqual( type, 'float' )
  70.355 +                elif isinstance( value, basestring ):
  70.356 +                    self.assertEqual( type, 'string' )
  70.357 +
  70.358 +    def test_getWorkflowInfo_dcworkflow_transitions( self ):
  70.359 +
  70.360 +        from Products.DCWorkflow.exportimport import TRIGGER_TYPES
  70.361 +
  70.362 +        WF_ID = 'dcworkflow_transitions'
  70.363 +
  70.364 +        site = self._initSite()
  70.365 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.366 +        self._initTransitions( dcworkflow )
  70.367 +
  70.368 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.369 +        info = configurator.getWorkflowInfo( WF_ID )
  70.370 +
  70.371 +        transition_info = info[ 'transition_info' ]
  70.372 +        self.assertEqual( len( transition_info ), len( _WF_TRANSITIONS ) )
  70.373 +
  70.374 +        ids = [ x[ 'id' ] for x in transition_info ]
  70.375 +
  70.376 +        for k in _WF_TRANSITIONS.keys():
  70.377 +            self.failUnless( k in ids )
  70.378 +
  70.379 +        for info in transition_info:
  70.380 +
  70.381 +            expected = _WF_TRANSITIONS[ info[ 'id' ] ]
  70.382 +
  70.383 +            self.assertEqual( info[ 'title' ], expected[ 0 ] )
  70.384 +            self.assertEqual( info[ 'description' ], expected[ 1 ] )
  70.385 +            self.assertEqual( info[ 'new_state_id' ], expected[ 2 ] )
  70.386 +            self.assertEqual( info[ 'trigger_type' ]
  70.387 +                            , TRIGGER_TYPES[ expected[ 3 ] ] )
  70.388 +            self.assertEqual( info[ 'script_name' ], expected[ 4 ] )
  70.389 +            self.assertEqual( info[ 'after_script_name' ], expected[ 5 ] )
  70.390 +            self.assertEqual( info[ 'actbox_name' ], expected[ 6 ] )
  70.391 +            self.assertEqual( info[ 'actbox_url' ], expected[ 7 ] )
  70.392 +            self.assertEqual( info[ 'actbox_category' ], expected[ 8 ] )
  70.393 +
  70.394 +            variables = info[ 'variables' ]
  70.395 +            self.assertEqual( len( variables ), len( expected[ 9 ] ) )
  70.396 +
  70.397 +            for v_info in variables:
  70.398 +                self.assertEqual( v_info[ 'expr' ]
  70.399 +                                , expected[ 9 ][ v_info[ 'name' ] ] )
  70.400 +
  70.401 +            self._assertGuard( info, *expected[ -4: ] )
  70.402 +
  70.403 +    def test_getWorkflowInfo_dcworkflow_worklists( self ):
  70.404 +
  70.405 +        WF_ID = 'dcworkflow_worklists'
  70.406 +
  70.407 +        site = self._initSite()
  70.408 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.409 +        self._initWorklists( dcworkflow )
  70.410 +
  70.411 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.412 +        info = configurator.getWorkflowInfo( WF_ID )
  70.413 +
  70.414 +        worklist_info = info[ 'worklist_info' ]
  70.415 +        self.assertEqual( len( worklist_info ), len( _WF_WORKLISTS ) )
  70.416 +
  70.417 +        ids = [ x[ 'id' ] for x in worklist_info ]
  70.418 +
  70.419 +        for k in _WF_WORKLISTS.keys():
  70.420 +            self.failUnless( k in ids )
  70.421 +
  70.422 +        for info in worklist_info:
  70.423 +
  70.424 +            expected = _WF_WORKLISTS[ info[ 'id' ] ]
  70.425 +
  70.426 +            self.assertEqual( info[ 'title' ], expected[ 0 ] )
  70.427 +            self.assertEqual( info[ 'description' ], expected[ 1 ] )
  70.428 +            self.assertEqual( info[ 'actbox_name' ], expected[ 3 ] )
  70.429 +            self.assertEqual( info[ 'actbox_url' ], expected[ 4 ] )
  70.430 +            self.assertEqual( info[ 'actbox_category' ], expected[ 5 ] )
  70.431 +
  70.432 +            var_match = info[ 'var_match' ]
  70.433 +            self.assertEqual( len( var_match ), len( expected[ 2 ] ) )
  70.434 +
  70.435 +            for var_id, values_txt in var_match:
  70.436 +
  70.437 +                values = [ x.strip() for x in values_txt.split( ';' ) ]
  70.438 +                e_values = expected[ 2 ][ var_id ]
  70.439 +                self.assertEqual( len( values ), len( e_values ) )
  70.440 +
  70.441 +                for e_value in e_values:
  70.442 +                    self.failUnless( e_value in values )
  70.443 +
  70.444 +            self._assertGuard( info, *expected[ -4: ] )
  70.445 +
  70.446 +    def test_getWorkflowInfo_dcworkflow_scripts( self ):
  70.447 +
  70.448 +        WF_ID = 'dcworkflow_scripts'
  70.449 +
  70.450 +        site = self._initSite()
  70.451 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.452 +        self._initScripts( dcworkflow )
  70.453 +
  70.454 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.455 +        info = configurator.getWorkflowInfo( WF_ID )
  70.456 +
  70.457 +        script_info = info[ 'script_info' ]
  70.458 +        self.assertEqual( len( script_info ), len( _WF_SCRIPTS ) )
  70.459 +
  70.460 +        ids = [ x[ 'id' ] for x in script_info ]
  70.461 +
  70.462 +        for k in _WF_SCRIPTS.keys():
  70.463 +            self.failUnless( k in ids )
  70.464 +
  70.465 +        for info in script_info:
  70.466 +
  70.467 +            expected = _WF_SCRIPTS[ info[ 'id' ] ]
  70.468 +
  70.469 +            self.assertEqual( info[ 'meta_type' ], expected[ 0 ] )
  70.470 +
  70.471 +            if info[ 'meta_type' ] == PythonScript.meta_type:
  70.472 +                self.assertEqual( info[ 'filename' ]
  70.473 +                                , expected[ 2 ] % WF_ID )
  70.474 +            else:
  70.475 +                self.assertEqual( info[ 'filename' ], expected[ 2 ] )
  70.476 +
  70.477 +    def test_generateXML_empty( self ):
  70.478 +
  70.479 +        WF_ID = 'empty'
  70.480 +        WF_TITLE = 'Empty DCWorkflow'
  70.481 +        WF_INITIAL_STATE = 'initial'
  70.482 +
  70.483 +        site = self._initSite()
  70.484 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.485 +        dcworkflow.title = WF_TITLE
  70.486 +        dcworkflow.initial_state = WF_INITIAL_STATE
  70.487 +
  70.488 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.489 +
  70.490 +        self._compareDOM( configurator.generateWorkflowXML()
  70.491 +                        , _EMPTY_WORKFLOW_EXPORT % ( WF_ID
  70.492 +                                                   , WF_TITLE
  70.493 +                                                   , WF_INITIAL_STATE
  70.494 +                                                   ) )
  70.495 +
  70.496 +    def test_generateWorkflowXML_normal( self ):
  70.497 +
  70.498 +        WF_ID = 'normal'
  70.499 +        WF_TITLE = 'Normal DCWorkflow'
  70.500 +        WF_INITIAL_STATE = 'closed'
  70.501 +
  70.502 +        site = self._initSite()
  70.503 +        dcworkflow = self._initDCWorkflow( WF_ID )
  70.504 +        dcworkflow.title = WF_TITLE
  70.505 +        dcworkflow.initial_state = WF_INITIAL_STATE
  70.506 +        dcworkflow.permissions = _WF_PERMISSIONS
  70.507 +        self._initVariables( dcworkflow )
  70.508 +        self._initStates( dcworkflow )
  70.509 +        self._initTransitions( dcworkflow )
  70.510 +        self._initWorklists( dcworkflow )
  70.511 +        self._initScripts( dcworkflow )
  70.512 +
  70.513 +        configurator = self._makeOne(dcworkflow).__of__(site)
  70.514 +
  70.515 +        self._compareDOM( configurator.generateWorkflowXML()
  70.516 +                        , _NORMAL_WORKFLOW_EXPORT
  70.517 +                          % { 'workflow_id' : WF_ID
  70.518 +                            , 'title' : WF_TITLE
  70.519 +                            , 'initial_state' : WF_INITIAL_STATE
  70.520 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.521 +                            } )
  70.522 +
  70.523 +    def test_generateWorkflowXML_multiple( self ):
  70.524 +
  70.525 +        WF_ID_1 = 'dc1'
  70.526 +        WF_TITLE_1 = 'Normal DCWorkflow #1'
  70.527 +        WF_ID_2 = 'dc2'
  70.528 +        WF_TITLE_2 = 'Normal DCWorkflow #2'
  70.529 +        WF_INITIAL_STATE = 'closed'
  70.530 +
  70.531 +        site = self._initSite()
  70.532 +
  70.533 +        dcworkflow_1 = self._initDCWorkflow( WF_ID_1 )
  70.534 +        dcworkflow_1.title = WF_TITLE_1
  70.535 +        dcworkflow_1.initial_state = WF_INITIAL_STATE
  70.536 +        dcworkflow_1.permissions = _WF_PERMISSIONS
  70.537 +        self._initVariables( dcworkflow_1 )
  70.538 +        self._initStates( dcworkflow_1 )
  70.539 +        self._initTransitions( dcworkflow_1 )
  70.540 +        self._initWorklists( dcworkflow_1 )
  70.541 +        self._initScripts( dcworkflow_1 )
  70.542 +
  70.543 +        dcworkflow_2 = self._initDCWorkflow( WF_ID_2 )
  70.544 +        dcworkflow_2.title = WF_TITLE_2
  70.545 +        dcworkflow_2.initial_state = WF_INITIAL_STATE
  70.546 +        dcworkflow_2.permissions = _WF_PERMISSIONS
  70.547 +        self._initVariables( dcworkflow_2 )
  70.548 +        self._initStates( dcworkflow_2 )
  70.549 +        self._initTransitions( dcworkflow_2 )
  70.550 +        self._initWorklists( dcworkflow_2 )
  70.551 +        self._initScripts( dcworkflow_2 )
  70.552 +
  70.553 +        configurator = self._makeOne(dcworkflow_1).__of__(site)
  70.554 +
  70.555 +        self._compareDOM( configurator.generateWorkflowXML()
  70.556 +                        , _NORMAL_WORKFLOW_EXPORT
  70.557 +                          % { 'workflow_id' : WF_ID_1
  70.558 +                            , 'title' : WF_TITLE_1
  70.559 +                            , 'initial_state' : WF_INITIAL_STATE
  70.560 +                            , 'workflow_filename' : WF_ID_1.replace(' ', '_')
  70.561 +                            } )
  70.562 +
  70.563 +        configurator = self._makeOne(dcworkflow_2).__of__(site)
  70.564 +
  70.565 +        self._compareDOM( configurator.generateWorkflowXML()
  70.566 +                        , _NORMAL_WORKFLOW_EXPORT
  70.567 +                          % { 'workflow_id' : WF_ID_2
  70.568 +                            , 'title' : WF_TITLE_2
  70.569 +                            , 'initial_state' : WF_INITIAL_STATE
  70.570 +                            , 'workflow_filename' : WF_ID_2.replace(' ', '_')
  70.571 +                            } )
  70.572 +
  70.573 +    def test_parseWorkflowXML_empty( self ):
  70.574 +
  70.575 +        WF_ID = 'empty'
  70.576 +        WF_TITLE = 'Empty DCWorkflow'
  70.577 +        WF_INITIAL_STATE = 'initial'
  70.578 +
  70.579 +        site = self._initSite()
  70.580 +
  70.581 +        configurator = self._makeOne( site ).__of__( site )
  70.582 +
  70.583 +        ( workflow_id
  70.584 +        , title
  70.585 +        , state_variable
  70.586 +        , initial_state
  70.587 +        , states
  70.588 +        , transitions
  70.589 +        , variables
  70.590 +        , worklists
  70.591 +        , permissions
  70.592 +        , scripts
  70.593 +        ) = configurator.parseWorkflowXML( _EMPTY_WORKFLOW_EXPORT
  70.594 +                                         % ( WF_ID
  70.595 +                                           , WF_TITLE
  70.596 +                                           , WF_INITIAL_STATE
  70.597 +                                           ) )
  70.598 +
  70.599 +        self.assertEqual( len( states ), 0 )
  70.600 +        self.assertEqual( len( transitions ), 0 )
  70.601 +        self.assertEqual( len( variables ), 0 )
  70.602 +        self.assertEqual( len( worklists ), 0 )
  70.603 +        self.assertEqual( len( permissions ), 0 )
  70.604 +        self.assertEqual( len( scripts ), 0 )
  70.605 +
  70.606 +    def test_parseWorkflowXML_normal_attribs( self ):
  70.607 +
  70.608 +        WF_ID = 'normal'
  70.609 +        WF_TITLE = 'Normal DCWorkflow'
  70.610 +        WF_INITIAL_STATE = 'closed'
  70.611 +
  70.612 +        site = self._initSite()
  70.613 +
  70.614 +        configurator = self._makeOne( site ).__of__( site )
  70.615 +
  70.616 +        ( workflow_id
  70.617 +        , title
  70.618 +        , state_variable
  70.619 +        , initial_state
  70.620 +        , states
  70.621 +        , transitions
  70.622 +        , variables
  70.623 +        , worklists
  70.624 +        , permissions
  70.625 +        , scripts
  70.626 +        ) = configurator.parseWorkflowXML(
  70.627 +                          _NORMAL_WORKFLOW_EXPORT
  70.628 +                          % { 'workflow_id' : WF_ID
  70.629 +                            , 'title' : WF_TITLE
  70.630 +                            , 'initial_state' : WF_INITIAL_STATE
  70.631 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.632 +                            } )
  70.633 +
  70.634 +        self.assertEqual( workflow_id, WF_ID )
  70.635 +        self.assertEqual( title, WF_TITLE )
  70.636 +        self.assertEqual( state_variable, 'state' )
  70.637 +        self.assertEqual( initial_state, WF_INITIAL_STATE )
  70.638 +
  70.639 +    def test_parseWorkflowXML_normal_states( self ):
  70.640 +
  70.641 +        WF_ID = 'normal'
  70.642 +        WF_TITLE = 'Normal DCWorkflow'
  70.643 +        WF_INITIAL_STATE = 'closed'
  70.644 +
  70.645 +        site = self._initSite()
  70.646 +
  70.647 +        configurator = self._makeOne( site ).__of__( site )
  70.648 +
  70.649 +        ( workflow_id
  70.650 +        , title
  70.651 +        , state_variable
  70.652 +        , initial_state
  70.653 +        , states
  70.654 +        , transitions
  70.655 +        , variables
  70.656 +        , worklists
  70.657 +        , permissions
  70.658 +        , scripts
  70.659 +        ) = configurator.parseWorkflowXML(
  70.660 +                          _NORMAL_WORKFLOW_EXPORT
  70.661 +                          % { 'workflow_id' : WF_ID
  70.662 +                            , 'title' : WF_TITLE
  70.663 +                            , 'initial_state' : WF_INITIAL_STATE
  70.664 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.665 +                            } )
  70.666 +
  70.667 +        self.assertEqual( len( states ), len( _WF_STATES ) )
  70.668 +
  70.669 +        for state in states:
  70.670 +
  70.671 +            state_id = state[ 'state_id' ]
  70.672 +            self.failUnless( state_id in _WF_STATES )
  70.673 +
  70.674 +            expected = _WF_STATES[ state_id ]
  70.675 +
  70.676 +            self.assertEqual( state[ 'title' ], expected[ 0 ] )
  70.677 +
  70.678 +            description = ''.join( state[ 'description' ] )
  70.679 +            self.failUnless( expected[ 1 ] in description )
  70.680 +
  70.681 +            self.assertEqual( tuple( state[ 'transitions' ] ), expected[ 2 ] )
  70.682 +            self.assertEqual( state[ 'permissions' ], expected[ 3 ] )
  70.683 +            self.assertEqual( tuple( state[ 'groups' ] )
  70.684 +                            , tuple( expected[ 4 ] ) )
  70.685 +
  70.686 +            for k, v_info in state[ 'variables' ].items():
  70.687 +
  70.688 +                exp_value = expected[ 5 ][ k ]
  70.689 +                self.assertEqual( v_info[ 'value' ], str( exp_value ) )
  70.690 +
  70.691 +                if isinstance( exp_value, bool ):
  70.692 +                    self.assertEqual( v_info[ 'type' ], 'bool' )
  70.693 +                elif isinstance( exp_value, int ):
  70.694 +                    self.assertEqual( v_info[ 'type' ], 'int' )
  70.695 +                elif isinstance( exp_value, float ):
  70.696 +                    self.assertEqual( v_info[ 'type' ], 'float' )
  70.697 +                elif isinstance( exp_value, basestring ):
  70.698 +                    self.assertEqual( v_info[ 'type' ], 'string' )
  70.699 +
  70.700 +    def test_parseWorkflowXML_normal_transitions( self ):
  70.701 +
  70.702 +        from Products.DCWorkflow.exportimport import TRIGGER_TYPES
  70.703 +
  70.704 +        WF_ID = 'normal'
  70.705 +        WF_TITLE = 'Normal DCWorkflow'
  70.706 +        WF_INITIAL_STATE = 'closed'
  70.707 +
  70.708 +        site = self._initSite()
  70.709 +
  70.710 +        configurator = self._makeOne( site ).__of__( site )
  70.711 +
  70.712 +        ( workflow_id
  70.713 +        , title
  70.714 +        , state_variable
  70.715 +        , initial_state
  70.716 +        , states
  70.717 +        , transitions
  70.718 +        , variables
  70.719 +        , worklists
  70.720 +        , permissions
  70.721 +        , scripts
  70.722 +        ) = configurator.parseWorkflowXML(
  70.723 +                          _NORMAL_WORKFLOW_EXPORT
  70.724 +                          % { 'workflow_id' : WF_ID
  70.725 +                            , 'title' : WF_TITLE
  70.726 +                            , 'initial_state' : WF_INITIAL_STATE
  70.727 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.728 +                            } )
  70.729 +
  70.730 +        self.assertEqual( len( transitions ), len( _WF_TRANSITIONS ) )
  70.731 +
  70.732 +        for transition in transitions:
  70.733 +
  70.734 +            transition_id = transition[ 'transition_id' ]
  70.735 +            self.failUnless( transition_id in _WF_TRANSITIONS )
  70.736 +
  70.737 +            expected = _WF_TRANSITIONS[ transition_id ]
  70.738 +
  70.739 +            self.assertEqual( transition[ 'title' ], expected[ 0 ] )
  70.740 +
  70.741 +            description = ''.join( transition[ 'description' ] )
  70.742 +            self.failUnless( expected[ 1 ] in description )
  70.743 +
  70.744 +            self.assertEqual( transition[ 'new_state' ], expected[ 2 ] )
  70.745 +            self.assertEqual( transition[ 'trigger' ]
  70.746 +                            , TRIGGER_TYPES[ expected[ 3 ] ] )
  70.747 +            self.assertEqual( transition[ 'before_script' ], expected[ 4 ] )
  70.748 +            self.assertEqual( transition[ 'after_script' ]
  70.749 +                            , expected[ 5 ] )
  70.750 +
  70.751 +            action = transition[ 'action' ]
  70.752 +            self.assertEqual( action.get( 'name', '' ), expected[ 6 ] )
  70.753 +            self.assertEqual( action.get( 'url', '' ), expected[ 7 ] )
  70.754 +            self.assertEqual( action.get( 'category', '' ), expected[ 8 ] )
  70.755 +
  70.756 +            self.assertEqual( transition[ 'variables' ], expected[ 9 ] )
  70.757 +
  70.758 +            guard = transition[ 'guard' ]
  70.759 +            self.assertEqual( tuple( guard.get( 'permissions', () ) )
  70.760 +                            , expected[ 10 ] )
  70.761 +            self.assertEqual( tuple( guard.get( 'roles', () ) )
  70.762 +                            , expected[ 11 ] )
  70.763 +            self.assertEqual( tuple( guard.get( 'groups', () ) )
  70.764 +                            , expected[ 12 ] )
  70.765 +            self.assertEqual( guard.get( 'expression', '' ), expected[ 13 ] )
  70.766 +
  70.767 +    def test_parseWorkflowXML_normal_variables( self ):
  70.768 +
  70.769 +        from Products.DCWorkflow.exportimport import TRIGGER_TYPES
  70.770 +
  70.771 +        WF_ID = 'normal'
  70.772 +        WF_TITLE = 'Normal DCWorkflow'
  70.773 +        WF_INITIAL_STATE = 'closed'
  70.774 +
  70.775 +        site = self._initSite()
  70.776 +
  70.777 +        configurator = self._makeOne( site ).__of__( site )
  70.778 +
  70.779 +        ( workflow_id
  70.780 +        , title
  70.781 +        , state_variable
  70.782 +        , initial_state
  70.783 +        , states
  70.784 +        , transitions
  70.785 +        , variables
  70.786 +        , worklists
  70.787 +        , permissions
  70.788 +        , scripts
  70.789 +        ) = configurator.parseWorkflowXML(
  70.790 +                          _NORMAL_WORKFLOW_EXPORT
  70.791 +                          % { 'workflow_id' : WF_ID
  70.792 +                            , 'title' : WF_TITLE
  70.793 +                            , 'initial_state' : WF_INITIAL_STATE
  70.794 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.795 +                            } )
  70.796 +
  70.797 +        self.assertEqual( len( variables ), len( _WF_VARIABLES ) )
  70.798 +
  70.799 +        for variable in variables:
  70.800 +
  70.801 +            variable_id = variable[ 'variable_id' ]
  70.802 +            self.failUnless( variable_id in _WF_VARIABLES )
  70.803 +
  70.804 +            expected = _WF_VARIABLES[ variable_id ]
  70.805 +
  70.806 +            description = ''.join( variable[ 'description' ] )
  70.807 +            self.failUnless( expected[ 0 ] in description )
  70.808 +
  70.809 +            default = variable[ 'default' ]
  70.810 +            self.assertEqual( default[ 'value' ], expected[ 1 ] )
  70.811 +
  70.812 +            exp_type = 'n/a'
  70.813 +
  70.814 +            if expected[ 1 ]:
  70.815 +                exp_value = expected[ 1 ]
  70.816 +
  70.817 +                if isinstance( exp_value, bool ):
  70.818 +                    exp_type = 'bool'
  70.819 +                elif isinstance( exp_value, int ):
  70.820 +                    exp_type = 'int'
  70.821 +                elif isinstance( exp_value, float ):
  70.822 +                    exp_type = 'float'
  70.823 +                elif isinstance( exp_value, basestring ):
  70.824 +                    exp_type = 'string'
  70.825 +                else:
  70.826 +                    exp_type = 'XXX'
  70.827 +
  70.828 +            self.assertEqual( default[ 'type' ], exp_type )
  70.829 +            self.assertEqual( default[ 'expression' ], expected[ 2 ] )
  70.830 +
  70.831 +            self.assertEqual( variable[ 'for_catalog' ], expected[ 3 ] )
  70.832 +            self.assertEqual( variable[ 'for_status' ], expected[ 4 ] )
  70.833 +            self.assertEqual( variable[ 'update_always' ], expected[ 5 ] )
  70.834 +
  70.835 +            guard = variable[ 'guard' ]
  70.836 +            self.assertEqual( tuple( guard.get( 'permissions', () ) )
  70.837 +                            , expected[ 6 ] )
  70.838 +            self.assertEqual( tuple( guard.get( 'roles', () ) )
  70.839 +                            , expected[ 7 ] )
  70.840 +            self.assertEqual( tuple( guard.get( 'groups', () ) )
  70.841 +                            , expected[ 8 ] )
  70.842 +            self.assertEqual( guard.get( 'expression', '' ), expected[ 9 ] )
  70.843 +
  70.844 +    def test_parseWorkflowXML_normal_worklists( self ):
  70.845 +
  70.846 +        from Products.DCWorkflow.exportimport import TRIGGER_TYPES
  70.847 +
  70.848 +        WF_ID = 'normal'
  70.849 +        WF_TITLE = 'Normal DCWorkflow'
  70.850 +        WF_INITIAL_STATE = 'closed'
  70.851 +
  70.852 +        site = self._initSite()
  70.853 +
  70.854 +        configurator = self._makeOne( site ).__of__( site )
  70.855 +
  70.856 +        ( workflow_id
  70.857 +        , title
  70.858 +        , state_variable
  70.859 +        , initial_state
  70.860 +        , states
  70.861 +        , transitions
  70.862 +        , variables
  70.863 +        , worklists
  70.864 +        , permissions
  70.865 +        , scripts
  70.866 +        ) = configurator.parseWorkflowXML(
  70.867 +                          _NORMAL_WORKFLOW_EXPORT
  70.868 +                          % { 'workflow_id' : WF_ID
  70.869 +                            , 'title' : WF_TITLE
  70.870 +                            , 'initial_state' : WF_INITIAL_STATE
  70.871 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.872 +                            } )
  70.873 +
  70.874 +        self.assertEqual( len( worklists ), len( _WF_WORKLISTS ) )
  70.875 +
  70.876 +        for worklist in worklists:
  70.877 +
  70.878 +            worklist_id = worklist[ 'worklist_id' ]
  70.879 +            self.failUnless( worklist_id in _WF_WORKLISTS )
  70.880 +
  70.881 +            expected = _WF_WORKLISTS[ worklist_id ]
  70.882 +
  70.883 +            self.assertEqual( worklist[ 'title' ], expected[ 0 ] )
  70.884 +
  70.885 +            description = ''.join( worklist[ 'description' ] )
  70.886 +            self.failUnless( expected[ 1 ] in description )
  70.887 +
  70.888 +            self.assertEqual( tuple( worklist[ 'match' ] )
  70.889 +                            , tuple( expected[ 2 ] ) )
  70.890 +
  70.891 +            action = worklist[ 'action' ]
  70.892 +            self.assertEqual( action.get( 'name', '' ), expected[ 3 ] )
  70.893 +            self.assertEqual( action.get( 'url', '' ), expected[ 4 ] )
  70.894 +            self.assertEqual( action.get( 'category', '' ), expected[ 5 ] )
  70.895 +
  70.896 +            guard = worklist[ 'guard' ]
  70.897 +            self.assertEqual( tuple( guard.get( 'permissions', () ) )
  70.898 +                            , expected[ 6 ] )
  70.899 +            self.assertEqual( tuple( guard.get( 'roles', () ) )
  70.900 +                            , expected[ 7 ] )
  70.901 +            self.assertEqual( tuple( guard.get( 'groups', () ) )
  70.902 +                            , expected[ 8 ] )
  70.903 +            self.assertEqual( guard.get( 'expression', '' ), expected[ 9 ] )
  70.904 +
  70.905 +    def test_parseWorkflowXML_normal_permissions( self ):
  70.906 +
  70.907 +        from Products.DCWorkflow.exportimport import TRIGGER_TYPES
  70.908 +
  70.909 +        WF_ID = 'normal'
  70.910 +        WF_TITLE = 'Normal DCWorkflow'
  70.911 +        WF_INITIAL_STATE = 'closed'
  70.912 +
  70.913 +        site = self._initSite()
  70.914 +
  70.915 +        configurator = self._makeOne( site ).__of__( site )
  70.916 +
  70.917 +        ( workflow_id
  70.918 +        , title
  70.919 +        , state_variable
  70.920 +        , initial_state
  70.921 +        , states
  70.922 +        , transitions
  70.923 +        , variables
  70.924 +        , worklists
  70.925 +        , permissions
  70.926 +        , scripts
  70.927 +        ) = configurator.parseWorkflowXML(
  70.928 +                          _NORMAL_WORKFLOW_EXPORT
  70.929 +                          % { 'workflow_id' : WF_ID
  70.930 +                            , 'title' : WF_TITLE
  70.931 +                            , 'initial_state' : WF_INITIAL_STATE
  70.932 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.933 +                            } )
  70.934 +
  70.935 +        self.assertEqual( len( permissions ), len( _WF_PERMISSIONS ) )
  70.936 +
  70.937 +        for permission in permissions:
  70.938 +
  70.939 +            self.failUnless( permission in _WF_PERMISSIONS )
  70.940 +
  70.941 +    def test_parseWorkflowXML_normal_scripts( self ):
  70.942 +
  70.943 +        from Products.DCWorkflow.exportimport import TRIGGER_TYPES
  70.944 +
  70.945 +        WF_ID = 'normal'
  70.946 +        WF_TITLE = 'Normal DCWorkflow'
  70.947 +        WF_INITIAL_STATE = 'closed'
  70.948 +
  70.949 +        site = self._initSite()
  70.950 +
  70.951 +        configurator = self._makeOne( site ).__of__( site )
  70.952 +
  70.953 +        ( workflow_id
  70.954 +        , title
  70.955 +        , state_variable
  70.956 +        , initial_state
  70.957 +        , states
  70.958 +        , transitions
  70.959 +        , variables
  70.960 +        , worklists
  70.961 +        , permissions
  70.962 +        , scripts
  70.963 +        ) = configurator.parseWorkflowXML(
  70.964 +                          _NORMAL_WORKFLOW_EXPORT
  70.965 +                          % { 'workflow_id' : WF_ID
  70.966 +                            , 'title' : WF_TITLE
  70.967 +                            , 'initial_state' : WF_INITIAL_STATE
  70.968 +                            , 'workflow_filename' : WF_ID.replace(' ', '_')
  70.969 +                            } )
  70.970 +
  70.971 +        self.assertEqual( len( scripts ), len( _WF_SCRIPTS ) )
  70.972 +
  70.973 +        for script in scripts:
  70.974 +
  70.975 +            script_id = script[ 'script_id' ]
  70.976 +            self.failUnless( script_id in _WF_SCRIPTS )
  70.977 +
  70.978 +            expected = _WF_SCRIPTS[ script_id ]
  70.979 +
  70.980 +            self.assertEqual( script[ 'meta_type' ], expected[ 0 ] )
  70.981 +
  70.982 +            # Body is not kept as part of the workflow XML
  70.983 +
  70.984 +            if script[ 'meta_type' ] == PythonScript.meta_type:
  70.985 +                self.assertEqual( script[ 'filename' ]
  70.986 +                                , expected[ 2 ] % workflow_id )
  70.987 +            else:
  70.988 +                self.assertEqual( script[ 'filename' ], expected[ 2 ] )
  70.989 +
  70.990 +
  70.991 +_WF_PERMISSIONS = \
  70.992 +( 'Open content for modifications'
  70.993 +, 'Modify content'
  70.994 +, 'Query history'
  70.995 +, 'Restore expired content'
  70.996 +)
  70.997 +
  70.998 +_WF_GROUPS = \
  70.999 +( 'Content_owners'
 70.1000 +, 'Content_assassins'
 70.1001 +)
 70.1002 +
 70.1003 +_WF_VARIABLES = \
 70.1004 +{ 'when_opened':  ( 'Opened when'
 70.1005 +                  , ''
 70.1006 +                  , "python:None"
 70.1007 +                  , True
 70.1008 +                  , False
 70.1009 +                  , True
 70.1010 +                  , ( 'Query history', 'Open content for modifications' )
 70.1011 +                  , ()
 70.1012 +                  , ()
 70.1013 +                  , ""
 70.1014 +                  )
 70.1015 +, 'when_expired': ( 'Expired when'
 70.1016 +                  , ''
 70.1017 +                  , "nothing"
 70.1018 +                  , True
 70.1019 +                  , False
 70.1020 +                  , True
 70.1021 +                  , ( 'Query history', 'Open content for modifications' )
 70.1022 +                  , ()
 70.1023 +                  , ()
 70.1024 +                  , ""
 70.1025 +                  )
 70.1026 +, 'killed_by':    ( 'Killed by'
 70.1027 +                  , 'n/a'
 70.1028 +                  , ""
 70.1029 +                  , True
 70.1030 +                  , False
 70.1031 +                  , True
 70.1032 +                  , ()
 70.1033 +                  , ( 'Hangman', 'Sherrif' )
 70.1034 +                  , ()
 70.1035 +                  , ""
 70.1036 +                  )
 70.1037 +}
 70.1038 +
 70.1039 +_WF_STATES = \
 70.1040 +{ 'closed':  ( 'Closed'
 70.1041 +             , 'Closed for modifications'
 70.1042 +             , ( 'open', 'kill', 'expire' )
 70.1043 +             , { 'Modify content':  () }
 70.1044 +             , ()
 70.1045 +             , { 'is_opened':  False, 'is_closed':  True }
 70.1046 +             )
 70.1047 +, 'opened':  ( 'Opened'
 70.1048 +             , 'Open for modifications'
 70.1049 +             , ( 'close', 'kill', 'expire' )
 70.1050 +             , { 'Modify content':  [ 'Owner', 'Manager' ] }
 70.1051 +             , [ ( 'Content_owners', ( 'Owner', ) ) ]
 70.1052 +             , { 'is_opened':  True, 'is_closed':  False }
 70.1053 +             )
 70.1054 +, 'killed':  ( 'Killed'
 70.1055 +             , 'Permanently unavailable'
 70.1056 +             , ()
 70.1057 +             , {}
 70.1058 +             , ()
 70.1059 +             , {}
 70.1060 +             )
 70.1061 +, 'expired': ( 'Expired'
 70.1062 +             , 'Expiration date has passed'
 70.1063 +             , ( 'open', )
 70.1064 +             , { 'Modify content':  [ 'Owner', 'Manager' ] }
 70.1065 +             , ()
 70.1066 +             , { 'is_opened':  False, 'is_closed':  False }
 70.1067 +             )
 70.1068 +}
 70.1069 +
 70.1070 +_WF_TRANSITIONS = \
 70.1071 +{ 'open':    ( 'Open'
 70.1072 +             , 'Open the object for modifications'
 70.1073 +             , 'opened'
 70.1074 +             , TRIGGER_USER_ACTION
 70.1075 +             , 'before_open'
 70.1076 +             , ''
 70.1077 +             , 'Open'
 70.1078 +             , 'string:${object_url}/open_for_modifications'
 70.1079 +             , 'workflow'
 70.1080 +             , { 'when_opened' : 'object/ZopeTime' }
 70.1081 +             , ( 'Open content for modifications', )
 70.1082 +             , ()
 70.1083 +             , ()
 70.1084 +             , ""
 70.1085 +             )
 70.1086 +, 'close':   ( 'Close'
 70.1087 +             , 'Close the object for modifications'
 70.1088 +             , 'closed'
 70.1089 +             , TRIGGER_USER_ACTION
 70.1090 +             , ''
 70.1091 +             , 'after_close'
 70.1092 +             , 'Close'
 70.1093 +             , 'string:${object_url}/close_for_modifications'
 70.1094 +             , 'workflow'
 70.1095 +             , {}
 70.1096 +             , ()
 70.1097 +             , ( 'Owner', 'Manager' )
 70.1098 +             , ()
 70.1099 +             , ""
 70.1100 +             )
 70.1101 +, 'kill':    ( 'Kill'
 70.1102 +             , 'Make the object permanently unavailable.'
 70.1103 +             , 'killed'
 70.1104 +             , TRIGGER_USER_ACTION
 70.1105 +             , ''
 70.1106 +             , 'after_kill'
 70.1107 +             , 'Kill'
 70.1108 +             , 'string:${object_url}/kill_object'
 70.1109 +             , 'workflow'
 70.1110 +             , { 'killed_by' : 'string:${user/getId}' }
 70.1111 +             , ()
 70.1112 +             , ()
 70.1113 +             , ( 'Content_assassins', )
 70.1114 +             , ""
 70.1115 +             )
 70.1116 +, 'expire':  ( 'Expire'
 70.1117 +             , 'Retire objects whose expiration is past.'
 70.1118 +             , 'expired'
 70.1119 +             , TRIGGER_AUTOMATIC
 70.1120 +             , 'before_expire'
 70.1121 +             , ''
 70.1122 +             , ''
 70.1123 +             , ''
 70.1124 +             , ''
 70.1125 +             , { 'when_expired' : 'object/ZopeTime' }
 70.1126 +             , ()
 70.1127 +             , ()
 70.1128 +             , ()
 70.1129 +             , "python: object.expiration() <= object.ZopeTime()"
 70.1130 +             )
 70.1131 +}
 70.1132 +
 70.1133 +_WF_WORKLISTS = \
 70.1134 +{ 'expired_list':   ( 'Expired'
 70.1135 +                    , 'Worklist for expired content'
 70.1136 +                    , { 'state' : ( 'expired', ) }
 70.1137 +                    , 'Expired items'
 70.1138 +                    , 'string:${portal_url}/expired_items'
 70.1139 +                    , 'workflow'
 70.1140 +                    , ( 'Restore expired content', )
 70.1141 +                    , ()
 70.1142 +                    , ()
 70.1143 +                    , ""
 70.1144 +                    )
 70.1145 +, 'alive_list':     ( 'Alive'
 70.1146 +                    , 'Worklist for content not yet expired / killed'
 70.1147 +                    , { 'state' : ( 'open',  'closed' ) }
 70.1148 +                    , 'Expired items'
 70.1149 +                    , 'string:${portal_url}/expired_items'
 70.1150 +                    , 'workflow'
 70.1151 +                    , ( 'Restore expired content', )
 70.1152 +                    , ()
 70.1153 +                    , ()
 70.1154 +                    , ""
 70.1155 +                    )
 70.1156 +}
 70.1157 +
 70.1158 +_BEFORE_OPEN_SCRIPT = """\
 70.1159 +## Script (Python) "before_open"
 70.1160 +##bind container=container
 70.1161 +##bind context=context
 70.1162 +##bind namespace=
 70.1163 +##bind script=script
 70.1164 +##bind subpath=traverse_subpath
 70.1165 +##parameters=
 70.1166 +##title=
 70.1167 +##
 70.1168 +return 'before_open'
 70.1169 +"""
 70.1170 +
 70.1171 +_AFTER_CLOSE_SCRIPT = """\
 70.1172 +## Script (Python) "after_close"
 70.1173 +##bind container=container
 70.1174 +##bind context=context
 70.1175 +##bind namespace=
 70.1176 +##bind script=script
 70.1177 +##bind subpath=traverse_subpath
 70.1178 +##parameters=
 70.1179 +##title=
 70.1180 +##
 70.1181 +return 'after_close'
 70.1182 +"""
 70.1183 +
 70.1184 +_AFTER_KILL_SCRIPT = """\
 70.1185 +## Script (Python) "after_kill"
 70.1186 +##bind container=container
 70.1187 +##bind context=context
 70.1188 +##bind namespace=
 70.1189 +##bind script=script
 70.1190 +##bind subpath=traverse_subpath
 70.1191 +##parameters=
 70.1192 +##title=
 70.1193 +##
 70.1194 +return 'after_kill'
 70.1195 +"""
 70.1196 +
 70.1197 +_WF_SCRIPTS = \
 70.1198 +{ 'before_open':    ( PythonScript.meta_type
 70.1199 +                    , _BEFORE_OPEN_SCRIPT
 70.1200 +                    , 'workflows/%s/scripts/before_open.py'
 70.1201 +                    , None
 70.1202 +                    , None
 70.1203 +                    )
 70.1204 +, 'after_close':    ( PythonScript.meta_type
 70.1205 +                    , _AFTER_CLOSE_SCRIPT
 70.1206 +                    , 'workflows/%s/scripts/after_close.py'
 70.1207 +                    , None
 70.1208 +                    , None
 70.1209 +                    )
 70.1210 +, 'after_kill':     ( PythonScript.meta_type
 70.1211 +                    , _AFTER_KILL_SCRIPT
 70.1212 +                    , 'workflows/%s/scripts/after_kill.py'
 70.1213 +                    , None
 70.1214 +                    , None
 70.1215 +                    )
 70.1216 +, 'before_expire': ( ExternalMethod.meta_type
 70.1217 +                   , ''
 70.1218 +                   , ''
 70.1219 +                   , 'DCWorkflow.test_method'
 70.1220 +                   , 'test'
 70.1221 +                   )
 70.1222 +}
 70.1223 +
 70.1224 +_NORMAL_TOOL_EXPORT = """\
 70.1225 +<?xml version="1.0"?>
 70.1226 +<object name="portal_workflow" meta_type="Dummy Workflow Tool">
 70.1227 + <property name="title"></property>
 70.1228 + <object name="Non-DCWorkflow" meta_type="Dummy Workflow"/>
 70.1229 + <object name="dcworkflow" meta_type="Workflow"/>
 70.1230 + <bindings>
 70.1231 +  <default/>
 70.1232 + </bindings>
 70.1233 +</object>
 70.1234 +"""
 70.1235 +
 70.1236 +_NORMAL_TOOL_EXPORT_WITH_FILENAME = """\
 70.1237 +<?xml version="1.0"?>
 70.1238 +<object name="portal_workflow" meta_type="Dummy Workflow Tool">
 70.1239 + <property name="title"></property>
 70.1240 + <object name="Non-DCWorkflow" meta_type="Dummy Workflow"/>
 70.1241 + <object name="%(workflow_id)s" meta_type="Workflow"/>
 70.1242 + <bindings>
 70.1243 +  <default/>
 70.1244 + </bindings>
 70.1245 +</object>
 70.1246 +"""
 70.1247 +
 70.1248 +_FILENAME_TOOL_EXPORT = """\
 70.1249 +<?xml version="1.0"?>
 70.1250 +<object name="portal_workflow" meta_type="Dummy Workflow Tool">
 70.1251 + <property name="title"></property>
 70.1252 + <object name="name with spaces" meta_type="Workflow"/>
 70.1253 + <bindings>
 70.1254 +  <default/>
 70.1255 + </bindings>
 70.1256 +</object>
 70.1257 +"""
 70.1258 +
 70.1259 +_EMPTY_WORKFLOW_EXPORT = """\
 70.1260 +<?xml version="1.0"?>
 70.1261 +<dc-workflow
 70.1262 +    workflow_id="%s"
 70.1263 +    title="%s"
 70.1264 +    state_variable="state"
 70.1265 +    initial_state="%s">
 70.1266 +</dc-workflow>
 70.1267 +"""
 70.1268 +
 70.1269 +# Make sure old exports are still imported well. Changes:
 70.1270 +# - scripts are now in in a 'scripts' subdirectory
 70.1271 +_OLD_WORKFLOW_EXPORT = """\
 70.1272 +<?xml version="1.0"?>
 70.1273 +<dc-workflow
 70.1274 +    workflow_id="%(workflow_id)s"
 70.1275 +    title="%(title)s"
 70.1276 +    state_variable="state"
 70.1277 +    initial_state="%(initial_state)s">
 70.1278 + <permission>Open content for modifications</permission>
 70.1279 + <permission>Modify content</permission>
 70.1280 + <permission>Query history</permission>
 70.1281 + <permission>Restore expired content</permission>
 70.1282 + <state
 70.1283 +    state_id="closed"
 70.1284 +    title="Closed">
 70.1285 +  <description>Closed for modifications</description>
 70.1286 +  <exit-transition
 70.1287 +    transition_id="open"/>
 70.1288 +  <exit-transition
 70.1289 +    transition_id="kill"/>
 70.1290 +  <exit-transition
 70.1291 +    transition_id="expire"/>
 70.1292 +  <permission-map
 70.1293 +    acquired="False"
 70.1294 +    name="Modify content">
 70.1295 +  </permission-map>
 70.1296 +  <assignment
 70.1297 +    name="is_closed"
 70.1298 +    type="bool">True</assignment>
 70.1299 +  <assignment
 70.1300 +    name="is_opened"
 70.1301 +    type="bool">False</assignment>
 70.1302 + </state>
 70.1303 + <state
 70.1304 +    state_id="expired"
 70.1305 +    title="Expired">
 70.1306 +  <description>Expiration date has passed</description>
 70.1307 +  <exit-transition
 70.1308 +    transition_id="open"/>
 70.1309 +  <permission-map
 70.1310 +    acquired="True"
 70.1311 +    name="Modify content">
 70.1312 +   <permission-role>Owner</permission-role>
 70.1313 +   <permission-role>Manager</permission-role>
 70.1314 +  </permission-map>
 70.1315 +  <assignment
 70.1316 +    name="is_closed"
 70.1317 +    type="bool">False</assignment>
 70.1318 +  <assignment
 70.1319 +    name="is_opened"
 70.1320 +    type="bool">False</assignment>
 70.1321 + </state>
 70.1322 + <state
 70.1323 +    state_id="killed"
 70.1324 +    title="Killed">
 70.1325 +  <description>Permanently unavailable</description>
 70.1326 + </state>
 70.1327 + <state
 70.1328 +    state_id="opened"
 70.1329 +    title="Opened">
 70.1330 +  <description>Open for modifications</description>
 70.1331 +  <exit-transition
 70.1332 +    transition_id="close"/>
 70.1333 +  <exit-transition
 70.1334 +    transition_id="kill"/>
 70.1335 +  <exit-transition
 70.1336 +    transition_id="expire"/>
 70.1337 +  <permission-map
 70.1338 +    acquired="True"
 70.1339 +    name="Modify content">
 70.1340 +   <permission-role>Owner</permission-role>
 70.1341 +   <permission-role>Manager</permission-role>
 70.1342 +  </permission-map>
 70.1343 +  <group-map name="Content_owners">
 70.1344 +   <group-role>Owner</group-role>
 70.1345 +  </group-map>
 70.1346 +  <assignment
 70.1347 +    name="is_closed"
 70.1348 +    type="bool">False</assignment>
 70.1349 +  <assignment
 70.1350 +    name="is_opened"
 70.1351 +    type="bool">True</assignment>
 70.1352 + </state>
 70.1353 + <transition
 70.1354 +    transition_id="close"
 70.1355 +    title="Close"
 70.1356 +    trigger="USER"
 70.1357 +    new_state="closed"
 70.1358 +    before_script=""
 70.1359 +    after_script="after_close">
 70.1360 +  <description>Close the object for modifications</description>
 70.1361 +  <action
 70.1362 +    category="workflow"
 70.1363 +    url="string:${object_url}/close_for_modifications">Close</action>
 70.1364 +  <guard>
 70.1365 +   <guard-role>Owner</guard-role>
 70.1366 +   <guard-role>Manager</guard-role>
 70.1367 +  </guard>
 70.1368 + </transition>
 70.1369 + <transition
 70.1370 +    transition_id="expire"
 70.1371 +    title="Expire"
 70.1372 +    trigger="AUTOMATIC"
 70.1373 +    new_state="expired"
 70.1374 +    before_script="before_expire"
 70.1375 +    after_script="">
 70.1376 +  <description>Retire objects whose expiration is past.</description>
 70.1377 +  <guard>
 70.1378 +   <guard-expression>python: object.expiration() &lt;= object.ZopeTime()</guard-expression>
 70.1379 +  </guard>
 70.1380 +  <assignment
 70.1381 +    name="when_expired">object/ZopeTime</assignment>
 70.1382 + </transition>
 70.1383 + <transition
 70.1384 +    transition_id="kill"
 70.1385 +    title="Kill"
 70.1386 +    trigger="USER"
 70.1387 +    new_state="killed"
 70.1388 +    before_script=""
 70.1389 +    after_script="after_kill">
 70.1390 +  <description>Make the object permanently unavailable.</description>
 70.1391 +  <action
 70.1392 +    category="workflow"
 70.1393 +    url="string:${object_url}/kill_object">Kill</action>
 70.1394 +  <guard>
 70.1395 +   <guard-group>Content_assassins</guard-group>
 70.1396 +  </guard>
 70.1397 +  <assignment
 70.1398 +    name="killed_by">string:${user/getId}</assignment>
 70.1399 + </transition>
 70.1400 + <transition
 70.1401 +    transition_id="open"
 70.1402 +    title="Open"
 70.1403 +    trigger="USER"
 70.1404 +    new_state="opened"
 70.1405 +    before_script="before_open"
 70.1406 +    after_script="">
 70.1407 +  <description>Open the object for modifications</description>
 70.1408 +  <action
 70.1409 +    category="workflow"
 70.1410 +    url="string:${object_url}/open_for_modifications">Open</action>
 70.1411 +  <guard>
 70.1412 +   <guard-permission>Open content for modifications</guard-permission>
 70.1413 +  </guard>
 70.1414 +  <assignment
 70.1415 +    name="when_opened">object/ZopeTime</assignment>
 70.1416 + </transition>
 70.1417 + <worklist
 70.1418 +    worklist_id="alive_list"
 70.1419 +    title="Alive">
 70.1420 +  <description>Worklist for content not yet expired / killed</description>
 70.1421 +  <action
 70.1422 +    category="workflow"
 70.1423 +    url="string:${portal_url}/expired_items">Expired items</action>
 70.1424 +  <guard>
 70.1425 +   <guard-permission>Restore expired content</guard-permission>
 70.1426 +  </guard>
 70.1427 +  <match name="state" values="open; closed"/>
 70.1428 + </worklist>
 70.1429 + <worklist
 70.1430 +    worklist_id="expired_list"
 70.1431 +    title="Expired">
 70.1432 +  <description>Worklist for expired content</description>
 70.1433 +  <action
 70.1434 +    category="workflow"
 70.1435 +    url="string:${portal_url}/expired_items">Expired items</action>
 70.1436 +  <guard>
 70.1437 +   <guard-permission>Restore expired content</guard-permission>
 70.1438 +  </guard>
 70.1439 +  <match name="state" values="expired"/>
 70.1440 + </worklist>
 70.1441 + <variable
 70.1442 +    variable_id="killed_by"
 70.1443 +    for_catalog="True"
 70.1444 +    for_status="False"
 70.1445 +    update_always="True">
 70.1446 +   <description>Killed by</description>
 70.1447 +   <default>
 70.1448 +    <value type="string">n/a</value>
 70.1449 +   </default>
 70.1450 +   <guard>
 70.1451 +    <guard-role>Hangman</guard-role>
 70.1452 +    <guard-role>Sherrif</guard-role>
 70.1453 +   </guard>
 70.1454 + </variable>
 70.1455 + <variable
 70.1456 +    variable_id="when_expired"
 70.1457 +    for_catalog="True"
 70.1458 +    for_status="False"
 70.1459 +    update_always="True">
 70.1460 +   <description>Expired when</description>
 70.1461 +   <default>
 70.1462 +    <expression>nothing</expression>
 70.1463 +   </default>
 70.1464 +   <guard>
 70.1465 +    <guard-permission>Query history</guard-permission>
 70.1466 +    <guard-permission>Open content for modifications</guard-permission>
 70.1467 +   </guard>
 70.1468 + </variable>
 70.1469 + <variable
 70.1470 +    variable_id="when_opened"
 70.1471 +    for_catalog="True"
 70.1472 +    for_status="False"
 70.1473 +    update_always="True">
 70.1474 +   <description>Opened when</description>
 70.1475 +   <default>
 70.1476 +    <expression>python:None</expression>
 70.1477 +   </default>
 70.1478 +   <guard>
 70.1479 +    <guard-permission>Query history</guard-permission>
 70.1480 +    <guard-permission>Open content for modifications</guard-permission>
 70.1481 +   </guard>
 70.1482 + </variable>
 70.1483 + <script
 70.1484 +    script_id="after_close"
 70.1485 +    type="Script (Python)"
 70.1486 +    filename="workflows/%(workflow_filename)s/after_close.py"
 70.1487 +    module=""
 70.1488 +    function=""
 70.1489 +    />
 70.1490 + <script
 70.1491 +    script_id="after_kill"
 70.1492 +    type="Script (Python)"
 70.1493 +    filename="workflows/%(workflow_filename)s/after_kill.py"
 70.1494 +    module=""
 70.1495 +    function=""
 70.1496 +    />
 70.1497 + <script
 70.1498 +    script_id="before_expire"
 70.1499 +    type="External Method"
 70.1500 +    filename=""
 70.1501 +    module="DCWorkflow.test_method"
 70.1502 +    function="test"
 70.1503 +    />
 70.1504 + <script
 70.1505 +    script_id="before_open"
 70.1506 +    type="Script (Python)"
 70.1507 +    filename="workflows/%(workflow_filename)s/before_open.py"
 70.1508 +    module=""
 70.1509 +    function=""
 70.1510 +    />
 70.1511 +</dc-workflow>
 70.1512 +"""
 70.1513 +
 70.1514 +_NORMAL_WORKFLOW_EXPORT = """\
 70.1515 +<?xml version="1.0"?>
 70.1516 +<dc-workflow
 70.1517 +    workflow_id="%(workflow_id)s"
 70.1518 +    title="%(title)s"
 70.1519 +    state_variable="state"
 70.1520 +    initial_state="%(initial_state)s">
 70.1521 + <permission>Open content for modifications</permission>
 70.1522 + <permission>Modify content</permission>
 70.1523 + <permission>Query history</permission>
 70.1524 + <permission>Restore expired content</permission>
 70.1525 + <state
 70.1526 +    state_id="closed"
 70.1527 +    title="Closed">
 70.1528 +  <description>Closed for modifications</description>
 70.1529 +  <exit-transition
 70.1530 +    transition_id="open"/>
 70.1531 +  <exit-transition
 70.1532 +    transition_id="kill"/>
 70.1533 +  <exit-transition
 70.1534 +    transition_id="expire"/>
 70.1535 +  <permission-map
 70.1536 +    acquired="False"
 70.1537 +    name="Modify content">
 70.1538 +  </permission-map>
 70.1539 +  <assignment
 70.1540 +    name="is_closed"
 70.1541 +    type="bool">True</assignment>
 70.1542 +  <assignment
 70.1543 +    name="is_opened"
 70.1544 +    type="bool">False</assignment>
 70.1545 + </state>
 70.1546 + <state
 70.1547 +    state_id="expired"
 70.1548 +    title="Expired">
 70.1549 +  <description>Expiration date has passed</description>
 70.1550 +  <exit-transition
 70.1551 +    transition_id="open"/>
 70.1552 +  <permission-map
 70.1553 +    acquired="True"
 70.1554 +    name="Modify content">
 70.1555 +   <permission-role>Owner</permission-role>
 70.1556 +   <permission-role>Manager</permission-role>
 70.1557 +  </permission-map>
 70.1558 +  <assignment
 70.1559 +    name="is_closed"
 70.1560 +    type="bool">False</assignment>
 70.1561 +  <assignment
 70.1562 +    name="is_opened"
 70.1563 +    type="bool">False</assignment>
 70.1564 + </state>
 70.1565 + <state
 70.1566 +    state_id="killed"
 70.1567 +    title="Killed">
 70.1568 +  <description>Permanently unavailable</description>
 70.1569 + </state>
 70.1570 + <state
 70.1571 +    state_id="opened"
 70.1572 +    title="Opened">
 70.1573 +  <description>Open for modifications</description>
 70.1574 +  <exit-transition
 70.1575 +    transition_id="close"/>
 70.1576 +  <exit-transition
 70.1577 +    transition_id="kill"/>
 70.1578 +  <exit-transition
 70.1579 +    transition_id="expire"/>
 70.1580 +  <permission-map
 70.1581 +    acquired="True"
 70.1582 +    name="Modify content">
 70.1583 +   <permission-role>Owner</permission-role>
 70.1584 +   <permission-role>Manager</permission-role>
 70.1585 +  </permission-map>
 70.1586 +  <group-map name="Content_owners">
 70.1587 +   <group-role>Owner</group-role>
 70.1588 +  </group-map>
 70.1589 +  <assignment
 70.1590 +    name="is_closed"
 70.1591 +    type="bool">False</assignment>
 70.1592 +  <assignment
 70.1593 +    name="is_opened"
 70.1594 +    type="bool">True</assignment>
 70.1595 + </state>
 70.1596 + <transition
 70.1597 +    transition_id="close"
 70.1598 +    title="Close"
 70.1599 +    trigger="USER"
 70.1600 +    new_state="closed"
 70.1601 +    before_script=""
 70.1602 +    after_script="after_close">
 70.1603 +  <description>Close the object for modifications</description>
 70.1604 +  <action
 70.1605 +    category="workflow"
 70.1606 +    url="string:${object_url}/close_for_modifications">Close</action>
 70.1607 +  <guard>
 70.1608 +   <guard-role>Owner</guard-role>
 70.1609 +   <guard-role>Manager</guard-role>
 70.1610 +  </guard>
 70.1611 + </transition>
 70.1612 + <transition
 70.1613 +    transition_id="expire"
 70.1614 +    title="Expire"
 70.1615 +    trigger="AUTOMATIC"
 70.1616 +    new_state="expired"
 70.1617 +    before_script="before_expire"
 70.1618 +    after_script="">
 70.1619 +  <description>Retire objects whose expiration is past.</description>
 70.1620 +  <guard>
 70.1621 +   <guard-expression>python: object.expiration() &lt;= object.ZopeTime()</guard-expression>
 70.1622 +  </guard>
 70.1623 +  <assignment
 70.1624 +    name="when_expired">object/ZopeTime</assignment>
 70.1625 + </transition>
 70.1626 + <transition
 70.1627 +    transition_id="kill"
 70.1628 +    title="Kill"
 70.1629 +    trigger="USER"
 70.1630 +    new_state="killed"
 70.1631 +    before_script=""
 70.1632 +    after_script="after_kill">
 70.1633 +  <description>Make the object permanently unavailable.</description>
 70.1634 +  <action
 70.1635 +    category="workflow"
 70.1636 +    url="string:${object_url}/kill_object">Kill</action>
 70.1637 +  <guard>
 70.1638 +   <guard-group>Content_assassins</guard-group>
 70.1639 +  </guard>
 70.1640 +  <assignment
 70.1641 +    name="killed_by">string:${user/getId}</assignment>
 70.1642 + </transition>
 70.1643 + <transition
 70.1644 +    transition_id="open"
 70.1645 +    title="Open"
 70.1646 +    trigger="USER"
 70.1647 +    new_state="opened"
 70.1648 +    before_script="before_open"
 70.1649 +    after_script="">
 70.1650 +  <description>Open the object for modifications</description>
 70.1651 +  <action
 70.1652 +    category="workflow"
 70.1653 +    url="string:${object_url}/open_for_modifications">Open</action>
 70.1654 +  <guard>
 70.1655 +   <guard-permission>Open content for modifications</guard-permission>
 70.1656 +  </guard>
 70.1657 +  <assignment
 70.1658 +    name="when_opened">object/ZopeTime</assignment>
 70.1659 + </transition>
 70.1660 + <worklist
 70.1661 +    worklist_id="alive_list"
 70.1662 +    title="Alive">
 70.1663 +  <description>Worklist for content not yet expired / killed</description>
 70.1664 +  <action
 70.1665 +    category="workflow"
 70.1666 +    url="string:${portal_url}/expired_items">Expired items</action>
 70.1667 +  <guard>
 70.1668 +   <guard-permission>Restore expired content</guard-permission>
 70.1669 +  </guard>
 70.1670 +  <match name="state" values="open; closed"/>
 70.1671 + </worklist>
 70.1672 + <worklist
 70.1673 +    worklist_id="expired_list"
 70.1674 +    title="Expired">
 70.1675 +  <description>Worklist for expired content</description>
 70.1676 +  <action
 70.1677 +    category="workflow"
 70.1678 +    url="string:${portal_url}/expired_items">Expired items</action>
 70.1679 +  <guard>
 70.1680 +   <guard-permission>Restore expired content</guard-permission>
 70.1681 +  </guard>
 70.1682 +  <match name="state" values="expired"/>
 70.1683 + </worklist>
 70.1684 + <variable
 70.1685 +    variable_id="killed_by"
 70.1686 +    for_catalog="True"
 70.1687 +    for_status="False"
 70.1688 +    update_always="True">
 70.1689 +   <description>Killed by</description>
 70.1690 +   <default>
 70.1691 +    <value type="string">n/a</value>
 70.1692 +   </default>
 70.1693 +   <guard>
 70.1694 +    <guard-role>Hangman</guard-role>
 70.1695 +    <guard-role>Sherrif</guard-role>
 70.1696 +   </guard>
 70.1697 + </variable>
 70.1698 + <variable
 70.1699 +    variable_id="when_expired"
 70.1700 +    for_catalog="True"
 70.1701 +    for_status="False"
 70.1702 +    update_always="True">
 70.1703 +   <description>Expired when</description>
 70.1704 +   <default>
 70.1705 +    <expression>nothing</expression>
 70.1706 +   </default>
 70.1707 +   <guard>
 70.1708 +    <guard-permission>Query history</guard-permission>
 70.1709 +    <guard-permission>Open content for modifications</guard-permission>
 70.1710 +   </guard>
 70.1711 + </variable>
 70.1712 + <variable
 70.1713 +    variable_id="when_opened"
 70.1714 +    for_catalog="True"
 70.1715 +    for_status="False"
 70.1716 +    update_always="True">
 70.1717 +   <description>Opened when</description>
 70.1718 +   <default>
 70.1719 +    <expression>python:None</expression>
 70.1720 +   </default>
 70.1721 +   <guard>
 70.1722 +    <guard-permission>Query history</guard-permission>
 70.1723 +    <guard-permission>Open content for modifications</guard-permission>
 70.1724 +   </guard>
 70.1725 + </variable>
 70.1726 + <script
 70.1727 +    script_id="after_close"
 70.1728 +    type="Script (Python)"
 70.1729 +    filename="workflows/%(workflow_filename)s/scripts/after_close.py"
 70.1730 +    module=""
 70.1731 +    function=""
 70.1732 +    />
 70.1733 + <script
 70.1734 +    script_id="after_kill"
 70.1735 +    type="Script (Python)"
 70.1736 +    filename="workflows/%(workflow_filename)s/scripts/after_kill.py"
 70.1737 +    module=""
 70.1738 +    function=""
 70.1739 +    />
 70.1740 + <script
 70.1741 +    script_id="before_expire"
 70.1742 +    type="External Method"
 70.1743 +    filename=""
 70.1744 +    module="DCWorkflow.test_method"
 70.1745 +    function="test"
 70.1746 +    />
 70.1747 + <script
 70.1748 +    script_id="before_open"
 70.1749 +    type="Script (Python)"
 70.1750 +    filename="workflows/%(workflow_filename)s/scripts/before_open.py"
 70.1751 +    module=""
 70.1752 +    function=""
 70.1753 +    />
 70.1754 +</dc-workflow>
 70.1755 +"""
 70.1756 +
 70.1757 +class Test_exportWorkflow( _WorkflowSetup
 70.1758 +                         , _GuardChecker
 70.1759 +                         ):
 70.1760 +
 70.1761 +    def test_empty( self ):
 70.1762 +        from Products.CMFCore.exportimport.workflow import exportWorkflowTool
 70.1763 +
 70.1764 +        site = self._initSite()
 70.1765 +        context = DummyExportContext( site )
 70.1766 +        exportWorkflowTool( context )
 70.1767 +
 70.1768 +        self.assertEqual( len( context._wrote ), 1 )
 70.1769 +        filename, text, content_type = context._wrote[ 0 ]
 70.1770 +        self.assertEqual( filename, 'workflows.xml' )
 70.1771 +        self._compareDOM( text, _EMPTY_TOOL_EXPORT )
 70.1772 +        self.assertEqual( content_type, 'text/xml' )
 70.1773 +
 70.1774 +    def test_normal( self ):
 70.1775 +        from Products.CMFCore.exportimport.workflow import exportWorkflowTool
 70.1776 +
 70.1777 +        WF_ID_NON = 'non_dcworkflow'
 70.1778 +        WF_TITLE_NON = 'Non-DCWorkflow'
 70.1779 +        WF_ID_DC = 'dcworkflow'
 70.1780 +        WF_TITLE_DC = 'DCWorkflow'
 70.1781 +        WF_INITIAL_STATE = 'closed'
 70.1782 +
 70.1783 +        site = self._initSite()
 70.1784 +
 70.1785 +        wf_tool = site.portal_workflow
 70.1786 +        nondcworkflow = DummyWorkflow( WF_TITLE_NON )
 70.1787 +        nondcworkflow.title = WF_TITLE_NON
 70.1788 +        wf_tool._setObject( WF_ID_NON, nondcworkflow )
 70.1789 +
 70.1790 +        dcworkflow = self._initDCWorkflow( WF_ID_DC )
 70.1791 +        dcworkflow.title = WF_TITLE_DC
 70.1792 +        dcworkflow.initial_state = WF_INITIAL_STATE
 70.1793 +        dcworkflow.permissions = _WF_PERMISSIONS
 70.1794 +        self._initVariables( dcworkflow )
 70.1795 +        self._initStates( dcworkflow )
 70.1796 +        self._initTransitions( dcworkflow )
 70.1797 +        self._initWorklists( dcworkflow )
 70.1798 +        self._initScripts( dcworkflow )
 70.1799 +
 70.1800 +        context = DummyExportContext( site )
 70.1801 +        exportWorkflowTool( context )
 70.1802 +
 70.1803 +        # workflows list, wf defintion and 3 scripts
 70.1804 +        self.assertEqual( len( context._wrote ), 6 )
 70.1805 +
 70.1806 +        filename, text, content_type = context._wrote[ 0 ]
 70.1807 +        self.assertEqual( filename, 'workflows.xml' )
 70.1808 +        self._compareDOM( text, _NORMAL_TOOL_EXPORT )
 70.1809 +        self.assertEqual( content_type, 'text/xml' )
 70.1810 +
 70.1811 +        filename, text, content_type = context._wrote[ 2 ]
 70.1812 +        self.assertEqual( filename, 'workflows/%s/definition.xml' % WF_ID_DC )
 70.1813 +        self._compareDOM( text
 70.1814 +                        , _NORMAL_WORKFLOW_EXPORT
 70.1815 +                          % { 'workflow_id' : WF_ID_DC
 70.1816 +                            , 'title' : WF_TITLE_DC
 70.1817 +                            , 'initial_state' : WF_INITIAL_STATE
 70.1818 +                            , 'workflow_filename' : WF_ID_DC.replace(' ', '_')
 70.1819 +                            } )
 70.1820 +        self.assertEqual( content_type, 'text/xml' )
 70.1821 +
 70.1822 +        # just testing first script
 70.1823 +        filename, text, content_type = context._wrote[ 3 ]
 70.1824 +        self.assertEqual( filename, 'workflows/%s/scripts/after_close.py' % WF_ID_DC )
 70.1825 +        self.assertEqual( text, _AFTER_CLOSE_SCRIPT)
 70.1826 +        self.assertEqual( content_type, 'text/plain' )
 70.1827 +
 70.1828 +    def test_with_filenames( self ):
 70.1829 +        from Products.CMFCore.exportimport.workflow import exportWorkflowTool
 70.1830 +
 70.1831 +        WF_ID_DC = 'name with spaces'
 70.1832 +        WF_TITLE_DC = 'DCWorkflow with spaces'
 70.1833 +        WF_INITIAL_STATE = 'closed'
 70.1834 +
 70.1835 +        site = self._initSite()
 70.1836 +
 70.1837 +        dcworkflow = self._initDCWorkflow( WF_ID_DC )
 70.1838 +        dcworkflow.title = WF_TITLE_DC
 70.1839 +        dcworkflow.initial_state = WF_INITIAL_STATE
 70.1840 +        dcworkflow.permissions = _WF_PERMISSIONS
 70.1841 +        self._initVariables( dcworkflow )
 70.1842 +        self._initStates( dcworkflow )
 70.1843 +        self._initTransitions( dcworkflow )
 70.1844 +        self._initWorklists( dcworkflow )
 70.1845 +        self._initScripts( dcworkflow )
 70.1846 +
 70.1847 +        context = DummyExportContext( site )
 70.1848 +        exportWorkflowTool( context )
 70.1849 +
 70.1850 +        # workflows list, wf defintion and 3 scripts
 70.1851 +        self.assertEqual( len( context._wrote ), 5 )
 70.1852 +
 70.1853 +        filename, text, content_type = context._wrote[ 0 ]
 70.1854 +        self.assertEqual( filename, 'workflows.xml' )
 70.1855 +        self._compareDOM( text, _FILENAME_TOOL_EXPORT )
 70.1856 +        self.assertEqual( content_type, 'text/xml' )
 70.1857 +
 70.1858 +        filename, text, content_type = context._wrote[ 1 ]
 70.1859 +        self.assertEqual( filename
 70.1860 +                        , 'workflows/name_with_spaces/definition.xml' )
 70.1861 +        self._compareDOM( text
 70.1862 +                        , _NORMAL_WORKFLOW_EXPORT
 70.1863 +                          % { 'workflow_id' : WF_ID_DC
 70.1864 +                            , 'title' : WF_TITLE_DC
 70.1865 +                            , 'initial_state' : WF_INITIAL_STATE
 70.1866 +                            , 'workflow_filename' : WF_ID_DC.replace(' ', '_')
 70.1867 +                            } )
 70.1868 +        self.assertEqual( content_type, 'text/xml' )
 70.1869 +
 70.1870 +        # just testing first script
 70.1871 +        filename, text, content_type = context._wrote[ 2 ]
 70.1872 +        self.assertEqual( filename, 'workflows/%s/scripts/after_close.py' %
 70.1873 +                          WF_ID_DC.replace(' ', '_'))
 70.1874 +        self.assertEqual( text, _AFTER_CLOSE_SCRIPT)
 70.1875 +        self.assertEqual( content_type, 'text/plain' )
 70.1876 +
 70.1877 +class Test_importWorkflow( _WorkflowSetup
 70.1878 +                         , _GuardChecker
 70.1879 +                         ):
 70.1880 +
 70.1881 +    def _importNormalWorkflow( self, wf_id, wf_title, wf_initial_state ):
 70.1882 +        from Products.CMFCore.exportimport.workflow import importWorkflowTool
 70.1883 +
 70.1884 +        site = self._initSite()
 70.1885 +        wf_tool = site.portal_workflow
 70.1886 +        workflow_filename = wf_id.replace(' ', '_')
 70.1887 +
 70.1888 +        context = DummyImportContext( site )
 70.1889 +        context._files[ 'workflows.xml'
 70.1890 +                      ] = (_NORMAL_TOOL_EXPORT_WITH_FILENAME
 70.1891 +                            % { 'workflow_id' : wf_id
 70.1892 +                              }
 70.1893 +                          )
 70.1894 +
 70.1895 +        context._files[ 'workflows/%s/definition.xml' % workflow_filename
 70.1896 +                      ] = ( _NORMAL_WORKFLOW_EXPORT
 70.1897 +                            % { 'workflow_id' : wf_id
 70.1898 +                              , 'title' : wf_title
 70.1899 +                              , 'initial_state' : wf_initial_state
 70.1900 +                              , 'workflow_filename' : workflow_filename
 70.1901 +                              }
 70.1902 +                          )
 70.1903 +
 70.1904 +        context._files[ 'workflows/%s/scripts/after_close.py' % workflow_filename
 70.1905 +                      ] = _AFTER_CLOSE_SCRIPT
 70.1906 +
 70.1907 +        context._files[ 'workflows/%s/scripts/after_kill.py' % workflow_filename
 70.1908 +                      ] = _AFTER_KILL_SCRIPT
 70.1909 +
 70.1910 +        context._files[ 'workflows/%s/scripts/before_open.py' % workflow_filename
 70.1911 +                      ] = _BEFORE_OPEN_SCRIPT
 70.1912 +
 70.1913 +        importWorkflowTool( context )
 70.1914 +
 70.1915 +        return wf_tool
 70.1916 +
 70.1917 +    def _importOldWorkflow( self, wf_id, wf_title, wf_initial_state ):
 70.1918 +        from Products.CMFCore.exportimport.workflow import importWorkflowTool
 70.1919 +
 70.1920 +        site = self._initSite()
 70.1921 +        wf_tool = site.portal_workflow
 70.1922 +        workflow_filename = wf_id.replace(' ', '_')
 70.1923 +
 70.1924 +        context = DummyImportContext( site )
 70.1925 +        context._files[ 'workflows.xml'
 70.1926 +                      ] = (_NORMAL_TOOL_EXPORT_WITH_FILENAME
 70.1927 +                            % { 'workflow_id' : wf_id
 70.1928 +                              }
 70.1929 +                          )
 70.1930 +
 70.1931 +        context._files[ 'workflows/%s/definition.xml' % workflow_filename
 70.1932 +                      ] = ( _OLD_WORKFLOW_EXPORT
 70.1933 +                            % { 'workflow_id' : wf_id
 70.1934 +                              , 'title' : wf_title
 70.1935 +                              , 'initial_state' : wf_initial_state
 70.1936 +                              , 'workflow_filename' : workflow_filename
 70.1937 +                              }
 70.1938 +                          )
 70.1939 +
 70.1940 +        context._files[ 'workflows/%s/after_close.py' % workflow_filename
 70.1941 +                      ] = _AFTER_CLOSE_SCRIPT
 70.1942 +
 70.1943 +        context._files[ 'workflows/%s/after_kill.py' % workflow_filename
 70.1944 +                      ] = _AFTER_KILL_SCRIPT
 70.1945 +
 70.1946 +        context._files[ 'workflows/%s/before_open.py' % workflow_filename
 70.1947 +                      ] = _BEFORE_OPEN_SCRIPT
 70.1948 +
 70.1949 +        importWorkflowTool( context )
 70.1950 +
 70.1951 +        return wf_tool
 70.1952 +
 70.1953 +    def test_empty_default_purge( self ):
 70.1954 +        from Products.CMFCore.exportimport.workflow import importWorkflowTool
 70.1955 +
 70.1956 +        WF_ID_NON = 'non_dcworkflow_%s'
 70.1957 +        WF_TITLE_NON = 'Non-DCWorkflow #%s'
 70.1958 +
 70.1959 +        site = self._initSite()
 70.1960 +        wf_tool = site.portal_workflow
 70.1961 +
 70.1962 +        for i in range( 4 ):
 70.1963 +            nondcworkflow = DummyWorkflow( WF_TITLE_NON % i )
 70.1964 +            nondcworkflow.title = WF_TITLE_NON % i
 70.1965 +            wf_tool._setObject( WF_ID_NON % i, nondcworkflow )
 70.1966 +
 70.1967 +        wf_tool._default_chain = ( WF_ID_NON % 1, )
 70.1968 +        wf_tool._chains_by_type[ 'sometype' ] = ( WF_ID_NON % 2, )
 70.1969 +        self.assertEqual( len( wf_tool.objectIds() ), 4 )
 70.1970 +
 70.1971 +        context = DummyImportContext( site )
 70.1972 +        context._files[ 'workflows.xml' ] = _EMPTY_TOOL_EXPORT
 70.1973 +        importWorkflowTool( context )
 70.1974 +
 70.1975 +        self.assertEqual( len( wf_tool.objectIds() ), 0 )
 70.1976 +        self.assertEqual( len( wf_tool._default_chain ), 0 )
 70.1977 +        self.assertEqual( len( wf_tool._chains_by_type ), 0 )
 70.1978 +
 70.1979 +    def test_empty_explicit_purge( self ):
 70.1980 +        from Products.CMFCore.exportimport.workflow import importWorkflowTool
 70.1981 +
 70.1982 +        WF_ID_NON = 'non_dcworkflow_%s'
 70.1983 +        WF_TITLE_NON = 'Non-DCWorkflow #%s'
 70.1984 +
 70.1985 +        site = self._initSite()
 70.1986 +        wf_tool = site.portal_workflow
 70.1987 +
 70.1988 +        for i in range( 4 ):
 70.1989 +            nondcworkflow = DummyWorkflow( WF_TITLE_NON % i )
 70.1990 +            nondcworkflow.title = WF_TITLE_NON % i
 70.1991 +            wf_tool._setObject( WF_ID_NON % i, nondcworkflow )
 70.1992 +
 70.1993 +        wf_tool._default_chain = ( WF_ID_NON % 1, )
 70.1994 +        wf_tool._chains_by_type[ 'sometype' ] = ( WF_ID_NON % 2, )
 70.1995 +        self.assertEqual( len( wf_tool.objectIds() ), 4 )
 70.1996 +
 70.1997 +        context = DummyImportContext( site, True )
 70.1998 +        context._files[ 'workflows.xml' ] = _EMPTY_TOOL_EXPORT
 70.1999 +        importWorkflowTool( context )
 70.2000 +
 70.2001 +        self.assertEqual( len( wf_tool.objectIds() ), 0 )
 70.2002 +        self.assertEqual( len( wf_tool._default_chain ), 0 )
 70.2003 +        self.assertEqual( len( wf_tool._chains_by_type ), 0 )
 70.2004 +
 70.2005 +    def test_empty_skip_purge( self ):
 70.2006 +        from Products.CMFCore.exportimport.workflow import importWorkflowTool
 70.2007 +
 70.2008 +        WF_ID_NON = 'non_dcworkflow_%s'
 70.2009 +        WF_TITLE_NON = 'Non-DCWorkflow #%s'
 70.2010 +
 70.2011 +        site = self._initSite()
 70.2012 +        wf_tool = site.portal_workflow
 70.2013 +
 70.2014 +        for i in range( 4 ):
 70.2015 +            nondcworkflow = DummyWorkflow( WF_TITLE_NON % i )
 70.2016 +            nondcworkflow.title = WF_TITLE_NON % i
 70.2017 +            wf_tool._setObject( WF_ID_NON % i, nondcworkflow )
 70.2018 +
 70.2019 +        wf_tool._default_chain = ( WF_ID_NON % 1, )
 70.2020 +        wf_tool._chains_by_type[ 'sometype' ] = ( WF_ID_NON % 2, )
 70.2021 +        self.assertEqual( len( wf_tool.objectIds() ), 4 )
 70.2022 +
 70.2023 +        context = DummyImportContext( site, False )
 70.2024 +        context._files[ 'typestool.xml' ] = _EMPTY_TOOL_EXPORT
 70.2025 +        importWorkflowTool( context )
 70.2026 +
 70.2027 +        self.assertEqual( len( wf_tool.objectIds() ), 4 )
 70.2028 +        self.assertEqual( len( wf_tool._default_chain ), 1 )
 70.2029 +        self.assertEqual( wf_tool._default_chain[ 0 ], WF_ID_NON % 1 )
 70.2030 +        self.assertEqual( len( wf_tool._chains_by_type ), 1 )
 70.2031 +        self.assertEqual( wf_tool._chains_by_type[ 'sometype' ]
 70.2032 +                        , ( WF_ID_NON % 2, )
 70.2033 +                        )
 70.2034 +
 70.2035 +    def test_bindings_skip_purge( self ):
 70.2036 +        from Products.CMFCore.exportimport.workflow import importWorkflowTool
 70.2037 +
 70.2038 +        WF_ID_NON = 'non_dcworkflow_%s'
 70.2039 +        WF_TITLE_NON = 'Non-DCWorkflow #%s'
 70.2040 +
 70.2041 +        site = self._initSite()
 70.2042 +        wf_tool = site.portal_workflow
 70.2043 +
 70.2044 +        for i in range( 4 ):
 70.2045 +            nondcworkflow = DummyWorkflow( WF_TITLE_NON % i )
 70.2046 +            nondcworkflow.title = WF_TITLE_NON % i
 70.2047 +            wf_tool._setObject( WF_ID_NON % i, nondcworkflow )
 70.2048 +
 70.2049 +        wf_tool._default_chain = ( WF_ID_NON % 1, )
 70.2050 +        wf_tool._chains_by_type[ 'sometype' ] = ( WF_ID_NON % 2, )
 70.2051 +        self.assertEqual( len( wf_tool.objectIds() ), 4 )
 70.2052 +
 70.2053 +        context = DummyImportContext( site, False )
 70.2054 +        context._files[ 'workflows.xml' ] = _BINDINGS_TOOL_EXPORT
 70.2055 +        importWorkflowTool( context )
 70.2056 +
 70.2057 +        self.assertEqual( len( wf_tool.objectIds() ), 4 )
 70.2058 +        self.assertEqual( len( wf_tool._default_chain ), 2 )
 70.2059 +        self.assertEqual( wf_tool._default_chain[ 0 ], WF_ID_NON % 0 )
 70.2060 +        self.assertEqual( wf_tool._default_chain[ 1 ], WF_ID_NON % 1 )
 70.2061 +        self.assertEqual( len( wf_tool._chains_by_type ), 2 )
 70.2062 +        self.assertEqual( wf_tool._chains_by_type[ 'sometype' ]
 70.2063 +                        , ( WF_ID_NON % 2, )
 70.2064 +                        )
 70.2065 +        self.assertEqual( wf_tool._chains_by_type[ 'anothertype' ]
 70.2066 +                        , ( WF_ID_NON % 3, )
 70.2067 +                        )
 70.2068 +
 70.2069 +    def test_from_empty_dcworkflow_top_level( self ):
 70.2070 +
 70.2071 +        WF_ID = 'dcworkflow_tool'
 70.2072 +        WF_TITLE = 'DC Workflow testing tool'
 70.2073 +        WF_INITIAL_STATE = 'closed'
 70.2074 +
 70.2075 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2076 +
 70.2077 +        self.assertEqual( len( tool.objectIds() ), 2 )
 70.2078 +        self.assertEqual( tool.objectIds()[ 1 ], WF_ID )
 70.2079 +
 70.2080 +    def test_from_empty_dcworkflow_workflow_attrs( self ):
 70.2081 +
 70.2082 +        WF_ID = 'dcworkflow_attrs'
 70.2083 +        WF_TITLE = 'DC Workflow testing attrs'
 70.2084 +        WF_INITIAL_STATE = 'closed'
 70.2085 +
 70.2086 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2087 +
 70.2088 +        workflow = tool.objectValues()[ 1 ]
 70.2089 +        self.assertEqual( workflow.meta_type, DCWorkflowDefinition.meta_type )
 70.2090 +        self.assertEqual( workflow.title, WF_TITLE )
 70.2091 +        self.assertEqual( workflow.state_var, 'state' )
 70.2092 +        self.assertEqual( workflow.initial_state, WF_INITIAL_STATE )
 70.2093 +
 70.2094 +    def test_from_empty_dcworkflow_workflow_permissions( self ):
 70.2095 +
 70.2096 +        WF_ID = 'dcworkflow_permissions'
 70.2097 +        WF_TITLE = 'DC Workflow testing permissions'
 70.2098 +        WF_INITIAL_STATE = 'closed'
 70.2099 +
 70.2100 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2101 +
 70.2102 +        workflow = tool.objectValues()[ 1 ]
 70.2103 +
 70.2104 +        permissions = workflow.permissions
 70.2105 +        self.assertEqual( len( permissions ), len( _WF_PERMISSIONS ) )
 70.2106 +
 70.2107 +        for permission in permissions:
 70.2108 +            self.failUnless( permission in _WF_PERMISSIONS )
 70.2109 +
 70.2110 +    def test_from_empty_dcworkflow_workflow_variables( self ):
 70.2111 +
 70.2112 +        WF_ID = 'dcworkflow_variables'
 70.2113 +        WF_TITLE = 'DC Workflow testing variables'
 70.2114 +        WF_INITIAL_STATE = 'closed'
 70.2115 +
 70.2116 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2117 +
 70.2118 +        workflow = tool.objectValues()[ 1 ]
 70.2119 +
 70.2120 +        variables = workflow.variables
 70.2121 +
 70.2122 +        self.assertEqual( len( variables.objectItems() )
 70.2123 +                        , len( _WF_VARIABLES ) )
 70.2124 +
 70.2125 +        for id, variable in variables.objectItems():
 70.2126 +
 70.2127 +            expected = _WF_VARIABLES[ variable.getId() ]
 70.2128 +            self.failUnless( expected[ 0 ] in variable.description )
 70.2129 +            self.assertEqual( variable.default_value, expected[ 1 ] )
 70.2130 +            self.assertEqual( variable.getDefaultExprText(), expected[ 2 ] )
 70.2131 +            self.assertEqual( variable.for_catalog, expected[ 3 ] )
 70.2132 +            self.assertEqual( variable.for_status, expected[ 4 ] )
 70.2133 +            self.assertEqual( variable.update_always, expected[ 5 ] )
 70.2134 +
 70.2135 +            guard = variable.getInfoGuard()
 70.2136 +
 70.2137 +            self.assertEqual( guard.permissions, expected[ 6 ] )
 70.2138 +            self.assertEqual( guard.roles, expected[ 7 ] )
 70.2139 +            self.assertEqual( guard.groups, expected[ 8 ] )
 70.2140 +            self.assertEqual( guard.getExprText(), expected[ 9 ] )
 70.2141 +
 70.2142 +    def test_from_empty_dcworkflow_workflow_states( self ):
 70.2143 +
 70.2144 +        WF_ID = 'dcworkflow_states'
 70.2145 +        WF_TITLE = 'DC Workflow testing states'
 70.2146 +        WF_INITIAL_STATE = 'closed'
 70.2147 +
 70.2148 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2149 +
 70.2150 +        workflow = tool.objectValues()[ 1 ]
 70.2151 +
 70.2152 +        states = workflow.states
 70.2153 +
 70.2154 +        self.assertEqual( len( states.objectItems() )
 70.2155 +                        , len( _WF_STATES ) )
 70.2156 +
 70.2157 +        for id, state in states.objectItems():
 70.2158 +
 70.2159 +            expected = _WF_STATES[ state.getId() ]
 70.2160 +            self.assertEqual( state.title, expected[ 0 ] )
 70.2161 +            self.failUnless( expected[ 1 ] in state.description )
 70.2162 +
 70.2163 +            self.assertEqual( len( state.transitions ), len( expected[ 2 ] ) )
 70.2164 +
 70.2165 +            for transition_id in state.transitions:
 70.2166 +                self.failUnless( transition_id in expected[ 2 ] )
 70.2167 +
 70.2168 +            for permission in state.getManagedPermissions():
 70.2169 +
 70.2170 +                p_info = state.getPermissionInfo( permission )
 70.2171 +                p_expected = expected[ 3 ].get( permission, [] )
 70.2172 +
 70.2173 +                self.assertEqual( bool( p_info[ 'acquired' ] )
 70.2174 +                                , isinstance(p_expected, list) )
 70.2175 +
 70.2176 +                self.assertEqual( len( p_info[ 'roles' ] ), len( p_expected ) )
 70.2177 +
 70.2178 +                for role in p_info[ 'roles' ]:
 70.2179 +                    self.failIf( role not in p_expected )
 70.2180 +
 70.2181 +            group_roles = state.group_roles or {}
 70.2182 +            self.assertEqual( len( group_roles ), len( expected[ 4 ] ) )
 70.2183 +
 70.2184 +            for group_id, exp_roles in expected[ 4 ]:
 70.2185 +
 70.2186 +                self.assertEqual( len( state.getGroupInfo( group_id ) )
 70.2187 +                                , len( exp_roles ) )
 70.2188 +
 70.2189 +                for role in state.getGroupInfo( group_id ):
 70.2190 +                    self.failUnless( role in exp_roles )
 70.2191 +
 70.2192 +            self.assertEqual( len( state.getVariableValues() )
 70.2193 +                            , len( expected[ 5 ] ) )
 70.2194 +
 70.2195 +            for var_id, value in state.getVariableValues():
 70.2196 +
 70.2197 +                self.assertEqual( value, expected[ 5 ][ var_id ] )
 70.2198 +
 70.2199 +    def test_from_empty_dcworkflow_workflow_transitions( self ):
 70.2200 +
 70.2201 +        WF_ID = 'dcworkflow_transitions'
 70.2202 +        WF_TITLE = 'DC Workflow testing transitions'
 70.2203 +        WF_INITIAL_STATE = 'closed'
 70.2204 +
 70.2205 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2206 +
 70.2207 +        workflow = tool.objectValues()[ 1 ]
 70.2208 +
 70.2209 +        transitions = workflow.transitions
 70.2210 +
 70.2211 +        self.assertEqual( len( transitions.objectItems() )
 70.2212 +                        , len( _WF_TRANSITIONS ) )
 70.2213 +
 70.2214 +        for id, transition in transitions.objectItems():
 70.2215 +
 70.2216 +            expected = _WF_TRANSITIONS[ transition.getId() ]
 70.2217 +            self.assertEqual( transition.title, expected[ 0 ] )
 70.2218 +            self.failUnless( expected[ 1 ] in transition.description )
 70.2219 +            self.assertEqual( transition.new_state_id, expected[ 2 ] )
 70.2220 +            self.assertEqual( transition.trigger_type, expected[ 3 ] )
 70.2221 +            self.assertEqual( transition.script_name, expected[ 4 ] )
 70.2222 +            self.assertEqual( transition.after_script_name, expected[ 5 ] )
 70.2223 +            self.assertEqual( transition.actbox_name, expected[ 6 ] )
 70.2224 +            self.assertEqual( transition.actbox_url, expected[ 7 ] )
 70.2225 +            self.assertEqual( transition.actbox_category, expected[ 8 ] )
 70.2226 +
 70.2227 +            var_exprs = transition.var_exprs
 70.2228 +
 70.2229 +            self.assertEqual( len( var_exprs ), len( expected[ 9 ] ) )
 70.2230 +
 70.2231 +            for var_id, expr in var_exprs.items():
 70.2232 +                self.assertEqual( expr, expected[ 9 ][ var_id ] )
 70.2233 +
 70.2234 +            guard = transition.getGuard()
 70.2235 +
 70.2236 +            self.assertEqual( guard.permissions, expected[ 10 ] )
 70.2237 +            self.assertEqual( guard.roles, expected[ 11 ] )
 70.2238 +            self.assertEqual( guard.groups, expected[ 12 ] )
 70.2239 +            self.assertEqual( guard.getExprText(), expected[ 13 ] )
 70.2240 +
 70.2241 +    def test_from_empty_dcworkflow_workflow_worklists( self ):
 70.2242 +
 70.2243 +        WF_ID = 'dcworkflow_worklists'
 70.2244 +        WF_TITLE = 'DC Workflow testing worklists'
 70.2245 +        WF_INITIAL_STATE = 'closed'
 70.2246 +
 70.2247 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2248 +
 70.2249 +        workflow = tool.objectValues()[ 1 ]
 70.2250 +
 70.2251 +        worklists = workflow.worklists
 70.2252 +
 70.2253 +        self.assertEqual( len( worklists.objectItems() )
 70.2254 +                        , len( _WF_WORKLISTS ) )
 70.2255 +
 70.2256 +        for id, worklist in worklists.objectItems():
 70.2257 +
 70.2258 +            expected = _WF_WORKLISTS[ worklist.getId() ]
 70.2259 +            self.failUnless( expected[ 1 ] in worklist.description )
 70.2260 +
 70.2261 +            var_matches = worklist.var_matches
 70.2262 +
 70.2263 +            self.assertEqual( len( var_matches ), len( expected[ 2 ] ) )
 70.2264 +
 70.2265 +            for var_id, values in var_matches.items():
 70.2266 +                exp_values = expected[ 2 ][ var_id ]
 70.2267 +                self.assertEqual( len( values ), len( exp_values ) )
 70.2268 +
 70.2269 +                for value in values:
 70.2270 +                    self.failUnless( value in exp_values, values )
 70.2271 +
 70.2272 +            self.assertEqual( worklist.actbox_name, expected[ 3 ] )
 70.2273 +            self.assertEqual( worklist.actbox_url, expected[ 4 ] )
 70.2274 +            self.assertEqual( worklist.actbox_category, expected[ 5 ] )
 70.2275 +
 70.2276 +            guard = worklist.getGuard()
 70.2277 +
 70.2278 +            self.assertEqual( guard.permissions, expected[ 6 ] )
 70.2279 +            self.assertEqual( guard.roles, expected[ 7 ] )
 70.2280 +            self.assertEqual( guard.groups, expected[ 8 ] )
 70.2281 +            self.assertEqual( guard.getExprText(), expected[ 9 ] )
 70.2282 +
 70.2283 +    def test_from_old_dcworkflow_workflow_scripts( self ):
 70.2284 +
 70.2285 +        WF_ID = 'old_dcworkflow_scripts'
 70.2286 +        WF_TITLE = 'Old DC Workflow testing scripts'
 70.2287 +        WF_INITIAL_STATE = 'closed'
 70.2288 +
 70.2289 +        tool = self._importOldWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2290 +
 70.2291 +        workflow = tool.objectValues()[ 1 ]
 70.2292 +
 70.2293 +        scripts = workflow.scripts
 70.2294 +
 70.2295 +        self.assertEqual( len( scripts.objectItems() )
 70.2296 +                        , len( _WF_SCRIPTS ) )
 70.2297 +
 70.2298 +        for script_id, script in scripts.objectItems():
 70.2299 +
 70.2300 +            expected = _WF_SCRIPTS[ script_id ]
 70.2301 +
 70.2302 +            self.assertEqual( script.meta_type, expected[ 0 ] )
 70.2303 +
 70.2304 +            if script.meta_type == PythonScript.meta_type:
 70.2305 +                self.assertEqual( script.manage_FTPget(), expected[ 1 ] )
 70.2306 +
 70.2307 +    def test_from_empty_dcworkflow_workflow_scripts( self ):
 70.2308 +
 70.2309 +        WF_ID = 'dcworkflow_scripts'
 70.2310 +        WF_TITLE = 'DC Workflow testing scripts'
 70.2311 +        WF_INITIAL_STATE = 'closed'
 70.2312 +
 70.2313 +        tool = self._importNormalWorkflow( WF_ID, WF_TITLE, WF_INITIAL_STATE )
 70.2314 +
 70.2315 +        workflow = tool.objectValues()[ 1 ]
 70.2316 +
 70.2317 +        scripts = workflow.scripts
 70.2318 +
 70.2319 +        self.assertEqual( len( scripts.objectItems() )
 70.2320 +                        , len( _WF_SCRIPTS ) )
 70.2321 +
 70.2322 +        for script_id, script in scripts.objectItems():
 70.2323 +
 70.2324 +            expected = _WF_SCRIPTS[ script_id ]
 70.2325 +
 70.2326 +            self.assertEqual( script.meta_type, expected[ 0 ] )
 70.2327 +
 70.2328 +            if script.meta_type == PythonScript.meta_type:
 70.2329 +                self.assertEqual( script.manage_FTPget(), expected[ 1 ] )
 70.2330 +
 70.2331 +
 70.2332 +def test_suite():
 70.2333 +    return unittest.TestSuite((
 70.2334 +        unittest.makeSuite( WorkflowDefinitionConfiguratorTests ),
 70.2335 +        unittest.makeSuite( Test_exportWorkflow ),
 70.2336 +        unittest.makeSuite( Test_importWorkflow ),
 70.2337 +        ))
 70.2338 +
 70.2339 +if __name__ == '__main__':
 70.2340 +    unittest.main(defaultTest='test_suite')
    71.1 new file mode 100644
    71.2 --- /dev/null
    71.3 +++ b/tests/test_guard.py
    71.4 @@ -0,0 +1,269 @@
    71.5 +##############################################################################
    71.6 +#
    71.7 +# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
    71.8 +#
    71.9 +# This software is subject to the provisions of the Zope Public License,
   71.10 +# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
   71.11 +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
   71.12 +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   71.13 +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
   71.14 +# FOR A PARTICULAR PURPOSE.
   71.15 +#
   71.16 +##############################################################################
   71.17 +""" Guard tests.
   71.18 +
   71.19 +$Id: test_guard.py 40346 2005-11-23 17:15:03Z yuppie $
   71.20 +"""
   71.21 +
   71.22 +import unittest
   71.23 +import Testing
   71.24 +
   71.25 +from AccessControl import getSecurityManager
   71.26 +from Products.PageTemplates.TALES import CompilerError
   71.27 +
   71.28 +from Products.CMFCore.tests.base.dummy import DummyContent
   71.29 +from Products.CMFCore.tests.base.dummy import DummySite
   71.30 +from Products.CMFCore.tests.base.dummy import DummyTool
   71.31 +from Products.CMFCore.WorkflowTool import WorkflowTool
   71.32 +
   71.33 +from Products.DCWorkflow.Guard import Guard
   71.34 +from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
   71.35 +
   71.36 +
   71.37 +class TestGuard(unittest.TestCase):
   71.38 +
   71.39 +    def setUp(self):
   71.40 +        self.site = DummySite('site')
   71.41 +        self.site._setObject( 'portal_types', DummyTool() )
   71.42 +        self.site._setObject( 'portal_workflow', WorkflowTool() )
   71.43 +
   71.44 +        # Construct a workflow
   71.45 +        wftool = self.site.portal_workflow
   71.46 +        wftool._setObject('wf', DCWorkflowDefinition('wf'))
   71.47 +        wftool.setDefaultChain('wf')
   71.48 +
   71.49 +    def _getDummyWorkflow(self):
   71.50 +        return self.site.portal_workflow['wf']
   71.51 +
   71.52 +    def test_BaseGuardAPI(self):
   71.53 +
   71.54 +        #
   71.55 +        # Test guard basic API
   71.56 +        #
   71.57 +
   71.58 +        guard = Guard()
   71.59 +        self.assertNotEqual(guard, None)
   71.60 +
   71.61 +        # Test default values
   71.62 +        self.assertEqual(guard.getPermissionsText(), '')
   71.63 +        self.assertEqual(guard.getRolesText(), '')
   71.64 +        self.assertEqual(guard.getExprText(), '')
   71.65 +
   71.66 +        # Initialize the guard with empty values
   71.67 +        # not initialization
   71.68 +        guard_props = {'guard_permissions':'',
   71.69 +                       'guard_roles':'',
   71.70 +                       'guard_expr' :''}
   71.71 +        res = guard.changeFromProperties(guard_props)
   71.72 +        self.assert_(res==0)
   71.73 +
   71.74 +        # Test default values
   71.75 +        self.assertEqual(guard.getPermissionsText(), '')
   71.76 +        self.assertEqual(guard.getRolesText(), '')
   71.77 +        self.assertEqual(guard.getExprText(), '')
   71.78 +
   71.79 +        # Change guard
   71.80 +        guard_props = {'guard_roles':'Manager',
   71.81 +                       'guard_permissions':'',
   71.82 +                       'guard_expr' :''}
   71.83 +        res = guard.changeFromProperties(guard_props)
   71.84 +        self.assert_(res==1)
   71.85 +        self.assertEqual(guard.getRolesText(), 'Manager')
   71.86 +        self.assertEqual(guard.getPermissionsText(), '')
   71.87 +        self.assertEqual(guard.getExprText(), '')
   71.88 +
   71.89 +        # Change guard
   71.90 +        guard_props = {'guard_roles':'Manager;',
   71.91 +                       'guard_permissions':'',
   71.92 +                       'guard_expr' :''}
   71.93 +        res = guard.changeFromProperties(guard_props)
   71.94 +        self.assert_(res==1)
   71.95 +        # With one space after the ';'
   71.96 +        self.assertEqual(guard.getRolesText(), 'Manager; ')
   71.97 +        self.assertEqual(guard.getPermissionsText(), '')
   71.98 +        self.assertEqual(guard.getExprText(), '')
   71.99 +
  71.100 +        # Change guard
  71.101 +        guard_props = {'guard_roles':'Manager;Member',
  71.102 +                       'guard_permissions':'',
  71.103 +                       'guard_expr' :''}
  71.104 +        res = guard.changeFromProperties(guard_props)
  71.105 +        self.assert_(res==1)
  71.106 +        # With one space after the ';'
  71.107 +        self.assertEqual(guard.getRolesText(), 'Manager; Member')
  71.108 +        self.assertEqual(guard.getPermissionsText(), '')
  71.109 +        self.assertEqual(guard.getExprText(), '')
  71.110 +
  71.111 +        # Change guard
  71.112 +        guard_props = {'guard_roles':'Manager;Member',
  71.113 +                       'guard_permissions':'',
  71.114 +                       'guard_expr' :''}
  71.115 +        res = guard.changeFromProperties(guard_props)
  71.116 +        self.assert_(res==1)
  71.117 +        # With one space after the ';'
  71.118 +        self.assertEqual(guard.getRolesText(), 'Manager; Member')
  71.119 +        self.assertEqual(guard.getPermissionsText(), '')
  71.120 +        self.assertEqual(guard.getExprText(), '')
  71.121 +
  71.122 +        # Change guard
  71.123 +        guard_props = {'guard_roles':'Manager',
  71.124 +                       'guard_permissions':'',
  71.125 +                       'guard_expr' :''}
  71.126 +        res = guard.changeFromProperties(guard_props)
  71.127 +        self.assert_(res==1)
  71.128 +        self.assertEqual(guard.getRolesText(), 'Manager')
  71.129 +        self.assertEqual(guard.getPermissionsText(), '')
  71.130 +        self.assertEqual(guard.getExprText(), '')
  71.131 +
  71.132 +        # Change guard
  71.133 +        guard_props = {'guard_roles':'Manager',
  71.134 +                       'guard_permissions':'ManagePortal;',
  71.135 +                       'guard_expr' :''}
  71.136 +        res = guard.changeFromProperties(guard_props)
  71.137 +        self.assert_(res==1)
  71.138 +        self.assertEqual(guard.getRolesText(), 'Manager')
  71.139 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal; ')
  71.140 +        self.assertEqual(guard.getExprText(), '')
  71.141 +
  71.142 +        # Change guard
  71.143 +        guard_props = {'guard_roles':'Manager',
  71.144 +                       'guard_permissions':'ManagePortal',
  71.145 +                       'guard_expr' :''}
  71.146 +        res = guard.changeFromProperties(guard_props)
  71.147 +        self.assert_(res==1)
  71.148 +        self.assertEqual(guard.getRolesText(), 'Manager')
  71.149 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  71.150 +        self.assertEqual(guard.getExprText(), '')
  71.151 +
  71.152 +        # Change guard
  71.153 +        guard_props = {'guard_roles':'Manager',
  71.154 +                       'guard_permissions':'ManagePortal',
  71.155 +                       'guard_expr' :'python:1'}
  71.156 +        res = guard.changeFromProperties(guard_props)
  71.157 +        self.assert_(res==1)
  71.158 +        self.assertEqual(guard.getRolesText(), 'Manager')
  71.159 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  71.160 +        self.assertEqual(guard.getExprText(), 'python:1')
  71.161 +
  71.162 +        # Change guard
  71.163 +        guard_props = {'guard_roles':'Manager',
  71.164 +                       'guard_permissions':'ManagePortal',
  71.165 +                       'guard_expr' :'string:'}
  71.166 +        res = guard.changeFromProperties(guard_props)
  71.167 +        self.assert_(res==1)
  71.168 +        self.assertEqual(guard.getRolesText(), 'Manager')
  71.169 +        self.assertEqual(guard.getPermissionsText(), 'ManagePortal')
  71.170 +        self.assertEqual(guard.getExprText(), 'string:')
  71.171 +
  71.172 +        # Change guard with wrong TALES
  71.173 +        guard_props = {'guard_roles':'Manager',
  71.174