hooks/workflow.py
changeset 5556 9ab2b4c74baf
parent 5424 8ecbcbff9777
child 5685 17883ced01f8
equal deleted inserted replaced
5555:a64f48dd5fe4 5556:9ab2b4c74baf
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    14 # details.
    14 # details.
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Core hooks: workflow related hooks
    18 """Core hooks: workflow related hooks"""
    19 
    19 
    20 """
       
    21 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    22 
    21 
    23 from datetime import datetime
    22 from datetime import datetime
    24 
    23 
    25 from yams.schema import role_name
    24 from yams.schema import role_name
    26 
    25 
    27 from cubicweb import RepositoryError, ValidationError
    26 from cubicweb import RepositoryError, ValidationError
    28 from cubicweb.interfaces import IWorkflowable
    27 from cubicweb.selectors import implements, adaptable
    29 from cubicweb.selectors import implements
       
    30 from cubicweb.server import hook
    28 from cubicweb.server import hook
    31 
    29 
    32 
    30 
    33 def _change_state(session, x, oldstate, newstate):
    31 def _change_state(session, x, oldstate, newstate):
    34     nocheck = session.transaction_data.setdefault('skip-security', set())
    32     nocheck = session.transaction_data.setdefault('skip-security', set())
    49     """make initial state be a default state"""
    47     """make initial state be a default state"""
    50 
    48 
    51     def precommit_event(self):
    49     def precommit_event(self):
    52         session = self.session
    50         session = self.session
    53         entity = self.entity
    51         entity = self.entity
       
    52         iworkflowable = entity.cw_adapt_to('IWorkflowable')
    54         # if there is an initial state and the entity's state is not set,
    53         # if there is an initial state and the entity's state is not set,
    55         # use the initial state as a default state
    54         # use the initial state as a default state
    56         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
    55         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
    57                and entity.current_workflow:
    56                and iworkflowable.current_workflow:
    58             state = entity.current_workflow.initial
    57             state = iworkflowable.current_workflow.initial
    59             if state:
    58             if state:
    60                 session.add_relation(entity.eid, 'in_state', state.eid)
    59                 session.add_relation(entity.eid, 'in_state', state.eid)
    61                 _FireAutotransitionOp(session, entity=entity)
    60                 _FireAutotransitionOp(session, entity=entity)
    62 
    61 
    63 class _FireAutotransitionOp(hook.Operation):
    62 class _FireAutotransitionOp(hook.Operation):
    64     """try to fire auto transition after state changes"""
    63     """try to fire auto transition after state changes"""
    65 
    64 
    66     def precommit_event(self):
    65     def precommit_event(self):
    67         entity = self.entity
    66         entity = self.entity
    68         autotrs = list(entity.possible_transitions('auto'))
    67         iworkflowable = entity.cw_adapt_to('IWorkflowable')
       
    68         autotrs = list(iworkflowable.possible_transitions('auto'))
    69         if autotrs:
    69         if autotrs:
    70             assert len(autotrs) == 1
    70             assert len(autotrs) == 1
    71             entity.fire_transition(autotrs[0])
    71             iworkflowable.fire_transition(autotrs[0])
    72 
    72 
    73 
    73 
    74 class _WorkflowChangedOp(hook.Operation):
    74 class _WorkflowChangedOp(hook.Operation):
    75     """fix entity current state when changing its workflow"""
    75     """fix entity current state when changing its workflow"""
    76 
    76 
    80         session = self.session
    80         session = self.session
    81         pendingeids = session.transaction_data.get('pendingeids', ())
    81         pendingeids = session.transaction_data.get('pendingeids', ())
    82         if self.eid in pendingeids:
    82         if self.eid in pendingeids:
    83             return
    83             return
    84         entity = session.entity_from_eid(self.eid)
    84         entity = session.entity_from_eid(self.eid)
       
    85         iworkflowable = entity.cw_adapt_to('IWorkflowable')
    85         # check custom workflow has not been rechanged to another one in the same
    86         # check custom workflow has not been rechanged to another one in the same
    86         # transaction
    87         # transaction
    87         mainwf = entity.main_workflow
    88         mainwf = iworkflowable.main_workflow
    88         if mainwf.eid == self.wfeid:
    89         if mainwf.eid == self.wfeid:
    89             deststate = mainwf.initial
    90             deststate = mainwf.initial
    90             if not deststate:
    91             if not deststate:
    91                 qname = role_name('custom_workflow', 'subject')
    92                 qname = role_name('custom_workflow', 'subject')
    92                 msg = session._('workflow has no initial state')
    93                 msg = session._('workflow has no initial state')
    93                 raise ValidationError(entity.eid, {qname: msg})
    94                 raise ValidationError(entity.eid, {qname: msg})
    94             if mainwf.state_by_eid(entity.current_state.eid):
    95             if mainwf.state_by_eid(iworkflowable.current_state.eid):
    95                 # nothing to do
    96                 # nothing to do
    96                 return
    97                 return
    97             # if there are no history, simply go to new workflow's initial state
    98             # if there are no history, simply go to new workflow's initial state
    98             if not entity.workflow_history:
    99             if not iworkflowable.workflow_history:
    99                 if entity.current_state.eid != deststate.eid:
   100                 if iworkflowable.current_state.eid != deststate.eid:
   100                     _change_state(session, entity.eid,
   101                     _change_state(session, entity.eid,
   101                                   entity.current_state.eid, deststate.eid)
   102                                   iworkflowable.current_state.eid, deststate.eid)
   102                     _FireAutotransitionOp(session, entity=entity)
   103                     _FireAutotransitionOp(session, entity=entity)
   103                 return
   104                 return
   104             msg = session._('workflow changed to "%s"')
   105             msg = session._('workflow changed to "%s"')
   105             msg %= session._(mainwf.name)
   106             msg %= session._(mainwf.name)
   106             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
   107             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
   107             entity.change_state(deststate, msg, u'text/plain')
   108             iworkflowable.change_state(deststate, msg, u'text/plain')
   108 
   109 
   109 
   110 
   110 class _CheckTrExitPoint(hook.Operation):
   111 class _CheckTrExitPoint(hook.Operation):
   111 
   112 
   112     def precommit_event(self):
   113     def precommit_event(self):
   123 class _SubWorkflowExitOp(hook.Operation):
   124 class _SubWorkflowExitOp(hook.Operation):
   124 
   125 
   125     def precommit_event(self):
   126     def precommit_event(self):
   126         session = self.session
   127         session = self.session
   127         forentity = self.forentity
   128         forentity = self.forentity
       
   129         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   128         trinfo = self.trinfo
   130         trinfo = self.trinfo
   129         # we're in a subworkflow, check if we've reached an exit point
   131         # we're in a subworkflow, check if we've reached an exit point
   130         wftr = forentity.subworkflow_input_transition()
   132         wftr = iworkflowable.subworkflow_input_transition()
   131         if wftr is None:
   133         if wftr is None:
   132             # inconsistency detected
   134             # inconsistency detected
   133             qname = role_name('to_state', 'subject')
   135             qname = role_name('to_state', 'subject')
   134             msg = session._("state doesn't belong to entity's current workflow")
   136             msg = session._("state doesn't belong to entity's current workflow")
   135             raise ValidationError(self.trinfo.eid, {'to_state': msg})
   137             raise ValidationError(self.trinfo.eid, {'to_state': msg})
   136         tostate = wftr.get_exit_point(forentity, trinfo['to_state'])
   138         tostate = wftr.get_exit_point(forentity, trinfo['to_state'])
   137         if tostate is not None:
   139         if tostate is not None:
   138             # reached an exit point
   140             # reached an exit point
   139             msg = session._('exiting from subworkflow %s')
   141             msg = session._('exiting from subworkflow %s')
   140             msg %= session._(forentity.current_workflow.name)
   142             msg %= session._(iworkflowable.current_workflow.name)
   141             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
   143             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
   142             forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
   144             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
   143 
   145 
   144 
   146 
   145 # hooks ########################################################################
   147 # hooks ########################################################################
   146 
   148 
   147 class WorkflowHook(hook.Hook):
   149 class WorkflowHook(hook.Hook):
   149     category = 'worfklow'
   151     category = 'worfklow'
   150 
   152 
   151 
   153 
   152 class SetInitialStateHook(WorkflowHook):
   154 class SetInitialStateHook(WorkflowHook):
   153     __regid__ = 'wfsetinitial'
   155     __regid__ = 'wfsetinitial'
   154     __select__ = WorkflowHook.__select__ & implements(IWorkflowable)
   156     __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable')
   155     events = ('after_add_entity',)
   157     events = ('after_add_entity',)
   156 
   158 
   157     def __call__(self):
   159     def __call__(self):
   158         _SetInitialStateOp(self._cw, entity=self.entity)
   160         _SetInitialStateOp(self._cw, entity=self.entity)
   159 
   161 
   187         except KeyError:
   189         except KeyError:
   188             qname = role_name('wf_info_for', 'subject')
   190             qname = role_name('wf_info_for', 'subject')
   189             msg = session._('mandatory relation')
   191             msg = session._('mandatory relation')
   190             raise ValidationError(entity.eid, {qname: msg})
   192             raise ValidationError(entity.eid, {qname: msg})
   191         forentity = session.entity_from_eid(foreid)
   193         forentity = session.entity_from_eid(foreid)
       
   194         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   192         # then check it has a workflow set, unless we're in the process of changing
   195         # then check it has a workflow set, unless we're in the process of changing
   193         # entity's workflow
   196         # entity's workflow
   194         if session.transaction_data.get((forentity.eid, 'customwf')):
   197         if session.transaction_data.get((forentity.eid, 'customwf')):
   195             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
   198             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
   196             wf = session.entity_from_eid(wfeid)
   199             wf = session.entity_from_eid(wfeid)
   197         else:
   200         else:
   198             wf = forentity.current_workflow
   201             wf = iworkflowable.current_workflow
   199         if wf is None:
   202         if wf is None:
   200             msg = session._('related entity has no workflow set')
   203             msg = session._('related entity has no workflow set')
   201             raise ValidationError(entity.eid, {None: msg})
   204             raise ValidationError(entity.eid, {None: msg})
   202         # then check it has a state set
   205         # then check it has a state set
   203         fromstate = forentity.current_state
   206         fromstate = iworkflowable.current_state
   204         if fromstate is None:
   207         if fromstate is None:
   205             msg = session._('related entity has no state')
   208             msg = session._('related entity has no state')
   206             raise ValidationError(entity.eid, {None: msg})
   209             raise ValidationError(entity.eid, {None: msg})
   207         # True if we are coming back from subworkflow
   210         # True if we are coming back from subworkflow
   208         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
   211         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
   276     def __call__(self):
   279     def __call__(self):
   277         trinfo = self.entity
   280         trinfo = self.entity
   278         _change_state(self._cw, trinfo['wf_info_for'],
   281         _change_state(self._cw, trinfo['wf_info_for'],
   279                       trinfo['from_state'], trinfo['to_state'])
   282                       trinfo['from_state'], trinfo['to_state'])
   280         forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
   283         forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
   281         assert forentity.current_state.eid == trinfo['to_state']
   284         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   282         if forentity.main_workflow.eid != forentity.current_workflow.eid:
   285         assert iworkflowable.current_state.eid == trinfo['to_state']
       
   286         if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
   283             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
   287             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
   284 
   288 
   285 
   289 
   286 class CheckInStateChangeAllowed(WorkflowHook):
   290 class CheckInStateChangeAllowed(WorkflowHook):
   287     """check state apply, in case of direct in_state change using unsafe execute
   291     """check state apply, in case of direct in_state change using unsafe execute
   295         nocheck = session.transaction_data.get('skip-security', ())
   299         nocheck = session.transaction_data.get('skip-security', ())
   296         if (self.eidfrom, 'in_state', self.eidto) in nocheck:
   300         if (self.eidfrom, 'in_state', self.eidto) in nocheck:
   297             # state changed through TrInfo insertion, so we already know it's ok
   301             # state changed through TrInfo insertion, so we already know it's ok
   298             return
   302             return
   299         entity = session.entity_from_eid(self.eidfrom)
   303         entity = session.entity_from_eid(self.eidfrom)
   300         mainwf = entity.main_workflow
   304         iworkflowable = entity.cw_adapt_to('IWorkflowable')
       
   305         mainwf = iworkflowable.main_workflow
   301         if mainwf is None:
   306         if mainwf is None:
   302             msg = session._('entity has no workflow set')
   307             msg = session._('entity has no workflow set')
   303             raise ValidationError(entity.eid, {None: msg})
   308             raise ValidationError(entity.eid, {None: msg})
   304         for wf in mainwf.iter_workflows():
   309         for wf in mainwf.iter_workflows():
   305             if wf.state_by_eid(self.eidto):
   310             if wf.state_by_eid(self.eidto):
   307         else:
   312         else:
   308             qname = role_name('in_state', 'subject')
   313             qname = role_name('in_state', 'subject')
   309             msg = session._("state doesn't belong to entity's workflow. You may "
   314             msg = session._("state doesn't belong to entity's workflow. You may "
   310                             "want to set a custom workflow for this entity first.")
   315                             "want to set a custom workflow for this entity first.")
   311             raise ValidationError(self.eidfrom, {qname: msg})
   316             raise ValidationError(self.eidfrom, {qname: msg})
   312         if entity.current_workflow and wf.eid != entity.current_workflow.eid:
   317         if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
   313             qname = role_name('in_state', 'subject')
   318             qname = role_name('in_state', 'subject')
   314             msg = session._("state doesn't belong to entity's current workflow")
   319             msg = session._("state doesn't belong to entity's current workflow")
   315             raise ValidationError(self.eidfrom, {qname: msg})
   320             raise ValidationError(self.eidfrom, {qname: msg})
   316 
   321 
   317 
   322 
   357     __regid__ = 'wfdelcustom'
   362     __regid__ = 'wfdelcustom'
   358     events = ('after_delete_relation',)
   363     events = ('after_delete_relation',)
   359 
   364 
   360     def __call__(self):
   365     def __call__(self):
   361         entity = self._cw.entity_from_eid(self.eidfrom)
   366         entity = self._cw.entity_from_eid(self.eidfrom)
   362         typewf = entity.cwetype_workflow()
   367         typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow()
   363         if typewf is not None:
   368         if typewf is not None:
   364             _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
   369             _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
   365 
   370