hooks/workflow.py
changeset 2968 0e3460341023
parent 2880 bfc8e1831290
child 3024 bfaf056f1029
equal deleted inserted replaced
2902:dd9f2dd02f85 2968:0e3460341023
    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 
    16 
    17 
    17 
    18 
    18 def _change_state(session, x, oldstate, newstate):
    19 def previous_state(session, eid):
    19     nocheck = session.transaction_data.setdefault('skip-security', set())
    20     """return the state of the entity with the given eid,
    20     nocheck.add((x, 'in_state', oldstate))
    21     usually since it's changing in the current transaction. Due to internal
    21     nocheck.add((x, 'in_state', newstate))
    22     relation hooks, the relation may has been deleted at this point, so
    22     # delete previous state first in case we're using a super session
    23     we have handle that
    23     session.delete_relation(x, 'in_state', oldstate)
    24     """
    24     session.add_relation(x, 'in_state', newstate)
    25     # don't check eid has been added in the current transaction, we don't want
       
    26     # to miss previous state of entity whose state change in the same
       
    27     # transaction as it's being created
       
    28     pending = session.transaction_data.get('pendingrelations', ())
       
    29     for eidfrom, rtype, eidto in reversed(pending):
       
    30         if rtype == 'in_state' and eidfrom == eid:
       
    31             rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
       
    32                                    {'x': eidto}, 'x')
       
    33             return rset.get_entity(0, 0)
       
    34     rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
       
    35                            {'x': eid}, 'x')
       
    36     if rset:
       
    37         return rset.get_entity(0, 0)
       
    38 
       
    39 
       
    40 def relation_deleted(session, eidfrom, rtype, eidto):
       
    41     session.transaction_data.setdefault('pendingrelations', []).append(
       
    42         (eidfrom, rtype, eidto))
       
    43 
    25 
    44 
    26 
    45 class _SetInitialStateOp(hook.Operation):
    27 class _SetInitialStateOp(hook.Operation):
    46     """make initial state be a default state"""
    28     """make initial state be a default state"""
    47 
    29 
    48     def precommit_event(self):
    30     def precommit_event(self):
    49         session = self.session
    31         session = self.session
    50         entity = self.entity
    32         entity = self.entity
    51         # if there is an initial state and the entity's state is not set,
    33         # if there is an initial state and the entity's state is not set,
    52         # use the initial state as a default state
    34         # use the initial state as a default state
    53         if not session.deleted_in_transaction(entity.eid) and not entity.in_state:
    35         pendingeids = session.transaction_data.get('pendingeids', ())
    54             rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
    36         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
    55                                    {'name': entity.id})
    37                and entity.current_workflow:
    56             if rset:
    38             state = entity.current_workflow.initial
    57                 session.add_relation(entity.eid, 'in_state', rset[0][0])
    39             if state:
       
    40                 # use super session to by-pass security checks
       
    41                 session.super_session.add_relation(entity.eid, 'in_state',
       
    42                                                    state.eid)
       
    43 
       
    44 class _WorkflowChangedOp(hook.Operation):
       
    45     """fix entity current state when changing its workflow"""
       
    46 
       
    47     def precommit_event(self):
       
    48         session = self.session
       
    49         if session.deleted_in_transaction(self.eid):
       
    50             return
       
    51         entity = session.entity_from_eid(self.eid)
       
    52         # notice that enforcment that new workflow apply to the entity's type is
       
    53         # done by schema rule, no need to check it here
       
    54         if entity.current_workflow.eid == self.wfeid:
       
    55             deststate = entity.current_workflow.initial
       
    56             if not deststate:
       
    57                 msg = session._('workflow has no initial state')
       
    58                 raise ValidationError(entity.eid, {'custom_workflow': msg})
       
    59             if entity.current_workflow.state_by_eid(entity.current_state.eid):
       
    60                 # nothing to do
       
    61                 return
       
    62             # if there are no history, simply go to new workflow's initial state
       
    63             if not entity.workflow_history:
       
    64                 if entity.current_state.eid != deststate.eid:
       
    65                     _change_state(session, entity.eid,
       
    66                                   entity.current_state.eid, deststate.eid)
       
    67                 return
       
    68             msg = session._('workflow changed to "%s"')
       
    69             msg %= entity.current_workflow.name
       
    70             entity.change_state(deststate.name, msg)
       
    71 
       
    72 
    58 
    73 
    59 class WorkflowHook(hook.Hook):
    74 class WorkflowHook(hook.Hook):
    60     __abstract__ = True
    75     __abstract__ = True
    61     category = 'worfklow'
    76     category = 'worfklow'
    62 
    77 
    79     def __call__(self):
    94     def __call__(self):
    80         self._cw.transaction_data.setdefault('pendingrelations', []).append(
    95         self._cw.transaction_data.setdefault('pendingrelations', []).append(
    81             (self.eidfrom, self.rtype, self.eidto))
    96             (self.eidfrom, self.rtype, self.eidto))
    82 
    97 
    83 
    98 
    84 class FireTransitionHook(PrepareStateChangeHook):
    99 class FireTransitionHook(WorkflowHook):
    85     """check the transition is allowed and record transition information"""
   100     """check the transition is allowed, add missing information. Expect that:
       
   101     * wf_info_for inlined relation is set
       
   102     * by_transition or to_state (managers only) inlined relation is set
       
   103     """
    86     __id__ = 'wffiretransition'
   104     __id__ = 'wffiretransition'
    87     events = ('before_add_relation',)
   105     __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
       
   106     events = ('before_add_entity',)
    88 
   107 
    89     def __call__(self):
   108     def __call__(self):
    90         session = self._cw
   109         session = self._cw
    91         eidfrom = self.eidfrom
   110         entity = self.entity
    92         eidto = self.eidto
   111         # first retreive entity to which the state change apply
    93         state = previous_state(session, eidfrom)
   112         try:
    94         etype = session.describe(eidfrom)[0]
   113             foreid = entity['wf_info_for']
    95         if not (session.is_super_session or 'managers' in session.user.groups):
   114         except KeyError:
    96             if not state is None:
   115             msg = session._('mandatory relation')
    97                 entity = session.entity_from_eid(eidfrom)
   116             raise ValidationError(entity.eid, {'wf_info_for': msg})
    98                 # we should find at least one transition going to this state
   117         forentity = session.entity_from_eid(foreid)
    99                 try:
   118         # then check it has a workflow set
   100                     iter(state.transitions(entity, eidto)).next()
   119         wf = forentity.current_workflow
   101                 except StopIteration:
   120         if wf is None:
   102                     msg = session._('transition is not allowed')
   121             msg = session._('related entity has no workflow set')
   103                     raise ValidationError(eidfrom, {'in_state': msg})
   122             raise ValidationError(entity.eid, {None: msg})
   104             else:
   123         # then check it has a state set
   105                 # not a transition
   124         fromstate = forentity.current_state
   106                 # check state is initial state if the workflow defines one
   125         if fromstate is None:
   107                 isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
   126             msg = session._('related entity has no state')
   108                                                 {'etype': etype})
   127             raise ValidationError(entity.eid, {None: msg})
   109                 if isrset and not eidto == isrset[0][0]:
   128         # no investigate the requested state change...
   110                     msg = session._('not the initial state for this entity')
   129         try:
   111                     raise ValidationError(eidfrom, {'in_state': msg})
   130             treid = entity['by_transition']
   112         eschema = session.repo.schema[etype]
   131         except KeyError:
   113         if not 'wf_info_for' in eschema.object_relations():
   132             # no transition set, check user is a manager and destination state is
   114             # workflow history not activated for this entity type
   133             # specified (and valid)
   115             return
   134             if not (session.is_super_session or 'managers' in session.user.groups):
   116         rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
   135                 msg = session._('mandatory relation')
   117         args = {'comment': session.get_shared_data('trcomment', None, pop=True),
   136                 raise ValidationError(entity.eid, {'by_transition': msg})
   118                 'e': eidfrom, 'ds': eidto}
   137             deststateeid = entity.get('to_state')
   119         cformat = session.get_shared_data('trcommentformat', None, pop=True)
   138             if not deststateeid:
   120         if cformat is not None:
   139                 msg = session._('mandatory relation')
   121             args['comment_format'] = cformat
   140                 raise ValidationError(entity.eid, {'by_transition': msg})
   122             rql += ', T comment_format %(comment_format)s'
   141             deststate = wf.state_by_eid(deststateeid)
   123         restriction = ['DS eid %(ds)s, E eid %(e)s']
   142             if deststate is None:
   124         if not state is None: # not a transition
   143                 msg = session._("state doesn't belong to entity's workflow")
   125             rql += ', T from_state FS'
   144                 raise ValidationError(entity.eid, {'to_state': msg})
   126             restriction.append('FS eid %(fs)s')
   145         else:
   127             args['fs'] = state.eid
   146             # check transition is valid and allowed
   128         rql = '%s WHERE %s' % (rql, ', '.join(restriction))
   147             tr = wf.transition_by_eid(treid)
   129         session.unsafe_execute(rql, args, 'e')
   148             if tr is None:
       
   149                 msg = session._("transition doesn't belong to entity's workflow")
       
   150                 raise ValidationError(entity.eid, {'by_transition': msg})
       
   151             if not tr.has_input_state(fromstate):
       
   152                 msg = session._("transition isn't allowed")
       
   153                 raise ValidationError(entity.eid, {'by_transition': msg})
       
   154             if not tr.may_be_fired(foreid):
       
   155                 msg = session._("transition may not be fired")
       
   156                 raise ValidationError(entity.eid, {'by_transition': msg})
       
   157             deststateeid = tr.destination().eid
       
   158         # everything is ok, add missing information on the trinfo entity
       
   159         entity['from_state'] = fromstate.eid
       
   160         entity['to_state'] = deststateeid
       
   161         nocheck = session.transaction_data.setdefault('skip-security', set())
       
   162         nocheck.add((entity.eid, 'from_state', fromstate.eid))
       
   163         nocheck.add((entity.eid, 'to_state', deststateeid))
       
   164 
       
   165 
       
   166 class FiredTransitionHook(WorkflowHook):
       
   167     """change related entity state"""
       
   168     __id__ = 'wffiretransition'
       
   169     __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
       
   170     events = ('after_add_entity',)
       
   171 
       
   172     def __call__(self):
       
   173         _change_state(self._cw, self.entity['wf_info_for'],
       
   174                       self.entity['from_state'], self.entity['to_state'])
   130 
   175 
   131 
   176 
   132 class SetModificationDateOnStateChange(WorkflowHook):
   177 class SetModificationDateOnStateChange(WorkflowHook):
   133     """update entity's modification date after changing its state"""
   178     """update entity's modification date after changing its state"""
   134     __id__ = 'wfsyncmdate'
   179     __id__ = 'wfsyncmdate'
   145                                   _cw_unsafe=True)
   190                                   _cw_unsafe=True)
   146         except RepositoryError, ex:
   191         except RepositoryError, ex:
   147             # usually occurs if entity is coming from a read-only source
   192             # usually occurs if entity is coming from a read-only source
   148             # (eg ldap user)
   193             # (eg ldap user)
   149             self.warning('cant change modification date for %s: %s', entity, ex)
   194             self.warning('cant change modification date for %s: %s', entity, ex)
       
   195 
       
   196 
       
   197 class SetCustomWorkflow(WorkflowHook):
       
   198     __id__ = 'wfsetcustom'
       
   199     __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow')
       
   200     events = ('after_add_relation',)
       
   201 
       
   202     def __call__(self):
       
   203         _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=self.eidto)
       
   204 
       
   205 
       
   206 class DelCustomWorkflow(SetCustomWorkflow):
       
   207     __id__ = 'wfdelcustom'
       
   208     events = ('after_delete_relation',)
       
   209 
       
   210     def __call__(self):
       
   211         entity = self._cw.entity_from_eid(self.eidfrom)
       
   212         typewf = entity.cwetype_workflow()
       
   213         if typewf is not None:
       
   214             _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
       
   215 
       
   216 
       
   217 
       
   218 class DelWorkflowHook(WorkflowHook):
       
   219     __id__ = 'wfdel'
       
   220     __select__ = WorkflowHook.__select__ & entity_implements('Workflow')
       
   221     events = ('after_delete_entity',)
       
   222 
       
   223     def __call__(self):
       
   224         # cleanup unused state and transition
       
   225         self._cw.execute('DELETE State X WHERE NOT X state_of Y')
       
   226         self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y')
       
   227