"""workflow definition and history related entities: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"fromwarningsimportwarnfromlogilab.common.decoratorsimportcached,clear_cachefromlogilab.common.deprecationimportdeprecatedfromcubicweb.entitiesimportAnyEntity,fetch_configfromcubicweb.interfacesimportIWorkflowablefromcubicweb.common.mixinsimportMI_REL_TRIGGERSclassWorkflow(AnyEntity):id='Workflow'@propertydefinitial(self):"""return the initial state for this workflow"""returnself.initial_stateandself.initial_state[0]orNonedefis_default_workflow_of(self,etype):"""return True if this workflow is the default workflow for the given entity type """returnany(etforetinself.reverse_default_workflowifet.name==etype)defafter_deletion_path(self):"""return (path, parameters) which should be used as redirect information when this entity is being deleted """ifself.workflow_of:returnself.workflow_of[0].rest_path(),{'vid':'workflow'}returnsuper(Workflow,self).after_deletion_path()defiter_workflows(self,_done=None):"""return an iterator on actual workflows, eg this workflow and its subworkflows """# infinite loop safety beltif_doneisNone:_done=set()yieldself_done.add(self.eid)fortrinself.req.execute('Any T WHERE T is WorkflowTransition, ''T transition_of WF, WF eid %(wf)s',{'wf':self.eid}).entities():iftr.subwf.eidin_done:continueforsubwfintr.subwf.iter_workflows(_done):yieldsubwf# state / transitions accessors ############################################defstate_by_name(self,statename):rset=self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, ''S state_of WF, WF eid %(wf)s',{'n':statename,'wf':self.eid},'wf')ifrset:returnrset.get_entity(0,0)returnNonedefstate_by_eid(self,eid):rset=self.req.execute('Any S, SN WHERE S name SN, S eid %(s)s, ''S state_of WF, WF eid %(wf)s',{'s':eid,'wf':self.eid},('wf','s'))ifrset:returnrset.get_entity(0,0)returnNonedeftransition_by_name(self,trname):rset=self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, ''T transition_of WF, WF eid %(wf)s',{'n':trname,'wf':self.eid},'wf')ifrset:returnrset.get_entity(0,0)returnNonedeftransition_by_eid(self,eid):rset=self.req.execute('Any T, TN WHERE T name TN, T eid %(t)s, ''T transition_of WF, WF eid %(wf)s',{'t':eid,'wf':self.eid},('wf','t'))ifrset:returnrset.get_entity(0,0)returnNone# wf construction methods ##################################################defadd_state(self,name,initial=False,**kwargs):"""method to ease workflow definition: add a state for one or more entity type(s) """state=self.req.create_entity('State',name=unicode(name),**kwargs)self.req.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',{'s':state.eid,'wf':self.eid},('s','wf'))ifinitial:assertnotself.initialself.req.execute('SET WF initial_state S ''WHERE S eid %(s)s, WF eid %(wf)s',{'s':state.eid,'wf':self.eid},('s','wf'))returnstatedefadd_transition(self,name,fromstates,tostate,requiredgroups=(),conditions=(),**kwargs):"""method to ease workflow definition: add a transition for one or more entity type(s), from one or more state and to a single state """tr=self.req.create_entity('Transition',name=unicode(name),**kwargs)self.req.execute('SET T transition_of WF ''WHERE T eid %(t)s, WF eid %(wf)s',{'t':tr.eid,'wf':self.eid},('t','wf'))forstateinfromstates:ifhasattr(state,'eid'):state=state.eidself.req.execute('SET S allowed_transition T ''WHERE S eid %(s)s, T eid %(t)s',{'s':state,'t':tr.eid},('s','t'))ifhasattr(tostate,'eid'):tostate=tostate.eidself.req.execute('SET T destination_state S ''WHERE S eid %(s)s, T eid %(t)s',{'t':tr.eid,'s':tostate},('s','t'))tr.set_transition_permissions(requiredgroups,conditions,reset=False)returntrclassBaseTransition(AnyEntity):"""customized class for abstract transition provides a specific may_be_fired method to check if the relation may be fired by the logged user """id='BaseTransition'fetch_attrs,fetch_order=fetch_config(['name'])def__init__(self,*args,**kwargs):ifself.id=='BaseTransition':raiseException('should not be instantiated')super(BaseTransition,self).__init__(*args,**kwargs)@propertydefworkflow(self):returnself.transition_of[0]defmay_be_fired(self,eid):"""return true if the logged user may fire this transition `eid` is the eid of the object on which we may fire the transition """user=self.req.user# check user is at least in one of the required groups if anygroups=frozenset(g.nameforginself.require_group)ifgroups:matches=user.matching_groups(groups)ifmatches:returnmatchesif'owners'ingroupsanduser.owns(eid):returnTrue# check one of the rql expression conditions matches if anyifself.condition:forrqlexprinself.condition:ifrqlexpr.check_expression(self.req,eid):returnTrueifself.conditionorgroups:returnFalsereturnTruedefafter_deletion_path(self):"""return (path, parameters) which should be used as redirect information when this entity is being deleted """ifself.transition_of:returnself.transition_of[0].rest_path(),{}returnsuper(Transition,self).after_deletion_path()defset_transition_permissions(self,requiredgroups=(),conditions=(),reset=True):"""set or add (if `reset` is False) groups and conditions for this transition """ifreset:self.req.execute('DELETE T require_group G WHERE T eid %(x)s',{'x':self.eid},'x')self.req.execute('DELETE T condition R WHERE T eid %(x)s',{'x':self.eid},'x')forgnameinrequiredgroups:### XXX ensure gname validityrset=self.req.execute('SET T require_group G ''WHERE T eid %(x)s, G name %(gn)s',{'x':self.eid,'gn':gname},'x')assertrset,'%s is not a known group'%gnameifisinstance(conditions,basestring):conditions=(conditions,)forexprinconditions:ifisinstance(expr,str):expr=unicode(expr)self.req.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ''X expression %(expr)s, T condition X ''WHERE T eid %(x)s',{'x':self.eid,'expr':expr},'x')# XXX clear caches?classTransition(BaseTransition):"""customized class for Transition entities"""id='Transition'defdestination(self):returnself.destination_state[0]defhas_input_state(self,state):ifhasattr(state,'eid'):state=state.eidreturnany(sforsinself.reverse_allowed_transitionifs.eid==state)classWorkflowTransition(BaseTransition):"""customized class for WorkflowTransition entities"""id='WorkflowTransition'@propertydefsubwf(self):returnself.subworkflow[0]defdestination(self):returnself.subwf.initialclassState(AnyEntity):"""customized class for State entities"""id='State'fetch_attrs,fetch_order=fetch_config(['name'])rest_attr='eid'@propertydefworkflow(self):returnself.state_of[0]defafter_deletion_path(self):"""return (path, parameters) which should be used as redirect information when this entity is being deleted """ifself.state_of:returnself.state_of[0].rest_path(),{}returnsuper(State,self).after_deletion_path()classTrInfo(AnyEntity):"""customized class for Transition information entities """id='TrInfo'fetch_attrs,fetch_order=fetch_config(['creation_date','comment'],pclass=None)# don't want modification_date@propertydeffor_entity(self):returnself.wf_info_for[0]@propertydefprevious_state(self):returnself.from_state[0]@propertydefnew_state(self):returnself.to_state[0]@propertydeftransition(self):returnself.by_transitionandself.by_transition[0]orNonedefafter_deletion_path(self):"""return (path, parameters) which should be used as redirect information when this entity is being deleted """ifself.for_entity:returnself.for_entity.rest_path(),{}return'view',{}classWorkflowableMixIn(object):"""base mixin providing workflow helper methods for workflowable entities. This mixin will be automatically set on class supporting the 'in_state' relation (which implies supporting 'wf_info_for' as well) """__implements__=(IWorkflowable,)@propertydefcurrent_workflow(self):"""return current workflow applied to this entity"""ifself.custom_workflow:returnself.custom_workflow[0]returnself.cwetype_workflow()@propertydefcurrent_state(self):"""return current state entity"""returnself.in_stateandself.in_state[0]orNone@propertydefstate(self):"""return current state name"""try:returnself.in_state[0].nameexceptIndexError:self.warning('entity %s has no state',self)returnNone@propertydefprintable_state(self):"""return current state name translated to context's language"""state=self.current_stateifstate:returnself.req._(state.name)returnu''@propertydefworkflow_history(self):"""return the workflow history for this entity (eg ordered list of TrInfo entities) """returnself.reverse_wf_info_fordeflatest_trinfo(self):"""return the latest transition information for this entity"""returnself.reverse_wf_info_for[-1]@cacheddefcwetype_workflow(self):"""return the default workflow for entities of this type"""# XXX CWEType methodwfrset=self.req.execute('Any WF WHERE X is ET, X eid %(x)s, ''WF workflow_of ET',{'x':self.eid},'x')iflen(wfrset)==1:returnwfrset.get_entity(0,0)iflen(wfrset)>1:forwfinwfrset.entities():ifwf.is_default_workflow_of(self.id):returnwfself.warning("can't find default workflow for %s",self.id)else:self.warning("can't find any workflow for %s",self.id)returnNonedefpossible_transitions(self):"""generates transition that MAY be fired for the given entity, expected to be in this state """ifself.current_stateisNoneorself.current_workflowisNone:returnrset=self.req.execute('Any T,N WHERE S allowed_transition T, S eid %(x)s, ''T name N, T transition_of WF, WF eid %(wfeid)s',{'x':self.current_state.eid,'wfeid':self.current_workflow.eid},'x')fortrinrset.entities():iftr.may_be_fired(self.eid):yieldtrdef_get_tr_kwargs(self,comment,commentformat):kwargs={}ifcommentisnotNone:kwargs['comment']=commentifcommentformatisnotNone:kwargs['comment_format']=commentformatreturnkwargsdeffire_transition(self,trname,comment=None,commentformat=None):"""change the entity's state by firing transition of the given name in entity's workflow """assertself.current_workflowtr=self.current_workflow.transition_by_name(trname)asserttrisnotNone,'not a %s transition: %s'%(self.id,state)# XXX try to find matching transition?self.req.create_entity('TrInfo',('by_transition','T'),('wf_info_for','E'),T=tr.eid,E=self.eid,**self._get_tr_kwargs(comment,commentformat))defchange_state(self,statename,comment=None,commentformat=None):"""change the entity's state to the state of the given name in entity's workflow. This method should only by used by manager to fix an entity's state when their is no matching transition, otherwise fire_transition should be used. """assertself.current_workflowifnotisinstance(statename,basestring):warn('give a state name')state=self.current_workflow.state_by_eid(statename)assertstateisnotNone,'not a %s state: %s'%(self.id,state)else:state=self.current_workflow.state_by_name(statename)# XXX try to find matching transition?self.req.create_entity('TrInfo',('to_state','S'),('wf_info_for','E'),S=state.eid,E=self.eid,**self._get_tr_kwargs(comment,commentformat))defclear_all_caches(self):super(WorkflowableMixIn,self).clear_all_caches()clear_cache(self,'cwetype_workflow')@deprecated('get transition from current workflow and use its may_be_fired method')defcan_pass_transition(self,trname):"""return the Transition instance if the current user can fire the transition with the given name, else None """tr=self.current_workflowandself.current_workflow.transition_by_name(trname)iftrandtr.may_be_fired(self.eid):returntr@property@deprecated('use printable_state')defdisplayable_state(self):returnself.req._(self.state)MI_REL_TRIGGERS[('in_state','subject')]=WorkflowableMixIn