"""Core hooks: check schema validity, unsure we are not deleting necessaryentities...:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"fromdatetimeimportdatetimefromcubicwebimportUnknownProperty,ValidationError,BadConnectionIdfromcubicweb.server.poolimportOperation,LateOperation,PreCommitOperationfromcubicweb.server.hookhelperimport(check_internal_entity,previous_state,get_user_sessions,rproperty)fromcubicweb.server.repositoryimportFTIndexEntityOpdefrelation_deleted(session,eidfrom,rtype,eidto):session.add_query_data('pendingrelations',(eidfrom,rtype,eidto))# 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 """ifnot'creation_date'inentity:entity['creation_date']=datetime.now()ifnot'modification_date'inentity:entity['modification_date']=datetime.now()defsetmtime_before_update_entity(session,entity):"""update an entity -> set modification date"""ifnot'modification_date'inentity:entity['modification_date']=datetime.now()classSetCreatorOp(PreCommitOperation):defprecommit_event(self):ifself.eidinself.session.query_data('pendingeids',()):# entity have been created and deleted in the same transactionreturnueid=self.session.user.eidexecute=self.session.unsafe_executeifnotexecute('Any X WHERE X created_by U, X eid %(x)s',{'x':self.eid},'x'):execute('SET X created_by U WHERE X eid %(x)s, U eid %(u)s',{'x':self.eid,'u':ueid},'x')defsetowner_after_add_entity(session,entity):"""create a new entity -> set owner and creator metadata"""asession=session.actual_session()ifnotasession.is_internal_session:session.unsafe_execute('SET X owned_by U WHERE X eid %(x)s, U eid %(u)s',{'x':entity.eid,'u':asession.user.eid},'x')SetCreatorOp(asession,eid=entity.eid)defsetis_after_add_entity(session,entity):"""create a new entity -> set is relation"""ifhasattr(entity,'_cw_recreating'):returnsession.unsafe_execute('SET X is E WHERE X eid %(x)s, E name %(name)s',{'x':entity.eid,'name':entity.id},'x')# XXX < 2.50 bw compatifnotsession.get_shared_data('do-not-insert-is_instance_of'):basetypes=entity.e_schema.ancestors()+[entity.e_schema]session.unsafe_execute('SET X is_instance_of E WHERE X eid %%(x)s, E name IN (%s)'%','.join("'%s'"%str(etype)foretypeinbasetypes),{'x':entity.eid},'x')defsetowner_after_add_user(session,entity):"""when a user has been created, add owned_by relation on itself"""session.unsafe_execute('SET X owned_by X WHERE X eid %(x)s',{'x':entity.eid},'x')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(eidto))elifftcontainer=='object':FTIndexEntityOp(session,entity=session.entity(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(eidto))FTIndexEntityOp(session,entity=session.entity(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 relationreturncomposite=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.sessionifnotself.eidinsession.query_data('pendingeids',()):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.query_data('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.query_data('pendingeids',()):returnifself.session.unsafe_execute(*self._rql()).rowcount<1:etype=self.session.describe(self.eid)[0]msg=self.session._('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')raiseValidationError(self.eid,{self.rtype:msg%{'rtype':self.rtype,'etype':etype,'eid':self.eid}})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.typein('owned_by','created_by','is','is_instance_of'):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"""card=rproperty(session,rtype,eidfrom,eidto,'cardinality')pendingeids=session.query_data('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 ###########################################################defbefore_add_in_state(session,fromeid,rtype,toeid):"""check the transition is allowed and record transition information """assertrtype=='in_state'state=previous_state(session,fromeid)etype=session.describe(fromeid)[0]ifnot(session.is_super_sessionor'managers'insession.user.groups):ifnotstateisNone:entity=session.entity(fromeid)# we should find at least one transition going to this statetry:iter(state.transitions(entity,toeid)).next()exceptStopIteration:msg=session._('transition is not allowed')raiseValidationError(fromeid,{'in_state':msg})else:# not a transition# check state is initial state if the workflow defines oneisrset=session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',{'etype':etype})ifisrsetandnottoeid==isrset[0][0]:msg=session._('not the initial state for this entity')raiseValidationError(fromeid,{'in_state':msg})eschema=session.repo.schema[etype]ifnot'wf_info_for'ineschema.object_relations():# workflow history not activated for this entity typereturnrql='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':fromeid,'ds':toeid}cformat=session.get_shared_data('trcommentformat',None,pop=True)ifcformatisnotNone:args['comment_format']=cformatrql+=', T comment_format %(comment_format)s'restriction=['DS eid %(ds)s, E eid %(e)s']ifnotstateisNone:# not a transitionrql+=', T from_state FS'restriction.append('FS eid %(fs)s')args['fs']=state.eidrql='%s WHERE %s'%(rql,', '.join(restriction))session.unsafe_execute(rql,args,'e')classSetInitialStateOp(PreCommitOperation):"""make initial state be a default state"""defprecommit_event(self):session=self.sessionentity=self.entityrset=session.execute('Any S WHERE ET initial_state S, ET name %(name)s',{'name':str(entity.e_schema)})# if there is an initial state and the entity's state is not set,# use the initial state as a default statependingeids=session.query_data('pendingeids',())ifrsetandnotentity.eidinpendingeidsandnotentity.in_state:session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',{'x':entity.eid,'s':rset[0][0]},'x')defset_initial_state_after_add(session,entity):SetInitialStateOp(session,entity=entity)def_register_wf_hooks(hm):"""register workflow related hooks on the hooks manager"""if'in_state'inhm.schema:hm.register_hook(before_add_in_state,'before_add_relation','in_state')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))# 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.unsafe_execute('SET P for_user U WHERE P eid %(x)s,U eid %(u)s',{'x':entity.eid,'u':session.user.eid},'x')else:AddCWPropertyOp(session,eprop=entity)defafter_update_eproperty(session,entity):key,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.query_data('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')