add_state/add_transition are now deprecated, only add_workflow remaining
"""schema hooks:- synchronize the living schema object with the persistent schema- perform physical update on the source when necessarychecking for schema consistency is done in hooks.py: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"fromyams.schemaimportBASE_TYPES,RelationSchema,RelationDefinitionSchemafromyams.buildobjsimportEntityType,RelationType,RelationDefinitionfromyams.schema2sqlimporteschema2sql,rschema2sql,type_from_constraintsfromlogilab.common.decoratorsimportclear_cachefromcubicwebimportValidationError,RepositoryErrorfromcubicweb.selectorsimportimplementsfromcubicweb.schemaimportMETA_RTYPES,VIRTUAL_RTYPES,CONSTRAINTS,display_namefromcubicweb.serverimporthook,schemaserialasssfromcubicweb.server.sqlutilsimportSQL_PREFIXTYPE_CONVERTER={# XXX'Boolean':bool,'Int':int,'Float':float,'Password':str,'String':unicode,'Date':unicode,'Datetime':unicode,'Time':unicode,}# core entity and relation types which can't be removedCORE_ETYPES=list(BASE_TYPES)+['CWEType','CWRType','CWUser','CWGroup','CWConstraint','CWAttribute','CWRelation']CORE_RTYPES=['eid','creation_date','modification_date','cwuri','login','upassword','name','is','instanceof','owned_by','created_by','in_group','relation_type','from_entity','to_entity','constrainted_by','read_permission','add_permission','delete_permission','updated_permission',]defget_constraints(session,entity):constraints=[]forcstreidinsession.transaction_data.get(entity.eid,()):cstrent=session.entity_from_eid(cstreid)cstr=CONSTRAINTS[cstrent.type].deserialize(cstrent.value)cstr.eid=cstreidconstraints.append(cstr)returnconstraintsdefgroup_mapping(cw):try:returncw.transaction_data['groupmap']exceptKeyError:cw.transaction_data['groupmap']=gmap=ss.group_mapping(cw)returngmapdefadd_inline_relation_column(session,etype,rtype):"""add necessary column and index for an inlined relation"""table=SQL_PREFIX+etypecolumn=SQL_PREFIX+rtypetry:session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'%(table,column)),rollback_on_failure=False)session.info('added column %s to table %s',column,table)except:# silent exception here, if this error has not been raised because the# column already exists, index creation will fail anywaysession.exception('error while adding column %s to table %s',table,column)# create index before alter table which may expectingly fail during test# (sqlite) while index creation should never fail (test for index existence# is done by the dbhelper)session.pool.source('system').create_index(session,table,column)session.info('added index on %s(%s)',table,column)session.transaction_data.setdefault('createdattrs',[]).append('%s.%s'%(etype,rtype))defcheck_valid_changes(session,entity,ro_attrs=('name','final')):errors={}# don't use getattr(entity, attr), we would get the modified value if anyforattrinentity.edited_attributes:ifattrinro_attrs:newval=entity.pop(attr)origval=getattr(entity,attr)ifnewval!=origval:errors[attr]=session._("can't change the %s attribute")% \display_name(session,attr)entity[attr]=newvaliferrors:raiseValidationError(entity.eid,errors)# operations for low-level database alteration ################################classDropTable(hook.Operation):"""actually remove a database from the instance's schema"""table=None# make pylint happydefprecommit_event(self):dropped=self.session.transaction_data.setdefault('droppedtables',set())ifself.tableindropped:return# already processeddropped.add(self.table)self.session.system_sql('DROP TABLE %s'%self.table)self.info('dropped table %s',self.table)classDropRelationTable(DropTable):def__init__(self,session,rtype):super(DropRelationTable,self).__init__(session,table='%s_relation'%rtype)session.transaction_data.setdefault('pendingrtypes',set()).add(rtype)classDropColumn(hook.Operation):"""actually remove the attribut's column from entity table in the system database """table=column=None# make pylint happydefprecommit_event(self):session,table,column=self.session,self.table,self.column# drop index if anysession.pool.source('system').drop_index(session,table,column)try:session.system_sql('ALTER TABLE %s DROP COLUMN %s'%(table,column),rollback_on_failure=False)self.info('dropped column %s from table %s',column,table)exceptException,ex:# not supported by sqlite for instanceself.error('error while altering table %s: %s',table,ex)# base operations for in-memory schema synchronization ########################classMemSchemaNotifyChanges(hook.SingleLastOperation):"""the update schema operation: special operation which should be called once and after all other schema operations. It will trigger internal structures rebuilding to consider schema changes. """def__init__(self,session):hook.SingleLastOperation.__init__(self,session)defprecommit_event(self):foreschemainself.session.repo.schema.entities():ifnoteschema.final:clear_cache(eschema,'ordered_relations')defcommit_event(self):rebuildinfered=self.session.data.get('rebuild-infered',True)repo=self.session.reporepo.set_schema(repo.schema,rebuildinfered=rebuildinfered)# CWUser class might have changed, update current session userscwuser_cls=self.session.vreg['etypes'].etype_class('CWUser')forsessioninrepo._sessions.values():session.user.__class__=cwuser_clsdefrollback_event(self):self.precommit_event()classMemSchemaOperation(hook.Operation):"""base class for schema operations"""def__init__(self,session,kobj=None,**kwargs):self.kobj=kobj# once Operation.__init__ has been called, event may be triggered, so# do this last !hook.Operation.__init__(self,session,**kwargs)# every schema operation is triggering a schema updateMemSchemaNotifyChanges(session)defprepare_constraints(self,subjtype,rtype,objtype):rdef=rtype.rdef(subjtype,objtype)constraints=rdef.constraintsself.constraints=list(constraints)rdef.constraints=self.constraintsclassMemSchemaEarlyOperation(MemSchemaOperation):definsert_index(self):"""schema operation which are inserted at the begining of the queue (typically to add/remove entity or relation types) """i=-1fori,opinenumerate(self.session.pending_operations):ifnotisinstance(op,MemSchemaEarlyOperation):returnireturni+1# operations for high-level source database alteration ########################classSourceDbCWETypeRename(hook.Operation):"""this operation updates physical storage accordingly"""oldname=newname=None# make pylint happydefprecommit_event(self):# we need sql to operate physical changes on the system databasesqlexec=self.session.system_sqlsqlexec('ALTER TABLE %s%s RENAME TO %s%s'%(SQL_PREFIX,self.oldname,SQL_PREFIX,self.newname))self.info('renamed table %s to %s',self.oldname,self.newname)sqlexec('UPDATE entities SET type=%s WHERE type=%s',(self.newname,self.oldname))sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',(self.newname,self.oldname))classSourceDbCWRTypeUpdate(hook.Operation):"""actually update some properties of a relation definition"""rschema=values=entity=None# make pylint happydefprecommit_event(self):session=self.sessionrschema=self.rschemaifrschema.finalornot'inlined'inself.values:return# nothing to doinlined=self.values['inlined']entity=self.entity# check in-lining is necessary / possibleifnotentity.inlined_changed(inlined):return# nothing to do# inlined changed, make necessary physical changes!sqlexec=self.session.system_sqlrtype=rschema.typeeidcolumn=SQL_PREFIX+'eid'ifnotinlined:# need to create the relation if it has not been already done by# another event of the same transactionifnotrschema.typeinsession.transaction_data.get('createdtables',()):tablesql=rschema2sql(rschema)# create the necessary tableforsqlintablesql.split(';'):ifsql.strip():sqlexec(sql)session.transaction_data.setdefault('createdtables',[]).append(rschema.type)# copy existant datacolumn=SQL_PREFIX+rtypeforetypeinrschema.subjects():table=SQL_PREFIX+str(etype)sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'%(rtype,eidcolumn,column,table,column))# drop existant columnsforetypeinrschema.subjects():DropColumn(session,table=SQL_PREFIX+str(etype),column=SQL_PREFIX+rtype)else:foretypeinrschema.subjects():try:add_inline_relation_column(session,str(etype),rtype)exceptException,ex:# the column probably already exists. this occurs when the# entity's type has just been added or if the column has not# been previously droppedself.error('error while altering table %s: %s',etype,ex)# copy existant data.# XXX don't use, it's not supported by sqlite (at least at when i tried it)#sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '# 'FROM %(rtype)s_relation '# 'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'# % locals())table=SQL_PREFIX+str(etype)cursor=sqlexec('SELECT eid_from, eid_to FROM %(table)s, ''%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s=''%(rtype)s_relation.eid_from'%locals())args=[{'val':eid_to,'x':eid}foreid,eid_toincursor.fetchall()]ifargs:column=SQL_PREFIX+rtypecursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'%(table,column,eidcolumn),args)# drop existant tableDropRelationTable(session,rtype)classSourceDbCWAttributeAdd(hook.Operation):"""an attribute relation (CWAttribute) has been added: * add the necessary column * set default on this column if any and possible * register an operation to add the relation definition to the instance's schema on commit constraints are handled by specific hooks """entity=None# make pylint happydefinit_rdef(self,**kwargs):entity=self.entityfromentity=entity.stypeself.session.execute('SET X ordernum Y+1 ''WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, ''X ordernum >= %(order)s, NOT X eid %(x)s',{'x':entity.eid,'se':fromentity.eid,'order':entity.ordernumor0})subj=str(fromentity.name)rtype=entity.rtype.nameobj=str(entity.otype.name)constraints=get_constraints(self.session,entity)rdef=RelationDefinition(subj,rtype,obj,description=entity.description,cardinality=entity.cardinality,constraints=constraints,order=entity.ordernum,eid=entity.eid,**kwargs)MemSchemaRDefAdd(self.session,rdef)returnrdefdefprecommit_event(self):session=self.sessionentity=self.entity# entity.defaultval is a string or None, but we need a correctly typed# valuedefault=entity.defaultvalifdefaultisnotNone:default=TYPE_CONVERTER[entity.otype.name](default)props={'default':default,'indexed':entity.indexed,'fulltextindexed':entity.fulltextindexed,'internationalizable':entity.internationalizable}rdef=self.init_rdef(**props)sysource=session.pool.source('system')attrtype=type_from_constraints(sysource.dbhelper,rdef.object,rdef.constraints)# XXX should be moved somehow into lgc.adbh: sqlite doesn't support to# add a new column with UNIQUE, it should be added after the ALTER TABLE# using ADD INDEXifsysource.dbdriver=='sqlite'and'UNIQUE'inattrtype:extra_unique_index=Trueattrtype=attrtype.replace(' UNIQUE','')else:extra_unique_index=False# added some str() wrapping query since some backend (eg psycopg) don't# allow unicode queriestable=SQL_PREFIX+rdef.subjectcolumn=SQL_PREFIX+rdef.nametry:session.system_sql(str('ALTER TABLE %s ADD COLUMN %s%s'%(table,column,attrtype)),rollback_on_failure=False)self.info('added column %s to table %s',table,column)exceptException,ex:# the column probably already exists. this occurs when# the entity's type has just been added or if the column# has not been previously droppedself.error('error while altering table %s: %s',table,ex)ifextra_unique_indexorentity.indexed:try:sysource.create_index(session,table,column,unique=extra_unique_index)exceptException,ex:self.error('error while creating index for %s.%s: %s',table,column,ex)# final relations are not infered, propagatetry:eschema=session.vreg.schema.eschema(rdef.subject)exceptKeyError:return# entity type currently being added# propagate attribute to children classesrschema=session.vreg.schema.rschema(rdef.name)# if relation type has been inserted in the same transaction, its final# attribute is still set to False, so we've to ensure it's Falserschema.final=True# XXX 'infered': True/False, not clear actuallyprops.update({'constraints':rdef.constraints,'description':rdef.description,'cardinality':rdef.cardinality,'constraints':rdef.constraints,'permissions':rdef.get_permissions(),'order':rdef.order})groupmap=group_mapping(session)forspecializationineschema.specialized_by(False):if(specialization,rdef.object)inrschema.rdefs:continuesperdef=RelationDefinitionSchema(specialization,rschema,rdef.object,props)forrql,argsinss.rdef2rql(rschema,str(specialization),rdef.object,sperdef,groupmap=groupmap):session.execute(rql,args)# set default value, using sql for performance and to avoid# modification_date updateifdefault:session.system_sql('UPDATE %s SET %s=%%(default)s'%(table,column),{'default':default})classSourceDbCWRelationAdd(SourceDbCWAttributeAdd):"""an actual relation has been added: * if this is an inlined relation, add the necessary column else if it's the first instance of this relation type, add the necessary table and set default permissions * register an operation to add the relation definition to the instance's schema on commit constraints are handled by specific hooks """entity=None# make pylint happydefprecommit_event(self):session=self.sessionentity=self.entityrdef=self.init_rdef(composite=entity.composite)schema=session.vreg.schemartype=rdef.namerschema=session.vreg.schema.rschema(rtype)# this have to be done before permissions settingifrschema.inlined:# need to add a column if the relation is inlined and if this is the# first occurence of "Subject relation Something" whatever Something# and if it has not been added during other event of the same# transactionkey='%s.%s'%(rdef.subject,rtype)try:alreadythere=bool(rschema.objects(rdef.subject))exceptKeyError:alreadythere=Falseifnot(alreadythereorkeyinsession.transaction_data.get('createdattrs',())):add_inline_relation_column(session,rdef.subject,rtype)else:# need to create the relation if no relation definition in the# schema and if it has not been added during other event of the same# transactionifnot(rschema.subjects()orrtypeinsession.transaction_data.get('createdtables',())):try:rschema=session.vreg.schema.rschema(rtype)tablesql=rschema2sql(rschema)exceptKeyError:# fake we add it to the schema now to get a correctly# initialized schema but remove it before doing anything# more dangerous...rschema=session.vreg.schema.add_relation_type(rdef)tablesql=rschema2sql(rschema)session.vreg.schema.del_relation_type(rtype)# create the necessary tableforsqlintablesql.split(';'):ifsql.strip():session.system_sql(sql)session.transaction_data.setdefault('createdtables',[]).append(rtype)classSourceDbRDefUpdate(hook.Operation):"""actually update some properties of a relation definition"""rschema=values=None# make pylint happydefprecommit_event(self):etype=self.kobj[0]table=SQL_PREFIX+etypecolumn=SQL_PREFIX+self.rschema.typeif'indexed'inself.values:sysource=self.session.pool.source('system')ifself.values['indexed']:sysource.create_index(self.session,table,column)else:sysource.drop_index(self.session,table,column)if'cardinality'inself.valuesandself.rschema.final:adbh=self.session.pool.source('system').dbhelperifnotadbh.alter_column_support:# not supported (and NOT NULL not set by yams in that case, so# no worry)returnatype=self.rschema.objects(etype)[0]constraints=self.rschema.rdef(etype,atype).constraintscoltype=type_from_constraints(adbh,atype,constraints,creating=False)# XXX check self.values['cardinality'][0] actually changed?sql=adbh.sql_set_null_allowed(table,column,coltype,self.values['cardinality'][0]!='1')self.session.system_sql(sql)classSourceDbCWConstraintAdd(hook.Operation):"""actually update constraint of a relation definition"""entity=None# make pylint happycancelled=Falsedefprecommit_event(self):rdef=self.entity.reverse_constrained_by[0]session=self.session# when the relation is added in the same transaction, the constraint# object is created by the operation adding the attribute or relation,# so there is nothing to do hereifsession.added_in_transaction(rdef.eid):returnrdefschema=session.vreg.schema.schema_by_eid(rdef.eid)subjtype,rtype,objtype=rdefschema.as_triple()cstrtype=self.entity.typeoldcstr=rtype.rdef(subjtype,objtype).constraint_by_type(cstrtype)newcstr=CONSTRAINTS[cstrtype].deserialize(self.entity.value)table=SQL_PREFIX+str(subjtype)column=SQL_PREFIX+str(rtype)# alter the physical schema on size constraint changesifnewcstr.type()=='SizeConstraint'and(oldcstrisNoneoroldcstr.max!=newcstr.max):adbh=self.session.pool.source('system').dbhelpercard=rtype.rdef(subjtype,objtype).cardinalitycoltype=type_from_constraints(adbh,objtype,[newcstr],creating=False)sql=adbh.sql_change_col_type(table,column,coltype,card!='1')try:session.system_sql(sql,rollback_on_failure=False)self.info('altered column %s of table %s: now VARCHAR(%s)',column,table,newcstr.max)exceptException,ex:# not supported by sqlite for instanceself.error('error while altering table %s: %s',table,ex)elifcstrtype=='UniqueConstraint'andoldcstrisNone:session.pool.source('system').create_index(self.session,table,column,unique=True)classSourceDbCWConstraintDel(hook.Operation):"""actually remove a constraint of a relation definition"""rtype=subjtype=objtype=None# make pylint happydefprecommit_event(self):cstrtype=self.cstr.type()table=SQL_PREFIX+str(self.subjtype)column=SQL_PREFIX+str(self.rtype)# alter the physical schema on size/unique constraint changesifcstrtype=='SizeConstraint':try:self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'%(table,column),rollback_on_failure=False)self.info('altered column %s of table %s: now TEXT',column,table)exceptException,ex:# not supported by sqlite for instanceself.error('error while altering table %s: %s',table,ex)elifcstrtype=='UniqueConstraint':self.session.pool.source('system').drop_index(self.session,table,column,unique=True)# operations for in-memory schema synchronization #############################classMemSchemaCWETypeAdd(MemSchemaEarlyOperation):"""actually add the entity type to the instance's schema"""eid=None# make pylint happydefcommit_event(self):self.session.vreg.schema.add_entity_type(self.kobj)classMemSchemaCWETypeRename(MemSchemaOperation):"""this operation updates physical storage accordingly"""oldname=newname=None# make pylint happydefcommit_event(self):self.session.vreg.schema.rename_entity_type(self.oldname,self.newname)classMemSchemaCWETypeDel(MemSchemaOperation):"""actually remove the entity type from the instance's schema"""defcommit_event(self):try:# del_entity_type also removes entity's relationsself.session.vreg.schema.del_entity_type(self.kobj)exceptKeyError:# s/o entity type have already been deletedpassclassMemSchemaCWRTypeAdd(MemSchemaEarlyOperation):"""actually add the relation type to the instance's schema"""eid=None# make pylint happydefcommit_event(self):self.session.vreg.schema.add_relation_type(self.kobj)classMemSchemaCWRTypeUpdate(MemSchemaOperation):"""actually update some properties of a relation definition"""rschema=values=None# make pylint happydefcommit_event(self):# structure should be clean, not need to remove entity's relations# at this pointself.rschema.__dict__.update(self.values)classMemSchemaCWRTypeDel(MemSchemaOperation):"""actually remove the relation type from the instance's schema"""defcommit_event(self):try:self.session.vreg.schema.del_relation_type(self.kobj)exceptKeyError:# s/o entity type have already been deletedpassclassMemSchemaRDefAdd(MemSchemaEarlyOperation):"""actually add the attribute relation definition to the instance's schema """defcommit_event(self):self.session.vreg.schema.add_relation_def(self.kobj)classMemSchemaRDefUpdate(MemSchemaOperation):"""actually update some properties of a relation definition"""rschema=values=None# make pylint happydefcommit_event(self):# structure should be clean, not need to remove entity's relations# at this pointself.rschema.rdefs[self.kobj].update(self.values)classMemSchemaRDefDel(MemSchemaOperation):"""actually remove the relation definition from the instance's schema"""defcommit_event(self):subjtype,rtype,objtype=self.kobjtry:self.session.vreg.schema.del_relation_def(subjtype,rtype,objtype)exceptKeyError:# relation type may have been already deletedpassclassMemSchemaCWConstraintAdd(MemSchemaOperation):"""actually update constraint of a relation definition has to be called before SourceDbCWConstraintAdd """cancelled=Falsedefprecommit_event(self):rdef=self.entity.reverse_constrained_by[0]# when the relation is added in the same transaction, the constraint# object is created by the operation adding the attribute or relation,# so there is nothing to do hereifself.session.added_in_transaction(rdef.eid):self.cancelled=Truereturnrdef=self.session.vreg.schema.schema_by_eid(rdef.eid)subjtype,rtype,objtype=rdef.as_triple()self.prepare_constraints(subjtype,rtype,objtype)cstrtype=self.entity.typeself.cstr=rtype.rdef(subjtype,objtype).constraint_by_type(cstrtype)self.newcstr=CONSTRAINTS[cstrtype].deserialize(self.entity.value)self.newcstr.eid=self.entity.eiddefcommit_event(self):ifself.cancelled:return# in-place modificationifnotself.cstrisNone:self.constraints.remove(self.cstr)self.constraints.append(self.newcstr)classMemSchemaCWConstraintDel(MemSchemaOperation):"""actually remove a constraint of a relation definition has to be called before SourceDbCWConstraintDel """rtype=subjtype=objtype=None# make pylint happydefprecommit_event(self):self.prepare_constraints(self.subjtype,self.rtype,self.objtype)defcommit_event(self):self.constraints.remove(self.cstr)classMemSchemaPermissionAdd(MemSchemaOperation):"""synchronize schema when a *_permission relation has been added on a group """defcommit_event(self):"""the observed connections pool has been commited"""try:erschema=self.session.vreg.schema.schema_by_eid(self.eid)exceptKeyError:# duh, schema not found, log error and skip operationself.error('no schema for %s',self.eid)returnperms=list(erschema.action_permissions(self.action))ifhasattr(self,'group_eid'):perm=self.session.entity_from_eid(self.group_eid).nameelse:perm=erschema.rql_expression(self.expr)try:perms.index(perm)self.warning('%s already in permissions for %s on %s',perm,self.action,erschema)exceptValueError:perms.append(perm)erschema.set_action_permissions(self.action,perms)classMemSchemaPermissionDel(MemSchemaPermissionAdd):"""synchronize schema when a *_permission relation has been deleted from a group """defcommit_event(self):"""the observed connections pool has been commited"""try:erschema=self.session.vreg.schema.schema_by_eid(self.eid)exceptKeyError:# duh, schema not found, log error and skip operationself.error('no schema for %s',self.eid)returnifisinstance(erschema,RelationSchema):# XXX 3.6 migrationreturnperms=list(erschema.action_permissions(self.action))ifhasattr(self,'group_eid'):perm=self.session.entity_from_eid(self.group_eid).nameelse:perm=erschema.rql_expression(self.expr)try:perms.remove(perm)erschema.set_action_permissions(self.action,perms)exceptValueError:self.error('can\'t remove permission %s for %s on %s',perm,self.action,erschema)classMemSchemaSpecializesAdd(MemSchemaOperation):defcommit_event(self):eschema=self.session.vreg.schema.schema_by_eid(self.etypeeid)parenteschema=self.session.vreg.schema.schema_by_eid(self.parentetypeeid)eschema._specialized_type=parenteschema.typeparenteschema._specialized_by.append(eschema.type)classMemSchemaSpecializesDel(MemSchemaOperation):defcommit_event(self):try:eschema=self.session.vreg.schema.schema_by_eid(self.etypeeid)parenteschema=self.session.vreg.schema.schema_by_eid(self.parentetypeeid)exceptKeyError:# etype removed, nothing to doreturneschema._specialized_type=Noneparenteschema._specialized_by.remove(eschema.type)classSyncSchemaHook(hook.Hook):__abstract__=Truecategory='syncschema'# CWEType hooks ################################################################classDelCWETypeHook(SyncSchemaHook):"""before deleting a CWEType entity: * check that we don't remove a core entity type * cascade to delete related CWAttribute and CWRelation entities * instantiate an operation to delete the entity type on commit """__regid__='syncdelcwetype'__select__=SyncSchemaHook.__select__&implements('CWEType')events=('before_delete_entity',)def__call__(self):# final entities can't be deleted, don't care about thatname=self.entity.nameifnameinCORE_ETYPES:raiseValidationError(self.entity.eid,{None:self._cw._('can\'t be deleted')})# delete every entities of this typeself._cw.unsafe_execute('DELETE %s X'%name)DropTable(self._cw,table=SQL_PREFIX+name)MemSchemaCWETypeDel(self._cw,name)classAfterDelCWETypeHook(DelCWETypeHook):__regid__='wfcleanup'events=('after_delete_entity',)def__call__(self):# workflow cleanupself._cw.execute('DELETE Workflow X WHERE NOT X workflow_of Y')classAfterAddCWETypeHook(DelCWETypeHook):"""after adding a CWEType entity: * create the necessary table * set creation_date and modification_date by creating the necessary CWAttribute entities * add owned_by relation by creating the necessary CWRelation entity * register an operation to add the entity type to the instance's schema on commit """__regid__='syncaddcwetype'events=('after_add_entity',)def__call__(self):entity=self.entityifentity.get('final'):returnschema=self._cw.vreg.schemaname=entity['name']etype=EntityType(name=name,description=entity.get('description'),meta=entity.get('meta'))# don't care about final# fake we add it to the schema now to get a correctly initialized schema# but remove it before doing anything more dangerous...schema=self._cw.vreg.schemaeschema=schema.add_entity_type(etype)# generate table sql and rql to add metadatatablesql=eschema2sql(self._cw.pool.source('system').dbhelper,eschema,prefix=SQL_PREFIX)relrqls=[]forrtypein(META_RTYPES-VIRTUAL_RTYPES):rschema=schema[rtype]sampletype=rschema.subjects()[0]desttype=rschema.objects()[0]props=rschema.rdef(sampletype,desttype)relrqls+=list(ss.rdef2rql(rschema,name,desttype,props,groupmap=group_mapping(self._cw)))# now remove it !schema.del_entity_type(name)# create the necessary tableforsqlintablesql.split(';'):ifsql.strip():self._cw.system_sql(sql)# register operation to modify the schema on commit# this have to be done before adding other relations definitions# or permission settingsetype.eid=entity.eidMemSchemaCWETypeAdd(self._cw,etype)# add meta relationsforrql,kwargsinrelrqls:self._cw.execute(rql,kwargs)classBeforeUpdateCWETypeHook(DelCWETypeHook):"""check name change, handle final"""__regid__='syncupdatecwetype'events=('before_update_entity',)def__call__(self):entity=self.entitycheck_valid_changes(self._cw,entity,ro_attrs=('final',))# don't use getattr(entity, attr), we would get the modified value if anyif'name'inentity.edited_attributes:newname=entity.pop('name')oldname=entity.nameifnewname.lower()!=oldname.lower():SourceDbCWETypeRename(self._cw,oldname=oldname,newname=newname)MemSchemaCWETypeRename(self._cw,oldname=oldname,newname=newname)entity['name']=newname# CWRType hooks ################################################################classDelCWRTypeHook(SyncSchemaHook):"""before deleting a CWRType entity: * check that we don't remove a core relation type * cascade to delete related CWAttribute and CWRelation entities * instantiate an operation to delete the relation type on commit """__regid__='syncdelcwrtype'__select__=SyncSchemaHook.__select__&implements('CWRType')events=('before_delete_entity',)def__call__(self):name=self.entity.nameifnameinCORE_RTYPES:raiseValidationError(self.entity.eid,{None:self._cw._('can\'t be deleted')})# delete relation definitions using this relation typeself._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',{'x':self.entity.eid})self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',{'x':self.entity.eid})MemSchemaCWRTypeDel(self._cw,name)classAfterAddCWRTypeHook(DelCWRTypeHook):"""after a CWRType entity has been added: * register an operation to add the relation type to the instance's schema on commit We don't know yet this point if a table is necessary """__regid__='syncaddcwrtype'events=('after_add_entity',)def__call__(self):entity=self.entityrtype=RelationType(name=entity.name,description=entity.get('description'),meta=entity.get('meta',False),inlined=entity.get('inlined',False),symetric=entity.get('symetric',False),eid=entity.eid)MemSchemaCWRTypeAdd(self._cw,rtype)classBeforeUpdateCWRTypeHook(DelCWRTypeHook):"""check name change, handle final"""__regid__='checkupdatecwrtype'events=('before_update_entity',)def__call__(self):check_valid_changes(self._cw,self.entity)classAfterUpdateCWRTypeHook(DelCWRTypeHook):__regid__='syncupdatecwrtype'events=('after_update_entity',)def__call__(self):entity=self.entityrschema=self._cw.vreg.schema.rschema(entity.name)newvalues={}forpropin('meta','symetric','inlined'):ifpropinentity:newvalues[prop]=entity[prop]ifnewvalues:MemSchemaCWRTypeUpdate(self._cw,rschema=rschema,values=newvalues)SourceDbCWRTypeUpdate(self._cw,rschema=rschema,values=newvalues,entity=entity)defcheck_valid_changes(session,entity,ro_attrs=('name','final')):errors={}# don't use getattr(entity, attr), we would get the modified value if anyforattrinro_attrs:ifattrinentity.edited_attributes:origval,newval=hook.entity_oldnewvalue(entity,attr)ifnewval!=origval:errors[attr]=session._("can't change the %s attribute")% \display_name(session,attr)iferrors:raiseValidationError(entity.eid,errors)classAfterDelRelationTypeHook(SyncSchemaHook):"""before deleting a CWAttribute or CWRelation entity: * if this is a final or inlined relation definition, instantiate an operation to drop necessary column, else if this is the last instance of a non final relation, instantiate an operation to drop necessary table * instantiate an operation to delete the relation definition on commit * delete the associated relation type when necessary """__regid__='syncdelrelationtype'__select__=SyncSchemaHook.__select__&hook.match_rtype('relation_type')events=('after_delete_relation',)def__call__(self):session=self._cwrdef=session.vreg.schema.schema_by_eid(self.eidfrom)subjschema,rschema,objschema=rdef.as_triple()pendings=session.transaction_data.get('pendingeids',())pendingrdefs=session.transaction_data.setdefault('pendingrdefs',set())# first delete existing relation if necessaryifrschema.final:rdeftype='CWAttribute'pendingrdefs.add((subjschema,rschema))else:rdeftype='CWRelation'pendingrdefs.add((subjschema,rschema,objschema))ifnot(subjschema.eidinpendingsorobjschema.eidinpendings):session.execute('DELETE X %s Y WHERE X is %s, Y is %s'%(rschema,subjschema,objschema))execute=session.unsafe_executerset=execute('Any COUNT(X) WHERE X is %s, X relation_type R,''R eid %%(x)s'%rdeftype,{'x':self.eidto})lastrel=rset[0][0]==0# we have to update physical schema systematically for final and inlined# relations, but only if it's the last instance for this relation type# for other relationsif(rschema.finalorrschema.inlined):rset=execute('Any COUNT(X) WHERE X is %s, X relation_type R, ''R eid %%(x)s, X from_entity E, E name %%(name)s'%rdeftype,{'x':self.eidto,'name':str(subjschema)})ifrset[0][0]==0andnotsubjschema.eidinpendings:ptypes=session.transaction_data.setdefault('pendingrtypes',set())ptypes.add(rschema.type)DropColumn(session,table=SQL_PREFIX+subjschema.type,column=SQL_PREFIX+rschema.type)eliflastrel:DropRelationTable(session,rschema.type)# if this is the last instance, drop associated relation typeiflastrelandnotself.eidtoinpendings:execute('DELETE CWRType X WHERE X eid %(x)s',{'x':self.eidto},'x')MemSchemaRDefDel(session,(subjschema,rschema,objschema))# CWAttribute / CWRelation hooks ###############################################classAfterAddCWAttributeHook(SyncSchemaHook):__regid__='syncaddcwattribute'__select__=SyncSchemaHook.__select__&implements('CWAttribute')events=('after_add_entity',)def__call__(self):SourceDbCWAttributeAdd(self._cw,entity=self.entity)classAfterAddCWRelationHook(AfterAddCWAttributeHook):__regid__='syncaddcwrelation'__select__=SyncSchemaHook.__select__&implements('CWRelation')def__call__(self):SourceDbCWRelationAdd(self._cw,entity=self.entity)classAfterUpdateCWRDefHook(SyncSchemaHook):__regid__='syncaddcwattribute'__select__=SyncSchemaHook.__select__&implements('CWAttribute','CWRelation')events=('after_update_entity',)def__call__(self):entity=self.entityifself._cw.deleted_in_transaction(entity.eid):returndesttype=entity.otype.namerschema=self._cw.vreg.schema[entity.rtype.name]newvalues={}forpropinRelationDefinitionSchema.rproperty_defs(desttype):ifprop=='constraints':continueifprop=='order':prop='ordernum'ifpropinentity.edited_attributes:newvalues[prop]=entity[prop]ifnewvalues:subjtype=entity.stype.nameMemSchemaRDefUpdate(self._cw,kobj=(subjtype,desttype),rschema=rschema,values=newvalues)SourceDbRDefUpdate(self._cw,kobj=(subjtype,desttype),rschema=rschema,values=newvalues)# constraints synchronization hooks ############################################classAfterAddCWConstraintHook(SyncSchemaHook):__regid__='syncaddcwconstraint'__select__=SyncSchemaHook.__select__&implements('CWConstraint')events=('after_add_entity','after_update_entity')def__call__(self):MemSchemaCWConstraintAdd(self._cw,entity=self.entity)SourceDbCWConstraintAdd(self._cw,entity=self.entity)classAfterAddConstrainedByHook(SyncSchemaHook):__regid__='syncdelconstrainedby'__select__=SyncSchemaHook.__select__&hook.match_rtype('constrained_by')events=('after_add_relation',)def__call__(self):ifself._cw.added_in_transaction(self.eidfrom):self._cw.transaction_data.setdefault(self.eidfrom,[]).append(self.eidto)classBeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):__regid__='syncdelconstrainedby'events=('before_delete_relation',)def__call__(self):ifself._cw.deleted_in_transaction(self.eidfrom):returnschema=self._cw.vreg.schemaentity=self._cw.entity_from_eid(self.eidto)rdef=schema.schema_by_eid(self.eidfrom)try:cstr=rdef.constraint_by_type(entity.type)exceptIndexError:self._cw.critical('constraint type no more accessible')else:subjtype,rtype,objtype=rdef.as_triple()SourceDbCWConstraintDel(self._cw,subjtype=subjtype,rtype=rtype,objtype=objtype,cstr=cstr)MemSchemaCWConstraintDel(self._cw,subjtype=subjtype,rtype=rtype,objtype=objtype,cstr=cstr)# permissions synchronization hooks ############################################classAfterAddPermissionHook(SyncSchemaHook):"""added entity/relation *_permission, need to update schema"""__regid__='syncaddperm'__select__=SyncSchemaHook.__select__&hook.match_rtype('read_permission','add_permission','delete_permission','update_permission')events=('after_add_relation',)def__call__(self):action=self.rtype.split('_',1)[0]ifself._cw.describe(self.eidto)[0]=='CWGroup':MemSchemaPermissionAdd(self._cw,action=action,eid=self.eidfrom,group_eid=self.eidto)else:# RQLExpressionexpr=self._cw.entity_from_eid(self.eidto).expressionMemSchemaPermissionAdd(self._cw,action=action,eid=self.eidfrom,expr=expr)classBeforeDelPermissionHook(AfterAddPermissionHook):"""delete entity/relation *_permission, need to update schema skip the operation if the related type is being deleted """__regid__='syncdelperm'events=('before_delete_relation',)def__call__(self):ifself._cw.deleted_in_transaction(self.eidfrom):returnaction=self.rtype.split('_',1)[0]ifself._cw.describe(self.eidto)[0]=='CWGroup':MemSchemaPermissionDel(self._cw,action=action,eid=self.eidfrom,group_eid=self.eidto)else:# RQLExpressionexpr=self._cw.entity_from_eid(self.eidto).expressionMemSchemaPermissionDel(self._cw,action=action,eid=self.eidfrom,expr=expr)# specializes synchronization hooks ############################################classAfterAddSpecializesHook(SyncSchemaHook):__regid__='syncaddspecializes'__select__=SyncSchemaHook.__select__&hook.match_rtype('specializes')events=('after_add_relation',)def__call__(self):MemSchemaSpecializesAdd(self._cw,etypeeid=self.eidfrom,parentetypeeid=self.eidto)classAfterDelSpecializesHook(SyncSchemaHook):__regid__='syncdelspecializes'__select__=SyncSchemaHook.__select__&hook.match_rtype('specializes')events=('after_delete_relation',)def__call__(self):MemSchemaSpecializesDel(self._cw,etypeeid=self.eidfrom,parentetypeeid=self.eidto)