somewhat painful backport of 3.5 branch, should mostly be ok
"""Core hooks: workflow related hooks:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"fromdatetimeimportdatetimefromcubicwebimportRepositoryError,ValidationErrorfromcubicweb.interfacesimportIWorkflowablefromcubicweb.selectorsimportentity_implementsfromcubicweb.serverimporthookdef_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 sessionsession.delete_relation(x,'in_state',oldstate)session.add_relation(x,'in_state',newstate)class_SetInitialStateOp(hook.Operation):"""make initial state be a default state"""defprecommit_event(self):session=self.sessionentity=self.entity# if there is an initial state and the entity's state is not set,# use the initial state as a default statependingeids=session.transaction_data.get('pendingeids',())ifnot(session.deleted_in_transaction(entity.eid)orentity.in_state) \andentity.current_workflow:state=entity.current_workflow.initialifstate:# use super session to by-pass security checkssession.super_session.add_relation(entity.eid,'in_state',state.eid)class_WorkflowChangedOp(hook.Operation):"""fix entity current state when changing its workflow"""defprecommit_event(self):session=self.sessionifsession.deleted_in_transaction(self.eid):returnentity=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 hereifentity.current_workflow.eid==self.wfeid:deststate=entity.current_workflow.initialifnotdeststate:msg=session._('workflow has no initial state')raiseValidationError(entity.eid,{'custom_workflow':msg})ifentity.current_workflow.state_by_eid(entity.current_state.eid):# nothing to doreturn# if there are no history, simply go to new workflow's initial stateifnotentity.workflow_history:ifentity.current_state.eid!=deststate.eid:_change_state(session,entity.eid,entity.current_state.eid,deststate.eid)returnmsg=session._('workflow changed to "%s"')msg%=entity.current_workflow.nameentity.change_state(deststate.name,msg)classWorkflowHook(hook.Hook):__abstract__=Truecategory='worfklow'classSetInitialStateHook(WorkflowHook):__id__='wfsetinitial'__select__=WorkflowHook.__select__&entity_implements(IWorkflowable)events=('after_add_entity',)def__call__(self):_SetInitialStateOp(self._cw,entity=self.entity)classPrepareStateChangeHook(WorkflowHook):"""record previous state information"""__id__='cwdelstate'__select__=WorkflowHook.__select__&hook.match_rtype('in_state')events=('before_delete_relation',)def__call__(self):self._cw.transaction_data.setdefault('pendingrelations',[]).append((self.eidfrom,self.rtype,self.eidto))classFireTransitionHook(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'__select__=WorkflowHook.__select__&entity_implements('TrInfo')events=('before_add_entity',)def__call__(self):session=self._cwentity=self.entity# first retreive entity to which the state change applytry:foreid=entity['wf_info_for']exceptKeyError:msg=session._('mandatory relation')raiseValidationError(entity.eid,{'wf_info_for':msg})forentity=session.entity_from_eid(foreid)# then check it has a workflow setwf=forentity.current_workflowifwfisNone:msg=session._('related entity has no workflow set')raiseValidationError(entity.eid,{None:msg})# then check it has a state setfromstate=forentity.current_stateiffromstateisNone:msg=session._('related entity has no state')raiseValidationError(entity.eid,{None:msg})# no investigate the requested state change...try:treid=entity['by_transition']exceptKeyError:# no transition set, check user is a manager and destination state is# specified (and valid)ifnot(session.is_super_sessionor'managers'insession.user.groups):msg=session._('mandatory relation')raiseValidationError(entity.eid,{'by_transition':msg})deststateeid=entity.get('to_state')ifnotdeststateeid:msg=session._('mandatory relation')raiseValidationError(entity.eid,{'by_transition':msg})deststate=wf.state_by_eid(deststateeid)ifdeststateisNone:msg=session._("state doesn't belong to entity's workflow")raiseValidationError(entity.eid,{'to_state':msg})else:# check transition is valid and allowedtr=wf.transition_by_eid(treid)iftrisNone:msg=session._("transition doesn't belong to entity's workflow")raiseValidationError(entity.eid,{'by_transition':msg})ifnottr.has_input_state(fromstate):msg=session._("transition isn't allowed")raiseValidationError(entity.eid,{'by_transition':msg})ifnottr.may_be_fired(foreid):msg=session._("transition may not be fired")raiseValidationError(entity.eid,{'by_transition':msg})deststateeid=tr.destination().eid# everything is ok, add missing information on the trinfo entityentity['from_state']=fromstate.eidentity['to_state']=deststateeidnocheck=session.transaction_data.setdefault('skip-security',set())nocheck.add((entity.eid,'from_state',fromstate.eid))nocheck.add((entity.eid,'to_state',deststateeid))classFiredTransitionHook(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'])classSetModificationDateOnStateChange(WorkflowHook):"""update entity's modification date after changing its state"""__id__='wfsyncmdate'__select__=WorkflowHook.__select__&hook.match_rtype('in_state')events=('after_add_relation',)def__call__(self):ifself._cw.added_in_transaction(self.eidfrom):# new entity, not neededreturnentity=self._cw.entity_from_eid(self.eidfrom)try:entity.set_attributes(modification_date=datetime.now(),_cw_unsafe=True)exceptRepositoryError,ex:# 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)classSetCustomWorkflow(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)classDelCustomWorkflow(SetCustomWorkflow):__id__='wfdelcustom'events=('after_delete_relation',)def__call__(self):entity=self._cw.entity_from_eid(self.eidfrom)typewf=entity.cwetype_workflow()iftypewfisnotNone:_WorkflowChangedOp(self._cw,eid=self.eidfrom,wfeid=typewf.eid)classDelWorkflowHook(WorkflowHook):__id__='wfdel'__select__=WorkflowHook.__select__&entity_implements('Workflow')events=('after_delete_entity',)def__call__(self):# cleanup unused state and transitionself._cw.execute('DELETE State X WHERE NOT X state_of Y')self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y')