--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/hooks.py Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,567 @@
+"""Core hooks: check schema validity, unsure we are not deleting necessary
+entities...
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from mx.DateTime import now
+
+from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+
+from cubicweb.common.uilib import soup2xhtml
+
+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
+
+def relation_deleted(session, eidfrom, rtype, eidto):
+ session.add_query_data('pendingrelations', (eidfrom, rtype, eidto))
+
+
+# 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
+ """
+ if not 'creation_date' in entity:
+ entity['creation_date'] = now()
+ if not 'modification_date' in entity:
+ entity['modification_date'] = now()
+
+def setmtime_before_update_entity(session, entity):
+ """update an entity -> set modification date"""
+ if not 'modification_date' in entity:
+ entity['modification_date'] = now()
+
+class SetCreatorOp(PreCommitOperation):
+
+ def precommit_event(self):
+ if self.eid in self.session.query_data('pendingeids', ()):
+ # entity have been created and deleted in the same transaction
+ return
+ ueid = self.session.user.eid
+ execute = self.session.unsafe_execute
+ if not execute('Any X WHERE X created_by U, X eid %(x)s',
+ {'x': self.eid}, 'x'):
+ execute('SET X created_by U WHERE X eid %(x)s, U eid %(u)s',
+ {'x': self.eid, 'u': ueid}, 'x')
+
+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.unsafe_execute('SET X owned_by U WHERE X eid %(x)s, U eid %(u)s',
+ {'x': entity.eid, 'u': asession.user.eid}, 'x')
+ SetCreatorOp(asession, eid=entity.eid)
+
+def setis_after_add_entity(session, entity):
+ """create a new entity -> set is relation"""
+ session.unsafe_execute('SET X is E WHERE X eid %(x)s, E name %(name)s',
+ {'x': entity.eid, 'name': entity.id}, 'x')
+ # XXX < 2.50 bw compat
+ if not session.get_shared_data('do-not-insert-is_instance_of'):
+ basetypes = entity.e_schema.ancestors() + [entity.e_schema]
+ session.unsafe_execute('SET X is_instance_of E WHERE X eid %%(x)s, E name IN (%s)' %
+ ','.join("'%s'" % str(etype) for etype in basetypes),
+ {'x': entity.eid}, 'x')
+
+def setowner_after_add_user(session, entity):
+ """when a user has been created, add owned_by relation on itself"""
+ session.unsafe_execute('SET X owned_by X WHERE X eid %(x)s',
+ {'x': entity.eid}, 'x')
+
+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(eidto))
+ elif ftcontainer == 'object':
+ FTIndexEntityOp(session, entity=session.entity(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(eidto))
+ FTIndexEntityOp(session, entity=session.entity(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
+ 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 'EUser' in hm.schema:
+ hm.register_hook(setowner_after_add_user, 'after_add_entity', 'EUser')
+
+# 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
+ if not self.eid in session.query_data('pendingeids', ()):
+ etype = session.describe(self.eid)[0]
+ session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
+ % (etype, self.relation),
+ {'x': self.eid}, 'x')
+
+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.query_data('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 tidy_html_fields(object):
+ """tidy HTML in rich text strings
+
+ FIXME: (adim) the whole idea of having a class is to store the
+ event type. There might be another way to get dynamically the
+ event inside the hook function.
+ """
+ # FIXME hooks manager use func_name to register
+ func_name = 'tidy_html_field'
+
+ def __init__(self, event):
+ self.event = event
+
+ def __call__(self, session, entity):
+ for attr in entity.formatted_attrs():
+ value = entity.get(attr)
+ # text was not changed
+ if self.event == 'before_add_entity':
+ fmt = entity.get('%s_format' % attr)
+ else:
+ fmt = entity.get_value('%s_format' % attr)
+ if value and fmt == 'text/html':
+ entity[attr] = soup2xhtml(value, session.encoding)
+
+
+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.query_data('pendingeids', ()):
+ return
+ if self.session.unsafe_execute(*self._rql()).rowcount < 1:
+ etype = self.session.describe(self.eid)[0]
+ msg = self.session._('at least one relation %s is required on %s(%s)')
+ raise ValidationError(self.eid, {self.rtype: msg % (self.rtype,
+ etype, self.eid)})
+
+ 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 ('owned_by', 'created_by', 'is', 'is_instance_of'):
+ 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"""
+ card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
+ pendingeids = session.query_data('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', 'EGroup')
+
+ #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', '')
+ hm.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
+ hm.register_hook(tidy_html_fields('before_update_entity'), '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', 'EUser')
+ 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(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
+ rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
+ {'name': str(entity.e_schema)})
+ # if there is an initial state and the entity's state is not set,
+ # use the initial state as a default state
+ pendingeids = session.query_data('pendingeids', ())
+ if rset and not entity.eid in pendingeids and not entity.in_state:
+ session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+ {'x' : entity.eid, 's' : rset[0][0]}, 'x')
+
+
+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))
+
+
+# EProperty hooks #############################################################
+
+
+class DelEPropertyOp(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 ChangeEPropertyOp(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 AddEPropertyOp(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 ChangeEPropertyOp 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.unsafe_execute('SET P for_user U WHERE P eid %(x)s,U eid %(u)s',
+ {'x': entity.eid, 'u': session.user.eid}, 'x')
+ else:
+ AddEPropertyOp(session, eprop=entity)
+
+def after_update_eproperty(session, entity):
+ 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):
+ ChangeEPropertyOp(session, epropdict=session_.user.properties,
+ key=key, value=value)
+ else:
+ # site wide properties
+ ChangeEPropertyOp(session, epropdict=session.vreg.eprop_values,
+ key=key, value=value)
+
+def before_del_eproperty(session, eid):
+ for eidfrom, rtype, eidto in session.query_data('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]
+ DelEPropertyOp(session, epropdict=session.vreg.eprop_values, key=key)
+
+def after_add_for_user(session, fromeid, rtype, toeid):
+ if not session.describe(fromeid)[0] == 'EProperty':
+ 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):
+ ChangeEPropertyOp(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):
+ DelEPropertyOp(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', 'EProperty')
+ hm.register_hook(after_update_eproperty, 'after_update_entity', 'EProperty')
+ hm.register_hook(before_del_eproperty, 'before_delete_entity', 'EProperty')
+ hm.register_hook(after_add_for_user, 'after_add_relation', 'for_user')
+ hm.register_hook(before_del_for_user, 'before_delete_relation', 'for_user')