hooks/workflow.py
changeset 3024 bfaf056f1029
parent 2968 0e3460341023
child 3072 6fb42c53f6df
equal deleted inserted replaced
3023:7864fee8b4ec 3024:bfaf056f1029
    11 
    11 
    12 from cubicweb import RepositoryError, ValidationError
    12 from cubicweb import RepositoryError, ValidationError
    13 from cubicweb.interfaces import IWorkflowable
    13 from cubicweb.interfaces import IWorkflowable
    14 from cubicweb.selectors import entity_implements
    14 from cubicweb.selectors import entity_implements
    15 from cubicweb.server import hook
    15 from cubicweb.server import hook
       
    16 from cubicweb.entities.wfobjs import WorkflowTransition
    16 
    17 
    17 
    18 
    18 def _change_state(session, x, oldstate, newstate):
    19 def _change_state(session, x, oldstate, newstate):
    19     nocheck = session.transaction_data.setdefault('skip-security', set())
    20     nocheck = session.transaction_data.setdefault('skip-security', set())
    20     nocheck.add((x, 'in_state', oldstate))
    21     nocheck.add((x, 'in_state', oldstate))
    22     # delete previous state first in case we're using a super session
    23     # delete previous state first in case we're using a super session
    23     session.delete_relation(x, 'in_state', oldstate)
    24     session.delete_relation(x, 'in_state', oldstate)
    24     session.add_relation(x, 'in_state', newstate)
    25     session.add_relation(x, 'in_state', newstate)
    25 
    26 
    26 
    27 
       
    28 # operations ###################################################################
       
    29 
    27 class _SetInitialStateOp(hook.Operation):
    30 class _SetInitialStateOp(hook.Operation):
    28     """make initial state be a default state"""
    31     """make initial state be a default state"""
    29 
    32 
    30     def precommit_event(self):
    33     def precommit_event(self):
    31         session = self.session
    34         session = self.session
    32         entity = self.entity
    35         entity = self.entity
    33         # if there is an initial state and the entity's state is not set,
    36         # if there is an initial state and the entity's state is not set,
    34         # use the initial state as a default state
    37         # use the initial state as a default state
    35         pendingeids = session.transaction_data.get('pendingeids', ())
       
    36         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
    38         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
    37                and entity.current_workflow:
    39                and entity.current_workflow:
    38             state = entity.current_workflow.initial
    40             state = entity.current_workflow.initial
    39             if state:
    41             if state:
    40                 # use super session to by-pass security checks
    42                 # use super session to by-pass security checks
    43 
    45 
    44 class _WorkflowChangedOp(hook.Operation):
    46 class _WorkflowChangedOp(hook.Operation):
    45     """fix entity current state when changing its workflow"""
    47     """fix entity current state when changing its workflow"""
    46 
    48 
    47     def precommit_event(self):
    49     def precommit_event(self):
       
    50         # notice that enforcement that new workflow apply to the entity's type is
       
    51         # done by schema rule, no need to check it here
    48         session = self.session
    52         session = self.session
    49         if session.deleted_in_transaction(self.eid):
    53         pendingeids = session.transaction_data.get('pendingeids', ())
       
    54         if self.eid in pendingeids:
    50             return
    55             return
    51         entity = session.entity_from_eid(self.eid)
    56         entity = session.entity_from_eid(self.eid)
    52         # notice that enforcment that new workflow apply to the entity's type is
    57         # check custom workflow has not been rechanged to another one in the same
    53         # done by schema rule, no need to check it here
    58         # transaction
    54         if entity.current_workflow.eid == self.wfeid:
    59         mainwf = entity.main_workflow
    55             deststate = entity.current_workflow.initial
    60         if mainwf.eid == self.wfeid:
       
    61             deststate = mainwf.initial
    56             if not deststate:
    62             if not deststate:
    57                 msg = session._('workflow has no initial state')
    63                 msg = session._('workflow has no initial state')
    58                 raise ValidationError(entity.eid, {'custom_workflow': msg})
    64                 raise ValidationError(entity.eid, {'custom_workflow': msg})
    59             if entity.current_workflow.state_by_eid(entity.current_state.eid):
    65             if mainwf.state_by_eid(entity.current_state.eid):
    60                 # nothing to do
    66                 # nothing to do
    61                 return
    67                 return
    62             # if there are no history, simply go to new workflow's initial state
    68             # if there are no history, simply go to new workflow's initial state
    63             if not entity.workflow_history:
    69             if not entity.workflow_history:
    64                 if entity.current_state.eid != deststate.eid:
    70                 if entity.current_state.eid != deststate.eid:
    65                     _change_state(session, entity.eid,
    71                     _change_state(session, entity.eid,
    66                                   entity.current_state.eid, deststate.eid)
    72                                   entity.current_state.eid, deststate.eid)
    67                 return
    73                 return
    68             msg = session._('workflow changed to "%s"')
    74             msg = session._('workflow changed to "%s"')
    69             msg %= entity.current_workflow.name
    75             msg %= session._(mainwf.name)
    70             entity.change_state(deststate.name, msg)
    76             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
    71 
    77             entity.change_state(deststate, msg, u'text/plain')
    72 
    78 
       
    79 
       
    80 class _CheckTrExitPoint(hook.Operation):
       
    81 
       
    82     def precommit_event(self):
       
    83         tr = self.session.entity_from_eid(self.treid)
       
    84         outputs = set()
       
    85         for ep in tr.subworkflow_exit:
       
    86             if ep.subwf_state.eid in outputs:
       
    87                 msg = self.session._("can't have multiple exits on the same state")
       
    88                 raise ValidationError(self.treid, {'subworkflow_exit': msg})
       
    89             outputs.add(ep.subwf_state.eid)
       
    90 
       
    91 
       
    92 # hooks ########################################################################
    73 
    93 
    74 class WorkflowHook(hook.Hook):
    94 class WorkflowHook(hook.Hook):
    75     __abstract__ = True
    95     __abstract__ = True
    76     category = 'worfklow'
    96     category = 'worfklow'
    77 
    97 
   113             foreid = entity['wf_info_for']
   133             foreid = entity['wf_info_for']
   114         except KeyError:
   134         except KeyError:
   115             msg = session._('mandatory relation')
   135             msg = session._('mandatory relation')
   116             raise ValidationError(entity.eid, {'wf_info_for': msg})
   136             raise ValidationError(entity.eid, {'wf_info_for': msg})
   117         forentity = session.entity_from_eid(foreid)
   137         forentity = session.entity_from_eid(foreid)
   118         # then check it has a workflow set
   138         # then check it has a workflow set, unless we're in the process of changing
   119         wf = forentity.current_workflow
   139         # entity's workflow
       
   140         if session.transaction_data.get((forentity.eid, 'customwf')):
       
   141             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
       
   142             wf = session.entity_from_eid(wfeid)
       
   143         else:
       
   144             wf = forentity.current_workflow
   120         if wf is None:
   145         if wf is None:
   121             msg = session._('related entity has no workflow set')
   146             msg = session._('related entity has no workflow set')
   122             raise ValidationError(entity.eid, {None: msg})
   147             raise ValidationError(entity.eid, {None: msg})
   123         # then check it has a state set
   148         # then check it has a state set
   124         fromstate = forentity.current_state
   149         fromstate = forentity.current_state
   125         if fromstate is None:
   150         if fromstate is None:
   126             msg = session._('related entity has no state')
   151             msg = session._('related entity has no state')
   127             raise ValidationError(entity.eid, {None: msg})
   152             raise ValidationError(entity.eid, {None: msg})
       
   153         # True if we are coming back from subworkflow
       
   154         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
       
   155         cowpowers = session.is_super_session or 'managers' in session.user.groups
   128         # no investigate the requested state change...
   156         # no investigate the requested state change...
   129         try:
   157         try:
   130             treid = entity['by_transition']
   158             treid = entity['by_transition']
   131         except KeyError:
   159         except KeyError:
   132             # no transition set, check user is a manager and destination state is
   160             # no transition set, check user is a manager and destination state is
   133             # specified (and valid)
   161             # specified (and valid)
   134             if not (session.is_super_session or 'managers' in session.user.groups):
   162             if not cowpowers:
   135                 msg = session._('mandatory relation')
   163                 msg = session._('mandatory relation')
   136                 raise ValidationError(entity.eid, {'by_transition': msg})
   164                 raise ValidationError(entity.eid, {'by_transition': msg})
   137             deststateeid = entity.get('to_state')
   165             deststateeid = entity.get('to_state')
   138             if not deststateeid:
   166             if not deststateeid:
   139                 msg = session._('mandatory relation')
   167                 msg = session._('mandatory relation')
   140                 raise ValidationError(entity.eid, {'by_transition': msg})
   168                 raise ValidationError(entity.eid, {'by_transition': msg})
   141             deststate = wf.state_by_eid(deststateeid)
   169             deststate = wf.state_by_eid(deststateeid)
   142             if deststate is None:
   170             if not cowpowers and deststate is None:
   143                 msg = session._("state doesn't belong to entity's workflow")
   171                 msg = entity.req._("state doesn't belong to entity's workflow")
   144                 raise ValidationError(entity.eid, {'to_state': msg})
   172                 raise ValidationError(entity.eid, {'to_state': msg})
   145         else:
   173         else:
   146             # check transition is valid and allowed
   174             # check transition is valid and allowed, unless we're coming back from
   147             tr = wf.transition_by_eid(treid)
   175             # subworkflow
   148             if tr is None:
   176             tr = session.entity_from_eid(treid)
   149                 msg = session._("transition doesn't belong to entity's workflow")
   177             if swtr is None:
   150                 raise ValidationError(entity.eid, {'by_transition': msg})
   178                 if tr is None:
   151             if not tr.has_input_state(fromstate):
   179                     msg = session._("transition doesn't belong to entity's workflow")
   152                 msg = session._("transition isn't allowed")
   180                     raise ValidationError(entity.eid, {'by_transition': msg})
   153                 raise ValidationError(entity.eid, {'by_transition': msg})
   181                 if not tr.has_input_state(fromstate):
   154             if not tr.may_be_fired(foreid):
   182                     msg = session._("transition isn't allowed")
   155                 msg = session._("transition may not be fired")
   183                     raise ValidationError(entity.eid, {'by_transition': msg})
   156                 raise ValidationError(entity.eid, {'by_transition': msg})
   184                 if not tr.may_be_fired(foreid):
   157             deststateeid = tr.destination().eid
   185                     msg = session._("transition may not be fired")
       
   186                     raise ValidationError(entity.eid, {'by_transition': msg})
       
   187             if entity.get('to_state'):
       
   188                 deststateeid = entity['to_state']
       
   189                 if not cowpowers and deststateeid != tr.destination().eid:
       
   190                     msg = session._("transition isn't allowed")
       
   191                     raise ValidationError(entity.eid, {'by_transition': msg})
       
   192                 if swtr is None:
       
   193                     deststate = session.entity_from_eid(deststateeid)
       
   194                     if not cowpowers and deststate is None:
       
   195                         msg = entity.req._("state doesn't belong to entity's workflow")
       
   196                         raise ValidationError(entity.eid, {'to_state': msg})
       
   197             else:
       
   198                 deststateeid = tr.destination().eid
   158         # everything is ok, add missing information on the trinfo entity
   199         # everything is ok, add missing information on the trinfo entity
   159         entity['from_state'] = fromstate.eid
   200         entity['from_state'] = fromstate.eid
   160         entity['to_state'] = deststateeid
   201         entity['to_state'] = deststateeid
   161         nocheck = session.transaction_data.setdefault('skip-security', set())
   202         nocheck = session.transaction_data.setdefault('skip-security', set())
   162         nocheck.add((entity.eid, 'from_state', fromstate.eid))
   203         nocheck.add((entity.eid, 'from_state', fromstate.eid))
   168     __id__ = 'wffiretransition'
   209     __id__ = 'wffiretransition'
   169     __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
   210     __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
   170     events = ('after_add_entity',)
   211     events = ('after_add_entity',)
   171 
   212 
   172     def __call__(self):
   213     def __call__(self):
   173         _change_state(self._cw, self.entity['wf_info_for'],
   214         session = self._cw
   174                       self.entity['from_state'], self.entity['to_state'])
   215         entity = self.entity
       
   216         _change_state(session, entity['wf_info_for'],
       
   217                       entity['from_state'], entity['to_state'])
       
   218         forentity = session.entity_from_eid(entity['wf_info_for'])
       
   219         assert forentity.current_state.eid == entity['to_state']
       
   220         if forentity.main_workflow.eid != forentity.current_workflow.eid:
       
   221             # we're in a subworkflow, check if we've reached an exit point
       
   222             wftr = forentity.subworkflow_input_transition()
       
   223             if wftr is None:
       
   224                 # inconsistency detected
       
   225                 msg = entity.req._("state doesn't belong to entity's current workflow")
       
   226                 raise ValidationError(entity.eid, {'to_state': msg})
       
   227             tostate = wftr.get_exit_point(entity['to_state'])
       
   228             if tostate is not None:
       
   229                 # reached an exit point
       
   230                 msg = session._('exiting from subworkflow %s')
       
   231                 msg %= session._(forentity.current_workflow.name)
       
   232                 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
       
   233                 # XXX iirk
       
   234                 req = forentity._cw
       
   235                 forentity._cw = session.super_session
       
   236                 try:
       
   237                     trinfo = forentity.change_state(tostate, msg, u'text/plain',
       
   238                                                     tr=wftr)
       
   239                 finally:
       
   240                     forentity._cw = req
       
   241 
       
   242 
       
   243 class CheckInStateChangeAllowed(WorkflowHook):
       
   244     """check state apply, in case of direct in_state change using unsafe_execute
       
   245     """
       
   246     __id__ = 'wfcheckinstate'
       
   247     __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
       
   248     events = ('before_add_relation',)
       
   249 
       
   250     def __call__(self):
       
   251         session = self._cw
       
   252         nocheck = session.transaction_data.setdefault('skip-security', ())
       
   253         if (self.eidfrom, 'in_state', self.eidto) in nocheck:
       
   254             # state changed through TrInfo insertion, so we already know it's ok
       
   255             return
       
   256         entity = session.entity_from_eid(self.eidfrom)
       
   257         mainwf = entity.main_workflow
       
   258         if mainwf is None:
       
   259             msg = session._('entity has no workflow set')
       
   260             raise ValidationError(entity.eid, {None: msg})
       
   261         for wf in mainwf.iter_workflows():
       
   262             if wf.state_by_eid(self.eidto):
       
   263                 break
       
   264         else:
       
   265             msg = session._("state doesn't belong to entity's workflow. You may "
       
   266                             "want to set a custom workflow for this entity first.")
       
   267             raise ValidationError(self.eidfrom, {'in_state': msg})
       
   268         if entity.current_workflow and wf.eid != entity.current_workflow.eid:
       
   269             msg = session._("state doesn't belong to entity's current workflow")
       
   270             raise ValidationError(self.eidfrom, {'in_state': msg})
   175 
   271 
   176 
   272 
   177 class SetModificationDateOnStateChange(WorkflowHook):
   273 class SetModificationDateOnStateChange(WorkflowHook):
   178     """update entity's modification date after changing its state"""
   274     """update entity's modification date after changing its state"""
   179     __id__ = 'wfsyncmdate'
   275     __id__ = 'wfsyncmdate'
   192             # usually occurs if entity is coming from a read-only source
   288             # usually occurs if entity is coming from a read-only source
   193             # (eg ldap user)
   289             # (eg ldap user)
   194             self.warning('cant change modification date for %s: %s', entity, ex)
   290             self.warning('cant change modification date for %s: %s', entity, ex)
   195 
   291 
   196 
   292 
       
   293 class CheckWorkflowTransitionExitPoint(WorkflowHook):
       
   294     """check that there is no multiple exits from the same state"""
       
   295     __id__ = 'wfcheckwftrexit'
       
   296     __select__ = WorkflowHook.__select__ & hook.match_rtype('subworkflow_exit')
       
   297     events = ('after_add_relation',)
       
   298 
       
   299     def __call__(self):
       
   300         _CheckTrExitPoint(self._cw, treid=self.eidfrom)
       
   301 
       
   302 
   197 class SetCustomWorkflow(WorkflowHook):
   303 class SetCustomWorkflow(WorkflowHook):
   198     __id__ = 'wfsetcustom'
   304     __id__ = 'wfsetcustom'
   199     __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow')
   305     __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow')
   200     events = ('after_add_relation',)
   306     events = ('after_add_relation',)
   201 
   307