[hooks] major refactoring:
* they are no "proper" appobject, selected when an event is fired according to its context
* new module cubicweb.server.hook containing the Hook class and Operation class
* deprecated SystemHook and PreCommitOperation classes
* rewrite core server hooks as appobjects in the cubicweb/hooks directory
-> deprecates hooksmanager, remove hooks, schemahooks, securityhooks cubicweb.server sub-modules
-> new cubicweb.hooks sub-package
-> get back to a (somewhat) working state
--- a/debian/cubicweb-dev.install.in Fri Aug 14 09:20:33 2009 +0200
+++ b/debian/cubicweb-dev.install.in Fri Aug 14 09:26:41 2009 +0200
@@ -6,6 +6,7 @@
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/test usr/lib/PY_VERSION/site-packages/cubicweb/ext/
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/test usr/lib/PY_VERSION/site-packages/cubicweb/server/
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/web/test usr/lib/PY_VERSION/site-packages/cubicweb/web/
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/etwist/test usr/lib/PY_VERSION/site-packages/cubicweb/etwist/
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/goa/test usr/lib/PY_VERSION/site-packages/cubicweb/goa/
--- a/debian/cubicweb-server.install.in Fri Aug 14 09:20:33 2009 +0200
+++ b/debian/cubicweb-server.install.in Fri Aug 14 09:26:41 2009 +0200
@@ -1,4 +1,5 @@
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/ usr/lib/PY_VERSION/site-packages/cubicweb
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/ usr/lib/PY_VERSION/site-packages/cubicweb
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/ usr/lib/PY_VERSION/site-packages/cubicweb
debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/schemas/ usr/lib/PY_VERSION/site-packages/cubicweb
debian/tmp/usr/share/cubicweb/migration/ usr/share/cubicweb/
--- a/debian/rules Fri Aug 14 09:20:33 2009 +0200
+++ b/debian/rules Fri Aug 14 09:26:41 2009 +0200
@@ -48,6 +48,7 @@
# Remove unittests directory (should be available in cubicweb-dev only)
rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test
+ rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/hooks/test
rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test
rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test
rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/__init__.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,1 @@
+"""core hooks"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/integrity.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,228 @@
+"""Core hooks: check for data integrity according to the instance'schema
+validity
+
+: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"
+
+from cubicweb import ValidationError
+from cubicweb.selectors import entity_implements
+from cubicweb.server.hook import Hook
+from cubicweb.server.pool import LateOperation, PreCommitOperation
+from cubicweb.server.hookhelper import rproperty
+
+# 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'))
+
+
+class _CheckRequiredRelationOperation(LateOperation):
+ """checking relation cardinality has to be done after commit in
+ case the relation is being replaced
+ """
+ eid, rtype = None, None
+
+ def precommit_event(self):
+ # recheck pending eids
+ if self.eid in self.session.transaction_data.get('pendingeids', ()):
+ return
+ if self.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}
+ raise ValidationError(self.eid, {self.rtype: msg})
+
+ def commit_event(self):
+ pass
+
+ def _rql(self):
+ raise NotImplementedError()
+
+
+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'
+
+
+class CheckCardinalityHook(Hook):
+ """check cardinalities are satisfied"""
+ __id__ = 'checkcard'
+ category = 'integrity'
+ events = ('after_add_entity', 'before_delete_relation')
+
+ def __call__(self):
+ getattr(self, self.event)()
+
+ def checkrel_if_necessary(self, opcls, rtype, eid):
+ """check an equivalent operation has not already been added"""
+ for op in self.cw_req.pending_operations:
+ if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
+ break
+ else:
+ opcls(self.cw_req, rtype=rtype, eid=eid)
+
+ def after_add_entity(self):
+ eid = self.entity.eid
+ eschema = self.entity.e_schema
+ for rschema, targetschemas, x in eschema.relation_definitions():
+ # skip automatically handled relations
+ if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
+ continue
+ if x == 'subject':
+ subjtype = eschema
+ objtype = targetschemas[0].type
+ cardindex = 0
+ opcls = _CheckSRelationOp
+ else:
+ subjtype = targetschemas[0].type
+ objtype = eschema
+ cardindex = 1
+ opcls = _CheckORelationOp
+ card = rschema.rproperty(subjtype, objtype, 'cardinality')
+ if card[cardindex] in '1+':
+ self.checkrel_if_necessary(opcls, rschema.type, eid)
+
+ def before_delete_relation(self):
+ rtype = self.rtype
+ if rtype in DONT_CHECK_RTYPES_ON_DEL:
+ return
+ session = self.cw_req
+ eidfrom, eidto = self.eidfrom, self.eidto
+ card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
+ pendingrdefs = session.transaction_data.get('pendingrdefs', ())
+ if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
+ return
+ pendingeids = session.transaction_data.get('pendingeids', ())
+ if card[0] in '1+' and not eidfrom in pendingeids:
+ self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
+ if card[1] in '1+' and not eidto in pendingeids:
+ self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
+
+
+class _CheckConstraintsOp(LateOperation):
+ """check a new relation satisfy its constraints
+ """
+ def precommit_event(self):
+ eidfrom, rtype, eidto = self.rdef
+ # first check related entities have not been deleted in the same
+ # transaction
+ pending = self.session.transaction_data.get('pendingeids', ())
+ if eidfrom in pending:
+ return
+ if eidto in pending:
+ return
+ for constraint in self.constraints:
+ try:
+ constraint.repo_check(self.session, eidfrom, rtype, eidto)
+ except NotImplementedError:
+ self.critical('can\'t check constraint %s, not supported',
+ constraint)
+
+ def commit_event(self):
+ pass
+
+
+class CheckConstraintHook(Hook):
+ """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.
+ """
+ __id__ = 'checkconstraint'
+ category = 'integrity'
+ events = ('after_add_relation',)
+ def __call__(self):
+ constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
+ 'constraints')
+ if constraints:
+ _CheckConstraintsOp(self.cw_req, constraints=constraints,
+ rdef=(self.eidfrom, self.rtype, self.eidto))
+
+class CheckUniqueHook(Hook):
+ __id__ = 'checkunique'
+ category = 'integrity'
+ events = ('before_add_entity', 'before_update_entity')
+
+ def __call__(self):
+ entity = self.entity
+ eschema = entity.e_schema
+ for attr in entity.edited_attributes:
+ val = entity[attr]
+ if val is None:
+ continue
+ if eschema.subject_relation(attr).is_final() and \
+ eschema.has_unique_values(attr):
+ rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
+ rset = self.cw_req.unsafe_execute(rql, {'val': val})
+ if rset and rset[0][0] != entity.eid:
+ msg = self.cw_req._('the value "%s" is already used, use another one')
+ raise ValidationError(entity.eid, {attr: msg % val})
+
+
+class _DelayedDeleteOp(PreCommitOperation):
+ """delete the object of composite relation except if the relation
+ has actually been redirected to another composite
+ """
+
+ def precommit_event(self):
+ session = self.session
+ # don't do anything if the entity is being created or deleted
+ if not (self.eid in session.transaction_data.get('pendingeids', ()) or
+ self.eid in session.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')
+
+
+class DeleteCompositeOrphanHook(Hook):
+ """delete the composed of a composite relation when this relation is deleted
+ """
+ __id__ = 'deletecomposite'
+ category = 'integrity'
+ events = ('before_delete_relation',)
+ def __call__(self):
+ composite = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
+ 'composite')
+ if composite == 'subject':
+ _DelayedDeleteOp(self.cw_req, eid=self.eidto,
+ relation='Y %s X' % self.rtype)
+ elif composite == 'object':
+ _DelayedDeleteOp(self.cw_req, eid=self.eidfrom,
+ relation='X %s Y' % self.rtype)
+
+
+class DontRemoveOwnersGroupHook(Hook):
+ """delete the composed of a composite relation when this relation is deleted
+ """
+ __id__ = 'checkownersgroup'
+ __select__ = Hook.__select__ & entity_implements('CWGroup')
+ category = 'integrity'
+ events = ('before_delete_entity', 'before_update_entity')
+
+ def __call__(self):
+ if self.event == 'before_delete_entity' and self.entity.name == 'owners':
+ raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
+ elif self.event == 'before_update_entity' and 'name' in self.entity.edited_attribute:
+ newname = self.entity.pop('name')
+ oldname = self.entity.name
+ if oldname == 'owners' and newname != oldname:
+ raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')})
+ self.entity['name'] = newname
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/metadata.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,159 @@
+"""Core hooks: set generic metadata
+
+: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"
+
+
+from datetime import datetime
+
+from cubicweb.selectors import entity_implements
+from cubicweb.server.hook import Hook
+from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
+from cubicweb.server.hookhelper import rproperty
+from cubicweb.server.repository import FTIndexEntityOp
+
+
+def eschema_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)
+ if eschema.eid is None:
+ eschema.eid = session.unsafe_execute(
+ 'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0]
+ return eschema.eid
+
+
+class InitMetaAttrsHook(Hook):
+ """before create a new entity -> set creation and modification date
+
+ this is a conveniency hook, you shouldn't have to disable it
+ """
+ id = 'metaattrsinit'
+ events = ('before_add_entity',)
+ category = 'metadata'
+
+ def __call__(self):
+ timestamp = datetime.now()
+ self.entity.setdefault('creation_date', timestamp)
+ self.entity.setdefault('modification_date', timestamp)
+ if not self.cw_req.get_shared_data('do-not-insert-cwuri'):
+ cwuri = u'%seid/%s' % (self.cw_req.base_url(), self.entity.eid)
+ self.entity.setdefault('cwuri', cwuri)
+
+
+class UpdateMetaAttrsHook(Hook):
+ """update an entity -> set modification date"""
+ id = 'metaattrsupdate'
+ events = ('before_update_entity',)
+ category = 'metadata'
+ def __call__(self):
+ self.entity.setdefault('modification_date', datetime.now())
+
+
+class _SetCreatorOp(PreCommitOperation):
+
+ def precommit_event(self):
+ session = self.session
+ if self.entity.eid in session.transaction_data.get('pendingeids', ()):
+ # entity have been created and deleted in the same transaction
+ return
+ if not self.entity.created_by:
+ session.add_relation(self.entity.eid, 'created_by', session.user.eid)
+
+
+class SetIsHook(Hook):
+ """create a new entity -> set is relation"""
+ id = 'setis'
+ events = ('after_add_entity',)
+ category = 'metadata'
+ def __call__(self):
+ if hasattr(self.entity, '_cw_recreating'):
+ return
+ session = self.cw_req
+ entity = self.entity
+ try:
+ session.add_relation(entity.eid, 'is',
+ eschema_type_eid(session, entity.id))
+ except IndexError:
+ # during schema serialization, skip
+ return
+ for etype in entity.e_schema.ancestors() + [entity.e_schema]:
+ session.add_relation(entity.eid, 'is_instance_of',
+ eschema_type_eid(session, etype))
+
+
+class SetOwnershipHook(Hook):
+ """create a new entity -> set owner and creator metadata"""
+ id = 'setowner'
+ events = ('after_add_entity',)
+ category = 'metadata'
+ def __call__(self):
+ asession = self.cw_req.actual_session()
+ if not asession.is_internal_session:
+ self.cw_req.add_relation(self.entity.eid, 'owned_by', asession.user.eid)
+ _SetCreatorOp(asession, entity=self.entity)
+
+
+class _SyncOwnersOp(PreCommitOperation):
+ def precommit_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'))
+
+class SyncCompositeOwner(Hook):
+ """when adding composite relation, the composed should have the same owners
+ has the composite
+ """
+ id = 'synccompositeowner'
+ events = ('after_add_relation',)
+ category = 'metadata'
+ def __call__(self):
+ if self.rtype == 'wf_info_for':
+ # skip this special composite relation # XXX (syt) why?
+ return
+ eidfrom, eidto = self.eidfrom, self.eidto
+ composite = rproperty(self.cw_req, self.rtype, eidfrom, eidto, 'composite')
+ if composite == 'subject':
+ _SyncOwnersOp(self.cw_req, compositeeid=eidfrom, composedeid=eidto)
+ elif composite == 'object':
+ _SyncOwnersOp(self.cw_req, compositeeid=eidto, composedeid=eidfrom)
+
+
+class FixUserOwnershipHook(Hook):
+ """when a user has been created, add owned_by relation on itself"""
+ id = 'fixuserowner'
+ __select__ = Hook.__select__ & entity_implements('CWUser')
+ events = ('after_add_entity',)
+ category = 'metadata'
+ def __call__(self):
+ self.cw_req.add_relation(self.entity.eid, 'owned_by', self.entity.eid)
+
+
+class UpdateFTIHook(Hook):
+ """sync fulltext index when relevant relation is added / removed
+ """
+ id = 'updateftirel'
+ events = ('after_add_relation', 'after_delete_relation')
+ category = 'metadata'
+
+ def __call__(self):
+ rtype = self.rtype
+ session = self.cw_req
+ if self.event == 'after_add_relation':
+ # Reindexing the contained entity is enough since it will implicitly
+ # reindex the container entity.
+ ftcontainer = session.vreg.schema.rschema(rtype).fulltext_container
+ if ftcontainer == 'subject':
+ FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidto))
+ elif ftcontainer == 'object':
+ FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidfrom))
+ elif session.repo.schema.rschema(rtype).fulltext_container:
+ FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidto))
+ FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidfrom))
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/security.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,121 @@
+"""Security hooks: check permissions to add/delete/update entities according to
+the user connected to a session
+
+: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"
+
+from cubicweb import Unauthorized
+from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
+
+
+def check_entity_attributes(session, entity):
+ eid = entity.eid
+ eschema = entity.e_schema
+ # ._default_set is only there on entity creation to indicate unspecified
+ # attributes which has been set to a default value defined in the schema
+ defaults = getattr(entity, '_default_set', ())
+ try:
+ editedattrs = entity.edited_attributes
+ except AttributeError:
+ editedattrs = entity
+ for attr in editedattrs:
+ if attr in defaults:
+ continue
+ rschema = eschema.subject_relation(attr)
+ if rschema.is_final(): # non final relation are checked by other hooks
+ # add/delete should be equivalent (XXX: unify them into 'update' ?)
+ rschema.check_perm(session, 'add', eid)
+
+
+class _CheckEntityPermissionOp(hook.LateOperation):
+ def precommit_event(self):
+ #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
+ self.entity.check_perm(self.action)
+ check_entity_attributes(self.session, self.entity)
+
+ def commit_event(self):
+ pass
+
+
+class _CheckRelationPermissionOp(hook.LateOperation):
+ def precommit_event(self):
+ self.rschema.check_perm(self.session, self.action, self.eidfrom, self.eidto)
+
+ def commit_event(self):
+ pass
+
+
+class SecurityHook(hook.Hook):
+ __abstract__ = True
+ category = 'security'
+ __select__ = hook.Hook.__select__ & hook.regular_session()
+
+
+class AfterAddEntitySecurityHook(SecurityHook):
+ __id__ = 'securityafteraddentity'
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ _CheckEntityPermissionOp(self.cw_req, entity=self.entity, action='add')
+
+
+class AfterUpdateEntitySecurityHook(SecurityHook):
+ __id__ = 'securityafterupdateentity'
+ events = ('after_update_entity',)
+
+ def __call__(self):
+ try:
+ # check user has permission right now, if not retry at commit time
+ self.entity.check_perm('update')
+ check_entity_attributes(self.cw_req, self.entity)
+ except Unauthorized:
+ self.entity.clear_local_perm_cache('update')
+ _CheckEntityPermissionOp(self.cw_req, entity=self.entity, action='update')
+
+
+class BeforeDelEntitySecurityHook(SecurityHook):
+ __id__ = 'securitybeforedelentity'
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ self.entity.e_schema.check_perm(self.cw_req, 'delete', eid)
+
+
+class BeforeAddRelationSecurityHook(SecurityHook):
+ __id__ = 'securitybeforeaddrelation'
+ events = ('before_add_relation',)
+
+ def __call__(self):
+ if self.rtype in BEFORE_ADD_RELATIONS:
+ rschema = self.cw_req.repo.schema[self.rtype]
+ rschema.check_perm(self.cw_req, 'add', self.eidfrom, self.eidto)
+
+
+class AfterAddRelationSecurityHook(SecurityHook):
+ __id__ = 'securityafteraddrelation'
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ if not self.rtype in BEFORE_ADD_RELATIONS:
+ rschema = self.cw_req.repo.schema[self.rtype]
+ if self.rtype in ON_COMMIT_ADD_RELATIONS:
+ _CheckRelationPermissionOp(self.cw_req, action='add',
+ rschema=rschema,
+ eidfrom=self.eidfrom,
+ eidto=self.eidto)
+ else:
+ rschema.check_perm(self.cw_req, 'add', self.eidfrom, self.eidto)
+
+
+class BeforeDelRelationSecurityHook(SecurityHook):
+ __id__ = 'securitybeforedelrelation'
+ events = ('before_delete_relation',)
+
+ def __call__(self):
+ self.cw_req.repo.schema[self.rtype].check_perm(self.cw_req, 'delete',
+ self.eidfrom, self.eidto)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/syncschema.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,1100 @@
+"""schema hooks:
+
+- synchronize the living schema object with the persistent schema
+- perform physical update on the source when necessary
+
+checking for schema consistency is done in hooks.py
+
+: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"
+
+from yams.schema import BASE_TYPES
+from yams.buildobjs import EntityType, RelationType, RelationDefinition
+from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
+
+from cubicweb import ValidationError, RepositoryError
+from cubicweb.selectors import entity_implements
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
+from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server.sqlutils import SQL_PREFIX
+
+
+TYPE_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 removed
+CORE_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',
+ ]
+
+def get_constraints(session, entity):
+ constraints = []
+ for cstreid in session.transaction_data.get(entity.eid, ()):
+ cstrent = session.entity_from_eid(cstreid)
+ cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
+ cstr.eid = cstreid
+ constraints.append(cstr)
+ return constraints
+
+def add_inline_relation_column(session, etype, rtype):
+ """add necessary column and index for an inlined relation"""
+ table = SQL_PREFIX + etype
+ column = SQL_PREFIX + rtype
+ try:
+ 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 anyway
+ session.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))
+
+def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
+ errors = {}
+ # don't use getattr(entity, attr), we would get the modified value if any
+ for attr in entity.edited_attributes:
+ if attr in ro_attrs:
+ newval = entity.pop(attr)
+ origval = getattr(entity, attr)
+ if newval != origval:
+ errors[attr] = session._("can't change the %s attribute") % \
+ display_name(session, attr)
+ entity[attr] = newval
+ if errors:
+ raise ValidationError(entity.eid, errors)
+
+
+# operations for low-level database alteration ################################
+
+class DropTable(hook.Operation):
+ """actually remove a database from the instance's schema"""
+ table = None # make pylint happy
+ def precommit_event(self):
+ dropped = self.session.transaction_data.setdefault('droppedtables',
+ set())
+ if self.table in dropped:
+ return # already processed
+ dropped.add(self.table)
+ self.session.system_sql('DROP TABLE %s' % self.table)
+ self.info('dropped table %s', self.table)
+
+
+class DropRelationTable(DropTable):
+ def __init__(self, session, rtype):
+ super(DropRelationTable, self).__init__(
+ session, table='%s_relation' % rtype)
+ session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
+
+
+class DropColumn(hook.Operation):
+ """actually remove the attribut's column from entity table in the system
+ database
+ """
+ table = column = None # make pylint happy
+ def precommit_event(self):
+ session, table, column = self.session, self.table, self.column
+ # drop index if any
+ session.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)
+ except Exception, ex:
+ # not supported by sqlite for instance
+ self.error('error while altering table %s: %s', table, ex)
+
+
+# base operations for in-memory schema synchronization ########################
+
+class MemSchemaNotifyChanges(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):
+ self.repo = session.repo
+ hook.SingleLastOperation.__init__(self, session)
+
+ def commit_event(self):
+ self.repo.set_schema(self.repo.schema)
+
+
+class MemSchemaOperation(hook.Operation):
+ """base class for schema operations"""
+ def __init__(self, session, kobj=None, **kwargs):
+ self.schema = session.schema
+ 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 update
+ MemSchemaNotifyChanges(session)
+
+ def prepare_constraints(self, subjtype, rtype, objtype):
+ constraints = rtype.rproperty(subjtype, objtype, 'constraints')
+ self.constraints = list(constraints)
+ rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
+
+
+class MemSchemaEarlyOperation(MemSchemaOperation):
+ def insert_index(self):
+ """schema operation which are inserted at the begining of the queue
+ (typically to add/remove entity or relation types)
+ """
+ i = -1
+ for i, op in enumerate(self.session.pending_operations):
+ if not isinstance(op, MemSchemaEarlyOperation):
+ return i
+ return i + 1
+
+
+class MemSchemaPermOperation(MemSchemaOperation):
+ """base class to synchronize schema permission definitions"""
+ def __init__(self, session, perm, etype_eid):
+ self.perm = perm
+ try:
+ self.name = session.entity_from_eid(etype_eid).name
+ except IndexError:
+ self.error('changing permission of a no more existant type #%s',
+ etype_eid)
+ else:
+ hook.Operation.__init__(self, session)
+
+
+# operations for high-level source database alteration ########################
+
+class SourceDbCWETypeRename(hook.Operation):
+ """this operation updates physical storage accordingly"""
+ oldname = newname = None # make pylint happy
+
+ def precommit_event(self):
+ # we need sql to operate physical changes on the system database
+ sqlexec = self.session.system_sql
+ sqlexec('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))
+
+
+class SourceDbCWRTypeUpdate(hook.Operation):
+ """actually update some properties of a relation definition"""
+ rschema = values = entity = None # make pylint happy
+
+ def precommit_event(self):
+ session = self.session
+ rschema = self.rschema
+ if rschema.is_final() or not 'inlined' in self.values:
+ return # nothing to do
+ inlined = self.values['inlined']
+ entity = self.entity
+ # check in-lining is necessary / possible
+ if not entity.inlined_changed(inlined):
+ return # nothing to do
+ # inlined changed, make necessary physical changes!
+ sqlexec = self.session.system_sql
+ rtype = rschema.type
+ eidcolumn = SQL_PREFIX + 'eid'
+ if not inlined:
+ # need to create the relation if it has not been already done by
+ # another event of the same transaction
+ if not rschema.type in session.transaction_data.get('createdtables', ()):
+ tablesql = rschema2sql(rschema)
+ # create the necessary table
+ for sql in tablesql.split(';'):
+ if sql.strip():
+ sqlexec(sql)
+ session.transaction_data.setdefault('createdtables', []).append(
+ rschema.type)
+ # copy existant data
+ column = SQL_PREFIX + rtype
+ for etype in rschema.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 columns
+ for etype in rschema.subjects():
+ DropColumn(session, table=SQL_PREFIX + str(etype),
+ column=SQL_PREFIX + rtype)
+ else:
+ for etype in rschema.subjects():
+ try:
+ add_inline_relation_column(session, str(etype), rtype)
+ except Exception, 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 dropped
+ self.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} for eid, eid_to in cursor.fetchall()]
+ if args:
+ column = SQL_PREFIX + rtype
+ cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
+ % (table, column, eidcolumn), args)
+ # drop existant table
+ DropRelationTable(session, rtype)
+
+
+class SourceDbCWAttributeAdd(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 happy
+
+ def init_rdef(self, **kwargs):
+ entity = self.entity
+ fromentity = entity.stype
+ self.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.ordernum or 0})
+ subj = str(fromentity.name)
+ rtype = entity.rtype.name
+ obj = 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)
+ return rdef
+
+ def precommit_event(self):
+ session = self.session
+ entity = self.entity
+ # entity.defaultval is a string or None, but we need a correctly typed
+ # value
+ default = entity.defaultval
+ if default is not None:
+ default = TYPE_CONVERTER[entity.otype.name](default)
+ rdef = self.init_rdef(default=default,
+ indexed=entity.indexed,
+ fulltextindexed=entity.fulltextindexed,
+ internationalizable=entity.internationalizable)
+ 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 INDEX
+ if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+ extra_unique_index = True
+ attrtype = attrtype.replace(' UNIQUE', '')
+ else:
+ extra_unique_index = False
+ # added some str() wrapping query since some backend (eg psycopg) don't
+ # allow unicode queries
+ table = SQL_PREFIX + rdef.subject
+ column = SQL_PREFIX + rdef.name
+ try:
+ 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)
+ except Exception, 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 dropped
+ self.error('error while altering table %s: %s', table, ex)
+ if extra_unique_index or entity.indexed:
+ try:
+ sysource.create_index(session, table, column,
+ unique=extra_unique_index)
+ except Exception, ex:
+ self.error('error while creating index for %s.%s: %s',
+ table, column, ex)
+
+
+class SourceDbCWRelationAdd(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 happy
+
+ def precommit_event(self):
+ session = self.session
+ entity = self.entity
+ rdef = self.init_rdef(composite=entity.composite)
+ schema = session.schema
+ rtype = rdef.name
+ rschema = session.schema.rschema(rtype)
+ # this have to be done before permissions setting
+ if rschema.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
+ # transaction
+ key = '%s.%s' % (rdef.subject, rtype)
+ try:
+ alreadythere = bool(rschema.objects(rdef.subject))
+ except KeyError:
+ alreadythere = False
+ if not (alreadythere or
+ key in session.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
+ # transaction
+ if not (rschema.subjects() or
+ rtype in session.transaction_data.get('createdtables', ())):
+ try:
+ rschema = session.schema.rschema(rtype)
+ tablesql = rschema2sql(rschema)
+ except KeyError:
+ # fake we add it to the schema now to get a correctly
+ # initialized schema but remove it before doing anything
+ # more dangerous...
+ rschema = session.schema.add_relation_type(rdef)
+ tablesql = rschema2sql(rschema)
+ session.schema.del_relation_type(rtype)
+ # create the necessary table
+ for sql in tablesql.split(';'):
+ if sql.strip():
+ session.system_sql(sql)
+ session.transaction_data.setdefault('createdtables', []).append(
+ rtype)
+
+
+class SourceDbRDefUpdate(hook.Operation):
+ """actually update some properties of a relation definition"""
+ rschema = values = None # make pylint happy
+
+ def precommit_event(self):
+ etype = self.kobj[0]
+ table = SQL_PREFIX + etype
+ column = SQL_PREFIX + self.rschema.type
+ if 'indexed' in self.values:
+ sysource = self.session.pool.source('system')
+ if self.values['indexed']:
+ sysource.create_index(self.session, table, column)
+ else:
+ sysource.drop_index(self.session, table, column)
+ if 'cardinality' in self.values and self.rschema.is_final():
+ adbh = self.session.pool.source('system').dbhelper
+ if not adbh.alter_column_support:
+ # not supported (and NOT NULL not set by yams in that case, so
+ # no worry)
+ return
+ atype = self.rschema.objects(etype)[0]
+ constraints = self.rschema.rproperty(etype, atype, 'constraints')
+ coltype = 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)
+
+
+class SourceDbCWConstraintAdd(hook.Operation):
+ """actually update constraint of a relation definition"""
+ entity = None # make pylint happy
+ cancelled = False
+
+ def precommit_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 here
+ if rdef.eid in session.transaction_data.get('neweids', ()):
+ return
+ subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
+ cstrtype = self.entity.type
+ oldcstr = rtype.constraint_by_type(subjtype, objtype, 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 changes
+ if newcstr.type() == 'SizeConstraint' and (
+ oldcstr is None or oldcstr.max != newcstr.max):
+ adbh = self.session.pool.source('system').dbhelper
+ card = rtype.rproperty(subjtype, objtype, 'cardinality')
+ coltype = 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)
+ except Exception, ex:
+ # not supported by sqlite for instance
+ self.error('error while altering table %s: %s', table, ex)
+ elif cstrtype == 'UniqueConstraint' and oldcstr is None:
+ session.pool.source('system').create_index(
+ self.session, table, column, unique=True)
+
+
+class SourceDbCWConstraintDel(hook.Operation):
+ """actually remove a constraint of a relation definition"""
+ rtype = subjtype = objtype = None # make pylint happy
+
+ def precommit_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 changes
+ if cstrtype == '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)
+ except Exception, ex:
+ # not supported by sqlite for instance
+ self.error('error while altering table %s: %s', table, ex)
+ elif cstrtype == 'UniqueConstraint':
+ self.session.pool.source('system').drop_index(
+ self.session, table, column, unique=True)
+
+
+# operations for in-memory schema synchronization #############################
+
+class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
+ """actually add the entity type to the instance's schema"""
+ eid = None # make pylint happy
+ def commit_event(self):
+ self.schema.add_entity_type(self.kobj)
+
+
+class MemSchemaCWETypeRename(MemSchemaOperation):
+ """this operation updates physical storage accordingly"""
+ oldname = newname = None # make pylint happy
+
+ def commit_event(self):
+ self.session.schema.rename_entity_type(self.oldname, self.newname)
+
+
+class MemSchemaCWETypeDel(MemSchemaOperation):
+ """actually remove the entity type from the instance's schema"""
+ def commit_event(self):
+ try:
+ # del_entity_type also removes entity's relations
+ self.schema.del_entity_type(self.kobj)
+ except KeyError:
+ # s/o entity type have already been deleted
+ pass
+
+
+class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
+ """actually add the relation type to the instance's schema"""
+ eid = None # make pylint happy
+ def commit_event(self):
+ rschema = self.schema.add_relation_type(self.kobj)
+ rschema.set_default_groups()
+
+
+class MemSchemaCWRTypeUpdate(MemSchemaOperation):
+ """actually update some properties of a relation definition"""
+ rschema = values = None # make pylint happy
+
+ def commit_event(self):
+ # structure should be clean, not need to remove entity's relations
+ # at this point
+ self.rschema.__dict__.update(self.values)
+
+
+class MemSchemaCWRTypeDel(MemSchemaOperation):
+ """actually remove the relation type from the instance's schema"""
+ def commit_event(self):
+ try:
+ self.schema.del_relation_type(self.kobj)
+ except KeyError:
+ # s/o entity type have already been deleted
+ pass
+
+
+class MemSchemaRDefAdd(MemSchemaEarlyOperation):
+ """actually add the attribute relation definition to the instance's
+ schema
+ """
+ def commit_event(self):
+ self.schema.add_relation_def(self.kobj)
+
+
+class MemSchemaRDefUpdate(MemSchemaOperation):
+ """actually update some properties of a relation definition"""
+ rschema = values = None # make pylint happy
+
+ def commit_event(self):
+ # structure should be clean, not need to remove entity's relations
+ # at this point
+ self.rschema._rproperties[self.kobj].update(self.values)
+
+
+class MemSchemaRDefDel(MemSchemaOperation):
+ """actually remove the relation definition from the instance's schema"""
+ def commit_event(self):
+ subjtype, rtype, objtype = self.kobj
+ try:
+ self.schema.del_relation_def(subjtype, rtype, objtype)
+ except KeyError:
+ # relation type may have been already deleted
+ pass
+
+
+class MemSchemaCWConstraintAdd(MemSchemaOperation):
+ """actually update constraint of a relation definition
+
+ has to be called before SourceDbCWConstraintAdd
+ """
+ cancelled = False
+
+ def precommit_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 here
+ if rdef.eid in self.session.transaction_data.get('neweids', ()):
+ self.cancelled = True
+ return
+ subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
+ self.prepare_constraints(subjtype, rtype, objtype)
+ cstrtype = self.entity.type
+ self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+ self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+ self.newcstr.eid = self.entity.eid
+
+ def commit_event(self):
+ if self.cancelled:
+ return
+ # in-place modification
+ if not self.cstr is None:
+ self.constraints.remove(self.cstr)
+ self.constraints.append(self.newcstr)
+
+
+class MemSchemaCWConstraintDel(MemSchemaOperation):
+ """actually remove a constraint of a relation definition
+
+ has to be called before SourceDbCWConstraintDel
+ """
+ rtype = subjtype = objtype = None # make pylint happy
+ def precommit_event(self):
+ self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
+
+ def commit_event(self):
+ self.constraints.remove(self.cstr)
+
+
+class MemSchemaPermCWGroupAdd(MemSchemaPermOperation):
+ """synchronize schema when a *_permission relation has been added on a group
+ """
+ def __init__(self, session, perm, etype_eid, group_eid):
+ self.group = session.entity_from_eid(group_eid).name
+ super(MemSchemaPermCWGroupAdd, self).__init__(
+ session, perm, etype_eid)
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ try:
+ erschema = self.schema[self.name]
+ except KeyError:
+ # duh, schema not found, log error and skip operation
+ self.error('no schema for %s', self.name)
+ return
+ groups = list(erschema.get_groups(self.perm))
+ try:
+ groups.index(self.group)
+ self.warning('group %s already have permission %s on %s',
+ self.group, self.perm, erschema.type)
+ except ValueError:
+ groups.append(self.group)
+ erschema.set_groups(self.perm, groups)
+
+
+class MemSchemaPermCWGroupDel(MemSchemaPermCWGroupAdd):
+ """synchronize schema when a *_permission relation has been deleted from a
+ group
+ """
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ try:
+ erschema = self.schema[self.name]
+ except KeyError:
+ # duh, schema not found, log error and skip operation
+ self.error('no schema for %s', self.name)
+ return
+ groups = list(erschema.get_groups(self.perm))
+ try:
+ groups.remove(self.group)
+ erschema.set_groups(self.perm, groups)
+ except ValueError:
+ self.error('can\'t remove permission %s on %s to group %s',
+ self.perm, erschema.type, self.group)
+
+
+class MemSchemaPermRQLExpressionAdd(MemSchemaPermOperation):
+ """synchronize schema when a *_permission relation has been added on a rql
+ expression
+ """
+ def __init__(self, session, perm, etype_eid, expression):
+ self.expr = expression
+ super(MemSchemaPermRQLExpressionAdd, self).__init__(
+ session, perm, etype_eid)
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ try:
+ erschema = self.schema[self.name]
+ except KeyError:
+ # duh, schema not found, log error and skip operation
+ self.error('no schema for %s', self.name)
+ return
+ exprs = list(erschema.get_rqlexprs(self.perm))
+ exprs.append(erschema.rql_expression(self.expr))
+ erschema.set_rqlexprs(self.perm, exprs)
+
+
+class MemSchemaPermRQLExpressionDel(MemSchemaPermRQLExpressionAdd):
+ """synchronize schema when a *_permission relation has been deleted from an
+ rql expression
+ """
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ try:
+ erschema = self.schema[self.name]
+ except KeyError:
+ # duh, schema not found, log error and skip operation
+ self.error('no schema for %s', self.name)
+ return
+ rqlexprs = list(erschema.get_rqlexprs(self.perm))
+ for i, rqlexpr in enumerate(rqlexprs):
+ if rqlexpr.expression == self.expr:
+ rqlexprs.pop(i)
+ break
+ else:
+ self.error('can\'t remove permission %s on %s for expression %s',
+ self.perm, erschema.type, self.expr)
+ return
+ erschema.set_rqlexprs(self.perm, rqlexprs)
+
+
+# deletion hooks ###############################################################
+
+class DelCWETypeHook(hook.Hook):
+ """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
+ """
+ __id__ = 'syncdelcwetype'
+ __select__ = hook.Hook.__select__ & entity_implements('CWEType')
+ category = 'syncschema'
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ # final entities can't be deleted, don't care about that
+ name = self.entity.name
+ if name in CORE_ETYPES:
+ raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
+ # delete every entities of this type
+ self.cw_req.unsafe_execute('DELETE %s X' % name)
+ DropTable(self.cw_req, table=SQL_PREFIX + name)
+ MemSchemaCWETypeDel(self.cw_req, name)
+
+
+class AfterDelCWETypeHook(DelCWETypeHook):
+ __id__ = 'wfcleanup'
+ events = ('after_delete_entity',)
+
+ def __call__(self):
+ # workflow cleanup
+ self.cw_req.execute('DELETE State X WHERE NOT X state_of Y')
+ self.cw_req.execute('DELETE Transition X WHERE NOT X transition_of Y')
+
+
+class AfterAddCWETypeHook(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
+ """
+ __id__ = 'syncaddcwetype'
+ events = ('before_add_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ if entity.get('final'):
+ return
+ schema = self.cw_req.schema
+ name = 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_req.schema
+ eschema = schema.add_entity_type(etype)
+ eschema.set_default_groups()
+ # generate table sql and rql to add metadata
+ tablesql = eschema2sql(self.cw_req.pool.source('system').dbhelper, eschema,
+ prefix=SQL_PREFIX)
+ relrqls = []
+ for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+ rschema = schema[rtype]
+ sampletype = rschema.subjects()[0]
+ desttype = rschema.objects()[0]
+ props = rschema.rproperties(sampletype, desttype)
+ relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
+ # now remove it !
+ schema.del_entity_type(name)
+ # create the necessary table
+ for sql in tablesql.split(';'):
+ if sql.strip():
+ self.cw_req.system_sql(sql)
+ # register operation to modify the schema on commit
+ # this have to be done before adding other relations definitions
+ # or permission settings
+ etype.eid = entity.eid
+ MemSchemaCWETypeAdd(self.cw_req, etype)
+ # add meta relations
+ for rql, kwargs in relrqls:
+ self.cw_req.execute(rql, kwargs)
+
+
+class BeforeUpdateCWETypeHook(DelCWETypeHook):
+ """check name change, handle final"""
+ __id__ = 'syncupdatecwetype'
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ check_valid_changes(self.cw_req, entity, ro_attrs=('final',))
+ # don't use getattr(entity, attr), we would get the modified value if any
+ if 'name' in entity.edited_attributes:
+ newname = entity.pop('name')
+ oldname = entity.name
+ if newname.lower() != oldname.lower():
+ SourceDbCWETypeRename(self.cw_req, oldname=oldname, newname=newname)
+ MemSchemaCWETypeRename(self.cw_req, oldname=oldname, newname=newname)
+ entity['name'] = newname
+
+class DelCWRTypeHook(hook.Hook):
+ """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
+ """
+ __id__ = 'syncdelcwrtype'
+ __select__ = hook.Hook.__select__ & entity_implements('CWRType')
+ category = 'syncschema'
+ events = ('before_delete_entity',)
+ def __call__(self):
+ name = self.entity.name
+ if name in CORE_ETYPES:
+ raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
+ # delete relation definitions using this relation type
+ self.cw_req.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
+ {'x': self.entity.eid})
+ self.cw_req.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
+ {'x': self.entity.eid})
+ MemSchemaCWRTypeDel(self.cw_req, name)
+
+
+class AfterAddCWRTypeHook(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
+ """
+ __id__ = 'syncaddcwrtype'
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ rtype = 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_req, rtype)
+
+
+class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
+ """check name change, handle final"""
+ __id__ = 'checkupdatecwrtype'
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ check_valid_changes(self.cw_req, self.entity)
+
+
+class AfterUpdateCWRTypeHook(DelCWRTypeHook):
+ __id__ = 'syncupdatecwrtype'
+ events = ('after_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ rschema = self.cw_req.schema.rschema(entity.name)
+ newvalues = {}
+ for prop in ('meta', 'symetric', 'inlined'):
+ if prop in entity:
+ newvalues[prop] = entity[prop]
+ if newvalues:
+ MemSchemaCWRTypeUpdate(self.cw_req, rschema=rschema, values=newvalues)
+ SourceDbCWRTypeUpdate(self.cw_req, rschema=rschema, values=newvalues,
+ entity=entity)
+
+
+
+class AfterDelRelationTypeHook(hook.Hook):
+ """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
+ """
+ __id__ = 'syncdelrelationtype'
+ __select__ = hook.Hook.__select__ & hook.match_rtype('relation_type')
+ category = 'syncschema'
+ events = ('after_delete_relation',)
+
+ def __call__(self):
+ session = self.cw_req
+ subjschema, rschema, objschema = session.schema.schema_by_eid(self.eidfrom)
+ pendings = session.transaction_data.get('pendingeids', ())
+ # first delete existing relation if necessary
+ if rschema.is_final():
+ rdeftype = 'CWAttribute'
+ else:
+ rdeftype = 'CWRelation'
+ if not (subjschema.eid in pendings or objschema.eid in pendings):
+ pending = session.transaction_data.setdefault('pendingrdefs', set())
+ pending.add((subjschema, rschema, objschema))
+ session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
+ % (rschema, subjschema, objschema))
+ execute = session.unsafe_execute
+ rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+ 'R eid %%(x)s' % rdeftype, {'x': rteid})
+ 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 relations
+
+ if (rschema.is_final() or rschema.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': rteid, 'name': str(subjschema)})
+ if rset[0][0] == 0 and not subjschema.eid in pendings:
+ ptypes = session.transaction_data.setdefault('pendingrtypes', set())
+ ptypes.add(rschema.type)
+ DropColumn(session, table=SQL_PREFIX + subjschema.type,
+ column=SQL_PREFIX + rschema.type)
+ elif lastrel:
+ DropRelationTable(session, rschema.type)
+ # if this is the last instance, drop associated relation type
+ if lastrel and not rteid in pendings:
+ execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
+ MemSchemaRDefDel(session, (subjschema, rschema, objschema))
+
+
+class AfterAddCWAttributeHook(hook.Hook):
+ __id__ = 'syncaddcwattribute'
+ __select__ = hook.Hook.__select__ & entity_implements('CWAttribute')
+ category = 'syncschema'
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ SourceDbCWAttributeAdd(self.cw_req, entity=self.entity)
+
+
+class AfterAddCWRelationHook(AfterAddCWAttributeHook):
+ __id__ = 'syncaddcwrelation'
+ __select__ = hook.Hook.__select__ & entity_implements('CWRelation')
+
+ def __call__(self):
+ SourceDbCWRelationAdd(self.cw_req, entity=self.entity)
+
+
+class AfterUpdateCWRDefHook(hook.Hook):
+ __id__ = 'syncaddcwattribute'
+ __select__ = hook.Hook.__select__ & entity_implements('CWAttribute', 'CWRelation')
+ category = 'syncschema'
+ events = ('after_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ if entity.eid in self.cw_req.transaction_data.get('pendingeids', ()):
+ return
+ desttype = entity.otype.name
+ rschema = self.cw_req.schema[entity.rtype.name]
+ newvalues = {}
+ for prop in rschema.rproperty_defs(desttype):
+ if prop == 'constraints':
+ continue
+ if prop == 'order':
+ prop = 'ordernum'
+ if prop in entity.edited_attributes:
+ newvalues[prop] = entity[prop]
+ if newvalues:
+ subjtype = entity.stype.name
+ MemSchemaRDefUpdate(self.cw_req, kobj=(subjtype, desttype),
+ rschema=rschema, values=newvalues)
+ SourceDbRDefUpdate(self.cw_req, kobj=(subjtype, desttype),
+ rschema=rschema, values=newvalues)
+
+
+# constraints synchronization hooks ############################################
+
+class AfterAddCWConstraintHook(hook.Hook):
+ __id__ = 'syncaddcwconstraint'
+ __select__ = hook.Hook.__select__ & entity_implements('CWConstraint')
+ category = 'syncschema'
+ events = ('after_add_entity', 'after_update_entity')
+
+ def __call__(self):
+ MemSchemaCWConstraintAdd(self.cw_req, entity=self.entity)
+ SourceDbCWConstraintAdd(self.cw_req, entity=self.entity)
+
+
+class AfterAddConstrainedByHook(hook.Hook):
+ __id__ = 'syncdelconstrainedby'
+ __select__ = hook.Hook.__select__ & hook.match_rtype('constrainted_by')
+ category = 'syncschema'
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ if self.eidfrom in self.cw_req.transaction_data.get('neweids', ()):
+ self.cw_req.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
+
+
+class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):
+ __id__ = 'syncdelconstrainedby'
+ events = ('before_delete_relation',)
+
+ def __call__(self):
+ if self.eidfrom in self.cw_req.transaction_data.get('pendingeids', ()):
+ return
+ schema = self.cw_req.schema
+ entity = self.cw_req.entity_from_eid(self.eidto)
+ subjtype, rtype, objtype = schema.schema_by_eid(self.eidfrom)
+ try:
+ cstr = rtype.constraint_by_type(subjtype, objtype,
+ entity.cstrtype[0].name)
+ except IndexError:
+ self.cw_req.critical('constraint type no more accessible')
+ else:
+ SourceDbCWConstraintDel(self.cw_req, subjtype=subjtype, rtype=rtype,
+ objtype=objtype, cstr=cstr)
+ MemSchemaCWConstraintDel(self.cw_req, subjtype=subjtype, rtype=rtype,
+ objtype=objtype, cstr=cstr)
+
+
+# permissions synchronization hooks ############################################
+
+
+class AfterAddPermissionHook(hook.Hook):
+ """added entity/relation *_permission, need to update schema"""
+ __id__ = 'syncaddperm'
+ __select__ = hook.Hook.__select__ & hook.match_rtype(
+ 'read_permission', 'add_permission', 'delete_permission',
+ 'update_permission')
+ category = 'syncschema'
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ perm = self.rtype.split('_', 1)[0]
+ if self.cw_req.describe(self.eidto)[0] == 'CWGroup':
+ MemSchemaPermCWGroupAdd(self.cw_req, perm, self.eidfrom, self.eidto)
+ else: # RQLExpression
+ expr = self.cw_req.entity_from_eid(self.eidto).expression
+ MemSchemaPermRQLExpressionAdd(self.cw_req, perm, self.eidfrom, expr)
+
+
+class BeforeDelPermissionHook(AfterAddPermissionHook):
+ """delete entity/relation *_permission, need to update schema
+
+ skip the operation if the related type is being deleted
+ """
+ __id__ = 'syncdelperm'
+ events = ('before_delete_relation',)
+
+ def __call__(self):
+ if self.eidfrom in self.cw_req.transaction_data.get('pendingeids', ()):
+ return
+ perm = self.rtype.split('_', 1)[0]
+ if self.cw_req.describe(self.eidto)[0] == 'CWGroup':
+ MemSchemaPermCWGroupDel(self.cw_req, perm, self.eidfrom, self.eidto)
+ else: # RQLExpression
+ expr = self.cw_req.entity_from_eid(self.eidto).expression
+ MemSchemaPermRQLExpressionDel(self.cw_req, perm, self.eidfrom, expr)
+
+
+
+class ModifySpecializesHook(hook.Hook):
+ __id__ = 'syncspecializes'
+ __select__ = hook.Hook.__select__ & hook.match_rtype('specializes')
+ category = 'syncschema'
+ events = ('after_add_relation', 'after_delete_relation')
+
+ def __call__(self):
+ # registering a schema operation will trigger a call to
+ # repo.set_schema() on commit which will in turn rebuild
+ # infered relation definitions
+ MemSchemaNotifyChanges(self.cw_req)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/syncsession.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,229 @@
+"""Core hooks: synchronize living session on persistent data changes
+
+: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"
+
+from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+from cubicweb.selectors import entity_implements
+from cubicweb.server.hook import Hook, match_rtype
+from cubicweb.server.pool import Operation
+from cubicweb.server.hookhelper import get_user_sessions
+
+
+# user/groups synchronisation #################################################
+
+class _GroupOperation(Operation):
+ """base class for group operation"""
+ geid = None
+ def __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]
+
+
+class _DeleteGroupOp(_GroupOperation):
+ """synchronize user when a in_group relation has been deleted"""
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ groups = self.cnxuser.groups
+ try:
+ groups.remove(self.group)
+ except KeyError:
+ self.error('user %s not in group %s', self.cnxuser, self.group)
+ return
+
+
+class _AddGroupOp(_GroupOperation):
+ """synchronize user when a in_group relation has been added"""
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ groups = self.cnxuser.groups
+ if self.group in groups:
+ self.warning('user %s already in group %s', self.cnxuser,
+ self.group)
+ return
+ groups.add(self.group)
+
+
+class SyncInGroupHook(Hook):
+ __id__ = 'syncingroup'
+ __select__ = Hook.__select__ & match_rtype('in_group')
+ events = ('after_delete_relation', 'after_add_relation')
+ category = 'syncsession'
+
+ def __call__(self):
+ if self.event == 'after_delete_relation':
+ opcls = _DeleteGroupOp
+ else:
+ opcls = _AddGroupOp
+ for session in get_user_sessions(self.cw_req.repo, self.eidfrom):
+ opcls(self.cw_req, cnxuser=session.user, geid=self.eidto)
+
+
+class _DelUserOp(Operation):
+ """close associated user's session when it is deleted"""
+ def __init__(self, session, cnxid):
+ self.cnxid = cnxid
+ Operation.__init__(self, session)
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ try:
+ self.repo.close(self.cnxid)
+ except BadConnectionId:
+ pass # already closed
+
+
+class CloseDeletedUserSessionsHook(Hook):
+ __id__ = 'closession'
+ __select__ = Hook.__select__ & entity_implements('CWUser')
+ events = ('after_delete_entity',)
+ category = 'syncsession'
+
+ def __call__(self):
+ """modify user permission, need to update users"""
+ for session in get_user_sessions(self.cw_req.repo, self.entity.eid):
+ _DelUserOp(self.cw_req, session.id)
+
+
+# CWProperty hooks #############################################################
+
+
+class _DelCWPropertyOp(Operation):
+ """a user's custom properties has been deleted"""
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ try:
+ del self.epropdict[self.key]
+ except KeyError:
+ self.error('%s has no associated value', self.key)
+
+
+class _ChangeCWPropertyOp(Operation):
+ """a user's custom properties has been added/changed"""
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ self.epropdict[self.key] = self.value
+
+
+class _AddCWPropertyOp(Operation):
+ """a user's custom properties has been added/changed"""
+
+ def commit_event(self):
+ """the observed connections pool has been commited"""
+ eprop = self.eprop
+ if not eprop.for_user:
+ self.repo.vreg.eprop_values[eprop.pkey] = eprop.value
+ # if for_user is set, update is handled by a ChangeCWPropertyOp operation
+
+
+class AddCWPropertyHook(Hook):
+ __id__ = 'addcwprop'
+ __select__ = Hook.__select__ & entity_implements('CWProperty')
+ category = 'syncsession'
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ key, value = self.entity.pkey, self.entity.value
+ session = self.cw_req
+ try:
+ value = session.vreg.typed_value(key, value)
+ except UnknownProperty:
+ raise ValidationError(self.entity.eid,
+ {'pkey': session._('unknown property key')})
+ except ValueError, ex:
+ raise ValidationError(self.entity.eid,
+ {'value': session._(str(ex))})
+ if not session.user.matching_groups('managers'):
+ session.add_relation(entity.eid, 'for_user', session.user.eid)
+ else:
+ _AddCWPropertyOp(session, eprop=entity)
+
+
+class UpdateCWPropertyHook(AddCWPropertyHook):
+ __id__ = 'updatecwprop'
+ events = ('after_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ if not ('pkey' in entity.edited_attributes or
+ 'value' in entity.edited_attributes):
+ return
+ key, value = entity.pkey, entity.value
+ session = self.cw_req
+ try:
+ value = session.vreg.typed_value(key, value)
+ except UnknownProperty:
+ return
+ except ValueError, ex:
+ raise ValidationError(entity.eid, {'value': session._(str(ex))})
+ if entity.for_user:
+ for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
+ _ChangeCWPropertyOp(session, epropdict=session_.user.properties,
+ key=key, value=value)
+ else:
+ # site wide properties
+ _ChangeCWPropertyOp(session, epropdict=session.vreg.eprop_values,
+ key=key, value=value)
+
+
+class DeleteCWPropertyHook(AddCWPropertyHook):
+ __id__ = 'delcwprop'
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ eid = self.entity.eid
+ session = self.cw_req
+ for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
+ if rtype == 'for_user' and eidfrom == self.entity.eid:
+ # if for_user was set, delete has already been handled
+ break
+ else:
+ _DelCWPropertyOp(session, epropdict=session.vreg.eprop_values, key=entity.pkey)
+
+
+class AddForUserRelationHook(Hook):
+ __id__ = 'addcwpropforuser'
+ __select__ = Hook.__select__ & match_rtype('for_user')
+ events = ('after_add_relation',)
+ category = 'syncsession'
+
+ def __call__(self):
+ session = self.cw_req
+ eidfrom = self.eidfrom
+ if not session.describe(eidfrom)[0] == 'CWProperty':
+ return
+ key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
+ {'x': eidfrom}, 'x')[0]
+ if session.vreg.property_info(key)['sitewide']:
+ raise ValidationError(eidfrom,
+ {'for_user': session._("site-wide property can't be set for user")})
+ for session_ in get_user_sessions(session.repo, self.eidto):
+ _ChangeCWPropertyOp(session, epropdict=session_.user.properties,
+ key=key, value=value)
+
+
+class DelForUserRelationHook(AddForUserRelationHook):
+ __id__ = 'delcwpropforuser'
+ events = ('after_delete_relation',)
+
+ def __call__(self):
+ session = self.cw_req
+ key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
+ {'x': self.eidfrom}, 'x')[0][0]
+ session.transaction_data.setdefault('pendingrelations', []).append(
+ (self.eidfrom, self.rtype, self.eidto))
+ for session_ in get_user_sessions(session.repo, self.eidto):
+ _DelCWPropertyOp(session, epropdict=session_.user.properties, key=key)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/workflow.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,106 @@
+"""Core hooks: workflow related hooks
+
+: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"
+
+from cubicweb import ValidationError
+from cubicweb.interfaces import IWorkflowable
+from cubicweb.selectors import entity_implements
+from cubicweb.server.hook import Hook, match_rtype
+from cubicweb.server.pool import PreCommitOperation
+from cubicweb.server.hookhelper import previous_state
+
+
+def relation_deleted(session, eidfrom, rtype, eidto):
+ session.transaction_data.setdefault('pendingrelations', []).append(
+ (eidfrom, rtype, eidto))
+
+
+class _SetInitialStateOp(PreCommitOperation):
+ """make initial state be a default state"""
+
+ def precommit_event(self):
+ session = self.session
+ entity = self.entity
+ # if there is an initial state and the entity's state is not set,
+ # use the initial state as a default state
+ pendingeids = session.transaction_data.get('pendingeids', ())
+ if not entity.eid in pendingeids and not entity.in_state:
+ rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
+ {'name': entity.id})
+ if rset:
+ session.add_relation(entity.eid, 'in_state', rset[0][0])
+
+
+class SetInitialStateHook(Hook):
+ __id__ = 'wfsetinitial'
+ __select__ = Hook.__select__ & entity_implements(IWorkflowable)
+ category = 'worfklow'
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ _SetInitialStateOp(self.cw_req, entity=self.entity)
+
+
+class PrepareStateChangeHook(Hook):
+ """record previous state information"""
+ __id__ = 'cwdelstate'
+ __select__ = Hook.__select__ & match_rtype('in_state')
+ category = 'worfklow'
+ events = ('before_delete_relation',)
+
+ def __call__(self):
+ self.cw_req.transaction_data.setdefault('pendingrelations', []).append(
+ (self.eidfrom, self.rtype, self.eidto))
+
+
+class FireTransitionHook(PrepareStateChangeHook):
+ """check the transition is allowed and record transition information"""
+ __id__ = 'wffiretransition'
+ events = ('before_add_relation',)
+
+ def __call__(self):
+ session = self.cw_req
+ eidfrom = self.eidfrom
+ eidto = self.eidto
+ state = previous_state(session, eidfrom)
+ etype = session.describe(eidfrom)[0]
+ if not (session.is_super_session or 'managers' in session.user.groups):
+ if not state is None:
+ entity = session.entity_from_eid(eidfrom)
+ # we should find at least one transition going to this state
+ try:
+ iter(state.transitions(entity, eidto)).next()
+ except StopIteration:
+ msg = session._('transition is not allowed')
+ raise ValidationError(eidfrom, {'in_state': msg})
+ else:
+ # not a transition
+ # check state is initial state if the workflow defines one
+ isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
+ {'etype': etype})
+ if isrset and not eidto == isrset[0][0]:
+ msg = session._('not the initial state for this entity')
+ raise ValidationError(eidfrom, {'in_state': msg})
+ eschema = session.repo.schema[etype]
+ if not 'wf_info_for' in eschema.object_relations():
+ # workflow history not activated for this entity type
+ return
+ rql = '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': eidfrom, 'ds': eidto}
+ cformat = session.get_shared_data('trcommentformat', None, pop=True)
+ if cformat is not None:
+ args['comment_format'] = cformat
+ rql += ', T comment_format %(comment_format)s'
+ restriction = ['DS eid %(ds)s, E eid %(e)s']
+ if not state is None: # not a transition
+ rql += ', T from_state FS'
+ restriction.append('FS eid %(fs)s')
+ args['fs'] = state.eid
+ rql = '%s WHERE %s' % (rql, ', '.join(restriction))
+ session.unsafe_execute(rql, args, 'e')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/hook.py Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,311 @@
+"""Hooks management
+
+This module defined the `Hook` class and registry and a set of abstract classes
+for operations.
+
+
+Hooks are called before / after any individual update of entities / relations
+in the repository and on special events such as server startup or shutdown.
+
+
+Operations may be registered by hooks during a transaction, which will be
+fired when the pool is commited or rollbacked.
+
+
+Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
+after_update_entity, before_delete_entity, after_delete_entity) all have an
+`entity` attribute
+
+Relation (eg before_add_relation, after_add_relation, before_delete_relation,
+after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
+
+Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`
+attribute, but *their `cw_req` attribute is None*.
+
+Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
+`timestamp` attributes, but *their `cw_req` attribute is None*.
+
+Session hooks (eg session_open, session_close) have no special attribute.
+
+
+: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"
+
+from warnings import warn
+from logging import getLogger
+
+from logilab.common.decorators import classproperty
+from logilab.common.logging_ext import set_log_methods
+
+from cubicweb.cwvreg import CWRegistry, VRegistry
+from cubicweb.selectors import (objectify_selector, lltrace, match_search_state,
+ entity_implements)
+from cubicweb.appobject import AppObject
+
+
+ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity',
+ 'before_update_entity', 'after_update_entity',
+ 'before_delete_entity', 'after_delete_entity'))
+RELATIONS_HOOKS = set(('before_add_relation', 'after_add_relation' ,
+ 'before_delete_relation','after_delete_relation'))
+SYSTEM_HOOKS = set(('server_backup', 'server_restore',
+ 'server_startup', 'server_shutdown',
+ 'session_open', 'session_close'))
+ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
+
+
+class HooksRegistry(CWRegistry):
+
+ def register(self, obj, **kwargs):
+ for event in obj.events:
+ if event not in ALL_HOOKS:
+ raise Exception('bad event %s on %s' % (event, obj))
+ super(HooksRegistry, self).register(obj, **kwargs)
+
+ def call_hooks(self, event, req=None, **kwargs):
+ kwargs['event'] = event
+ # XXX remove .enabled
+ for hook in sorted([x for x in self.possible_objects(req, **kwargs)
+ if x.enabled], key=lambda x: x.order):
+ hook()
+
+VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
+
+
+# some hook specific selectors #################################################
+
+@objectify_selector
+@lltrace
+def match_event(cls, req, **kwargs):
+ if kwargs.get('event') in cls.events:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def enabled_category(cls, req, **kwargs):
+ if req is None:
+ # server startup / shutdown event
+ config = kwargs['repo'].config
+ else:
+ config = req.vreg.config
+ if enabled_category in config.disabled_hooks_categories:
+ return 0
+ return 1
+
+@objectify_selector
+@lltrace
+def regular_session(cls, req, **kwargs):
+ if req is None or req.is_super_session:
+ return 0
+ return 1
+
+class match_rtype(match_search_state):
+ """accept if parameters specified as initializer arguments are specified
+ in named arguments given to the selector
+
+ :param *expected: parameters (eg `basestring`) which are expected to be
+ found in named arguments (kwargs)
+ """
+
+ @lltrace
+ def __call__(self, cls, req, *args, **kwargs):
+ return kwargs.get('rtype') in self.expected
+
+
+# base class for hook ##########################################################
+
+class Hook(AppObject):
+ __registry__ = 'hooks'
+ __select__ = match_event() & enabled_category()
+ # set this in derivated classes
+ events = None
+ category = None
+ order = 0
+ # XXX deprecates
+ enabled = True
+
+ @classproperty
+ def __id__(cls):
+ warn('[3.5] %s: please specify an id for your hook' % cls)
+ return str(id(cls))
+
+ @classmethod
+ def __registered__(cls, vreg):
+ super(Hook, cls).__registered__(vreg)
+ if getattr(cls, 'accepts', None):
+ warn('[3.5] %s: accepts is deprecated, define proper __select__' % cls)
+ rtypes = []
+ for ertype in cls.accepts:
+ if ertype.islower():
+ rtypes.append(ertype)
+ else:
+ cls.__select__ = cls.__select__ & entity_implements(ertype)
+ if rtypes:
+ cls.__select__ = cls.__select__ & match_rtype(*rtypes)
+ return cls
+
+ known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp'))
+ def __init__(self, req, event, **kwargs):
+ for arg in self.known_args:
+ if arg in kwargs:
+ setattr(self, arg, kwargs.pop(arg))
+ super(Hook, self).__init__(req, **kwargs)
+ self.event = event
+
+ def __call__(self):
+ if hasattr(self, 'call'):
+ warn('[3.5] %s: call is deprecated, implements __call__' % self.__class__)
+ if self.event.endswith('_relation'):
+ self.call(self.cw_req, self.eidfrom, self.rtype, self.eidto)
+ elif 'delete' in self.event:
+ self.call(self.cw_req, self.entity.eid)
+ elif self.event.startswith('server_'):
+ self.call(self.repo)
+ elif self.event.startswith('session_'):
+ self.call(self.cw_req)
+ else:
+ self.call(self.cw_req, self.entity)
+
+set_log_methods(Hook, getLogger('cubicweb.hook'))
+
+
+# abstract classes for operation ###############################################
+
+class Operation(object):
+ """an operation is triggered on connections pool events related to
+ commit / rollback transations. Possible events are:
+
+ precommit:
+ the pool is preparing to commit. You shouldn't do anything things which
+ has to be reverted if the commit fail at this point, but you can freely
+ do any heavy computation or raise an exception if the commit can't go.
+ You can add some new operation during this phase but their precommit
+ event won't be triggered
+
+ commit:
+ the pool is preparing to commit. You should avoid to do to expensive
+ stuff or something that may cause an exception in this event
+
+ revertcommit:
+ if an operation failed while commited, this event is triggered for
+ all operations which had their commit event already to let them
+ revert things (including the operation which made fail the commit)
+
+ rollback:
+ the transaction has been either rollbacked either
+ * intentionaly
+ * a precommit event failed, all operations are rollbacked
+ * a commit event failed, all operations which are not been triggered for
+ commit are rollbacked
+
+ order of operations may be important, and is controlled according to:
+ * operation's class
+ """
+
+ def __init__(self, session, **kwargs):
+ self.session = session
+ self.user = session.user
+ self.repo = session.repo
+ self.schema = session.repo.schema
+ self.config = session.repo.config
+ self.__dict__.update(kwargs)
+ self.register(session)
+ # execution information
+ self.processed = None # 'precommit', 'commit'
+ self.failed = False
+
+ def register(self, session):
+ session.add_operation(self, self.insert_index())
+
+ def insert_index(self):
+ """return the index of the lastest instance which is not a
+ LateOperation instance
+ """
+ for i, op in enumerate(self.session.pending_operations):
+ if isinstance(op, (LateOperation, SingleLastOperation)):
+ return i
+ return None
+
+ def handle_event(self, event):
+ """delegate event handling to the opertaion"""
+ getattr(self, event)()
+
+ def precommit_event(self):
+ """the observed connections pool is preparing a commit"""
+
+ def revertprecommit_event(self):
+ """an error went when pre-commiting this operation or a later one
+
+ should revert pre-commit's changes but take care, they may have not
+ been all considered if it's this operation which failed
+ """
+
+ def commit_event(self):
+ """the observed connections pool is commiting"""
+
+ def revertcommit_event(self):
+ """an error went when commiting this operation or a later one
+
+ should revert commit's changes but take care, they may have not
+ been all considered if it's this operation which failed
+ """
+
+ def rollback_event(self):
+ """the observed connections pool has been rollbacked
+
+ do nothing by default, the operation will just be removed from the pool
+ operation list
+ """
+
+set_log_methods(Operation, getLogger('cubicweb.session'))
+
+
+class LateOperation(Operation):
+ """special operation which should be called after all possible (ie non late)
+ operations
+ """
+ def insert_index(self):
+ """return the index of the lastest instance which is not a
+ SingleLastOperation instance
+ """
+ for i, op in enumerate(self.session.pending_operations):
+ if isinstance(op, SingleLastOperation):
+ return i
+ return None
+
+
+class SingleOperation(Operation):
+ """special operation which should be called once"""
+ def register(self, session):
+ """override register to handle cases where this operation has already
+ been added
+ """
+ operations = session.pending_operations
+ index = self.equivalent_index(operations)
+ if index is not None:
+ equivalent = operations.pop(index)
+ else:
+ equivalent = None
+ session.add_operation(self, self.insert_index())
+ return equivalent
+
+ def equivalent_index(self, operations):
+ """return the index of the equivalent operation if any"""
+ equivalents = [i for i, op in enumerate(operations)
+ if op.__class__ is self.__class__]
+ if equivalents:
+ return equivalents[0]
+ return None
+
+
+class SingleLastOperation(SingleOperation):
+ """special operation which should be called once and after all other
+ operations
+ """
+ def insert_index(self):
+ return None
--- a/server/hooks.py Fri Aug 14 09:20:33 2009 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,600 +0,0 @@
-"""Core hooks: check schema validity, unsure we are not deleting necessary
-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"
-
-from datetime import datetime
-
-from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-
-from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
-from cubicweb.server.hookhelper import (check_internal_entity, previous_state,
- get_user_sessions, rproperty)
-from cubicweb.server.repository import FTIndexEntityOp
-
-# 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'))
-
-
-def relation_deleted(session, eidfrom, rtype, eidto):
- session.transaction_data.setdefault('pendingrelations', []).append(
- (eidfrom, rtype, eidto))
-
-def eschema_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)
- if eschema.eid is None:
- eschema.eid = session.unsafe_execute(
- 'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0]
- return eschema.eid
-
-
-# base meta-data handling ######################################################
-
-def setctime_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)
- if not session.get_shared_data('do-not-insert-cwuri'):
- entity.setdefault('cwuri', u'%seid/%s' % (session.base_url(), entity.eid))
-
-
-def setmtime_before_update_entity(session, entity):
- """update an entity -> set modification date"""
- entity.setdefault('modification_date', datetime.now())
-
-
-class SetCreatorOp(PreCommitOperation):
-
- def precommit_event(self):
- session = self.session
- if self.entity.eid in session.transaction_data.get('pendingeids', ()):
- # entity have been created and deleted in the same transaction
- return
- if not self.entity.created_by:
- session.add_relation(self.entity.eid, 'created_by', session.user.eid)
-
-
-def setowner_after_add_entity(session, entity):
- """create a new entity -> set owner and creator metadata"""
- asession = session.actual_session()
- if not asession.is_internal_session:
- session.add_relation(entity.eid, 'owned_by', asession.user.eid)
- SetCreatorOp(asession, entity=entity)
-
-
-def setis_after_add_entity(session, entity):
- """create a new entity -> set is relation"""
- if hasattr(entity, '_cw_recreating'):
- return
- try:
- session.add_relation(entity.eid, 'is',
- eschema_type_eid(session, entity.id))
- except IndexError:
- # during schema serialization, skip
- return
- # XXX < 2.50 bw compat
- if not session.get_shared_data('do-not-insert-is_instance_of'):
- for etype in entity.e_schema.ancestors() + [entity.e_schema]:
- session.add_relation(entity.eid, 'is_instance_of',
- eschema_type_eid(session, etype))
-
-
-def setowner_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)
-
-
-def fti_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_container
- if ftcontainer == 'subject':
- FTIndexEntityOp(session, entity=session.entity_from_eid(eidto))
- elif ftcontainer == 'object':
- FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom))
-
-
-def fti_update_after_delete_relation(session, eidfrom, rtype, eidto):
- """sync fulltext index when relevant relation is deleted. Reindexing both
- entities is necessary.
- """
- if session.repo.schema.rschema(rtype).fulltext_container:
- FTIndexEntityOp(session, entity=session.entity_from_eid(eidto))
- FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom))
-
-
-class SyncOwnersOp(PreCommitOperation):
-
- def precommit_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'))
-
-
-def sync_owner_after_add_composite_relation(session, eidfrom, rtype, eidto):
- """when adding composite relation, the composed should have the same owners
- has the composite
- """
- if rtype == 'wf_info_for':
- # skip this special composite relation # XXX (syt) why?
- return
- composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
- if composite == 'subject':
- SyncOwnersOp(session, compositeeid=eidfrom, composedeid=eidto)
- elif composite == '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' in hm.schema:
- hm.register_hook(setis_after_add_entity, 'after_add_entity', '')
- if 'CWUser' in hm.schema:
- hm.register_hook(setowner_after_add_user, 'after_add_entity', 'CWUser')
-
-
-# core hooks ##################################################################
-
-class DelayedDeleteOp(PreCommitOperation):
- """delete the object of composite relation except if the relation
- has actually been redirected to another composite
- """
-
- def precommit_event(self):
- session = self.session
- # don't do anything if the entity is being created or deleted
- if not (self.eid in session.transaction_data.get('pendingeids', ()) or
- self.eid in session.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')
-
-
-def handle_composite_before_del_relation(session, eidfrom, rtype, eidto):
- """delete the object of composite relation"""
- composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
- if composite == 'subject':
- DelayedDeleteOp(session, eid=eidto, relation='Y %s X' % rtype)
- elif composite == 'object':
- DelayedDeleteOp(session, eid=eidfrom, relation='X %s Y' % rtype)
-
-
-def before_del_group(session, eid):
- """check that we don't remove the owners group"""
- check_internal_entity(session, eid, ('owners',))
-
-
-# schema validation hooks #####################################################
-
-class CheckConstraintsOperation(LateOperation):
- """check a new relation satisfy its constraints
- """
- def precommit_event(self):
- eidfrom, rtype, eidto = self.rdef
- # first check related entities have not been deleted in the same
- # transaction
- pending = self.session.transaction_data.get('pendingeids', ())
- if eidfrom in pending:
- return
- if eidto in pending:
- return
- for constraint in self.constraints:
- try:
- constraint.repo_check(self.session, eidfrom, rtype, eidto)
- except NotImplementedError:
- self.critical('can\'t check constraint %s, not supported',
- constraint)
-
- def commit_event(self):
- pass
-
-
-def cstrcheck_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')
- if constraints:
- CheckConstraintsOperation(session, constraints=constraints,
- rdef=(eidfrom, rtype, eidto))
-
-def uniquecstrcheck_before_modification(session, entity):
- eschema = entity.e_schema
- for attr, val in entity.items():
- if val is None:
- continue
- if eschema.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})
- if rset and rset[0][0] != entity.eid:
- msg = session._('the value "%s" is already used, use another one')
- raise ValidationError(entity.eid, {attr: msg % val})
-
-
-class CheckRequiredRelationOperation(LateOperation):
- """checking relation cardinality has to be done after commit in
- case the relation is being replaced
- """
- eid, rtype = None, None
-
- def precommit_event(self):
- # recheck pending eids
- if self.eid in self.session.transaction_data.get('pendingeids', ()):
- return
- if self.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}
- raise ValidationError(self.eid, {self.rtype: msg})
-
- def commit_event(self):
- pass
-
- def _rql(self):
- raise NotImplementedError()
-
-
-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'
-
-
-def checkrel_if_necessary(session, opcls, rtype, eid):
- """check an equivalent operation has not already been added"""
- for op in session.pending_operations:
- if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
- break
- else:
- opcls(session, rtype=rtype, eid=eid)
-
-
-def cardinalitycheck_after_add_entity(session, entity):
- """check cardinalities are satisfied"""
- eid = entity.eid
- for rschema, targetschemas, x in entity.e_schema.relation_definitions():
- # skip automatically handled relations
- if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
- continue
- if x == 'subject':
- subjtype = entity.e_schema
- objtype = targetschemas[0].type
- cardindex = 0
- opcls = CheckSRelationOp
- else:
- subjtype = targetschemas[0].type
- objtype = entity.e_schema
- cardindex = 1
- opcls = CheckORelationOp
- card = rschema.rproperty(subjtype, objtype, 'cardinality')
- if card[cardindex] in '1+':
- checkrel_if_necessary(session, opcls, rschema.type, eid)
-
-
-def cardinalitycheck_before_del_relation(session, eidfrom, rtype, eidto):
- """check cardinalities are satisfied"""
- if rtype in DONT_CHECK_RTYPES_ON_DEL:
- return
- card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
- pendingrdefs = session.transaction_data.get('pendingrdefs', ())
- if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
- return
- pendingeids = session.transaction_data.get('pendingeids', ())
- if card[0] in '1+' and not eidfrom in pendingeids:
- checkrel_if_necessary(session, CheckSRelationOp, rtype, eidfrom)
- if card[1] in '1+' and not eidto in pendingeids:
- 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 #################################################
-
-class GroupOperation(Operation):
- """base class for group operation"""
- geid = None
- def __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]
-
-
-class DeleteGroupOp(GroupOperation):
- """synchronize user when a in_group relation has been deleted"""
- def commit_event(self):
- """the observed connections pool has been commited"""
- groups = self.cnxuser.groups
- try:
- groups.remove(self.group)
- except KeyError:
- self.error('user %s not in group %s', self.cnxuser, self.group)
- return
-
-
-def after_del_in_group(session, fromeid, rtype, toeid):
- """modify user permission, need to update users"""
- for session_ in get_user_sessions(session.repo, fromeid):
- DeleteGroupOp(session, cnxuser=session_.user, geid=toeid)
-
-
-class AddGroupOp(GroupOperation):
- """synchronize user when a in_group relation has been added"""
- def commit_event(self):
- """the observed connections pool has been commited"""
- groups = self.cnxuser.groups
- if self.group in groups:
- self.warning('user %s already in group %s', self.cnxuser,
- self.group)
- return
- groups.add(self.group)
-
-
-def after_add_in_group(session, fromeid, rtype, toeid):
- """modify user permission, need to update users"""
- for session_ in get_user_sessions(session.repo, fromeid):
- AddGroupOp(session, cnxuser=session_.user, geid=toeid)
-
-
-class DelUserOp(Operation):
- """synchronize user when a in_group relation has been added"""
- def __init__(self, session, cnxid):
- self.cnxid = cnxid
- Operation.__init__(self, session)
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- try:
- self.repo.close(self.cnxid)
- except BadConnectionId:
- pass # already closed
-
-
-def after_del_user(session, eid):
- """modify user permission, need to update users"""
- for session_ in get_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 ###########################################################
-
-def before_add_in_state(session, fromeid, rtype, toeid):
- """check the transition is allowed and record transition information
- """
- assert rtype == 'in_state'
- state = previous_state(session, fromeid)
- etype = session.describe(fromeid)[0]
- if not (session.is_super_session or 'managers' in session.user.groups):
- if not state is None:
- entity = session.entity_from_eid(fromeid)
- # we should find at least one transition going to this state
- try:
- iter(state.transitions(entity, toeid)).next()
- except StopIteration:
- msg = session._('transition is not allowed')
- raise ValidationError(fromeid, {'in_state': msg})
- else:
- # not a transition
- # check state is initial state if the workflow defines one
- isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
- {'etype': etype})
- if isrset and not toeid == isrset[0][0]:
- msg = session._('not the initial state for this entity')
- raise ValidationError(fromeid, {'in_state': msg})
- eschema = session.repo.schema[etype]
- if not 'wf_info_for' in eschema.object_relations():
- # workflow history not activated for this entity type
- return
- rql = '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)
- if cformat is not None:
- args['comment_format'] = cformat
- rql += ', T comment_format %(comment_format)s'
- restriction = ['DS eid %(ds)s, E eid %(e)s']
- if not state is None: # not a transition
- rql += ', T from_state FS'
- restriction.append('FS eid %(fs)s')
- args['fs'] = state.eid
- rql = '%s WHERE %s' % (rql, ', '.join(restriction))
- session.unsafe_execute(rql, args, 'e')
-
-
-class SetInitialStateOp(PreCommitOperation):
- """make initial state be a default state"""
-
- def precommit_event(self):
- session = self.session
- entity = self.entity
- # if there is an initial state and the entity's state is not set,
- # use the initial state as a default state
- pendingeids = session.transaction_data.get('pendingeids', ())
- if not entity.eid in pendingeids and not entity.in_state:
- rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
- {'name': entity.id})
- if rset:
- session.add_relation(entity.eid, 'in_state', rset[0][0])
-
-
-def set_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' in hm.schema:
- hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
- hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
- for eschema in hm.schema.entities():
- if 'in_state' in eschema.subject_relations():
- hm.register_hook(set_initial_state_after_add, 'after_add_entity',
- str(eschema))
-
-
-# CWProperty hooks #############################################################
-
-
-class DelCWPropertyOp(Operation):
- """a user's custom properties has been deleted"""
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- try:
- del self.epropdict[self.key]
- except KeyError:
- self.error('%s has no associated value', self.key)
-
-
-class ChangeCWPropertyOp(Operation):
- """a user's custom properties has been added/changed"""
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- self.epropdict[self.key] = self.value
-
-
-class AddCWPropertyOp(Operation):
- """a user's custom properties has been added/changed"""
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- eprop = self.eprop
- if not eprop.for_user:
- self.repo.vreg.eprop_values[eprop.pkey] = eprop.value
- # if for_user is set, update is handled by a ChangeCWPropertyOp operation
-
-
-def after_add_eproperty(session, entity):
- key, value = entity.pkey, entity.value
- try:
- value = session.vreg.typed_value(key, value)
- except UnknownProperty:
- raise ValidationError(entity.eid, {'pkey': session._('unknown property key')})
- except ValueError, ex:
- raise ValidationError(entity.eid, {'value': session._(str(ex))})
- if not session.user.matching_groups('managers'):
- session.add_relation(entity.eid, 'for_user', session.user.eid)
- else:
- AddCWPropertyOp(session, eprop=entity)
-
-
-def after_update_eproperty(session, entity):
- if not ('pkey' in entity.edited_attributes or
- 'value' in entity.edited_attributes):
- return
- key, value = entity.pkey, entity.value
- try:
- value = session.vreg.typed_value(key, value)
- except UnknownProperty:
- return
- except ValueError, ex:
- raise ValidationError(entity.eid, {'value': session._(str(ex))})
- if entity.for_user:
- for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
- ChangeCWPropertyOp(session, epropdict=session_.user.properties,
- key=key, value=value)
- else:
- # site wide properties
- ChangeCWPropertyOp(session, epropdict=session.vreg.eprop_values,
- key=key, value=value)
-
-
-def before_del_eproperty(session, eid):
- for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
- if rtype == 'for_user' and eidfrom == eid:
- # if for_user was set, delete has already been handled
- break
- else:
- 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)
-
-
-def after_add_for_user(session, fromeid, rtype, toeid):
- if not session.describe(fromeid)[0] == 'CWProperty':
- return
- key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
- {'x': fromeid}, 'x')[0]
- if session.vreg.property_info(key)['sitewide']:
- raise ValidationError(fromeid,
- {'for_user': session._("site-wide property can't be set for user")})
- for session_ in get_user_sessions(session.repo, toeid):
- ChangeCWPropertyOp(session, epropdict=session_.user.properties,
- key=key, value=value)
-
-
-def before_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)
- for session_ in get_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')
--- a/server/hooksmanager.py Fri Aug 14 09:20:33 2009 +0200
+++ b/server/hooksmanager.py Fri Aug 14 09:26:41 2009 +0200
@@ -1,270 +1,4 @@
-"""Hooks management
-
-Hooks are called before / after any individual update of entities / relations
-in the repository.
-
-Here is the prototype of the different hooks:
-
-* filtered on the entity's type:
-
- before_add_entity (session, entity)
- after_add_entity (session, entity)
- before_update_entity (session, entity)
- after_update_entity (session, entity)
- before_delete_entity (session, eid)
- after_delete_entity (session, eid)
-
-* filtered on the relation's type:
-
- before_add_relation (session, fromeid, rtype, toeid)
- after_add_relation (session, fromeid, rtype, toeid)
- before_delete_relation (session, fromeid, rtype, toeid)
- after_delete_relation (session, fromeid, rtype, toeid)
-
-
-: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"
-
-ENTITIES_HOOKS = ('before_add_entity', 'after_add_entity',
- 'before_update_entity', 'after_update_entity',
- 'before_delete_entity', 'after_delete_entity')
-RELATIONS_HOOKS = ('before_add_relation', 'after_add_relation' ,
- 'before_delete_relation','after_delete_relation')
-SYSTEM_HOOKS = ('server_backup', 'server_restore',
- 'server_startup', 'server_shutdown',
- 'session_open', 'session_close')
-
-ALL_HOOKS = frozenset(ENTITIES_HOOKS + RELATIONS_HOOKS + SYSTEM_HOOKS)
-
-class HooksManager(object):
- """handle hooks registration and calls
- """
- verification_hooks_activated = True
-
- def __init__(self, schema):
- self.set_schema(schema)
-
- def set_schema(self, schema):
- self._hooks = {}
- self.schema = schema
- self._init_hooks(schema)
-
- def register_hooks(self, hooks):
- """register a dictionary of hooks :
-
- {'event': {'entity or relation type': [callbacks list]}}
- """
- for event, subevents in hooks.items():
- for subevent, callbacks in subevents.items():
- for callback in callbacks:
- self.register_hook(callback, event, subevent)
-
- def register_hook(self, function, event, etype=''):
- """register a function to call when <event> occurs
-
- <etype> is an entity/relation type or an empty string.
-
- If etype is the empty string, the function will be called at each event,
- else the function will be called only when event occurs on an entity or
- relation of the given type.
- """
- assert event in ALL_HOOKS, '%r NOT IN %r' % (event, ALL_HOOKS)
- assert (not event in SYSTEM_HOOKS or not etype), (event, etype)
- etype = etype or ''
- try:
- self._hooks[event][etype].append(function)
- self.debug('registered hook %s on %s (%s)', event, etype or 'any',
- function.func_name)
-
- except KeyError:
- self.error('can\'t register hook %s on %s (%s)',
- event, etype or 'any', function.func_name)
-
- def unregister_hook(self, function_or_cls, event=None, etype=''):
- """unregister a function to call when <event> occurs, or a Hook subclass.
- In the later case, event/type information are extracted from the given
- class.
- """
- if isinstance(function_or_cls, type) and issubclass(function_or_cls, Hook):
- for event, ertype in function_or_cls.register_to(self.schema):
- for hook in self._hooks[event][ertype]:
- if getattr(hook, 'im_self', None).__class__ is function_or_cls:
- self._hooks[event][ertype].remove(hook)
- self.info('unregister hook %s on %s (%s)', event, etype,
- function_or_cls.__name__)
- break
- else:
- self.warning("can't unregister hook %s on %s (%s), not found",
- event, etype, function_or_cls.__name__)
- else:
- assert event in ALL_HOOKS, event
- etype = etype or ''
- self.info('unregister hook %s on %s (%s)', event, etype,
- function_or_cls.func_name)
- self._hooks[event][etype].remove(function_or_cls)
-
- def call_hooks(self, __event, __type='', *args, **kwargs):
- """call hook matching event and optional type"""
- if __type:
- self.info('calling hooks for event %s (%s)', __event, __type)
- else:
- self.info('calling hooks for event %s', __event)
- # call generic hooks first
- for hook in self._hooks[__event]['']:
- #print '[generic]', hook.__name__
- hook(*args, **kwargs)
- if __type:
- for hook in self._hooks[__event][__type]:
- #print '[%s]'%__type, hook.__name__
- hook(*args, **kwargs)
-
- def _init_hooks(self, schema):
- """initialize the hooks map"""
- for hook_event in ENTITIES_HOOKS:
- self._hooks[hook_event] = {'': []}
- for etype in schema.entities():
- self._hooks[hook_event][etype] = []
- for hook_event in RELATIONS_HOOKS:
- self._hooks[hook_event] = {'': []}
- for r_type in schema.relations():
- self._hooks[hook_event][r_type] = []
- for hook_event in SYSTEM_HOOKS:
- self._hooks[hook_event] = {'': []}
-
- def register_system_hooks(self, config):
- """register system hooks according to the configuration"""
- self.info('register core hooks')
- from cubicweb.server.hooks import _register_metadata_hooks, _register_wf_hooks
- _register_metadata_hooks(self)
- self.info('register workflow hooks')
- _register_wf_hooks(self)
- if config.core_hooks:
- from cubicweb.server.hooks import _register_core_hooks
- _register_core_hooks(self)
- if config.schema_hooks:
- from cubicweb.server.schemahooks import _register_schema_hooks
- self.info('register schema hooks')
- _register_schema_hooks(self)
- if config.usergroup_hooks:
- from cubicweb.server.hooks import _register_usergroup_hooks
- from cubicweb.server.hooks import _register_eproperty_hooks
- self.info('register user/group hooks')
- _register_usergroup_hooks(self)
- _register_eproperty_hooks(self)
- if config.security_hooks:
- from cubicweb.server.securityhooks import register_security_hooks
- self.info('register security hooks')
- register_security_hooks(self)
- if not self.verification_hooks_activated:
- self.deactivate_verification_hooks()
-
- def deactivate_verification_hooks(self):
- from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
- cardinalitycheck_before_del_relation,
- cstrcheck_after_add_relation,
- uniquecstrcheck_before_modification)
- self.warning('deactivating verification hooks')
- self.verification_hooks_activated = False
- self.unregister_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
- self.unregister_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
- self.unregister_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
- self.unregister_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
- self.unregister_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-# self.unregister_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
-# self.unregister_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-
- def reactivate_verification_hooks(self):
- from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
- cardinalitycheck_before_del_relation,
- cstrcheck_after_add_relation,
- uniquecstrcheck_before_modification)
- self.warning('reactivating verification hooks')
- self.verification_hooks_activated = True
- self.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
- self.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
- self.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
- self.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
- self.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-# self.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
-# self.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-
-from cubicweb.selectors import yes
-from cubicweb.appobject import AppObject
-
-class autoid(type):
- """metaclass to create an unique 'id' attribute on the class using it"""
- # XXX is this metaclass really necessary ?
- def __new__(mcs, name, bases, classdict):
- cls = super(autoid, mcs).__new__(mcs, name, bases, classdict)
- cls.id = str(id(cls))
- return cls
-
-class Hook(AppObject):
- __metaclass__ = autoid
- __registry__ = 'hooks'
- __select__ = yes()
- # set this in derivated classes
- events = None
- accepts = None
- enabled = True
-
- def __init__(self, event=None):
- super(Hook, self).__init__(None)
- self.event = event
-
- @classmethod
- def __registered__(cls, vreg):
- super(Hook, cls).__registered__(vreg)
- return cls()
-
- @classmethod
- def register_to(cls, schema):
- if not cls.enabled:
- cls.warning('%s hook has been disabled', cls)
- return
- done = set()
- assert isinstance(cls.events, (tuple, list)), \
- '%s: events is expected to be a tuple, not %s' % (
- cls, type(cls.events))
- for event in cls.events:
- if event in SYSTEM_HOOKS:
- assert not cls.accepts or cls.accepts == ('Any',), \
- '%s doesnt make sense on %s' % (cls.accepts, event)
- cls.accepts = ('Any',)
- for ertype in cls.accepts:
- if (event, ertype) in done:
- continue
- yield event, ertype
- done.add((event, ertype))
- try:
- eschema = schema.eschema(ertype)
- except KeyError:
- # relation schema
- pass
- else:
- for eetype in eschema.specialized_by():
- if (event, eetype) in done:
- continue
- yield event, str(eetype)
- done.add((event, eetype))
-
-
- def make_callback(self, event):
- if len(self.events) == 1:
- return self.call
- return self.__class__(event=event).call
-
- def call(self):
- raise NotImplementedError
-
-class SystemHook(Hook):
- accepts = ()
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager'))
-set_log_methods(Hook, getLogger('cubicweb.hooks'))
+from logilab.common.deprecation import class_renamed, class_moved
+from cubicweb.server.hook import Hook
+SystemHook = class_renamed('SystemHook', Hook)
+Hook = class_moved(Hook)
--- a/server/migractions.py Fri Aug 14 09:20:33 2009 +0200
+++ b/server/migractions.py Fri Aug 14 09:26:41 2009 +0200
@@ -276,7 +276,7 @@
from cubicweb.server.hooks import setowner_after_add_entity
self.repo.hm.unregister_hook(setowner_after_add_entity,
'after_add_entity', '')
- self.deactivate_verification_hooks()
+ self.cmd_deactivate_verification_hooks()
self.info('executing %s', apc)
confirm = self.confirm
execscript_confirm = self.execscript_confirm
@@ -290,7 +290,7 @@
if self.config.free_wheel:
self.repo.hm.register_hook(setowner_after_add_entity,
'after_add_entity', '')
- self.reactivate_verification_hooks()
+ self.cmd_reactivate_verification_hooks()
# schema synchronization internals ########################################
@@ -1073,10 +1073,10 @@
return ForRqlIterator(self, rql, None, ask_confirm)
def cmd_deactivate_verification_hooks(self):
- self.repo.hm.deactivate_verification_hooks()
+ self.config.disabled_hooks_categories.add('integrity')
def cmd_reactivate_verification_hooks(self):
- self.repo.hm.reactivate_verification_hooks()
+ self.config.disabled_hooks_categories.remove('integrity')
# broken db commands ######################################################
--- a/server/pool.py Fri Aug 14 09:20:33 2009 +0200
+++ b/server/pool.py Fri Aug 14 09:26:41 2009 +0200
@@ -1,13 +1,7 @@
-"""CubicWeb server connections pool :
-
-* the rql repository has a limited number of connections pools, each of them
- dealing with a set of connections on each source used by the repository
-
-* operation may be registered by hooks during a transaction, which will be
- fired when the pool is commited or rollbacked
-
-This module defined the `ConnectionsPool` class and a set of abstract classes
-for operation.
+"""CubicWeb server connections pool : the repository has a limited number of
+connections pools, each of them dealing with a set of connections on each source
+used by the repository. A connections pools (`ConnectionsPool`) is an
+abstraction for a group of connection to each source.
:organization: Logilab
@@ -129,152 +123,11 @@
self.source_cnxs[source.uri] = (source, cnx)
self._cursors.pop(source.uri, None)
-
-class Operation(object):
- """an operation is triggered on connections pool events related to
- commit / rollback transations. Possible events are:
-
- precommit:
- the pool is preparing to commit. You shouldn't do anything things which
- has to be reverted if the commit fail at this point, but you can freely
- do any heavy computation or raise an exception if the commit can't go.
- You can add some new operation during this phase but their precommit
- event won't be triggered
-
- commit:
- the pool is preparing to commit. You should avoid to do to expensive
- stuff or something that may cause an exception in this event
-
- revertcommit:
- if an operation failed while commited, this event is triggered for
- all operations which had their commit event already to let them
- revert things (including the operation which made fail the commit)
-
- rollback:
- the transaction has been either rollbacked either
- * intentionaly
- * a precommit event failed, all operations are rollbacked
- * a commit event failed, all operations which are not been triggered for
- commit are rollbacked
-
- order of operations may be important, and is controlled according to:
- * operation's class
- """
-
- def __init__(self, session, **kwargs):
- self.session = session
- self.user = session.user
- self.repo = session.repo
- self.schema = session.repo.schema
- self.config = session.repo.config
- self.__dict__.update(kwargs)
- self.register(session)
- # execution information
- self.processed = None # 'precommit', 'commit'
- self.failed = False
-
- def register(self, session):
- session.add_operation(self, self.insert_index())
-
- def insert_index(self):
- """return the index of the lastest instance which is not a
- LateOperation instance
- """
- for i, op in enumerate(self.session.pending_operations):
- if isinstance(op, (LateOperation, SingleLastOperation)):
- return i
- return None
-
- def handle_event(self, event):
- """delegate event handling to the opertaion"""
- getattr(self, event)()
-
- def precommit_event(self):
- """the observed connections pool is preparing a commit"""
-
- def revertprecommit_event(self):
- """an error went when pre-commiting this operation or a later one
-
- should revert pre-commit's changes but take care, they may have not
- been all considered if it's this operation which failed
- """
-
- def commit_event(self):
- """the observed connections pool is commiting"""
- raise NotImplementedError()
-
- def revertcommit_event(self):
- """an error went when commiting this operation or a later one
-
- should revert commit's changes but take care, they may have not
- been all considered if it's this operation which failed
- """
-
- def rollback_event(self):
- """the observed connections pool has been rollbacked
-
- do nothing by default, the operation will just be removed from the pool
- operation list
- """
-
-
-class PreCommitOperation(Operation):
- """base class for operation only defining a precommit operation
- """
-
- def precommit_event(self):
- """the observed connections pool is preparing a commit"""
- raise NotImplementedError()
-
- def commit_event(self):
- """the observed connections pool is commiting"""
-
-
-class LateOperation(Operation):
- """special operation which should be called after all possible (ie non late)
- operations
- """
- def insert_index(self):
- """return the index of the lastest instance which is not a
- SingleLastOperation instance
- """
- for i, op in enumerate(self.session.pending_operations):
- if isinstance(op, SingleLastOperation):
- return i
- return None
-
-
-class SingleOperation(Operation):
- """special operation which should be called once"""
- def register(self, session):
- """override register to handle cases where this operation has already
- been added
- """
- operations = session.pending_operations
- index = self.equivalent_index(operations)
- if index is not None:
- equivalent = operations.pop(index)
- else:
- equivalent = None
- session.add_operation(self, self.insert_index())
- return equivalent
-
- def equivalent_index(self, operations):
- """return the index of the equivalent operation if any"""
- equivalents = [i for i, op in enumerate(operations)
- if op.__class__ is self.__class__]
- if equivalents:
- return equivalents[0]
- return None
-
-
-class SingleLastOperation(SingleOperation):
- """special operation which should be called once and after all other
- operations
- """
- def insert_index(self):
- return None
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(Operation, getLogger('cubicweb.session'))
+from cubicweb.server.hook import (Operation, LateOperation, SingleOperation,
+ SingleLastOperation)
+from logilab.common.deprecation import class_moved, class_renamed
+Operation = class_moved(Operation)
+PreCommitOperation = class_renamed('PreCommitOperation', Operation)
+LateOperation = class_moved(LateOperation)
+SingleOperation = class_moved(SingleOperation)
+SingleLastOperation = class_moved(SingleLastOperation)
--- a/server/repository.py Fri Aug 14 09:20:33 2009 +0200
+++ b/server/repository.py Fri Aug 14 09:26:41 2009 +0200
@@ -41,7 +41,6 @@
from cubicweb.server.session import Session, InternalSession
from cubicweb.server.querier import QuerierHelper
from cubicweb.server.sources import get_source
-from cubicweb.server.hooksmanager import HooksManager
from cubicweb.server.hookhelper import rproperty
@@ -173,8 +172,6 @@
self._type_source_cache = {}
# cache (extid, source uri) -> eid
self._extid_cache = {}
- # create the hooks manager
- self.hm = HooksManager(self.schema)
# open some connections pools
self._available_pools = Queue.Queue()
self._available_pools.put_nowait(ConnectionsPool(self.sources))
@@ -185,7 +182,7 @@
# usually during repository creation
self.warning("set fs instance'schema as bootstrap schema")
config.bootstrap_cubes()
- self.set_bootstrap_schema(self.config.load_schema())
+ self.set_schema(self.config.load_schema(), resetvreg=False)
# need to load the Any and CWUser entity types
self.vreg.schema = self.schema
etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
@@ -222,13 +219,13 @@
self.pools.append(ConnectionsPool(self.sources))
self._available_pools.put_nowait(self.pools[-1])
self._shutting_down = False
+ self.hm = vreg['hooks']
if not (config.creating or config.repairing):
# call instance level initialisation hooks
self.hm.call_hooks('server_startup', repo=self)
# register a task to cleanup expired session
self.looping_task(self.config['session-time']/3.,
self.clean_sessions)
- CW_EVENT_MANAGER.bind('after-registry-load', self.reset_hooks)
# internals ###############################################################
@@ -248,22 +245,13 @@
# full reload of all appobjects
self.vreg.reset()
self.vreg.set_schema(schema)
- self.reset_hooks()
-
- def reset_hooks(self):
- self.hm.set_schema(self.schema)
- self.hm.register_system_hooks(self.config)
- # instance specific hooks
- if self.config.instance_hooks:
- self.info('loading instance hooks')
- self.hm.register_hooks(self.config.load_hooks(self.vreg))
def fill_schema(self):
"""lod schema from the repository"""
from cubicweb.server.schemaserial import deserialize_schema
self.info('loading schema from the repository')
appschema = CubicWebSchema(self.config.appid)
- self.set_bootstrap_schema(self.config.load_bootstrap_schema())
+ self.set_schema(self.config.load_bootstrap_schema(), resetvreg=False)
self.debug('deserializing db schema into %s %#x', appschema.name, id(appschema))
session = self.internal_session()
try:
@@ -277,39 +265,10 @@
raise Exception('Is the database initialised ? (cause: %s)' %
(ex.args and ex.args[0].strip() or 'unknown')), \
None, sys.exc_info()[-1]
- self.info('set the actual schema')
- # XXX have to do this since CWProperty isn't in the bootstrap schema
- # it'll be redone in set_schema
- self.set_bootstrap_schema(appschema)
- # 2.49 migration
- if exists(join(self.config.apphome, 'vc.conf')):
- session.set_pool()
- if not 'template' in file(join(self.config.apphome, 'vc.conf')).read():
- # remaning from cubicweb < 2.38...
- session.execute('DELETE CWProperty X WHERE X pkey "system.version.template"')
- session.commit()
finally:
session.close()
+ self.set_schema(appschema)
self.config.init_cubes(self.get_cubes())
- self.set_schema(appschema)
-
- def set_bootstrap_schema(self, schema):
- """disable hooks when setting a bootstrap schema, but restore
- the configuration for the next time
- """
- config = self.config
- # XXX refactor
- config.core_hooks = False
- config.usergroup_hooks = False
- config.schema_hooks = False
- config.notification_hooks = False
- config.instance_hooks = False
- self.set_schema(schema, resetvreg=False)
- config.core_hooks = True
- config.usergroup_hooks = True
- config.schema_hooks = True
- config.notification_hooks = True
- config.instance_hooks = True
def start_looping_tasks(self):
assert isinstance(self._looping_tasks, list), 'already started'
@@ -578,7 +537,7 @@
user.clear_related_cache()
self._sessions[session.id] = session
self.info('opened %s', session)
- self.hm.call_hooks('session_open', session=session)
+ self.hm.call_hooks('session_open', session)
# commit session at this point in case write operation has been done
# during `session_open` hooks
session.commit()
@@ -669,7 +628,7 @@
checkshuttingdown=checkshuttingdown)
# operation uncommited before close are rollbacked before hook is called
session.rollback()
- self.hm.call_hooks('session_close', session=session)
+ self.hm.call_hooks('session_close', session)
# commit session at this point in case write operation has been done
# during `session_close` hooks
session.commit()
@@ -850,11 +809,11 @@
entity = source.before_entity_insertion(session, extid, etype, eid)
entity._cw_recreating = True
if source.should_call_hooks:
- self.hm.call_hooks('before_add_entity', etype, session, entity)
+ self.hm.call_hooks('before_add_entity', session, entity=entity)
# XXX add fti op ?
source.after_entity_insertion(session, extid, entity)
if source.should_call_hooks:
- self.hm.call_hooks('after_add_entity', etype, session, entity)
+ self.hm.call_hooks('after_add_entity', session, entity=entity)
if reset_pool:
session.reset_pool()
return eid
@@ -875,12 +834,12 @@
self._type_source_cache[eid] = (etype, source.uri, extid)
entity = source.before_entity_insertion(session, extid, etype, eid)
if source.should_call_hooks:
- self.hm.call_hooks('before_add_entity', etype, session, entity)
+ self.hm.call_hooks('before_add_entity', session, entity=entity)
# XXX call add_info with complete=False ?
self.add_info(session, entity, source, extid)
source.after_entity_insertion(session, extid, entity)
if source.should_call_hooks:
- self.hm.call_hooks('after_add_entity', etype, session, entity)
+ self.hm.call_hooks('after_add_entity', session, entity=entity)
else:
# minimal meta-data
session.execute('SET X is E WHERE X eid %(x)s, E name %(name)s',
@@ -998,13 +957,13 @@
relations = []
# if inlined relations are specified, fill entity's related cache to
# avoid unnecessary queries
- for attr in entity.keys():
+ entity.edited_attributes = set(entity)
+ for attr in entity.edited_attributes:
rschema = eschema.subject_relation(attr)
if not rschema.is_final(): # inlined relation
relations.append((attr, entity[attr]))
if source.should_call_hooks:
- self.hm.call_hooks('before_add_entity', etype, session, entity)
- entity.edited_attributes = entity.keys()
+ self.hm.call_hooks('before_add_entity', session, entity=entity)
entity.set_defaults()
entity.check(creation=True)
source.add_entity(session, entity)
@@ -1035,13 +994,13 @@
session.update_rel_cache_add(entity.eid, attr, value)
# trigger after_add_entity after after_add_relation
if source.should_call_hooks:
- self.hm.call_hooks('after_add_entity', etype, session, entity)
+ self.hm.call_hooks('after_add_entity', session, entity=entity)
# call hooks for inlined relations
for attr, value in relations:
- self.hm.call_hooks('before_add_relation', attr, session,
- entity.eid, attr, value)
- self.hm.call_hooks('after_add_relation', attr, session,
- entity.eid, attr, value)
+ self.hm.call_hooks('before_add_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=value)
+ self.hm.call_hooks('after_add_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=value)
return entity.eid
def glob_update_entity(self, session, entity, edited_attributes):
@@ -1074,19 +1033,18 @@
if previous_value == entity[attr]:
previous_value = None
else:
- self.hm.call_hooks('before_delete_relation', attr,
- session, entity.eid, attr,
- previous_value)
+ self.hm.call_hooks('before_delete_relation', session,
+ eidfrom=entity.eid, rtype=attr,
+ eidto=previous_value)
relations.append((attr, entity[attr], previous_value))
source = self.source_from_eid(entity.eid, session)
if source.should_call_hooks:
# call hooks for inlined relations
for attr, value, _ in relations:
- self.hm.call_hooks('before_add_relation', attr, session,
- entity.eid, attr, value)
+ self.hm.call_hooks('before_add_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=value)
if not only_inline_rels:
- self.hm.call_hooks('before_update_entity', etype, session,
- entity)
+ self.hm.call_hooks('before_update_entity', session, entity=entity)
source.update_entity(session, entity)
if not only_inline_rels:
if need_fti_update and self.do_fti:
@@ -1094,15 +1052,14 @@
# one indexable attribute
FTIndexEntityOp(session, entity=entity)
if source.should_call_hooks:
- self.hm.call_hooks('after_update_entity', etype, session,
- entity)
+ self.hm.call_hooks('after_update_entity', session, entity=entity)
if source.should_call_hooks:
for attr, value, prevvalue in relations:
# if the relation is already cached, update existant cache
relcache = entity.relation_cached(attr, 'subject')
if prevvalue:
- self.hm.call_hooks('after_delete_relation', attr, session,
- entity.eid, attr, prevvalue)
+ self.hm.call_hooks('after_delete_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
if relcache is not None:
session.update_rel_cache_del(entity.eid, attr, prevvalue)
del_existing_rel_if_needed(session, entity.eid, attr, value)
@@ -1111,8 +1068,8 @@
else:
entity.set_related_cache(attr, 'subject',
session.eid_rset(value))
- self.hm.call_hooks('after_add_relation', attr, session,
- entity.eid, attr, value)
+ self.hm.call_hooks('after_add_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=value)
def glob_delete_entity(self, session, eid):
"""delete an entity and all related entities from the repository"""
@@ -1125,11 +1082,12 @@
server.DEBUG |= (server.DBG_SQL | server.DBG_RQL | server.DBG_MORE)
source = self.sources_by_uri[uri]
if source.should_call_hooks:
- self.hm.call_hooks('before_delete_entity', etype, session, eid)
+ entity = session.entity_from_eid(eid)
+ self.hm.call_hooks('before_delete_entity', session, entity=entity)
self._delete_info(session, eid)
source.delete_entity(session, etype, eid)
if source.should_call_hooks:
- self.hm.call_hooks('after_delete_entity', etype, session, eid)
+ self.hm.call_hooks('after_delete_entity', session, entity=entity)
# don't clear cache here this is done in a hook on commit
def glob_add_relation(self, session, subject, rtype, object):
@@ -1139,14 +1097,14 @@
source = self.locate_relation_source(session, subject, rtype, object)
if source.should_call_hooks:
del_existing_rel_if_needed(session, subject, rtype, object)
- self.hm.call_hooks('before_add_relation', rtype, session,
- subject, rtype, object)
+ self.hm.call_hooks('before_add_relation', session,
+ eidfrom=subject, rtype=rtype, eidto=object)
source.add_relation(session, subject, rtype, object)
rschema = self.schema.rschema(rtype)
session.update_rel_cache_add(subject, rtype, object, rschema.symetric)
if source.should_call_hooks:
- self.hm.call_hooks('after_add_relation', rtype, session,
- subject, rtype, object)
+ self.hm.call_hooks('after_add_relation', session,
+ eidfrom=subject, rtype=rtype, eidto=object)
def glob_delete_relation(self, session, subject, rtype, object):
"""delete a relation from the repository"""
@@ -1154,8 +1112,8 @@
print 'DELETE relation', subject, rtype, object
source = self.locate_relation_source(session, subject, rtype, object)
if source.should_call_hooks:
- self.hm.call_hooks('before_delete_relation', rtype, session,
- subject, rtype, object)
+ self.hm.call_hooks('before_delete_relation', session,
+ eidfrom=subject, rtype=rtype, eidto=object)
source.delete_relation(session, subject, rtype, object)
rschema = self.schema.rschema(rtype)
session.update_rel_cache_del(subject, rtype, object, rschema.symetric)
@@ -1164,8 +1122,8 @@
# stored so try to delete both
source.delete_relation(session, object, rtype, subject)
if source.should_call_hooks:
- self.hm.call_hooks('after_delete_relation', rtype, session,
- subject, rtype, object)
+ self.hm.call_hooks('after_delete_relation', session,
+ eidfrom=subject, rtype=rtype, eidto=object)
# pyro handling ###########################################################
--- a/server/schemahooks.py Fri Aug 14 09:20:33 2009 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1057 +0,0 @@
-"""schema hooks:
-
-- synchronize the living schema object with the persistent schema
-- perform physical update on the source when necessary
-
-checking for schema consistency is done in hooks.py
-
-: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"
-
-from yams.schema import BASE_TYPES
-from yams.buildobjs import EntityType, RelationType, RelationDefinition
-from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
-
-
-from cubicweb import ValidationError, RepositoryError
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
-from cubicweb.server import schemaserial as ss
-from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation
-from cubicweb.server.hookhelper import (entity_attr, entity_name,
- check_internal_entity)
-
-
-TYPE_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 removed
-CORE_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',
- ]
-
-def get_constraints(session, entity):
- constraints = []
- for cstreid in session.transaction_data.get(entity.eid, ()):
- cstrent = session.entity_from_eid(cstreid)
- cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
- cstr.eid = cstreid
- constraints.append(cstr)
- return constraints
-
-def add_inline_relation_column(session, etype, rtype):
- """add necessary column and index for an inlined relation"""
- table = SQL_PREFIX + etype
- column = SQL_PREFIX + rtype
- try:
- 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 anyway
- session.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))
-
-
-# operations for low-level database alteration ################################
-
-class DropTable(PreCommitOperation):
- """actually remove a database from the instance's schema"""
- table = None # make pylint happy
- def precommit_event(self):
- dropped = self.session.transaction_data.setdefault('droppedtables',
- set())
- if self.table in dropped:
- return # already processed
- dropped.add(self.table)
- self.session.system_sql('DROP TABLE %s' % self.table)
- self.info('dropped table %s', self.table)
-
-
-class DropRelationTable(DropTable):
- def __init__(self, session, rtype):
- super(DropRelationTable, self).__init__(
- session, table='%s_relation' % rtype)
- session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
-
-
-class DropColumn(PreCommitOperation):
- """actually remove the attribut's column from entity table in the system
- database
- """
- table = column = None # make pylint happy
- def precommit_event(self):
- session, table, column = self.session, self.table, self.column
- # drop index if any
- session.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)
- except Exception, ex:
- # not supported by sqlite for instance
- self.error('error while altering table %s: %s', table, ex)
-
-
-# base operations for in-memory schema synchronization ########################
-
-class MemSchemaNotifyChanges(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):
- self.repo = session.repo
- SingleLastOperation.__init__(self, session)
-
- def commit_event(self):
- self.repo.set_schema(self.repo.schema)
-
-
-class MemSchemaOperation(Operation):
- """base class for schema operations"""
- def __init__(self, session, kobj=None, **kwargs):
- self.schema = session.schema
- self.kobj = kobj
- # once Operation.__init__ has been called, event may be triggered, so
- # do this last !
- Operation.__init__(self, session, **kwargs)
- # every schema operation is triggering a schema update
- MemSchemaNotifyChanges(session)
-
- def prepare_constraints(self, subjtype, rtype, objtype):
- constraints = rtype.rproperty(subjtype, objtype, 'constraints')
- self.constraints = list(constraints)
- rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
-
-
-class MemSchemaEarlyOperation(MemSchemaOperation):
- def insert_index(self):
- """schema operation which are inserted at the begining of the queue
- (typically to add/remove entity or relation types)
- """
- i = -1
- for i, op in enumerate(self.session.pending_operations):
- if not isinstance(op, MemSchemaEarlyOperation):
- return i
- return i + 1
-
-
-class MemSchemaPermissionOperation(MemSchemaOperation):
- """base class to synchronize schema permission definitions"""
- def __init__(self, session, perm, etype_eid):
- self.perm = perm
- try:
- self.name = entity_name(session, etype_eid)
- except IndexError:
- self.error('changing permission of a no more existant type #%s',
- etype_eid)
- else:
- Operation.__init__(self, session)
-
-
-# operations for high-level source database alteration ########################
-
-class SourceDbCWETypeRename(PreCommitOperation):
- """this operation updates physical storage accordingly"""
- oldname = newname = None # make pylint happy
-
- def precommit_event(self):
- # we need sql to operate physical changes on the system database
- sqlexec = self.session.system_sql
- sqlexec('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))
-
-
-class SourceDbCWRTypeUpdate(PreCommitOperation):
- """actually update some properties of a relation definition"""
- rschema = values = entity = None # make pylint happy
-
- def precommit_event(self):
- session = self.session
- rschema = self.rschema
- if rschema.is_final() or not 'inlined' in self.values:
- return # nothing to do
- inlined = self.values['inlined']
- entity = self.entity
- # check in-lining is necessary / possible
- if not entity.inlined_changed(inlined):
- return # nothing to do
- # inlined changed, make necessary physical changes!
- sqlexec = self.session.system_sql
- rtype = rschema.type
- eidcolumn = SQL_PREFIX + 'eid'
- if not inlined:
- # need to create the relation if it has not been already done by
- # another event of the same transaction
- if not rschema.type in session.transaction_data.get('createdtables', ()):
- tablesql = rschema2sql(rschema)
- # create the necessary table
- for sql in tablesql.split(';'):
- if sql.strip():
- sqlexec(sql)
- session.transaction_data.setdefault('createdtables', []).append(
- rschema.type)
- # copy existant data
- column = SQL_PREFIX + rtype
- for etype in rschema.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 columns
- for etype in rschema.subjects():
- DropColumn(session, table=SQL_PREFIX + str(etype),
- column=SQL_PREFIX + rtype)
- else:
- for etype in rschema.subjects():
- try:
- add_inline_relation_column(session, str(etype), rtype)
- except Exception, 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 dropped
- self.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} for eid, eid_to in cursor.fetchall()]
- if args:
- column = SQL_PREFIX + rtype
- cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
- % (table, column, eidcolumn), args)
- # drop existant table
- DropRelationTable(session, rtype)
-
-
-class SourceDbCWAttributeAdd(PreCommitOperation):
- """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 happy
-
- def init_rdef(self, **kwargs):
- entity = self.entity
- fromentity = entity.stype
- self.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.ordernum or 0})
- subj = str(fromentity.name)
- rtype = entity.rtype.name
- obj = 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)
- return rdef
-
- def precommit_event(self):
- session = self.session
- entity = self.entity
- # entity.defaultval is a string or None, but we need a correctly typed
- # value
- default = entity.defaultval
- if default is not None:
- default = TYPE_CONVERTER[entity.otype.name](default)
- rdef = self.init_rdef(default=default,
- indexed=entity.indexed,
- fulltextindexed=entity.fulltextindexed,
- internationalizable=entity.internationalizable)
- 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 INDEX
- if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
- extra_unique_index = True
- attrtype = attrtype.replace(' UNIQUE', '')
- else:
- extra_unique_index = False
- # added some str() wrapping query since some backend (eg psycopg) don't
- # allow unicode queries
- table = SQL_PREFIX + rdef.subject
- column = SQL_PREFIX + rdef.name
- try:
- 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)
- except Exception, 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 dropped
- self.error('error while altering table %s: %s', table, ex)
- if extra_unique_index or entity.indexed:
- try:
- sysource.create_index(session, table, column,
- unique=extra_unique_index)
- except Exception, ex:
- self.error('error while creating index for %s.%s: %s',
- table, column, ex)
-
-
-class SourceDbCWRelationAdd(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 happy
-
- def precommit_event(self):
- session = self.session
- entity = self.entity
- rdef = self.init_rdef(composite=entity.composite)
- schema = session.schema
- rtype = rdef.name
- rschema = session.schema.rschema(rtype)
- # this have to be done before permissions setting
- if rschema.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
- # transaction
- key = '%s.%s' % (rdef.subject, rtype)
- try:
- alreadythere = bool(rschema.objects(rdef.subject))
- except KeyError:
- alreadythere = False
- if not (alreadythere or
- key in session.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
- # transaction
- if not (rschema.subjects() or
- rtype in session.transaction_data.get('createdtables', ())):
- try:
- rschema = session.schema.rschema(rtype)
- tablesql = rschema2sql(rschema)
- except KeyError:
- # fake we add it to the schema now to get a correctly
- # initialized schema but remove it before doing anything
- # more dangerous...
- rschema = session.schema.add_relation_type(rdef)
- tablesql = rschema2sql(rschema)
- session.schema.del_relation_type(rtype)
- # create the necessary table
- for sql in tablesql.split(';'):
- if sql.strip():
- session.system_sql(sql)
- session.transaction_data.setdefault('createdtables', []).append(
- rtype)
-
-
-class SourceDbRDefUpdate(PreCommitOperation):
- """actually update some properties of a relation definition"""
- rschema = values = None # make pylint happy
-
- def precommit_event(self):
- etype = self.kobj[0]
- table = SQL_PREFIX + etype
- column = SQL_PREFIX + self.rschema.type
- if 'indexed' in self.values:
- sysource = self.session.pool.source('system')
- if self.values['indexed']:
- sysource.create_index(self.session, table, column)
- else:
- sysource.drop_index(self.session, table, column)
- if 'cardinality' in self.values and self.rschema.is_final():
- adbh = self.session.pool.source('system').dbhelper
- if not adbh.alter_column_support:
- # not supported (and NOT NULL not set by yams in that case, so
- # no worry)
- return
- atype = self.rschema.objects(etype)[0]
- constraints = self.rschema.rproperty(etype, atype, 'constraints')
- coltype = 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)
-
-
-class SourceDbCWConstraintAdd(PreCommitOperation):
- """actually update constraint of a relation definition"""
- entity = None # make pylint happy
- cancelled = False
-
- def precommit_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 here
- if rdef.eid in session.transaction_data.get('neweids', ()):
- return
- subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
- cstrtype = self.entity.type
- oldcstr = rtype.constraint_by_type(subjtype, objtype, 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 changes
- if newcstr.type() == 'SizeConstraint' and (
- oldcstr is None or oldcstr.max != newcstr.max):
- adbh = self.session.pool.source('system').dbhelper
- card = rtype.rproperty(subjtype, objtype, 'cardinality')
- coltype = 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)
- except Exception, ex:
- # not supported by sqlite for instance
- self.error('error while altering table %s: %s', table, ex)
- elif cstrtype == 'UniqueConstraint' and oldcstr is None:
- session.pool.source('system').create_index(
- self.session, table, column, unique=True)
-
-
-class SourceDbCWConstraintDel(PreCommitOperation):
- """actually remove a constraint of a relation definition"""
- rtype = subjtype = objtype = None # make pylint happy
-
- def precommit_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 changes
- if cstrtype == '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)
- except Exception, ex:
- # not supported by sqlite for instance
- self.error('error while altering table %s: %s', table, ex)
- elif cstrtype == 'UniqueConstraint':
- self.session.pool.source('system').drop_index(
- self.session, table, column, unique=True)
-
-
-# operations for in-memory schema synchronization #############################
-
-class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
- """actually add the entity type to the instance's schema"""
- eid = None # make pylint happy
- def commit_event(self):
- self.schema.add_entity_type(self.kobj)
-
-
-class MemSchemaCWETypeRename(MemSchemaOperation):
- """this operation updates physical storage accordingly"""
- oldname = newname = None # make pylint happy
-
- def commit_event(self):
- self.session.schema.rename_entity_type(self.oldname, self.newname)
-
-
-class MemSchemaCWETypeDel(MemSchemaOperation):
- """actually remove the entity type from the instance's schema"""
- def commit_event(self):
- try:
- # del_entity_type also removes entity's relations
- self.schema.del_entity_type(self.kobj)
- except KeyError:
- # s/o entity type have already been deleted
- pass
-
-
-class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
- """actually add the relation type to the instance's schema"""
- eid = None # make pylint happy
- def commit_event(self):
- rschema = self.schema.add_relation_type(self.kobj)
- rschema.set_default_groups()
-
-
-class MemSchemaCWRTypeUpdate(MemSchemaOperation):
- """actually update some properties of a relation definition"""
- rschema = values = None # make pylint happy
-
- def commit_event(self):
- # structure should be clean, not need to remove entity's relations
- # at this point
- self.rschema.__dict__.update(self.values)
-
-
-class MemSchemaCWRTypeDel(MemSchemaOperation):
- """actually remove the relation type from the instance's schema"""
- def commit_event(self):
- try:
- self.schema.del_relation_type(self.kobj)
- except KeyError:
- # s/o entity type have already been deleted
- pass
-
-
-class MemSchemaRDefAdd(MemSchemaEarlyOperation):
- """actually add the attribute relation definition to the instance's
- schema
- """
- def commit_event(self):
- self.schema.add_relation_def(self.kobj)
-
-
-class MemSchemaRDefUpdate(MemSchemaOperation):
- """actually update some properties of a relation definition"""
- rschema = values = None # make pylint happy
-
- def commit_event(self):
- # structure should be clean, not need to remove entity's relations
- # at this point
- self.rschema._rproperties[self.kobj].update(self.values)
-
-
-class MemSchemaRDefDel(MemSchemaOperation):
- """actually remove the relation definition from the instance's schema"""
- def commit_event(self):
- subjtype, rtype, objtype = self.kobj
- try:
- self.schema.del_relation_def(subjtype, rtype, objtype)
- except KeyError:
- # relation type may have been already deleted
- pass
-
-
-class MemSchemaCWConstraintAdd(MemSchemaOperation):
- """actually update constraint of a relation definition
-
- has to be called before SourceDbCWConstraintAdd
- """
- cancelled = False
-
- def precommit_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 here
- if rdef.eid in self.session.transaction_data.get('neweids', ()):
- self.cancelled = True
- return
- subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
- self.prepare_constraints(subjtype, rtype, objtype)
- cstrtype = self.entity.type
- self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
- self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
- self.newcstr.eid = self.entity.eid
-
- def commit_event(self):
- if self.cancelled:
- return
- # in-place modification
- if not self.cstr is None:
- self.constraints.remove(self.cstr)
- self.constraints.append(self.newcstr)
-
-
-class MemSchemaCWConstraintDel(MemSchemaOperation):
- """actually remove a constraint of a relation definition
-
- has to be called before SourceDbCWConstraintDel
- """
- rtype = subjtype = objtype = None # make pylint happy
- def precommit_event(self):
- self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
-
- def commit_event(self):
- self.constraints.remove(self.cstr)
-
-
-class MemSchemaPermissionCWGroupAdd(MemSchemaPermissionOperation):
- """synchronize schema when a *_permission relation has been added on a group
- """
- def __init__(self, session, perm, etype_eid, group_eid):
- self.group = entity_name(session, group_eid)
- super(MemSchemaPermissionCWGroupAdd, self).__init__(
- session, perm, etype_eid)
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- try:
- erschema = self.schema[self.name]
- except KeyError:
- # duh, schema not found, log error and skip operation
- self.error('no schema for %s', self.name)
- return
- groups = list(erschema.get_groups(self.perm))
- try:
- groups.index(self.group)
- self.warning('group %s already have permission %s on %s',
- self.group, self.perm, erschema.type)
- except ValueError:
- groups.append(self.group)
- erschema.set_groups(self.perm, groups)
-
-
-class MemSchemaPermissionCWGroupDel(MemSchemaPermissionCWGroupAdd):
- """synchronize schema when a *_permission relation has been deleted from a
- group
- """
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- try:
- erschema = self.schema[self.name]
- except KeyError:
- # duh, schema not found, log error and skip operation
- self.error('no schema for %s', self.name)
- return
- groups = list(erschema.get_groups(self.perm))
- try:
- groups.remove(self.group)
- erschema.set_groups(self.perm, groups)
- except ValueError:
- self.error('can\'t remove permission %s on %s to group %s',
- self.perm, erschema.type, self.group)
-
-
-class MemSchemaPermissionRQLExpressionAdd(MemSchemaPermissionOperation):
- """synchronize schema when a *_permission relation has been added on a rql
- expression
- """
- def __init__(self, session, perm, etype_eid, expression):
- self.expr = expression
- super(MemSchemaPermissionRQLExpressionAdd, self).__init__(
- session, perm, etype_eid)
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- try:
- erschema = self.schema[self.name]
- except KeyError:
- # duh, schema not found, log error and skip operation
- self.error('no schema for %s', self.name)
- return
- exprs = list(erschema.get_rqlexprs(self.perm))
- exprs.append(erschema.rql_expression(self.expr))
- erschema.set_rqlexprs(self.perm, exprs)
-
-
-class MemSchemaPermissionRQLExpressionDel(MemSchemaPermissionRQLExpressionAdd):
- """synchronize schema when a *_permission relation has been deleted from an
- rql expression
- """
-
- def commit_event(self):
- """the observed connections pool has been commited"""
- try:
- erschema = self.schema[self.name]
- except KeyError:
- # duh, schema not found, log error and skip operation
- self.error('no schema for %s', self.name)
- return
- rqlexprs = list(erschema.get_rqlexprs(self.perm))
- for i, rqlexpr in enumerate(rqlexprs):
- if rqlexpr.expression == self.expr:
- rqlexprs.pop(i)
- break
- else:
- self.error('can\'t remove permission %s on %s for expression %s',
- self.perm, erschema.type, self.expr)
- return
- erschema.set_rqlexprs(self.perm, rqlexprs)
-
-
-# deletion hooks ###############################################################
-
-def before_del_eetype(session, eid):
- """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
- """
- # final entities can't be deleted, don't care about that
- name = check_internal_entity(session, eid, CORE_ETYPES)
- # delete every entities of this type
- session.unsafe_execute('DELETE %s X' % name)
- DropTable(session, table=SQL_PREFIX + name)
- MemSchemaCWETypeDel(session, name)
-
-
-def after_del_eetype(session, eid):
- # workflow cleanup
- session.execute('DELETE State X WHERE NOT X state_of Y')
- session.execute('DELETE Transition X WHERE NOT X transition_of Y')
-
-
-def before_del_ertype(session, eid):
- """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
- """
- name = check_internal_entity(session, eid, CORE_RTYPES)
- # delete relation definitions using this relation type
- session.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
- {'x': eid})
- session.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
- {'x': eid})
- MemSchemaCWRTypeDel(session, name)
-
-
-def after_del_relation_type(session, rdefeid, rtype, rteid):
- """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
- """
- subjschema, rschema, objschema = session.schema.schema_by_eid(rdefeid)
- pendings = session.transaction_data.get('pendingeids', ())
- # first delete existing relation if necessary
- if rschema.is_final():
- rdeftype = 'CWAttribute'
- else:
- rdeftype = 'CWRelation'
- if not (subjschema.eid in pendings or objschema.eid in pendings):
- pending = session.transaction_data.setdefault('pendingrdefs', set())
- pending.add((subjschema, rschema, objschema))
- session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
- % (rschema, subjschema, objschema))
- execute = session.unsafe_execute
- rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
- 'R eid %%(x)s' % rdeftype, {'x': rteid})
- 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 relations
-
- if (rschema.is_final() or rschema.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': rteid, 'name': str(subjschema)})
- if rset[0][0] == 0 and not subjschema.eid in pendings:
- ptypes = session.transaction_data.setdefault('pendingrtypes', set())
- ptypes.add(rschema.type)
- DropColumn(session, table=SQL_PREFIX + subjschema.type,
- column=SQL_PREFIX + rschema.type)
- elif lastrel:
- DropRelationTable(session, rschema.type)
- # if this is the last instance, drop associated relation type
- if lastrel and not rteid in pendings:
- execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
- MemSchemaRDefDel(session, (subjschema, rschema, objschema))
-
-
-# addition hooks ###############################################################
-
-def before_add_eetype(session, entity):
- """before adding a CWEType entity:
- * check that we are not using an existing entity type,
- """
- name = entity['name']
- schema = session.schema
- if name in schema and schema[name].eid is not None:
- raise RepositoryError('an entity type %s already exists' % name)
-
-def after_add_eetype(session, entity):
- """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
- """
- if entity.get('final'):
- return
- schema = session.schema
- name = 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 = session.schema
- eschema = schema.add_entity_type(etype)
- eschema.set_default_groups()
- # generate table sql and rql to add metadata
- tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema,
- prefix=SQL_PREFIX)
- relrqls = []
- for rtype in (META_RTYPES - VIRTUAL_RTYPES):
- rschema = schema[rtype]
- sampletype = rschema.subjects()[0]
- desttype = rschema.objects()[0]
- props = rschema.rproperties(sampletype, desttype)
- relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
- # now remove it !
- schema.del_entity_type(name)
- # create the necessary table
- for sql in tablesql.split(';'):
- if sql.strip():
- session.system_sql(sql)
- # register operation to modify the schema on commit
- # this have to be done before adding other relations definitions
- # or permission settings
- etype.eid = entity.eid
- MemSchemaCWETypeAdd(session, etype)
- # add meta relations
- for rql, kwargs in relrqls:
- session.execute(rql, kwargs)
-
-
-def before_add_ertype(session, entity):
- """before adding a CWRType entity:
- * check that we are not using an existing relation type,
- * register an operation to add the relation type to the instance's
- schema on commit
-
- We don't know yeat this point if a table is necessary
- """
- name = entity['name']
- if name in session.schema.relations():
- raise RepositoryError('a relation type %s already exists' % name)
-
-
-def after_add_ertype(session, entity):
- """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 yeat this point if a table is necessary
- """
- rtype = RelationType(name=entity['name'],
- description=entity.get('description'),
- meta=entity.get('meta', False),
- inlined=entity.get('inlined', False),
- symetric=entity.get('symetric', False))
- rtype.eid = entity.eid
- MemSchemaCWRTypeAdd(session, rtype)
-
-
-def after_add_efrdef(session, entity):
- SourceDbCWAttributeAdd(session, entity=entity)
-
-def after_add_enfrdef(session, entity):
- SourceDbCWRelationAdd(session, entity=entity)
-
-
-# update hooks #################################################################
-
-def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
- errors = {}
- # don't use getattr(entity, attr), we would get the modified value if any
- for attr in ro_attrs:
- origval = entity_attr(session, entity.eid, attr)
- if entity.get(attr, origval) != origval:
- errors[attr] = session._("can't change the %s attribute") % \
- display_name(session, attr)
- if errors:
- raise ValidationError(entity.eid, errors)
-
-def before_update_eetype(session, entity):
- """check name change, handle final"""
- check_valid_changes(session, entity, ro_attrs=('final',))
- # don't use getattr(entity, attr), we would get the modified value if any
- oldname = entity_attr(session, entity.eid, 'name')
- newname = entity.get('name', oldname)
- if newname.lower() != oldname.lower():
- SourceDbCWETypeRename(session, oldname=oldname, newname=newname)
- MemSchemaCWETypeRename(session, oldname=oldname, newname=newname)
-
-def before_update_ertype(session, entity):
- """check name change, handle final"""
- check_valid_changes(session, entity)
-
-
-def after_update_erdef(session, entity):
- if entity.eid in session.transaction_data.get('pendingeids', ()):
- return
- desttype = entity.otype.name
- rschema = session.schema[entity.rtype.name]
- newvalues = {}
- for prop in rschema.rproperty_defs(desttype):
- if prop == 'constraints':
- continue
- if prop == 'order':
- prop = 'ordernum'
- if prop in entity.edited_attributes:
- newvalues[prop] = entity[prop]
- if newvalues:
- subjtype = entity.stype.name
- MemSchemaRDefUpdate(session, kobj=(subjtype, desttype),
- rschema=rschema, values=newvalues)
- SourceDbRDefUpdate(session, kobj=(subjtype, desttype),
- rschema=rschema, values=newvalues)
-
-def after_update_ertype(session, entity):
- rschema = session.schema.rschema(entity.name)
- newvalues = {}
- for prop in ('meta', 'symetric', 'inlined'):
- if prop in entity:
- newvalues[prop] = entity[prop]
- if newvalues:
- MemSchemaCWRTypeUpdate(session, rschema=rschema, values=newvalues)
- SourceDbCWRTypeUpdate(session, rschema=rschema, values=newvalues,
- entity=entity)
-
-# constraints synchronization hooks ############################################
-
-def after_add_econstraint(session, entity):
- MemSchemaCWConstraintAdd(session, entity=entity)
- SourceDbCWConstraintAdd(session, entity=entity)
-
-
-def after_update_econstraint(session, entity):
- MemSchemaCWConstraintAdd(session, entity=entity)
- SourceDbCWConstraintAdd(session, entity=entity)
-
-
-def before_delete_constrained_by(session, fromeid, rtype, toeid):
- if not fromeid in session.transaction_data.get('pendingeids', ()):
- schema = session.schema
- entity = session.entity_from_eid(toeid)
- subjtype, rtype, objtype = schema.schema_by_eid(fromeid)
- try:
- cstr = rtype.constraint_by_type(subjtype, objtype,
- entity.cstrtype[0].name)
- except IndexError:
- session.critical('constraint type no more accessible')
- else:
- SourceDbCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
- objtype=objtype, cstr=cstr)
- MemSchemaCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
- objtype=objtype, cstr=cstr)
-
-
-def after_add_constrained_by(session, fromeid, rtype, toeid):
- if fromeid in session.transaction_data.get('neweids', ()):
- session.transaction_data.setdefault(fromeid, []).append(toeid)
-
-
-# permissions synchronization hooks ############################################
-
-def after_add_permission(session, subject, rtype, object):
- """added entity/relation *_permission, need to update schema"""
- perm = rtype.split('_', 1)[0]
- if session.describe(object)[0] == 'CWGroup':
- MemSchemaPermissionCWGroupAdd(session, perm, subject, object)
- else: # RQLExpression
- expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
- {'x': object}, 'x')[0][0]
- MemSchemaPermissionRQLExpressionAdd(session, perm, subject, expr)
-
-
-def before_del_permission(session, subject, rtype, object):
- """delete entity/relation *_permission, need to update schema
-
- skip the operation if the related type is being deleted
- """
- if subject in session.transaction_data.get('pendingeids', ()):
- return
- perm = rtype.split('_', 1)[0]
- if session.describe(object)[0] == 'CWGroup':
- MemSchemaPermissionCWGroupDel(session, perm, subject, object)
- else: # RQLExpression
- expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
- {'x': object}, 'x')[0][0]
- MemSchemaPermissionRQLExpressionDel(session, perm, subject, expr)
-
-
-def rebuild_infered_relations(session, subject, rtype, object):
- # registering a schema operation will trigger a call to
- # repo.set_schema() on commit which will in turn rebuild
- # infered relation definitions
- MemSchemaNotifyChanges(session)
-
-
-def _register_schema_hooks(hm):
- """register schema related hooks on the hooks manager"""
- # schema synchronisation #####################
- # before/after add
- hm.register_hook(before_add_eetype, 'before_add_entity', 'CWEType')
- hm.register_hook(before_add_ertype, 'before_add_entity', 'CWRType')
- hm.register_hook(after_add_eetype, 'after_add_entity', 'CWEType')
- hm.register_hook(after_add_ertype, 'after_add_entity', 'CWRType')
- hm.register_hook(after_add_efrdef, 'after_add_entity', 'CWAttribute')
- hm.register_hook(after_add_enfrdef, 'after_add_entity', 'CWRelation')
- # before/after update
- hm.register_hook(before_update_eetype, 'before_update_entity', 'CWEType')
- hm.register_hook(before_update_ertype, 'before_update_entity', 'CWRType')
- hm.register_hook(after_update_ertype, 'after_update_entity', 'CWRType')
- hm.register_hook(after_update_erdef, 'after_update_entity', 'CWAttribute')
- hm.register_hook(after_update_erdef, 'after_update_entity', 'CWRelation')
- # before/after delete
- hm.register_hook(before_del_eetype, 'before_delete_entity', 'CWEType')
- hm.register_hook(after_del_eetype, 'after_delete_entity', 'CWEType')
- hm.register_hook(before_del_ertype, 'before_delete_entity', 'CWRType')
- hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type')
- hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes')
- hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes')
- # constraints synchronization hooks
- hm.register_hook(after_add_econstraint, 'after_add_entity', 'CWConstraint')
- hm.register_hook(after_update_econstraint, 'after_update_entity', 'CWConstraint')
- hm.register_hook(before_delete_constrained_by, 'before_delete_relation', 'constrained_by')
- hm.register_hook(after_add_constrained_by, 'after_add_relation', 'constrained_by')
- # permissions synchronisation ################
- for perm in ('read_permission', 'add_permission',
- 'delete_permission', 'update_permission'):
- hm.register_hook(after_add_permission, 'after_add_relation', perm)
- hm.register_hook(before_del_permission, 'before_delete_relation', perm)
--- a/server/securityhooks.py Fri Aug 14 09:20:33 2009 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-"""Security hooks: check permissions to add/delete/update entities according to
-the user connected to a session
-
-: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"
-
-from cubicweb import Unauthorized
-from cubicweb.server.pool import LateOperation
-from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS
-
-def check_entity_attributes(session, entity):
- eid = entity.eid
- eschema = entity.e_schema
- # ._default_set is only there on entity creation to indicate unspecified
- # attributes which has been set to a default value defined in the schema
- defaults = getattr(entity, '_default_set', ())
- try:
- editedattrs = entity.edited_attributes
- except AttributeError:
- editedattrs = entity.keys()
- for attr in editedattrs:
- if attr in defaults:
- continue
- rschema = eschema.subject_relation(attr)
- if rschema.is_final(): # non final relation are checked by other hooks
- # add/delete should be equivalent (XXX: unify them into 'update' ?)
- rschema.check_perm(session, 'add', eid)
-
-
-class CheckEntityPermissionOp(LateOperation):
- def precommit_event(self):
- #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
- self.entity.check_perm(self.action)
- check_entity_attributes(self.session, self.entity)
-
- def commit_event(self):
- pass
-
-
-class CheckRelationPermissionOp(LateOperation):
- def precommit_event(self):
- self.rschema.check_perm(self.session, self.action, self.fromeid, self.toeid)
-
- def commit_event(self):
- pass
-
-def after_add_entity(session, entity):
- if not session.is_super_session:
- CheckEntityPermissionOp(session, entity=entity, action='add')
-
-def after_update_entity(session, entity):
- if not session.is_super_session:
- try:
- # check user has permission right now, if not retry at commit time
- entity.check_perm('update')
- check_entity_attributes(session, entity)
- except Unauthorized:
- entity.clear_local_perm_cache('update')
- CheckEntityPermissionOp(session, entity=entity, action='update')
-
-def before_del_entity(session, eid):
- if not session.is_super_session:
- eschema = session.repo.schema[session.describe(eid)[0]]
- eschema.check_perm(session, 'delete', eid)
-
-
-def before_add_relation(session, fromeid, rtype, toeid):
- if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
- rschema = session.repo.schema[rtype]
- rschema.check_perm(session, 'add', fromeid, toeid)
-
-def after_add_relation(session, fromeid, rtype, toeid):
- if not rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
- rschema = session.repo.schema[rtype]
- if rtype in ON_COMMIT_ADD_RELATIONS:
- CheckRelationPermissionOp(session, action='add', rschema=rschema,
- fromeid=fromeid, toeid=toeid)
- else:
- rschema.check_perm(session, 'add', fromeid, toeid)
-
-def before_del_relation(session, fromeid, rtype, toeid):
- if not session.is_super_session:
- session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid)
-
-def register_security_hooks(hm):
- """register meta-data related hooks on the hooks manager"""
- hm.register_hook(after_add_entity, 'after_add_entity', '')
- hm.register_hook(after_update_entity, 'after_update_entity', '')
- hm.register_hook(before_del_entity, 'before_delete_entity', '')
- hm.register_hook(before_add_relation, 'before_add_relation', '')
- hm.register_hook(after_add_relation, 'after_add_relation', '')
- hm.register_hook(before_del_relation, 'before_delete_relation', '')
-
--- a/server/serverconfig.py Fri Aug 14 09:20:33 2009 +0200
+++ b/server/serverconfig.py Fri Aug 14 09:26:41 2009 +0200
@@ -82,7 +82,7 @@
else:
BACKUP_DIR = '/var/lib/cubicweb/backup/'
- cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['sobjects'])
+ cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['sobjects', 'hooks'])
cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['sobjects', 'hooks'])
options = merge_options((
@@ -185,14 +185,9 @@
# check user's state at login time
consider_user_state = True
- # hooks registration configuration
+ # hooks activation configuration
# all hooks should be activated during normal execution
- core_hooks = True
- usergroup_hooks = True
- schema_hooks = True
- notification_hooks = True
- security_hooks = True
- instance_hooks = True
+ disabled_hooks_categories = set()
# should some hooks be deactivated during [pre|post]create script execution
free_wheel = False
@@ -256,22 +251,6 @@
"""pyro is always enabled in standalone repository configuration"""
return True
- def load_hooks(self, vreg):
- hooks = {}
- try:
- apphookdefs = vreg['hooks'].all_objects()
- except RegistryNotFound:
- return hooks
- for hookdef in apphookdefs:
- # XXX < 3.5 bw compat
- hookdef.__dict__['config'] = self
- for event, ertype in hookdef.register_to(vreg.schema):
- if ertype == 'Any':
- ertype = ''
- cb = hookdef.make_callback(event)
- hooks.setdefault(event, {}).setdefault(ertype, []).append(cb)
- return hooks
-
def load_schema(self, expand_cubes=False, **kwargs):
from cubicweb.schema import CubicWebSchemaLoader
if expand_cubes:
--- a/test/unittest_cwconfig.py Fri Aug 14 09:20:33 2009 +0200
+++ b/test/unittest_cwconfig.py Fri Aug 14 09:26:41 2009 +0200
@@ -77,7 +77,7 @@
def test_vregistry_path(self):
self.assertEquals([unabsolutize(p) for p in self.config.vregistry_path()],
- ['entities', 'web/views', 'sobjects',
+ ['entities', 'web/views', 'sobjects', 'hooks',
'file/entities.py', 'file/views', 'file/hooks.py',
'email/entities.py', 'email/views', 'email/hooks.py',
'test/data/entities.py'])
--- a/test/unittest_entity.py Fri Aug 14 09:20:33 2009 +0200
+++ b/test/unittest_entity.py Fri Aug 14 09:26:41 2009 +0200
@@ -101,7 +101,7 @@
user = self.entity('Any X WHERE X eid %(x)s', {'x':self.user().eid}, 'x')
adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
self.commit()
- self.assertEquals(user._related_cache.keys(), [])
+ self.assertEquals(user._related_cache, {})
email = user.primary_email[0]
self.assertEquals(sorted(user._related_cache), ['primary_email_subject'])
self.assertEquals(email._related_cache.keys(), ['primary_email_object'])