diff -r dd9f2dd02f85 -r 0e3460341023 hooks/workflow.py --- a/hooks/workflow.py Tue Aug 18 09:25:44 2009 +0200 +++ b/hooks/workflow.py Fri Aug 21 16:26:20 2009 +0200 @@ -15,31 +15,13 @@ from cubicweb.server import hook - -def previous_state(session, eid): - """return the state of the entity with the given eid, - usually since it's changing in the current transaction. Due to internal - relation hooks, the relation may has been deleted at this point, so - we have handle that - """ - # don't check eid has been added in the current transaction, we don't want - # to miss previous state of entity whose state change in the same - # transaction as it's being created - pending = session.transaction_data.get('pendingrelations', ()) - for eidfrom, rtype, eidto in reversed(pending): - if rtype == 'in_state' and eidfrom == eid: - rset = session.execute('Any S,N WHERE S eid %(x)s, S name N', - {'x': eidto}, 'x') - return rset.get_entity(0, 0) - rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N', - {'x': eid}, 'x') - if rset: - return rset.get_entity(0, 0) - - -def relation_deleted(session, eidfrom, rtype, eidto): - session.transaction_data.setdefault('pendingrelations', []).append( - (eidfrom, rtype, eidto)) +def _change_state(session, x, oldstate, newstate): + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((x, 'in_state', oldstate)) + nocheck.add((x, 'in_state', newstate)) + # delete previous state first in case we're using a super session + session.delete_relation(x, 'in_state', oldstate) + session.add_relation(x, 'in_state', newstate) class _SetInitialStateOp(hook.Operation): @@ -50,11 +32,44 @@ entity = self.entity # if there is an initial state and the entity's state is not set, # use the initial state as a default state - if not session.deleted_in_transaction(entity.eid) and not entity.in_state: - rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s', - {'name': entity.id}) - if rset: - session.add_relation(entity.eid, 'in_state', rset[0][0]) + pendingeids = session.transaction_data.get('pendingeids', ()) + if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ + and entity.current_workflow: + state = entity.current_workflow.initial + if state: + # use super session to by-pass security checks + session.super_session.add_relation(entity.eid, 'in_state', + state.eid) + +class _WorkflowChangedOp(hook.Operation): + """fix entity current state when changing its workflow""" + + def precommit_event(self): + session = self.session + if session.deleted_in_transaction(self.eid): + return + entity = session.entity_from_eid(self.eid) + # notice that enforcment that new workflow apply to the entity's type is + # done by schema rule, no need to check it here + if entity.current_workflow.eid == self.wfeid: + deststate = entity.current_workflow.initial + if not deststate: + msg = session._('workflow has no initial state') + raise ValidationError(entity.eid, {'custom_workflow': msg}) + if entity.current_workflow.state_by_eid(entity.current_state.eid): + # nothing to do + return + # if there are no history, simply go to new workflow's initial state + if not entity.workflow_history: + if entity.current_state.eid != deststate.eid: + _change_state(session, entity.eid, + entity.current_state.eid, deststate.eid) + return + msg = session._('workflow changed to "%s"') + msg %= entity.current_workflow.name + entity.change_state(deststate.name, msg) + + class WorkflowHook(hook.Hook): __abstract__ = True @@ -81,52 +96,82 @@ (self.eidfrom, self.rtype, self.eidto)) -class FireTransitionHook(PrepareStateChangeHook): - """check the transition is allowed and record transition information""" +class FireTransitionHook(WorkflowHook): + """check the transition is allowed, add missing information. Expect that: + * wf_info_for inlined relation is set + * by_transition or to_state (managers only) inlined relation is set + """ __id__ = 'wffiretransition' - events = ('before_add_relation',) + __select__ = WorkflowHook.__select__ & entity_implements('TrInfo') + events = ('before_add_entity',) def __call__(self): session = self._cw - eidfrom = self.eidfrom - eidto = self.eidto - state = previous_state(session, eidfrom) - etype = session.describe(eidfrom)[0] - if not (session.is_super_session or 'managers' in session.user.groups): - if not state is None: - entity = session.entity_from_eid(eidfrom) - # we should find at least one transition going to this state - try: - iter(state.transitions(entity, eidto)).next() - except StopIteration: - msg = session._('transition is not allowed') - raise ValidationError(eidfrom, {'in_state': msg}) - else: - # not a transition - # check state is initial state if the workflow defines one - isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s', - {'etype': etype}) - if isrset and not eidto == isrset[0][0]: - msg = session._('not the initial state for this entity') - raise ValidationError(eidfrom, {'in_state': msg}) - eschema = session.repo.schema[etype] - if not 'wf_info_for' in eschema.object_relations(): - # workflow history not activated for this entity type - return - rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s' - args = {'comment': session.get_shared_data('trcomment', None, pop=True), - 'e': eidfrom, 'ds': eidto} - cformat = session.get_shared_data('trcommentformat', None, pop=True) - if cformat is not None: - args['comment_format'] = cformat - rql += ', T comment_format %(comment_format)s' - restriction = ['DS eid %(ds)s, E eid %(e)s'] - if not state is None: # not a transition - rql += ', T from_state FS' - restriction.append('FS eid %(fs)s') - args['fs'] = state.eid - rql = '%s WHERE %s' % (rql, ', '.join(restriction)) - session.unsafe_execute(rql, args, 'e') + entity = self.entity + # first retreive entity to which the state change apply + try: + foreid = entity['wf_info_for'] + except KeyError: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'wf_info_for': msg}) + forentity = session.entity_from_eid(foreid) + # then check it has a workflow set + wf = forentity.current_workflow + if wf is None: + msg = session._('related entity has no workflow set') + raise ValidationError(entity.eid, {None: msg}) + # then check it has a state set + fromstate = forentity.current_state + if fromstate is None: + msg = session._('related entity has no state') + raise ValidationError(entity.eid, {None: msg}) + # no investigate the requested state change... + try: + treid = entity['by_transition'] + except KeyError: + # no transition set, check user is a manager and destination state is + # specified (and valid) + if not (session.is_super_session or 'managers' in session.user.groups): + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststateeid = entity.get('to_state') + if not deststateeid: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststate = wf.state_by_eid(deststateeid) + if deststate is None: + msg = session._("state doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'to_state': msg}) + else: + # check transition is valid and allowed + tr = wf.transition_by_eid(treid) + if tr is None: + msg = session._("transition doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'by_transition': msg}) + if not tr.has_input_state(fromstate): + msg = session._("transition isn't allowed") + raise ValidationError(entity.eid, {'by_transition': msg}) + if not tr.may_be_fired(foreid): + msg = session._("transition may not be fired") + raise ValidationError(entity.eid, {'by_transition': msg}) + deststateeid = tr.destination().eid + # everything is ok, add missing information on the trinfo entity + entity['from_state'] = fromstate.eid + entity['to_state'] = deststateeid + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((entity.eid, 'from_state', fromstate.eid)) + nocheck.add((entity.eid, 'to_state', deststateeid)) + + +class FiredTransitionHook(WorkflowHook): + """change related entity state""" + __id__ = 'wffiretransition' + __select__ = WorkflowHook.__select__ & entity_implements('TrInfo') + events = ('after_add_entity',) + + def __call__(self): + _change_state(self._cw, self.entity['wf_info_for'], + self.entity['from_state'], self.entity['to_state']) class SetModificationDateOnStateChange(WorkflowHook): @@ -147,3 +192,36 @@ # usually occurs if entity is coming from a read-only source # (eg ldap user) self.warning('cant change modification date for %s: %s', entity, ex) + + +class SetCustomWorkflow(WorkflowHook): + __id__ = 'wfsetcustom' + __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow') + events = ('after_add_relation',) + + def __call__(self): + _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=self.eidto) + + +class DelCustomWorkflow(SetCustomWorkflow): + __id__ = 'wfdelcustom' + events = ('after_delete_relation',) + + def __call__(self): + entity = self._cw.entity_from_eid(self.eidfrom) + typewf = entity.cwetype_workflow() + if typewf is not None: + _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid) + + + +class DelWorkflowHook(WorkflowHook): + __id__ = 'wfdel' + __select__ = WorkflowHook.__select__ & entity_implements('Workflow') + events = ('after_delete_entity',) + + def __call__(self): + # cleanup unused state and transition + self._cw.execute('DELETE State X WHERE NOT X state_of Y') + self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y') +