"""Core hooks: check for data integrity according to the instance'schemavalidity: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"fromthreadingimportLockfromcubicwebimportValidationErrorfromcubicweb.schemaimportRQLConstraint,RQLUniqueConstraintfromcubicweb.selectorsimportimplementsfromcubicweb.uilibimportsoup2xhtmlfromcubicweb.serverimporthook# 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'))_UNIQUE_CONSTRAINTS_LOCK=Lock()_UNIQUE_CONSTRAINTS_HOLDER=Nonedef_acquire_unique_cstr_lock(session):"""acquire the _UNIQUE_CONSTRAINTS_LOCK for the session. This lock used to avoid potential integrity pb when checking RQLUniqueConstraint in two different transactions, as explained in http://intranet.logilab.fr/jpl/ticket/36564 """if'uniquecstrholder'insession.transaction_data:return_UNIQUE_CONSTRAINTS_LOCK.acquire()session.transaction_data['uniquecstrholder']=True# register operation responsible to release the lock on commit/rollback_ReleaseUniqueConstraintsOperation(session)def_release_unique_cstr_lock(session):if'uniquecstrholder'insession.transaction_data:delsession.transaction_data['uniquecstrholder']_UNIQUE_CONSTRAINTS_LOCK.release()class_ReleaseUniqueConstraintsOperation(hook.Operation):defcommit_event(self):passdefpostcommit_event(self):_release_unique_cstr_lock(self.session)defrollback_event(self):_release_unique_cstr_lock(self.session)class_CheckRequiredRelationOperation(hook.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.session.deleted_in_transaction(self.eid):returnifself.rtypeinself.session.transaction_data.get('pendingrtypes',()):returnifself.session.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()class_CheckSRelationOp(_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'class_CheckORelationOp(_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'classIntegrityHook(hook.Hook):__abstract__=Truecategory='integrity'classCheckCardinalityHook(IntegrityHook):"""check cardinalities are satisfied"""__regid__='checkcard'events=('after_add_entity','before_delete_relation')def__call__(self):getattr(self,self.event)()defcheckrel_if_necessary(self,opcls,rtype,eid):"""check an equivalent operation has not already been added"""foropinself._cw.pending_operations:ifisinstance(op,opcls)andop.rtype==rtypeandop.eid==eid:breakelse:opcls(self._cw,rtype=rtype,eid=eid)defafter_add_entity(self):eid=self.entity.eideschema=self.entity.e_schemaforrschema,targetschemas,roleineschema.relation_definitions():# skip automatically handled relationsifrschema.typeinDONT_CHECK_RTYPES_ON_ADD:continueopcls=role=='subject'and_CheckSRelationOpor_CheckORelationOprdef=rschema.role_rdef(eschema,targetschemas[0],role)ifrdef.role_cardinality(role)in'1+':self.checkrel_if_necessary(opcls,rschema.type,eid)defbefore_delete_relation(self):rtype=self.rtypeifrtypeinDONT_CHECK_RTYPES_ON_DEL:returnsession=self._cweidfrom,eidto=self.eidfrom,self.eidtocard=session.schema_rproperty(rtype,eidfrom,eidto,'cardinality')pendingrdefs=session.transaction_data.get('pendingrdefs',())if(session.describe(eidfrom)[0],rtype,session.describe(eidto)[0])inpendingrdefs:returnifcard[0]in'1+'andnotsession.deleted_in_transaction(eidfrom):self.checkrel_if_necessary(_CheckSRelationOp,rtype,eidfrom)ifcard[1]in'1+'andnotsession.deleted_in_transaction(eidto):self.checkrel_if_necessary(_CheckORelationOp,rtype,eidto)class_CheckConstraintsOp(hook.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# transactionifself.session.deleted_in_transaction(eidfrom):returnifself.session.deleted_in_transaction(eidto):returnforconstraintinself.constraints:# XXX# * lock RQLConstraint as well?# * use a constraint id to use per constraint lock and avoid# unnecessary commit serialization ?ifisinstance(constraint,RQLUniqueConstraint):_acquire_unique_cstr_lock(self.session)try:constraint.repo_check(self.session,eidfrom,rtype,eidto)exceptNotImplementedError:self.critical('can\'t check constraint %s, not supported',constraint)defcommit_event(self):passclassCheckConstraintHook(IntegrityHook):"""check the relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied (or unsatisfied) may be added later. """__regid__='checkconstraint'events=('after_add_relation',)def__call__(self):# XXX get only RQL[Unique]Constraints?constraints=self._cw.schema_rproperty(self.rtype,self.eidfrom,self.eidto,'constraints')ifconstraints:_CheckConstraintsOp(self._cw,constraints=constraints,rdef=(self.eidfrom,self.rtype,self.eidto))classCheckAttributeConstraintHook(IntegrityHook):"""check the attribute relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied (or unsatisfied) may be added later. """__regid__='checkattrconstraint'events=('after_add_entity','after_update_entity')def__call__(self):eschema=self.entity.e_schemaforattrinself.entity.edited_attributes:ifeschema.subjrels[attr].final:constraints=[cforcineschema.rdef(attr).constraintsifisinstance(c,(RQLUniqueConstraint,RQLConstraint))]ifconstraints:_CheckConstraintsOp(self._cw,constraints=constraints,rdef=(self.entity.eid,attr,None))classCheckUniqueHook(IntegrityHook):__regid__='checkunique'events=('before_add_entity','before_update_entity')def__call__(self):entity=self.entityeschema=entity.e_schemaforattrinentity.edited_attributes:ifeschema.subjrels[attr].finalandeschema.has_unique_values(attr):val=entity[attr]ifvalisNone:continuerql='%s X WHERE X %s%%(val)s'%(entity.e_schema,attr)rset=self._cw.execute(rql,{'val':val})ifrsetandrset[0][0]!=entity.eid:msg=self._cw._('the value "%s" is already used, use another one')raiseValidationError(entity.eid,{attr:msg%val})class_DelayedDeleteOp(hook.Operation):"""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(session.deleted_in_transaction(self.eid)orsession.added_in_transaction(self.eid)):etype=session.describe(self.eid)[0]session.execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'%(etype,self.relation),{'x':self.eid},'x')classDeleteCompositeOrphanHook(IntegrityHook):"""delete the composed of a composite relation when this relation is deleted """__regid__='deletecomposite'events=('before_delete_relation',)def__call__(self):# if the relation is being delete, don't delete composite's components# automaticallypendingrdefs=self._cw.transaction_data.get('pendingrdefs',())if(self._cw.describe(self.eidfrom)[0],self.rtype,self._cw.describe(self.eidto)[0])inpendingrdefs:returncomposite=self._cw.schema_rproperty(self.rtype,self.eidfrom,self.eidto,'composite')ifcomposite=='subject':_DelayedDeleteOp(self._cw,eid=self.eidto,relation='Y %s X'%self.rtype)elifcomposite=='object':_DelayedDeleteOp(self._cw,eid=self.eidfrom,relation='X %s Y'%self.rtype)classDontRemoveOwnersGroupHook(IntegrityHook):"""delete the composed of a composite relation when this relation is deleted """__regid__='checkownersgroup'__select__=IntegrityHook.__select__&implements('CWGroup')events=('before_delete_entity','before_update_entity')def__call__(self):ifself.event=='before_delete_entity'andself.entity.name=='owners':raiseValidationError(self.entity.eid,{None:self._cw._('can\'t be deleted')})elifself.event=='before_update_entity'and'name'inself.entity.edited_attributes:newname=self.entity.pop('name')oldname=self.entity.nameifoldname=='owners'andnewname!=oldname:raiseValidationError(self.entity.eid,{'name':self._cw._('can\'t be changed')})self.entity['name']=newnameclassTidyHtmlFields(IntegrityHook):"""tidy HTML in rich text strings"""__regid__='htmltidy'events=('before_add_entity','before_update_entity')def__call__(self):entity=self.entitymetaattrs=entity.e_schema.meta_attributes()formetaattr,(metadata,attr)inmetaattrs.iteritems():ifmetadata=='format'andattrinentity.edited_attributes:try:value=entity[attr]exceptKeyError:continue# no text to tidyifisinstance(value,unicode):# filter out None and Binaryifgetattr(entity,str(metaattr))=='text/html':entity[attr]=soup2xhtml(value,self._cw.encoding)classStripCWUserLoginHook(IntegrityHook):"""ensure user logins are stripped"""__regid__='stripuserlogin'__select__=IntegrityHook.__select__&implements('CWUser')events=('before_add_entity','before_update_entity',)def__call__(self):user=self.entityif'login'inuser.edited_attributesanduser.login:user.login=user.login.strip()