src/pyams_workflow/workflow.py
changeset 0 73acbfc13577
child 1 a222398c5920
equal deleted inserted replaced
-1:000000000000 0:73acbfc13577
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 from pyams_utils.traversing import get_parent
       
    13 
       
    14 __docformat__ = 'restructuredtext'
       
    15 
       
    16 
       
    17 # import standard library
       
    18 
       
    19 # import interfaces
       
    20 from pyams_workflow.interfaces import MANUAL, IWorkflow, InvalidTransitionError, IWorkflowState, IWorkflowVersions, \
       
    21     IWorkflowInfo, ConditionFailedError, WorkflowVersionTransitionEvent, WorkflowTransitionEvent, \
       
    22     NoTransitionAvailableError, AmbiguousTransitionError, SYSTEM, AUTOMATIC, IWorkflowManagedContent, IWorkflowVersion
       
    23 
       
    24 # import packages
       
    25 from pyams_utils.adapter import adapter_config
       
    26 from pyams_utils.registry import get_utility
       
    27 from pyams_utils.request import check_request
       
    28 from pyramid.httpexceptions import HTTPUnauthorized
       
    29 from pyramid.threadlocal import get_current_registry
       
    30 from zope.interface import implementer
       
    31 from zope.lifecycleevent import ObjectModifiedEvent
       
    32 
       
    33 
       
    34 def NullCondition(wf, context):
       
    35     """Null condition"""
       
    36     return True
       
    37 
       
    38 
       
    39 def NullAction(wf, context):
       
    40     """Null action"""
       
    41     pass
       
    42 
       
    43 
       
    44 def granted_permission(permission, context):
       
    45     return True
       
    46 
       
    47 
       
    48 class Transition(object):
       
    49     """Transition object
       
    50 
       
    51     A transition doesn't make anything by itself.
       
    52     Everything is handled by the workflow utility
       
    53     """
       
    54 
       
    55     def __init__(self, transition_id, title, source, destination,
       
    56                  condition=NullCondition,
       
    57                  action=NullAction,
       
    58                  trigger=MANUAL,
       
    59                  permission=None,
       
    60                  order=0,
       
    61                  **user_data):
       
    62         self.transition_id = transition_id
       
    63         self.title = title
       
    64         self.source = source
       
    65         self.destination = destination
       
    66         self.condition = condition
       
    67         self.action = action
       
    68         self.trigger = trigger
       
    69         self.permission = permission
       
    70         self.order = order
       
    71         self.user_data = user_data
       
    72 
       
    73 
       
    74 @implementer(IWorkflow)
       
    75 class Workflow(object):
       
    76     """Workflow utility"""
       
    77 
       
    78     def __init__(self, transitions, states, published_states=None):
       
    79         self.refresh(transitions)
       
    80         self.states = states
       
    81         self.published_states = published_states or set()
       
    82 
       
    83     def _register(self, transition):
       
    84         transitions = self._sources.setdefault(transition.source, {})
       
    85         transitions[transition.transition_id] = transition
       
    86         self._id_transitions[transition.transition_id] = transition
       
    87 
       
    88     def refresh(self, transitions):
       
    89         self._sources = {}
       
    90         self._id_transitions = {}
       
    91         for transition in transitions:
       
    92             self._register(transition)
       
    93 
       
    94     def get_transitions(self, source):
       
    95         try:
       
    96             return self._sources[source].values()
       
    97         except KeyError:
       
    98             return []
       
    99 
       
   100     def get_transition(self, source, transition_id):
       
   101         transition = self._id_transitions[transition_id]
       
   102         if transition.source != source:
       
   103             raise InvalidTransitionError(source)
       
   104         return transition
       
   105 
       
   106     def get_transition_by_id(self, transition_id):
       
   107         return self._id_transitions[transition_id]
       
   108 
       
   109 
       
   110 WORKFLOW_STATE_KEY = 'pyams_workflow.state'
       
   111 
       
   112 
       
   113 @adapter_config(context=IWorkflowVersion, provides=IWorkflowInfo)
       
   114 class WorkflowInfo(object):
       
   115     """Workflow info adapter"""
       
   116 
       
   117     def __init__(self, context):
       
   118         self.context = context
       
   119         self.wf = get_utility(IWorkflow, name=self.name)
       
   120 
       
   121     @property
       
   122     def parent(self):
       
   123         return get_parent(self.context, IWorkflowManagedContent)
       
   124 
       
   125     @property
       
   126     def name(self):
       
   127         return self.parent.workflow_name
       
   128 
       
   129     def fire_transition(self, transition_id, comment=None, side_effect=None, check_security=True):
       
   130         versions = IWorkflowVersions(self.parent)
       
   131         state = IWorkflowState(self.context)
       
   132         # this raises InvalidTransitionError if id is invalid for current state
       
   133         transition = self.wf.get_transition(state.state, transition_id)
       
   134         # check whether we may execute this workflow transition
       
   135         if check_security and transition.permission:
       
   136             request = check_request()
       
   137             if not request.has_permission(transition.permission):
       
   138                 raise HTTPUnauthorized()
       
   139         # now make sure transition can still work in this context
       
   140         if not transition.condition(self, self.context):
       
   141             raise ConditionFailedError()
       
   142         # perform action, return any result as new version
       
   143         result = transition.action(self, self.context)
       
   144         if result is not None:
       
   145             # clear result history
       
   146             IWorkflowState(result).history.clear()
       
   147             # stamp it with version
       
   148             versions.add_version(result, transition.destination)
       
   149             # execute any side effect:
       
   150             if side_effect is not None:
       
   151                 side_effect(result)
       
   152             event = WorkflowVersionTransitionEvent(result, self.context,
       
   153                                                    transition.source,
       
   154                                                    transition.destination,
       
   155                                                    transition, comment)
       
   156         else:
       
   157             versions.set_state(state.version_id, transition.destination)
       
   158             # execute any side effect
       
   159             if side_effect is not None:
       
   160                 side_effect(self.context)
       
   161             event = WorkflowTransitionEvent(self.context,
       
   162                                             transition.source,
       
   163                                             transition.destination,
       
   164                                             transition, comment)
       
   165         # change state of context or new object
       
   166         registry = get_current_registry()
       
   167         registry.notify(event)
       
   168         # send modified event for original or new object
       
   169         if result is None:
       
   170             registry.notify(ObjectModifiedEvent(self.context))
       
   171         else:
       
   172             registry.notify(ObjectModifiedEvent(result))
       
   173         return result
       
   174 
       
   175     def fire_transition_toward(self, state, comment=None, side_effect=None, check_security=True):
       
   176         transition_ids = self.get_fireable_transition_ids_toward(state, check_security)
       
   177         if not transition_ids:
       
   178             raise NoTransitionAvailableError(self.state(self.context).get_state(), state)
       
   179         if len(transition_ids) != 1:
       
   180             raise AmbiguousTransitionError(self.state(self.context).get_state(), state)
       
   181         return self.fire_transition(transition_ids[0], comment, side_effect, check_security)
       
   182 
       
   183     def fire_transition_for_versions(self, state, transition_id, comment=None):
       
   184         versions = IWorkflowVersions(self.parent)
       
   185         for version in versions.get_versions(state):
       
   186             IWorkflowInfo(version).fire_transition(transition_id, comment)
       
   187 
       
   188     def fire_automatic(self):
       
   189         for transition_id in self.get_automatic_transition_ids():
       
   190             try:
       
   191                 self.fire_transition(transition_id)
       
   192             except ConditionFailedError:
       
   193                 # if condition failed, that's fine, then we weren't
       
   194                 # ready to fire yet
       
   195                 pass
       
   196             else:
       
   197                 # if we actually managed to fire a transition,
       
   198                 # we're done with this one now.
       
   199                 return
       
   200 
       
   201     def has_version(self, state):
       
   202         wf_versions = IWorkflowVersions(self.parent)
       
   203         return wf_versions.has_version(state)
       
   204 
       
   205     def get_manual_transition_ids(self, check_security=True):
       
   206         if check_security:
       
   207             request = check_request()
       
   208             permission_checker = request.has_permission
       
   209         else:
       
   210             permission_checker = granted_permission
       
   211         return [transition.transition_id
       
   212                 for transition in sorted(self._get_transitions(MANUAL),
       
   213                                          key=lambda x: x.user_data.get('order', 999))
       
   214                 if transition.condition(self, self.context) and
       
   215                     permission_checker(transition.permission, self.context)]
       
   216 
       
   217     def get_system_transition_ids(self):
       
   218         # ignore permission checks
       
   219         return [transition.transition_id
       
   220                 for transition in sorted(self._get_transitions(SYSTEM),
       
   221                                          key=lambda x: x.user_data.get('order', 999))
       
   222                 if transition.condition(self, self.context)]
       
   223 
       
   224     def get_fireable_transition_ids(self, check_security=True):
       
   225         return (self.get_manual_transition_ids(check_security) +
       
   226                 self.get_system_transition_ids())
       
   227 
       
   228     def get_fireable_transition_ids_toward(self, state, check_security=True):
       
   229         result = []
       
   230         for transition_id in self.get_fireable_transition_ids(check_security):
       
   231             transition = self.wf.get_transition_by_id(transition_id)
       
   232             if transition.destination == state:
       
   233                 result.append(transition_id)
       
   234         return result
       
   235 
       
   236     def get_automatic_transition_ids(self):
       
   237         return [transition.transition_id for transition in
       
   238                 self._get_transitions(AUTOMATIC)]
       
   239 
       
   240     def has_automatic_transitions(self):
       
   241         return bool(self.get_automatic_transition_ids())
       
   242 
       
   243     def _get_transitions(self, trigger):
       
   244         # retrieve all possible transitions from workflow utility
       
   245         state = IWorkflowState(self.context)
       
   246         transitions = self.wf.get_transitions(state.state)
       
   247         # now filter these transitions to retrieve all possible
       
   248         # transitions in this context, and return their ids
       
   249         return [transition for transition in transitions if transition.trigger == trigger]