[uicfg, autoform] when a relation is inlined, put it in the generated section by default
"""Core hooks: check schema validity, unsure we are not deleting necessaryentities...: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"fromdatetimeimportdatetimefromcubicwebimportUnknownProperty,ValidationError,BadConnectionIdfromcubicweb.server.poolimportOperation,LateOperation,PreCommitOperationfromcubicweb.server.hookhelperimport(check_internal_entity,get_user_sessions,rproperty)fromcubicweb.server.repositoryimportFTIndexEntityOp# special relations that don't have to be checked for integrity, usually# because they are handled internally by hooks (so we trust ourselves)DONT_CHECK_RTYPES_ON_ADD=set(('owned_by','created_by','is','is_instance_of','wf_info_for','from_state','to_state'))DONT_CHECK_RTYPES_ON_DEL=set(('is','is_instance_of','wf_info_for','from_state','to_state'))defrelation_deleted(session,eidfrom,rtype,eidto):session.transaction_data.setdefault('pendingrelations',[]).append((eidfrom,rtype,eidto))defeschema_type_eid(session,etype):"""get eid of the CWEType entity for the given yams type"""eschema=session.repo.schema.eschema(etype)# eschema.eid is None if schema has been readen from the filesystem, not# from the database (eg during tests)ifeschema.eidisNone:eschema.eid=session.unsafe_execute('Any X WHERE X is CWEType, X name %(name)s',{'name':str(etype)})[0][0]returneschema.eid# base meta-data handling ######################################################defsetctime_before_add_entity(session,entity):"""before create a new entity -> set creation and modification date this is a conveniency hook, you shouldn't have to disable it """timestamp=datetime.now()entity.setdefault('creation_date',timestamp)entity.setdefault('modification_date',timestamp)ifnotsession.get_shared_data('do-not-insert-cwuri'):entity.setdefault('cwuri',u'%seid/%s'%(session.base_url(),entity.eid))defsetmtime_before_update_entity(session,entity):"""update an entity -> set modification date"""entity.setdefault('modification_date',datetime.now())classSetCreatorOp(PreCommitOperation):defprecommit_event(self):session=self.sessionifself.entity.eidinsession.transaction_data.get('pendingeids',()):# entity have been created and deleted in the same transactionreturnifnotself.entity.created_by:session.add_relation(self.entity.eid,'created_by',session.user.eid)defsetowner_after_add_entity(session,entity):"""create a new entity -> set owner and creator metadata"""asession=session.actual_session()ifnotasession.is_internal_session:session.add_relation(entity.eid,'owned_by',asession.user.eid)SetCreatorOp(asession,entity=entity)defsetis_after_add_entity(session,entity):"""create a new entity -> set is relation"""ifhasattr(entity,'_cw_recreating'):returntry:session.add_relation(entity.eid,'is',eschema_type_eid(session,entity.id))exceptIndexError:# during schema serialization, skipreturn# XXX < 2.50 bw compatifnotsession.get_shared_data('do-not-insert-is_instance_of'):foretypeinentity.e_schema.ancestors()+[entity.e_schema]:session.add_relation(entity.eid,'is_instance_of',eschema_type_eid(session,etype))defsetowner_after_add_user(session,entity):"""when a user has been created, add owned_by relation on itself"""session.add_relation(entity.eid,'owned_by',entity.eid)deffti_update_after_add_relation(session,eidfrom,rtype,eidto):"""sync fulltext index when relevant relation is added. Reindexing the contained entity is enough since it will implicitly reindex the container entity. """ftcontainer=session.repo.schema.rschema(rtype).fulltext_containerifftcontainer=='subject':FTIndexEntityOp(session,entity=session.entity_from_eid(eidto))elifftcontainer=='object':FTIndexEntityOp(session,entity=session.entity_from_eid(eidfrom))deffti_update_after_delete_relation(session,eidfrom,rtype,eidto):"""sync fulltext index when relevant relation is deleted. Reindexing both entities is necessary. """ifsession.repo.schema.rschema(rtype).fulltext_container:FTIndexEntityOp(session,entity=session.entity_from_eid(eidto))FTIndexEntityOp(session,entity=session.entity_from_eid(eidfrom))classSyncOwnersOp(PreCommitOperation):defprecommit_event(self):self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,''NOT EXISTS(X owned_by U, X eid %(x)s)',{'c':self.compositeeid,'x':self.composedeid},('c','x'))defsync_owner_after_add_composite_relation(session,eidfrom,rtype,eidto):"""when adding composite relation, the composed should have the same owners has the composite """ifrtype=='wf_info_for':# skip this special composite relation # XXX (syt) why?returncomposite=rproperty(session,rtype,eidfrom,eidto,'composite')ifcomposite=='subject':SyncOwnersOp(session,compositeeid=eidfrom,composedeid=eidto)elifcomposite=='object':SyncOwnersOp(session,compositeeid=eidto,composedeid=eidfrom)def_register_metadata_hooks(hm):"""register meta-data related hooks on the hooks manager"""hm.register_hook(setctime_before_add_entity,'before_add_entity','')hm.register_hook(setmtime_before_update_entity,'before_update_entity','')hm.register_hook(setowner_after_add_entity,'after_add_entity','')hm.register_hook(sync_owner_after_add_composite_relation,'after_add_relation','')hm.register_hook(fti_update_after_add_relation,'after_add_relation','')hm.register_hook(fti_update_after_delete_relation,'after_delete_relation','')if'is'inhm.schema:hm.register_hook(setis_after_add_entity,'after_add_entity','')if'CWUser'inhm.schema:hm.register_hook(setowner_after_add_user,'after_add_entity','CWUser')# core hooks ##################################################################classDelayedDeleteOp(PreCommitOperation):"""delete the object of composite relation except if the relation has actually been redirected to another composite """defprecommit_event(self):session=self.session# don't do anything if the entity is being created or deletedifnot(self.eidinsession.transaction_data.get('pendingeids',())orself.eidinsession.transaction_data.get('neweids',())):etype=session.describe(self.eid)[0]session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'%(etype,self.relation),{'x':self.eid},'x')defhandle_composite_before_del_relation(session,eidfrom,rtype,eidto):"""delete the object of composite relation"""composite=rproperty(session,rtype,eidfrom,eidto,'composite')ifcomposite=='subject':DelayedDeleteOp(session,eid=eidto,relation='Y %s X'%rtype)elifcomposite=='object':DelayedDeleteOp(session,eid=eidfrom,relation='X %s Y'%rtype)defbefore_del_group(session,eid):"""check that we don't remove the owners group"""check_internal_entity(session,eid,('owners',))# schema validation hooks #####################################################classCheckConstraintsOperation(LateOperation):"""check a new relation satisfy its constraints """defprecommit_event(self):eidfrom,rtype,eidto=self.rdef# first check related entities have not been deleted in the same# transactionpending=self.session.transaction_data.get('pendingeids',())ifeidfrominpending:returnifeidtoinpending:returnforconstraintinself.constraints:try:constraint.repo_check(self.session,eidfrom,rtype,eidto)exceptNotImplementedError:self.critical('can\'t check constraint %s, not supported',constraint)defcommit_event(self):passdefcstrcheck_after_add_relation(session,eidfrom,rtype,eidto):"""check the relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied may be added later. """constraints=rproperty(session,rtype,eidfrom,eidto,'constraints')ifconstraints:CheckConstraintsOperation(session,constraints=constraints,rdef=(eidfrom,rtype,eidto))defuniquecstrcheck_before_modification(session,entity):eschema=entity.e_schemaforattr,valinentity.items():ifvalisNone:continueifeschema.subject_relation(attr).is_final()and \eschema.has_unique_values(attr):rql='%s X WHERE X %s%%(val)s'%(entity.e_schema,attr)rset=session.unsafe_execute(rql,{'val':val})ifrsetandrset[0][0]!=entity.eid:msg=session._('the value "%s" is already used, use another one')raiseValidationError(entity.eid,{attr:msg%val})classCheckRequiredRelationOperation(LateOperation):"""checking relation cardinality has to be done after commit in case the relation is being replaced """eid,rtype=None,Nonedefprecommit_event(self):# recheck pending eidsifself.eidinself.session.transaction_data.get('pendingeids',()):returnifself.session.unsafe_execute(*self._rql()).rowcount<1:etype=self.session.describe(self.eid)[0]_=self.session._msg=_('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')msg%={'rtype':_(self.rtype),'etype':_(etype),'eid':self.eid}raiseValidationError(self.eid,{self.rtype:msg})defcommit_event(self):passdef_rql(self):raiseNotImplementedError()classCheckSRelationOp(CheckRequiredRelationOperation):"""check required subject relation"""def_rql(self):return'Any O WHERE S eid %%(x)s, S %s O'%self.rtype,{'x':self.eid},'x'classCheckORelationOp(CheckRequiredRelationOperation):"""check required object relation"""def_rql(self):return'Any S WHERE O eid %%(x)s, S %s O'%self.rtype,{'x':self.eid},'x'defcheckrel_if_necessary(session,opcls,rtype,eid):"""check an equivalent operation has not already been added"""foropinsession.pending_operations:ifisinstance(op,opcls)andop.rtype==rtypeandop.eid==eid:breakelse:opcls(session,rtype=rtype,eid=eid)defcardinalitycheck_after_add_entity(session,entity):"""check cardinalities are satisfied"""eid=entity.eidforrschema,targetschemas,xinentity.e_schema.relation_definitions():# skip automatically handled relationsifrschema.typeinDONT_CHECK_RTYPES_ON_ADD:continueifx=='subject':subjtype=entity.e_schemaobjtype=targetschemas[0].typecardindex=0opcls=CheckSRelationOpelse:subjtype=targetschemas[0].typeobjtype=entity.e_schemacardindex=1opcls=CheckORelationOpcard=rschema.rproperty(subjtype,objtype,'cardinality')ifcard[cardindex]in'1+':checkrel_if_necessary(session,opcls,rschema.type,eid)defcardinalitycheck_before_del_relation(session,eidfrom,rtype,eidto):"""check cardinalities are satisfied"""ifrtypeinDONT_CHECK_RTYPES_ON_DEL:returncard=rproperty(session,rtype,eidfrom,eidto,'cardinality')pendingrdefs=session.transaction_data.get('pendingrdefs',())if(session.describe(eidfrom)[0],rtype,session.describe(eidto)[0])inpendingrdefs:returnpendingeids=session.transaction_data.get('pendingeids',())ifcard[0]in'1+'andnoteidfrominpendingeids:checkrel_if_necessary(session,CheckSRelationOp,rtype,eidfrom)ifcard[1]in'1+'andnoteidtoinpendingeids:checkrel_if_necessary(session,CheckORelationOp,rtype,eidto)def_register_core_hooks(hm):hm.register_hook(handle_composite_before_del_relation,'before_delete_relation','')hm.register_hook(before_del_group,'before_delete_entity','CWGroup')#hm.register_hook(cstrcheck_before_update_entity, 'before_update_entity', '')hm.register_hook(cardinalitycheck_after_add_entity,'after_add_entity','')hm.register_hook(cardinalitycheck_before_del_relation,'before_delete_relation','')hm.register_hook(cstrcheck_after_add_relation,'after_add_relation','')hm.register_hook(uniquecstrcheck_before_modification,'before_add_entity','')hm.register_hook(uniquecstrcheck_before_modification,'before_update_entity','')# user/groups synchronisation #################################################classGroupOperation(Operation):"""base class for group operation"""geid=Nonedef__init__(self,session,*args,**kwargs):"""override to get the group name before actual groups manipulation: we may temporarily loose right access during a commit event, so no query should be emitted while comitting """rql='Any N WHERE G eid %(x)s, G name N'result=session.execute(rql,{'x':kwargs['geid']},'x',build_descr=False)Operation.__init__(self,session,*args,**kwargs)self.group=result[0][0]classDeleteGroupOp(GroupOperation):"""synchronize user when a in_group relation has been deleted"""defcommit_event(self):"""the observed connections pool has been commited"""groups=self.cnxuser.groupstry:groups.remove(self.group)exceptKeyError:self.error('user %s not in group %s',self.cnxuser,self.group)returndefafter_del_in_group(session,fromeid,rtype,toeid):"""modify user permission, need to update users"""forsession_inget_user_sessions(session.repo,fromeid):DeleteGroupOp(session,cnxuser=session_.user,geid=toeid)classAddGroupOp(GroupOperation):"""synchronize user when a in_group relation has been added"""defcommit_event(self):"""the observed connections pool has been commited"""groups=self.cnxuser.groupsifself.groupingroups:self.warning('user %s already in group %s',self.cnxuser,self.group)returngroups.add(self.group)defafter_add_in_group(session,fromeid,rtype,toeid):"""modify user permission, need to update users"""forsession_inget_user_sessions(session.repo,fromeid):AddGroupOp(session,cnxuser=session_.user,geid=toeid)classDelUserOp(Operation):"""synchronize user when a in_group relation has been added"""def__init__(self,session,cnxid):self.cnxid=cnxidOperation.__init__(self,session)defcommit_event(self):"""the observed connections pool has been commited"""try:self.repo.close(self.cnxid)exceptBadConnectionId:pass# already closeddefafter_del_user(session,eid):"""modify user permission, need to update users"""forsession_inget_user_sessions(session.repo,eid):DelUserOp(session,session_.id)def_register_usergroup_hooks(hm):"""register user/group related hooks on the hooks manager"""hm.register_hook(after_del_user,'after_delete_entity','CWUser')hm.register_hook(after_add_in_group,'after_add_relation','in_group')hm.register_hook(after_del_in_group,'after_delete_relation','in_group')# workflow handling ###########################################################fromcubicweb.entities.wfobjsimportWorkflowTransition,WorkflowExceptiondef_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 sessionfromsource=session.describe(x)[1]# don't try to remove previous state if in_state isn't stored in the system# sourceiffromsource=='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)defbefore_add_trinfo(session,entity):"""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 """# 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 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=session.is_super_sessionor'managers'insession.user.groups# 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: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)ifnotcowpowersanddeststateisNone:msg=entity.req._("state doesn't belong to entity's workflow")raiseValidationError(entity.eid,{'to_state':msg})else:# check transition is valid and allowed, unless we're coming back from# subworkflowtr=session.entity_from_eid(treid)ifswtrisNone: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})ifentity.get('to_state'):deststateeid=entity['to_state']ifnotcowpowersanddeststateeid!=tr.destination().eid:msg=session._("transition isn't allowed")raiseValidationError(entity.eid,{'by_transition':msg})ifswtrisNone:deststate=session.entity_from_eid(deststateeid)ifnotcowpowersanddeststateisNone:msg=entity.req._("state doesn't belong to entity's workflow")raiseValidationError(entity.eid,{'to_state':msg})else: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))defafter_add_trinfo(session,entity):"""change related entity state"""_change_state(session,entity['wf_info_for'],entity['from_state'],entity['to_state'])forentity=session.entity_from_eid(entity['wf_info_for'])assertforentity.current_state.eid==entity['to_state'],forentity.current_state.nameifforentity.main_workflow.eid!=forentity.current_workflow.eid:# we're in a subworkflow, check if we've reached an exit pointwftr=forentity.subworkflow_input_transition()ifwftrisNone:# inconsistency detectedmsg=entity.req._("state doesn't belong to entity's current workflow")raiseValidationError(entity.eid,{'to_state':msg})tostate=wftr.get_exit_point(entity['to_state'])iftostateisnotNone:# reached an exit pointmsg=session._('exiting from subworkflow %s')msg%=session._(forentity.current_workflow.name)session.transaction_data[(forentity.eid,'subwfentrytr')]=True# XXX iirkreq=forentity.reqforentity.req=session.super_sessiontry:trinfo=forentity.change_state(tostate,msg,u'text/plain',tr=wftr)finally:forentity.req=reqclassSetInitialStateOp(PreCommitOperation):"""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',())ifnotentity.eidinpendingeidsandnotentity.in_stateand \entity.main_workflow:state=entity.main_workflow.initialifstate:# use super session to by-pass security checkssession.super_session.add_relation(entity.eid,'in_state',state.eid)defset_initial_state_after_add(session,entity):SetInitialStateOp(session,entity=entity)defbefore_add_in_state(session,eidfrom,rtype,eidto):"""check state apply, in case of direct in_state change using unsafe_execute """nocheck=session.transaction_data.setdefault('skip-security',())if(eidfrom,'in_state',eidto)innocheck:# state changed through TrInfo insertion, so we already know it's okreturnentity=session.entity_from_eid(eidfrom)mainwf=entity.main_workflowifmainwfisNone:msg=session._('entity has no workflow set')raiseValidationError(entity.eid,{None:msg})forwfinmainwf.iter_workflows():ifwf.state_by_eid(eidto):breakelse:msg=session._("state doesn't belong to entity's workflow. You may ""want to set a custom workflow for this entity first.")raiseValidationError(eidfrom,{'in_state':msg})ifentity.current_workflowandwf.eid!=entity.current_workflow.eid:msg=session._("state doesn't belong to entity's current workflow")raiseValidationError(eidfrom,{'in_state':msg})classCheckTrExitPoint(PreCommitOperation):defprecommit_event(self):tr=self.session.entity_from_eid(self.treid)outputs=set()forepintr.subworkflow_exit:ifep.subwf_state.eidinoutputs:msg=self.session._("can't have multiple exits on the same state")raiseValidationError(self.treid,{'subworkflow_exit':msg})outputs.add(ep.subwf_state.eid)defafter_add_subworkflow_exit(session,eidfrom,rtype,eidto):CheckTrExitPoint(session,treid=eidfrom)classWorkflowChangedOp(PreCommitOperation):"""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:msg=session._('workflow has no initial state')raiseValidationError(entity.eid,{'custom_workflow':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)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')defset_custom_workflow(session,eidfrom,rtype,eidto):WorkflowChangedOp(session,eid=eidfrom,wfeid=eidto)defdel_custom_workflow(session,eidfrom,rtype,eidto):entity=session.entity_from_eid(eidfrom)typewf=entity.cwetype_workflow()iftypewfisnotNone:WorkflowChangedOp(session,eid=eidfrom,wfeid=typewf.eid)defafter_del_workflow(session,eid):# workflow cleanupsession.execute('DELETE State X WHERE NOT X state_of Y')session.execute('DELETE Transition X WHERE NOT X transition_of Y')def_register_wf_hooks(hm):"""register workflow related hooks on the hooks manager"""if'in_state'inhm.schema:hm.register_hook(before_add_trinfo,'before_add_entity','TrInfo')hm.register_hook(after_add_trinfo,'after_add_entity','TrInfo')#hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')foreschemainhm.schema.entities():if'in_state'ineschema.subject_relations():hm.register_hook(set_initial_state_after_add,'after_add_entity',str(eschema))hm.register_hook(set_custom_workflow,'after_add_relation','custom_workflow')hm.register_hook(del_custom_workflow,'after_delete_relation','custom_workflow')hm.register_hook(after_del_workflow,'after_delete_entity','Workflow')hm.register_hook(before_add_in_state,'before_add_relation','in_state')hm.register_hook(after_add_subworkflow_exit,'after_add_relation','subworkflow_exit')# CWProperty hooks #############################################################classDelCWPropertyOp(Operation):"""a user's custom properties has been deleted"""defcommit_event(self):"""the observed connections pool has been commited"""try:delself.epropdict[self.key]exceptKeyError:self.error('%s has no associated value',self.key)classChangeCWPropertyOp(Operation):"""a user's custom properties has been added/changed"""defcommit_event(self):"""the observed connections pool has been commited"""self.epropdict[self.key]=self.valueclassAddCWPropertyOp(Operation):"""a user's custom properties has been added/changed"""defcommit_event(self):"""the observed connections pool has been commited"""eprop=self.epropifnoteprop.for_user:self.repo.vreg.eprop_values[eprop.pkey]=eprop.value# if for_user is set, update is handled by a ChangeCWPropertyOp operationdefafter_add_eproperty(session,entity):key,value=entity.pkey,entity.valuetry:value=session.vreg.typed_value(key,value)exceptUnknownProperty:raiseValidationError(entity.eid,{'pkey':session._('unknown property key')})exceptValueError,ex:raiseValidationError(entity.eid,{'value':session._(str(ex))})ifnotsession.user.matching_groups('managers'):session.add_relation(entity.eid,'for_user',session.user.eid)else:AddCWPropertyOp(session,eprop=entity)defafter_update_eproperty(session,entity):ifnot('pkey'inentity.edited_attributesor'value'inentity.edited_attributes):returnkey,value=entity.pkey,entity.valuetry:value=session.vreg.typed_value(key,value)exceptUnknownProperty:returnexceptValueError,ex:raiseValidationError(entity.eid,{'value':session._(str(ex))})ifentity.for_user:forsession_inget_user_sessions(session.repo,entity.for_user[0].eid):ChangeCWPropertyOp(session,epropdict=session_.user.properties,key=key,value=value)else:# site wide propertiesChangeCWPropertyOp(session,epropdict=session.vreg.eprop_values,key=key,value=value)defbefore_del_eproperty(session,eid):foreidfrom,rtype,eidtoinsession.transaction_data.get('pendingrelations',()):ifrtype=='for_user'andeidfrom==eid:# if for_user was set, delete has already been handledbreakelse:key=session.execute('Any K WHERE P eid %(x)s, P pkey K',{'x':eid},'x')[0][0]DelCWPropertyOp(session,epropdict=session.vreg.eprop_values,key=key)defafter_add_for_user(session,fromeid,rtype,toeid):ifnotsession.describe(fromeid)[0]=='CWProperty':returnkey,value=session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',{'x':fromeid},'x')[0]ifsession.vreg.property_info(key)['sitewide']:raiseValidationError(fromeid,{'for_user':session._("site-wide property can't be set for user")})forsession_inget_user_sessions(session.repo,toeid):ChangeCWPropertyOp(session,epropdict=session_.user.properties,key=key,value=value)defbefore_del_for_user(session,fromeid,rtype,toeid):key=session.execute('Any K WHERE P eid %(x)s, P pkey K',{'x':fromeid},'x')[0][0]relation_deleted(session,fromeid,rtype,toeid)forsession_inget_user_sessions(session.repo,toeid):DelCWPropertyOp(session,epropdict=session_.user.properties,key=key)def_register_eproperty_hooks(hm):"""register workflow related hooks on the hooks manager"""hm.register_hook(after_add_eproperty,'after_add_entity','CWProperty')hm.register_hook(after_update_eproperty,'after_update_entity','CWProperty')hm.register_hook(before_del_eproperty,'before_delete_entity','CWProperty')hm.register_hook(after_add_for_user,'after_add_relation','for_user')hm.register_hook(before_del_for_user,'before_delete_relation','for_user')