# HG changeset patch # User Sylvain Thénault # Date 1250234801 -7200 # Node ID 04034421b072119d68e8666d29e527285ed9bde5 # Parent 7df3494ae657b10394e8c2ddf6a345b8fd40dae9 [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 diff -r 7df3494ae657 -r 04034421b072 debian/cubicweb-dev.install.in --- 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/ diff -r 7df3494ae657 -r 04034421b072 debian/cubicweb-server.install.in --- 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/ diff -r 7df3494ae657 -r 04034421b072 debian/rules --- 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 diff -r 7df3494ae657 -r 04034421b072 hooks/__init__.py --- /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""" diff -r 7df3494ae657 -r 04034421b072 hooks/integrity.py --- /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 + + diff -r 7df3494ae657 -r 04034421b072 hooks/metadata.py --- /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)) + diff -r 7df3494ae657 -r 04034421b072 hooks/security.py --- /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) + diff -r 7df3494ae657 -r 04034421b072 hooks/syncschema.py --- /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) diff -r 7df3494ae657 -r 04034421b072 hooks/syncsession.py --- /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) diff -r 7df3494ae657 -r 04034421b072 hooks/workflow.py --- /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') diff -r 7df3494ae657 -r 04034421b072 server/hook.py --- /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 diff -r 7df3494ae657 -r 04034421b072 server/hooks.py --- 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') diff -r 7df3494ae657 -r 04034421b072 server/hooksmanager.py --- 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 occurs - - 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 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) diff -r 7df3494ae657 -r 04034421b072 server/migractions.py --- 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 ###################################################### diff -r 7df3494ae657 -r 04034421b072 server/pool.py --- 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) diff -r 7df3494ae657 -r 04034421b072 server/repository.py --- 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 ########################################################### diff -r 7df3494ae657 -r 04034421b072 server/schemahooks.py --- 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) diff -r 7df3494ae657 -r 04034421b072 server/securityhooks.py --- 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', '') - diff -r 7df3494ae657 -r 04034421b072 server/serverconfig.py --- 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: diff -r 7df3494ae657 -r 04034421b072 test/unittest_cwconfig.py --- 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']) diff -r 7df3494ae657 -r 04034421b072 test/unittest_entity.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'])