hooks/workflow.py
changeset 9615 6ba726dbf4fd
parent 9469 032825bbacab
child 10666 7f6b5f023884
equal deleted inserted replaced
9614:e5ba755d8ca7 9615:6ba726dbf4fd
    26 from cubicweb import RepositoryError, validation_error
    26 from cubicweb import RepositoryError, validation_error
    27 from cubicweb.predicates import is_instance, adaptable
    27 from cubicweb.predicates import is_instance, adaptable
    28 from cubicweb.server import hook
    28 from cubicweb.server import hook
    29 
    29 
    30 
    30 
    31 def _change_state(session, x, oldstate, newstate):
    31 def _change_state(cnx, x, oldstate, newstate):
    32     nocheck = session.transaction_data.setdefault('skip-security', set())
    32     nocheck = cnx.transaction_data.setdefault('skip-security', set())
    33     nocheck.add((x, 'in_state', oldstate))
    33     nocheck.add((x, 'in_state', oldstate))
    34     nocheck.add((x, 'in_state', newstate))
    34     nocheck.add((x, 'in_state', newstate))
    35     # delete previous state first
    35     # delete previous state first
    36     session.delete_relation(x, 'in_state', oldstate)
    36     cnx.delete_relation(x, 'in_state', oldstate)
    37     session.add_relation(x, 'in_state', newstate)
    37     cnx.add_relation(x, 'in_state', newstate)
    38 
    38 
    39 
    39 
    40 # operations ###################################################################
    40 # operations ###################################################################
    41 
    41 
    42 class _SetInitialStateOp(hook.Operation):
    42 class _SetInitialStateOp(hook.Operation):
    43     """make initial state be a default state"""
    43     """make initial state be a default state"""
    44     entity = None # make pylint happy
    44     entity = None # make pylint happy
    45 
    45 
    46     def precommit_event(self):
    46     def precommit_event(self):
    47         session = self.session
    47         cnx = self.cnx
    48         entity = self.entity
    48         entity = self.entity
    49         iworkflowable = entity.cw_adapt_to('IWorkflowable')
    49         iworkflowable = entity.cw_adapt_to('IWorkflowable')
    50         # if there is an initial state and the entity's state is not set,
    50         # if there is an initial state and the entity's state is not set,
    51         # use the initial state as a default state
    51         # use the initial state as a default state
    52         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
    52         if not (cnx.deleted_in_transaction(entity.eid) or entity.in_state) \
    53                and iworkflowable.current_workflow:
    53                and iworkflowable.current_workflow:
    54             state = iworkflowable.current_workflow.initial
    54             state = iworkflowable.current_workflow.initial
    55             if state:
    55             if state:
    56                 session.add_relation(entity.eid, 'in_state', state.eid)
    56                 cnx.add_relation(entity.eid, 'in_state', state.eid)
    57                 _FireAutotransitionOp(session, entity=entity)
    57                 _FireAutotransitionOp(cnx, entity=entity)
    58 
    58 
    59 class _FireAutotransitionOp(hook.Operation):
    59 class _FireAutotransitionOp(hook.Operation):
    60     """try to fire auto transition after state changes"""
    60     """try to fire auto transition after state changes"""
    61     entity = None # make pylint happy
    61     entity = None # make pylint happy
    62 
    62 
    74     eid = wfeid = None # make pylint happy
    74     eid = wfeid = None # make pylint happy
    75 
    75 
    76     def precommit_event(self):
    76     def precommit_event(self):
    77         # notice that enforcement that new workflow apply to the entity's type is
    77         # notice that enforcement that new workflow apply to the entity's type is
    78         # done by schema rule, no need to check it here
    78         # done by schema rule, no need to check it here
    79         session = self.session
    79         cnx = self.cnx
    80         pendingeids = session.transaction_data.get('pendingeids', ())
    80         pendingeids = cnx.transaction_data.get('pendingeids', ())
    81         if self.eid in pendingeids:
    81         if self.eid in pendingeids:
    82             return
    82             return
    83         entity = session.entity_from_eid(self.eid)
    83         entity = cnx.entity_from_eid(self.eid)
    84         iworkflowable = entity.cw_adapt_to('IWorkflowable')
    84         iworkflowable = entity.cw_adapt_to('IWorkflowable')
    85         # check custom workflow has not been rechanged to another one in the same
    85         # check custom workflow has not been rechanged to another one in the same
    86         # transaction
    86         # transaction
    87         mainwf = iworkflowable.main_workflow
    87         mainwf = iworkflowable.main_workflow
    88         if mainwf.eid == self.wfeid:
    88         if mainwf.eid == self.wfeid:
    94                 # nothing to do
    94                 # nothing to do
    95                 return
    95                 return
    96             # if there are no history, simply go to new workflow's initial state
    96             # if there are no history, simply go to new workflow's initial state
    97             if not iworkflowable.workflow_history:
    97             if not iworkflowable.workflow_history:
    98                 if iworkflowable.current_state.eid != deststate.eid:
    98                 if iworkflowable.current_state.eid != deststate.eid:
    99                     _change_state(session, entity.eid,
    99                     _change_state(cnx, entity.eid,
   100                                   iworkflowable.current_state.eid, deststate.eid)
   100                                   iworkflowable.current_state.eid, deststate.eid)
   101                     _FireAutotransitionOp(session, entity=entity)
   101                     _FireAutotransitionOp(cnx, entity=entity)
   102                 return
   102                 return
   103             msg = session._('workflow changed to "%s"')
   103             msg = cnx._('workflow changed to "%s"')
   104             msg %= session._(mainwf.name)
   104             msg %= cnx._(mainwf.name)
   105             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
   105             cnx.transaction_data[(entity.eid, 'customwf')] = self.wfeid
   106             iworkflowable.change_state(deststate, msg, u'text/plain')
   106             iworkflowable.change_state(deststate, msg, u'text/plain')
   107 
   107 
   108 
   108 
   109 class _CheckTrExitPoint(hook.Operation):
   109 class _CheckTrExitPoint(hook.Operation):
   110     treid = None # make pylint happy
   110     treid = None # make pylint happy
   111 
   111 
   112     def precommit_event(self):
   112     def precommit_event(self):
   113         tr = self.session.entity_from_eid(self.treid)
   113         tr = self.cnx.entity_from_eid(self.treid)
   114         outputs = set()
   114         outputs = set()
   115         for ep in tr.subworkflow_exit:
   115         for ep in tr.subworkflow_exit:
   116             if ep.subwf_state.eid in outputs:
   116             if ep.subwf_state.eid in outputs:
   117                 msg = _("can't have multiple exits on the same state")
   117                 msg = _("can't have multiple exits on the same state")
   118                 raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
   118                 raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
   121 
   121 
   122 class _SubWorkflowExitOp(hook.Operation):
   122 class _SubWorkflowExitOp(hook.Operation):
   123     forentity = trinfo = None # make pylint happy
   123     forentity = trinfo = None # make pylint happy
   124 
   124 
   125     def precommit_event(self):
   125     def precommit_event(self):
   126         session = self.session
   126         cnx = self.cnx
   127         forentity = self.forentity
   127         forentity = self.forentity
   128         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   128         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   129         trinfo = self.trinfo
   129         trinfo = self.trinfo
   130         # we're in a subworkflow, check if we've reached an exit point
   130         # we're in a subworkflow, check if we've reached an exit point
   131         wftr = iworkflowable.subworkflow_input_transition()
   131         wftr = iworkflowable.subworkflow_input_transition()
   135             raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
   135             raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
   136         tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
   136         tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
   137         if tostate is not None:
   137         if tostate is not None:
   138             # reached an exit point
   138             # reached an exit point
   139             msg = _('exiting from subworkflow %s')
   139             msg = _('exiting from subworkflow %s')
   140             msg %= session._(iworkflowable.current_workflow.name)
   140             msg %= cnx._(iworkflowable.current_workflow.name)
   141             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
   141             cnx.transaction_data[(forentity.eid, 'subwfentrytr')] = True
   142             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
   142             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
   143 
   143 
   144 
   144 
   145 # hooks ########################################################################
   145 # hooks ########################################################################
   146 
   146 
   171     __regid__ = 'wffiretransition'
   171     __regid__ = 'wffiretransition'
   172     __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
   172     __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
   173     events = ('before_add_entity',)
   173     events = ('before_add_entity',)
   174 
   174 
   175     def __call__(self):
   175     def __call__(self):
   176         session = self._cw
   176         cnx = self._cw
   177         entity = self.entity
   177         entity = self.entity
   178         # first retreive entity to which the state change apply
   178         # first retreive entity to which the state change apply
   179         try:
   179         try:
   180             foreid = entity.cw_attr_cache['wf_info_for']
   180             foreid = entity.cw_attr_cache['wf_info_for']
   181         except KeyError:
   181         except KeyError:
   182             msg = _('mandatory relation')
   182             msg = _('mandatory relation')
   183             raise validation_error(entity, {('wf_info_for', 'subject'): msg})
   183             raise validation_error(entity, {('wf_info_for', 'subject'): msg})
   184         forentity = session.entity_from_eid(foreid)
   184         forentity = cnx.entity_from_eid(foreid)
   185         # see comment in the TrInfo entity definition
   185         # see comment in the TrInfo entity definition
   186         entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
   186         entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
   187         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   187         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   188         # then check it has a workflow set, unless we're in the process of changing
   188         # then check it has a workflow set, unless we're in the process of changing
   189         # entity's workflow
   189         # entity's workflow
   190         if session.transaction_data.get((forentity.eid, 'customwf')):
   190         if cnx.transaction_data.get((forentity.eid, 'customwf')):
   191             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
   191             wfeid = cnx.transaction_data[(forentity.eid, 'customwf')]
   192             wf = session.entity_from_eid(wfeid)
   192             wf = cnx.entity_from_eid(wfeid)
   193         else:
   193         else:
   194             wf = iworkflowable.current_workflow
   194             wf = iworkflowable.current_workflow
   195         if wf is None:
   195         if wf is None:
   196             msg = _('related entity has no workflow set')
   196             msg = _('related entity has no workflow set')
   197             raise validation_error(entity, {None: msg})
   197             raise validation_error(entity, {None: msg})
   199         fromstate = iworkflowable.current_state
   199         fromstate = iworkflowable.current_state
   200         if fromstate is None:
   200         if fromstate is None:
   201             msg = _('related entity has no state')
   201             msg = _('related entity has no state')
   202             raise validation_error(entity, {None: msg})
   202             raise validation_error(entity, {None: msg})
   203         # True if we are coming back from subworkflow
   203         # True if we are coming back from subworkflow
   204         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
   204         swtr = cnx.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
   205         cowpowers = (session.user.is_in_group('managers')
   205         cowpowers = (cnx.user.is_in_group('managers')
   206                      or not session.write_security)
   206                      or not cnx.write_security)
   207         # no investigate the requested state change...
   207         # no investigate the requested state change...
   208         try:
   208         try:
   209             treid = entity.cw_attr_cache['by_transition']
   209             treid = entity.cw_attr_cache['by_transition']
   210         except KeyError:
   210         except KeyError:
   211             # no transition set, check user is a manager and destination state
   211             # no transition set, check user is a manager and destination state
   222                 msg = _("state doesn't belong to entity's workflow")
   222                 msg = _("state doesn't belong to entity's workflow")
   223                 raise validation_error(entity, {('to_state', 'subject'): msg})
   223                 raise validation_error(entity, {('to_state', 'subject'): msg})
   224         else:
   224         else:
   225             # check transition is valid and allowed, unless we're coming back
   225             # check transition is valid and allowed, unless we're coming back
   226             # from subworkflow
   226             # from subworkflow
   227             tr = session.entity_from_eid(treid)
   227             tr = cnx.entity_from_eid(treid)
   228             if swtr is None:
   228             if swtr is None:
   229                 qname = ('by_transition', 'subject')
   229                 qname = ('by_transition', 'subject')
   230                 if tr is None:
   230                 if tr is None:
   231                     msg = _("transition doesn't belong to entity's workflow")
   231                     msg = _("transition doesn't belong to entity's workflow")
   232                     raise validation_error(entity, {qname: msg})
   232                     raise validation_error(entity, {qname: msg})
   241             if deststateeid is not None:
   241             if deststateeid is not None:
   242                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
   242                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
   243                     msg = _("transition isn't allowed")
   243                     msg = _("transition isn't allowed")
   244                     raise validation_error(entity, {('by_transition', 'subject'): msg})
   244                     raise validation_error(entity, {('by_transition', 'subject'): msg})
   245                 if swtr is None:
   245                 if swtr is None:
   246                     deststate = session.entity_from_eid(deststateeid)
   246                     deststate = cnx.entity_from_eid(deststateeid)
   247                     if not cowpowers and deststate is None:
   247                     if not cowpowers and deststate is None:
   248                         msg = _("state doesn't belong to entity's workflow")
   248                         msg = _("state doesn't belong to entity's workflow")
   249                         raise validation_error(entity, {('to_state', 'subject'): msg})
   249                         raise validation_error(entity, {('to_state', 'subject'): msg})
   250             else:
   250             else:
   251                 deststateeid = tr.destination(forentity).eid
   251                 deststateeid = tr.destination(forentity).eid
   252         # everything is ok, add missing information on the trinfo entity
   252         # everything is ok, add missing information on the trinfo entity
   253         entity.cw_edited['from_state'] = fromstate.eid
   253         entity.cw_edited['from_state'] = fromstate.eid
   254         entity.cw_edited['to_state'] = deststateeid
   254         entity.cw_edited['to_state'] = deststateeid
   255         nocheck = session.transaction_data.setdefault('skip-security', set())
   255         nocheck = cnx.transaction_data.setdefault('skip-security', set())
   256         nocheck.add((entity.eid, 'from_state', fromstate.eid))
   256         nocheck.add((entity.eid, 'from_state', fromstate.eid))
   257         nocheck.add((entity.eid, 'to_state', deststateeid))
   257         nocheck.add((entity.eid, 'to_state', deststateeid))
   258         _FireAutotransitionOp(session, entity=forentity)
   258         _FireAutotransitionOp(cnx, entity=forentity)
   259 
   259 
   260 
   260 
   261 class FiredTransitionHook(WorkflowHook):
   261 class FiredTransitionHook(WorkflowHook):
   262     """change related entity state and handle exit of subworkflow"""
   262     """change related entity state and handle exit of subworkflow"""
   263     __regid__ = 'wffiretransition'
   263     __regid__ = 'wffiretransition'
   283     __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
   283     __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
   284     events = ('before_add_relation',)
   284     events = ('before_add_relation',)
   285     category = 'integrity'
   285     category = 'integrity'
   286 
   286 
   287     def __call__(self):
   287     def __call__(self):
   288         session = self._cw
   288         cnx = self._cw
   289         nocheck = session.transaction_data.get('skip-security', ())
   289         nocheck = cnx.transaction_data.get('skip-security', ())
   290         if (self.eidfrom, 'in_state', self.eidto) in nocheck:
   290         if (self.eidfrom, 'in_state', self.eidto) in nocheck:
   291             # state changed through TrInfo insertion, so we already know it's ok
   291             # state changed through TrInfo insertion, so we already know it's ok
   292             return
   292             return
   293         entity = session.entity_from_eid(self.eidfrom)
   293         entity = cnx.entity_from_eid(self.eidfrom)
   294         iworkflowable = entity.cw_adapt_to('IWorkflowable')
   294         iworkflowable = entity.cw_adapt_to('IWorkflowable')
   295         mainwf = iworkflowable.main_workflow
   295         mainwf = iworkflowable.main_workflow
   296         if mainwf is None:
   296         if mainwf is None:
   297             msg = _('entity has no workflow set')
   297             msg = _('entity has no workflow set')
   298             raise validation_error(entity, {None: msg})
   298             raise validation_error(entity, {None: msg})