"""Core hooks: workflow related hooks:organization: Logilab:copyright: 2001-2010 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"fromdatetimeimportdatetimefromyams.schemaimportrole_namefromcubicwebimportRepositoryError,ValidationErrorfromcubicweb.interfacesimportIWorkflowablefromcubicweb.selectorsimportimplementsfromcubicweb.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 unless in_state isn't stored in the system# sourcefromsource=session.describe(x)[1]iffromsource=='system'or \notsession.repo.sources_by_uri[fromsource].support_relation('in_state'):session.delete_relation(x,'in_state',oldstate)session.add_relation(x,'in_state',newstate)# operations ###################################################################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 stateifnot(session.deleted_in_transaction(entity.eid)orentity.in_state) \andentity.current_workflow:state=entity.current_workflow.initialifstate:session.add_relation(entity.eid,'in_state',state.eid)_FireAutotransitionOp(session,entity=entity)class_FireAutotransitionOp(hook.Operation):"""try to fire auto transition after state changes"""defprecommit_event(self):entity=self.entityautotrs=list(entity.possible_transitions('auto'))ifautotrs:assertlen(autotrs)==1entity.fire_transition(autotrs[0])class_WorkflowChangedOp(hook.Operation):"""fix entity current state when changing its workflow"""defprecommit_event(self):# notice that enforcement that new workflow apply to the entity's type is# done by schema rule, no need to check it heresession=self.sessionpendingeids=session.transaction_data.get('pendingeids',())ifself.eidinpendingeids:returnentity=session.entity_from_eid(self.eid)# check custom workflow has not been rechanged to another one in the same# transactionmainwf=entity.main_workflowifmainwf.eid==self.wfeid:deststate=mainwf.initialifnotdeststate:qname=role_name('custom_workflow','subject')msg=session._('workflow has no initial state')raiseValidationError(entity.eid,{qname:msg})ifmainwf.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)_FireAutotransitionOp(session,entity=entity)returnmsg=session._('workflow changed to "%s"')msg%=session._(mainwf.name)session.transaction_data[(entity.eid,'customwf')]=self.wfeidentity.change_state(deststate,msg,u'text/plain')class_CheckTrExitPoint(hook.Operation):defprecommit_event(self):tr=self.session.entity_from_eid(self.treid)outputs=set()forepintr.subworkflow_exit:ifep.subwf_state.eidinoutputs:qname=role_name('subworkflow_exit','subject')msg=self.session._("can't have multiple exits on the same state")raiseValidationError(self.treid,{qname:msg})outputs.add(ep.subwf_state.eid)class_SubWorkflowExitOp(hook.Operation):defprecommit_event(self):session=self.sessionforentity=self.forentitytrinfo=self.trinfo# we're in a subworkflow, check if we've reached an exit pointwftr=forentity.subworkflow_input_transition()ifwftrisNone:# inconsistency detectedqname=role_name('to_state','subject')msg=session._("state doesn't belong to entity's current workflow")raiseValidationError(self.trinfo.eid,{'to_state':msg})tostate=wftr.get_exit_point(forentity,trinfo['to_state'])iftostateisnotNone:# reached an exit pointmsg=session._('exiting from subworkflow %s')msg%=session._(forentity.current_workflow.name)session.transaction_data[(forentity.eid,'subwfentrytr')]=Trueforentity.change_state(tostate,msg,u'text/plain',tr=wftr)# hooks ########################################################################classWorkflowHook(hook.Hook):__abstract__=Truecategory='worfklow'classSetInitialStateHook(WorkflowHook):__regid__='wfsetinitial'__select__=WorkflowHook.__select__&implements(IWorkflowable)events=('after_add_entity',)def__call__(self):_SetInitialStateOp(self._cw,entity=self.entity)classPrepareStateChangeHook(WorkflowHook):"""record previous state information"""__regid__='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 """__regid__='wffiretransition'__select__=WorkflowHook.__select__&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:qname=role_name('wf_info_for','subject')msg=session._('mandatory relation')raiseValidationError(entity.eid,{qname:msg})forentity=session.entity_from_eid(foreid)# then check it has a workflow set, unless we're in the process of changing# entity's workflowifsession.transaction_data.get((forentity.eid,'customwf')):wfeid=session.transaction_data[(forentity.eid,'customwf')]wf=session.entity_from_eid(wfeid)else:wf=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})# True if we are coming back from subworkflowswtr=session.transaction_data.pop((forentity.eid,'subwfentrytr'),None)cowpowers=('managers'insession.user.groupsornotsession.write_security)# 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)ifnotcowpowers:qname=role_name('by_transition','subject')msg=session._('mandatory relation')raiseValidationError(entity.eid,{qname:msg})deststateeid=entity.get('to_state')ifnotdeststateeid:qname=role_name('by_transition','subject')msg=session._('mandatory relation')raiseValidationError(entity.eid,{qname:msg})deststate=wf.state_by_eid(deststateeid)ifdeststateisNone:qname=role_name('to_state','subject')msg=session._("state doesn't belong to entity's workflow")raiseValidationError(entity.eid,{qname:msg})else:# check transition is valid and allowed, unless we're coming back# from subworkflowtr=session.entity_from_eid(treid)ifswtrisNone:qname=role_name('by_transition','subject')iftrisNone:msg=session._("transition doesn't belong to entity's workflow")raiseValidationError(entity.eid,{qname:msg})ifnottr.has_input_state(fromstate):msg=session._("transition %(tr)s isn't allowed from %(st)s")%{'tr':session._(tr.name),'st':session._(fromstate.name)}raiseValidationError(entity.eid,{qname:msg})ifnottr.may_be_fired(foreid):msg=session._("transition may not be fired")raiseValidationError(entity.eid,{qname:msg})ifentity.get('to_state'):deststateeid=entity['to_state']ifnotcowpowersanddeststateeid!=tr.destination(forentity).eid:qname=role_name('by_transition','subject')msg=session._("transition isn't allowed")raiseValidationError(entity.eid,{qname:msg})ifswtrisNone:deststate=session.entity_from_eid(deststateeid)ifnotcowpowersanddeststateisNone:qname=role_name('to_state','subject')msg=session._("state doesn't belong to entity's workflow")raiseValidationError(entity.eid,{qname:msg})else:deststateeid=tr.destination(forentity).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))_FireAutotransitionOp(session,entity=forentity)classFiredTransitionHook(WorkflowHook):"""change related entity state"""__regid__='wffiretransition'__select__=WorkflowHook.__select__&implements('TrInfo')events=('after_add_entity',)def__call__(self):trinfo=self.entity_change_state(self._cw,trinfo['wf_info_for'],trinfo['from_state'],trinfo['to_state'])forentity=self._cw.entity_from_eid(trinfo['wf_info_for'])assertforentity.current_state.eid==trinfo['to_state']ifforentity.main_workflow.eid!=forentity.current_workflow.eid:_SubWorkflowExitOp(self._cw,forentity=forentity,trinfo=trinfo)classCheckInStateChangeAllowed(WorkflowHook):"""check state apply, in case of direct in_state change using unsafe execute """__regid__='wfcheckinstate'__select__=WorkflowHook.__select__&hook.match_rtype('in_state')events=('before_add_relation',)def__call__(self):session=self._cwnocheck=session.transaction_data.get('skip-security',())if(self.eidfrom,'in_state',self.eidto)innocheck:# state changed through TrInfo insertion, so we already know it's okreturnentity=session.entity_from_eid(self.eidfrom)mainwf=entity.main_workflowifmainwfisNone:msg=session._('entity has no workflow set')raiseValidationError(entity.eid,{None:msg})forwfinmainwf.iter_workflows():ifwf.state_by_eid(self.eidto):breakelse:qname=role_name('in_state','subject')msg=session._("state doesn't belong to entity's workflow. You may ""want to set a custom workflow for this entity first.")raiseValidationError(self.eidfrom,{qname:msg})ifentity.current_workflowandwf.eid!=entity.current_workflow.eid:qname=role_name('in_state','subject')msg=session._("state doesn't belong to entity's current workflow")raiseValidationError(self.eidfrom,{qname:msg})classSetModificationDateOnStateChange(WorkflowHook):"""update entity's modification date after changing its state"""__regid__='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())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)classCheckWorkflowTransitionExitPoint(WorkflowHook):"""check that there is no multiple exits from the same state"""__regid__='wfcheckwftrexit'__select__=WorkflowHook.__select__&hook.match_rtype('subworkflow_exit')events=('after_add_relation',)def__call__(self):_CheckTrExitPoint(self._cw,treid=self.eidfrom)classSetCustomWorkflow(WorkflowHook):__regid__='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):__regid__='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)