server/hooks.py
changeset 0 b97547f5f1fa
child 62 ef06f71533d9
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """Core hooks: check schema validity, unsure we are not deleting necessary
       
     2 entities...
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from mx.DateTime import now
       
    11 
       
    12 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
       
    13 
       
    14 from cubicweb.common.uilib import soup2xhtml
       
    15 
       
    16 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
       
    17 from cubicweb.server.hookhelper import (check_internal_entity, previous_state,
       
    18                                      get_user_sessions, rproperty)
       
    19 from cubicweb.server.repository import FTIndexEntityOp
       
    20 
       
    21 def relation_deleted(session, eidfrom, rtype, eidto):
       
    22     session.add_query_data('pendingrelations', (eidfrom, rtype, eidto))
       
    23     
       
    24 
       
    25 # base meta-data handling #####################################################
       
    26 
       
    27 def setctime_before_add_entity(session, entity):
       
    28     """before create a new entity -> set creation and modification date
       
    29  
       
    30     this is a conveniency hook, you shouldn't have to disable it
       
    31     """
       
    32     if not 'creation_date' in entity:
       
    33         entity['creation_date'] = now()
       
    34     if not 'modification_date' in entity:
       
    35         entity['modification_date'] = now()
       
    36 
       
    37 def setmtime_before_update_entity(session, entity):
       
    38     """update an entity -> set modification date"""
       
    39     if not 'modification_date' in entity:
       
    40         entity['modification_date'] = now()
       
    41         
       
    42 class SetCreatorOp(PreCommitOperation):
       
    43         
       
    44     def precommit_event(self):
       
    45         if self.eid in self.session.query_data('pendingeids', ()):
       
    46             # entity have been created and deleted in the same transaction
       
    47             return
       
    48         ueid = self.session.user.eid
       
    49         execute = self.session.unsafe_execute
       
    50         if not execute('Any X WHERE X created_by U, X eid %(x)s',
       
    51                        {'x': self.eid}, 'x'): 
       
    52             execute('SET X created_by U WHERE X eid %(x)s, U eid %(u)s',
       
    53                     {'x': self.eid, 'u': ueid}, 'x')
       
    54 
       
    55 def setowner_after_add_entity(session, entity):
       
    56     """create a new entity -> set owner and creator metadata"""
       
    57     asession = session.actual_session()
       
    58     if not asession.is_internal_session:
       
    59         session.unsafe_execute('SET X owned_by U WHERE X eid %(x)s, U eid %(u)s',
       
    60                                {'x': entity.eid, 'u': asession.user.eid}, 'x')
       
    61         SetCreatorOp(asession, eid=entity.eid)
       
    62 
       
    63 def setis_after_add_entity(session, entity):
       
    64     """create a new entity -> set is relation"""
       
    65     session.unsafe_execute('SET X is E WHERE X eid %(x)s, E name %(name)s',
       
    66                            {'x': entity.eid, 'name': entity.id}, 'x')
       
    67     # XXX < 2.50 bw compat
       
    68     if not session.get_shared_data('do-not-insert-is_instance_of'):
       
    69         basetypes = entity.e_schema.ancestors() + [entity.e_schema]
       
    70         session.unsafe_execute('SET X is_instance_of E WHERE X eid %%(x)s, E name IN (%s)' %
       
    71                                ','.join("'%s'" % str(etype) for etype in basetypes),
       
    72                                {'x': entity.eid}, 'x')
       
    73 
       
    74 def setowner_after_add_user(session, entity):
       
    75     """when a user has been created, add owned_by relation on itself"""
       
    76     session.unsafe_execute('SET X owned_by X WHERE X eid %(x)s',
       
    77                            {'x': entity.eid}, 'x')
       
    78 
       
    79 def fti_update_after_add_relation(session, eidfrom, rtype, eidto):
       
    80     """sync fulltext index when relevant relation is added. Reindexing the
       
    81     contained entity is enough since it will implicitly reindex the container
       
    82     entity.
       
    83     """
       
    84     ftcontainer = session.repo.schema.rschema(rtype).fulltext_container
       
    85     if ftcontainer == 'subject':
       
    86         FTIndexEntityOp(session, entity=session.entity(eidto))
       
    87     elif ftcontainer == 'object':
       
    88         FTIndexEntityOp(session, entity=session.entity(eidfrom))
       
    89 
       
    90 def fti_update_after_delete_relation(session, eidfrom, rtype, eidto):
       
    91     """sync fulltext index when relevant relation is deleted. Reindexing both
       
    92     entities is necessary.
       
    93     """
       
    94     if session.repo.schema.rschema(rtype).fulltext_container:
       
    95         FTIndexEntityOp(session, entity=session.entity(eidto))
       
    96         FTIndexEntityOp(session, entity=session.entity(eidfrom))
       
    97     
       
    98 class SyncOwnersOp(PreCommitOperation):
       
    99         
       
   100     def precommit_event(self):
       
   101         self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
       
   102                                     'NOT EXISTS(X owned_by U, X eid %(x)s)',
       
   103                                     {'c': self.compositeeid, 'x': self.composedeid},
       
   104                                     ('c', 'x'))
       
   105         
       
   106 def sync_owner_after_add_composite_relation(session, eidfrom, rtype, eidto):
       
   107     """when adding composite relation, the composed should have the same owners
       
   108     has the composite
       
   109     """
       
   110     if rtype == 'wf_info_for':
       
   111         # skip this special composite relation
       
   112         return
       
   113     composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
       
   114     if composite == 'subject':
       
   115         SyncOwnersOp(session, compositeeid=eidfrom, composedeid=eidto)
       
   116     elif composite == 'object':
       
   117         SyncOwnersOp(session, compositeeid=eidto, composedeid=eidfrom)
       
   118     
       
   119 def _register_metadata_hooks(hm):
       
   120     """register meta-data related hooks on the hooks manager"""
       
   121     hm.register_hook(setctime_before_add_entity, 'before_add_entity', '')
       
   122     hm.register_hook(setmtime_before_update_entity, 'before_update_entity', '')
       
   123     hm.register_hook(setowner_after_add_entity, 'after_add_entity', '')
       
   124     hm.register_hook(sync_owner_after_add_composite_relation, 'after_add_relation', '')
       
   125     hm.register_hook(fti_update_after_add_relation, 'after_add_relation', '')
       
   126     hm.register_hook(fti_update_after_delete_relation, 'after_delete_relation', '')
       
   127     if 'is' in hm.schema:
       
   128         hm.register_hook(setis_after_add_entity, 'after_add_entity', '')
       
   129     if 'EUser' in hm.schema:
       
   130         hm.register_hook(setowner_after_add_user, 'after_add_entity', 'EUser')
       
   131             
       
   132 # core hooks ##################################################################
       
   133     
       
   134 class DelayedDeleteOp(PreCommitOperation):
       
   135     """delete the object of composite relation except if the relation
       
   136     has actually been redirected to another composite
       
   137     """
       
   138         
       
   139     def precommit_event(self):
       
   140         session = self.session
       
   141         if not self.eid in session.query_data('pendingeids', ()):
       
   142             etype = session.describe(self.eid)[0]
       
   143             session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
       
   144                                    % (etype, self.relation),
       
   145                                    {'x': self.eid}, 'x')
       
   146     
       
   147 def handle_composite_before_del_relation(session, eidfrom, rtype, eidto):
       
   148     """delete the object of composite relation"""
       
   149     composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
       
   150     if composite == 'subject':
       
   151         DelayedDeleteOp(session, eid=eidto, relation='Y %s X' % rtype)
       
   152     elif composite == 'object':
       
   153         DelayedDeleteOp(session, eid=eidfrom, relation='X %s Y' % rtype)
       
   154 
       
   155 def before_del_group(session, eid):
       
   156     """check that we don't remove the owners group"""
       
   157     check_internal_entity(session, eid, ('owners',))
       
   158 
       
   159 
       
   160 # schema validation hooks #####################################################
       
   161         
       
   162 class CheckConstraintsOperation(LateOperation):
       
   163     """check a new relation satisfy its constraints
       
   164     """
       
   165     def precommit_event(self):
       
   166         eidfrom, rtype, eidto = self.rdef
       
   167         # first check related entities have not been deleted in the same
       
   168         # transaction
       
   169         pending = self.session.query_data('pendingeids', ())
       
   170         if eidfrom in pending:
       
   171             return
       
   172         if eidto in pending:
       
   173             return
       
   174         for constraint in self.constraints:
       
   175             try:
       
   176                 constraint.repo_check(self.session, eidfrom, rtype, eidto)
       
   177             except NotImplementedError:
       
   178                 self.critical('can\'t check constraint %s, not supported',
       
   179                               constraint)
       
   180     
       
   181     def commit_event(self):
       
   182         pass
       
   183     
       
   184 def cstrcheck_after_add_relation(session, eidfrom, rtype, eidto):
       
   185     """check the relation satisfy its constraints
       
   186 
       
   187     this is delayed to a precommit time operation since other relation which
       
   188     will make constraint satisfied may be added later.
       
   189     """
       
   190     constraints = rproperty(session, rtype, eidfrom, eidto, 'constraints')
       
   191     if constraints:
       
   192         CheckConstraintsOperation(session, constraints=constraints,
       
   193                                   rdef=(eidfrom, rtype, eidto))
       
   194 
       
   195 def uniquecstrcheck_before_modification(session, entity):
       
   196     eschema = entity.e_schema
       
   197     for attr, val in entity.items():
       
   198         if val is None:
       
   199             continue
       
   200         if eschema.subject_relation(attr).is_final() and \
       
   201                eschema.has_unique_values(attr):
       
   202             rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
       
   203             rset = session.unsafe_execute(rql, {'val': val})
       
   204             if rset and rset[0][0] != entity.eid:
       
   205                 msg = session._('the value "%s" is already used, use another one')
       
   206                 raise ValidationError(entity.eid, {attr: msg % val})
       
   207 
       
   208 
       
   209 
       
   210 
       
   211 class tidy_html_fields(object):
       
   212     """tidy HTML in rich text strings
       
   213 
       
   214     FIXME: (adim) the whole idea of having a class is to store the
       
   215     event type. There might be another way to get dynamically the
       
   216     event inside the hook function.
       
   217     """
       
   218     # FIXME hooks manager use func_name to register
       
   219     func_name = 'tidy_html_field'
       
   220     
       
   221     def __init__(self, event):
       
   222         self.event = event
       
   223 
       
   224     def __call__(self, session, entity):
       
   225         for attr in entity.formatted_attrs():
       
   226             value = entity.get(attr)
       
   227             # text was not changed
       
   228             if self.event == 'before_add_entity':
       
   229                 fmt = entity.get('%s_format' % attr)
       
   230             else:
       
   231                 fmt = entity.get_value('%s_format' % attr)
       
   232             if value and fmt == 'text/html':
       
   233                 entity[attr] = soup2xhtml(value, session.encoding)
       
   234 
       
   235 
       
   236 class CheckRequiredRelationOperation(LateOperation):
       
   237     """checking relation cardinality has to be done after commit in
       
   238     case the relation is being replaced
       
   239     """
       
   240     eid, rtype = None, None
       
   241     
       
   242     def precommit_event(self):
       
   243         # recheck pending eids
       
   244         if self.eid in self.session.query_data('pendingeids', ()):
       
   245             return
       
   246         if self.session.unsafe_execute(*self._rql()).rowcount < 1:
       
   247             etype = self.session.describe(self.eid)[0]
       
   248             msg = self.session._('at least one relation %s is required on %s(%s)')
       
   249             raise ValidationError(self.eid, {self.rtype: msg % (self.rtype,
       
   250                                                                 etype, self.eid)})
       
   251     
       
   252     def commit_event(self):
       
   253         pass
       
   254         
       
   255     def _rql(self):
       
   256         raise NotImplementedError()
       
   257     
       
   258 class CheckSRelationOp(CheckRequiredRelationOperation):
       
   259     """check required subject relation"""
       
   260     def _rql(self):
       
   261         return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
       
   262     
       
   263 class CheckORelationOp(CheckRequiredRelationOperation):
       
   264     """check required object relation"""
       
   265     def _rql(self):
       
   266         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
       
   267 
       
   268 def checkrel_if_necessary(session, opcls, rtype, eid):
       
   269     """check an equivalent operation has not already been added"""
       
   270     for op in session.pending_operations:
       
   271         if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
       
   272             break
       
   273     else:
       
   274         opcls(session, rtype=rtype, eid=eid)
       
   275     
       
   276 def cardinalitycheck_after_add_entity(session, entity):
       
   277     """check cardinalities are satisfied"""
       
   278     eid = entity.eid
       
   279     for rschema, targetschemas, x in entity.e_schema.relation_definitions():
       
   280         # skip automatically handled relations
       
   281         if rschema.type in ('owned_by', 'created_by', 'is', 'is_instance_of'):
       
   282             continue
       
   283         if x == 'subject':
       
   284             subjtype = entity.e_schema
       
   285             objtype = targetschemas[0].type
       
   286             cardindex = 0
       
   287             opcls = CheckSRelationOp
       
   288         else:
       
   289             subjtype = targetschemas[0].type
       
   290             objtype = entity.e_schema
       
   291             cardindex = 1
       
   292             opcls = CheckORelationOp
       
   293         card = rschema.rproperty(subjtype, objtype, 'cardinality')
       
   294         if card[cardindex] in '1+':
       
   295             checkrel_if_necessary(session, opcls, rschema.type, eid)
       
   296 
       
   297 def cardinalitycheck_before_del_relation(session, eidfrom, rtype, eidto):
       
   298     """check cardinalities are satisfied"""
       
   299     card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
       
   300     pendingeids = session.query_data('pendingeids', ())
       
   301     if card[0] in '1+' and not eidfrom in pendingeids:
       
   302         checkrel_if_necessary(session, CheckSRelationOp, rtype, eidfrom)
       
   303     if card[1] in '1+' and not eidto in pendingeids:
       
   304         checkrel_if_necessary(session, CheckORelationOp, rtype, eidto)
       
   305 
       
   306 
       
   307 def _register_core_hooks(hm):
       
   308     hm.register_hook(handle_composite_before_del_relation, 'before_delete_relation', '')
       
   309     hm.register_hook(before_del_group, 'before_delete_entity', 'EGroup')
       
   310     
       
   311     #hm.register_hook(cstrcheck_before_update_entity, 'before_update_entity', '')
       
   312     hm.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
       
   313     hm.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
       
   314     hm.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
       
   315     hm.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
       
   316     hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
       
   317     hm.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
       
   318     hm.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
       
   319 
       
   320 
       
   321 # user/groups synchronisation #################################################
       
   322             
       
   323 class GroupOperation(Operation):
       
   324     """base class for group operation"""
       
   325     geid = None
       
   326     def __init__(self, session, *args, **kwargs):
       
   327         """override to get the group name before actual groups manipulation:
       
   328         
       
   329         we may temporarily loose right access during a commit event, so
       
   330         no query should be emitted while comitting
       
   331         """
       
   332         rql = 'Any N WHERE G eid %(x)s, G name N'
       
   333         result = session.execute(rql, {'x': kwargs['geid']}, 'x', build_descr=False)
       
   334         Operation.__init__(self, session, *args, **kwargs)
       
   335         self.group = result[0][0]
       
   336 
       
   337 class DeleteGroupOp(GroupOperation):
       
   338     """synchronize user when a in_group relation has been deleted"""
       
   339     def commit_event(self):
       
   340         """the observed connections pool has been commited"""
       
   341         groups = self.cnxuser.groups
       
   342         try:
       
   343             groups.remove(self.group)
       
   344         except KeyError:
       
   345             self.error('user %s not in group %s',  self.cnxuser, self.group)
       
   346             return
       
   347     
       
   348 def after_del_in_group(session, fromeid, rtype, toeid):
       
   349     """modify user permission, need to update users"""
       
   350     for session_ in get_user_sessions(session.repo, fromeid):
       
   351         DeleteGroupOp(session, cnxuser=session_.user, geid=toeid)
       
   352 
       
   353         
       
   354 class AddGroupOp(GroupOperation):
       
   355     """synchronize user when a in_group relation has been added"""
       
   356     def commit_event(self):
       
   357         """the observed connections pool has been commited"""
       
   358         groups = self.cnxuser.groups
       
   359         if self.group in groups:
       
   360             self.warning('user %s already in group %s', self.cnxuser,
       
   361                          self.group)
       
   362             return
       
   363         groups.add(self.group)
       
   364 
       
   365 def after_add_in_group(session, fromeid, rtype, toeid):
       
   366     """modify user permission, need to update users"""
       
   367     for session_ in get_user_sessions(session.repo, fromeid):
       
   368         AddGroupOp(session, cnxuser=session_.user, geid=toeid)
       
   369 
       
   370 
       
   371 class DelUserOp(Operation):
       
   372     """synchronize user when a in_group relation has been added"""
       
   373     def __init__(self, session, cnxid):
       
   374         self.cnxid = cnxid
       
   375         Operation.__init__(self, session)
       
   376         
       
   377     def commit_event(self):
       
   378         """the observed connections pool has been commited"""
       
   379         try:
       
   380             self.repo.close(self.cnxid)
       
   381         except BadConnectionId:
       
   382             pass # already closed
       
   383 
       
   384 def after_del_user(session, eid):
       
   385     """modify user permission, need to update users"""
       
   386     for session_ in get_user_sessions(session.repo, eid):
       
   387         DelUserOp(session, session_.id)
       
   388     
       
   389 def _register_usergroup_hooks(hm):
       
   390     """register user/group related hooks on the hooks manager"""
       
   391     hm.register_hook(after_del_user, 'after_delete_entity', 'EUser')
       
   392     hm.register_hook(after_add_in_group, 'after_add_relation', 'in_group')
       
   393     hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group')
       
   394 
       
   395 
       
   396 # workflow handling ###########################################################
       
   397 
       
   398 def before_add_in_state(session, fromeid, rtype, toeid):
       
   399     """check the transition is allowed and record transition information
       
   400     """
       
   401     assert rtype == 'in_state'
       
   402     state = previous_state(session, fromeid)
       
   403     etype = session.describe(fromeid)[0]
       
   404     if not (session.is_super_session or 'managers' in session.user.groups):
       
   405         if not state is None:
       
   406             entity = session.entity(fromeid)
       
   407             # we should find at least one transition going to this state
       
   408             try:
       
   409                 iter(state.transitions(entity, toeid)).next()
       
   410             except StopIteration:
       
   411                 msg = session._('transition is not allowed')
       
   412                 raise ValidationError(fromeid, {'in_state': msg})
       
   413         else:
       
   414             # not a transition
       
   415             # check state is initial state if the workflow defines one
       
   416             isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
       
   417                                             {'etype': etype})
       
   418             if isrset and not toeid == isrset[0][0]:
       
   419                 msg = session._('not the initial state for this entity')
       
   420                 raise ValidationError(fromeid, {'in_state': msg})
       
   421     eschema = session.repo.schema[etype]
       
   422     if not 'wf_info_for' in eschema.object_relations():
       
   423         # workflow history not activated for this entity type
       
   424         return
       
   425     rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
       
   426     args = {'comment': session.get_shared_data('trcomment', None, pop=True),
       
   427             'e': fromeid, 'ds': toeid}
       
   428     cformat = session.get_shared_data('trcommentformat', None, pop=True)
       
   429     if cformat is not None:
       
   430         args['comment_format'] = cformat
       
   431         rql += ', T comment_format %(comment_format)s'
       
   432     restriction = ['DS eid %(ds)s, E eid %(e)s']
       
   433     if not state is None: # not a transition
       
   434         rql += ', T from_state FS'
       
   435         restriction.append('FS eid %(fs)s')
       
   436         args['fs'] = state.eid
       
   437     rql = '%s WHERE %s' % (rql, ', '.join(restriction))
       
   438     session.unsafe_execute(rql, args, 'e')
       
   439 
       
   440 
       
   441 class SetInitialStateOp(PreCommitOperation):
       
   442     """make initial state be a default state"""
       
   443 
       
   444     def precommit_event(self):
       
   445         session = self.session
       
   446         entity = self.entity
       
   447         rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
       
   448                                {'name': str(entity.e_schema)})
       
   449         # if there is an initial state and the entity's state is not set,
       
   450         # use the initial state as a default state
       
   451         pendingeids = session.query_data('pendingeids', ())
       
   452         if rset and not entity.eid in pendingeids and not entity.in_state:
       
   453             session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
       
   454                                    {'x' : entity.eid, 's' : rset[0][0]}, 'x')
       
   455 
       
   456 
       
   457 def set_initial_state_after_add(session, entity):
       
   458     SetInitialStateOp(session, entity=entity)
       
   459     
       
   460 def _register_wf_hooks(hm):
       
   461     """register workflow related hooks on the hooks manager"""
       
   462     if 'in_state' in hm.schema:
       
   463         hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
       
   464         hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
       
   465         for eschema in hm.schema.entities():
       
   466             if 'in_state' in eschema.subject_relations():
       
   467                 hm.register_hook(set_initial_state_after_add, 'after_add_entity',
       
   468                                  str(eschema))
       
   469 
       
   470 
       
   471 # EProperty hooks #############################################################
       
   472 
       
   473 
       
   474 class DelEPropertyOp(Operation):
       
   475     """a user's custom properties has been deleted"""
       
   476     
       
   477     def commit_event(self):
       
   478         """the observed connections pool has been commited"""
       
   479         try:
       
   480             del self.epropdict[self.key]
       
   481         except KeyError:
       
   482             self.error('%s has no associated value', self.key)
       
   483 
       
   484 class ChangeEPropertyOp(Operation):
       
   485     """a user's custom properties has been added/changed"""
       
   486         
       
   487     def commit_event(self):
       
   488         """the observed connections pool has been commited"""
       
   489         self.epropdict[self.key] = self.value
       
   490 
       
   491 class AddEPropertyOp(Operation):
       
   492     """a user's custom properties has been added/changed"""
       
   493         
       
   494     def commit_event(self):
       
   495         """the observed connections pool has been commited"""
       
   496         eprop = self.eprop
       
   497         if not eprop.for_user:
       
   498             self.repo.vreg.eprop_values[eprop.pkey] = eprop.value
       
   499         # if for_user is set, update is handled by a ChangeEPropertyOp operation
       
   500 
       
   501 def after_add_eproperty(session, entity):
       
   502     key, value = entity.pkey, entity.value
       
   503     try:
       
   504         value = session.vreg.typed_value(key, value)
       
   505     except UnknownProperty:
       
   506         raise ValidationError(entity.eid, {'pkey': session._('unknown property key')})
       
   507     except ValueError, ex:
       
   508         raise ValidationError(entity.eid, {'value': session._(str(ex))})
       
   509     if not session.user.matching_groups('managers'):
       
   510         session.unsafe_execute('SET P for_user U WHERE P eid %(x)s,U eid %(u)s',
       
   511                                {'x': entity.eid, 'u': session.user.eid}, 'x')
       
   512     else:
       
   513         AddEPropertyOp(session, eprop=entity)
       
   514         
       
   515 def after_update_eproperty(session, entity):
       
   516     key, value = entity.pkey, entity.value
       
   517     try:
       
   518         value = session.vreg.typed_value(key, value)
       
   519     except UnknownProperty:
       
   520         return
       
   521     except ValueError, ex:
       
   522         raise ValidationError(entity.eid, {'value': session._(str(ex))})
       
   523     if entity.for_user:
       
   524         for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
       
   525             ChangeEPropertyOp(session, epropdict=session_.user.properties,
       
   526                               key=key, value=value)
       
   527     else:
       
   528         # site wide properties
       
   529         ChangeEPropertyOp(session, epropdict=session.vreg.eprop_values,
       
   530                           key=key, value=value)
       
   531         
       
   532 def before_del_eproperty(session, eid):
       
   533     for eidfrom, rtype, eidto in session.query_data('pendingrelations', ()):
       
   534         if rtype == 'for_user' and eidfrom == eid:
       
   535             # if for_user was set, delete has already been handled
       
   536             break
       
   537     else:
       
   538         key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
       
   539                               {'x': eid}, 'x')[0][0]
       
   540         DelEPropertyOp(session, epropdict=session.vreg.eprop_values, key=key)
       
   541 
       
   542 def after_add_for_user(session, fromeid, rtype, toeid):
       
   543     if not session.describe(fromeid)[0] == 'EProperty':
       
   544         return
       
   545     key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
       
   546                                  {'x': fromeid}, 'x')[0]
       
   547     if session.vreg.property_info(key)['sitewide']:
       
   548         raise ValidationError(fromeid,
       
   549                               {'for_user': session._("site-wide property can't be set for user")})
       
   550     for session_ in get_user_sessions(session.repo, toeid):
       
   551         ChangeEPropertyOp(session, epropdict=session_.user.properties,
       
   552                           key=key, value=value)
       
   553         
       
   554 def before_del_for_user(session, fromeid, rtype, toeid):
       
   555     key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
       
   556                           {'x': fromeid}, 'x')[0][0]
       
   557     relation_deleted(session, fromeid, rtype, toeid)
       
   558     for session_ in get_user_sessions(session.repo, toeid):
       
   559         DelEPropertyOp(session, epropdict=session_.user.properties, key=key)
       
   560 
       
   561 def _register_eproperty_hooks(hm):
       
   562     """register workflow related hooks on the hooks manager"""
       
   563     hm.register_hook(after_add_eproperty, 'after_add_entity', 'EProperty')
       
   564     hm.register_hook(after_update_eproperty, 'after_update_entity', 'EProperty')
       
   565     hm.register_hook(before_del_eproperty, 'before_delete_entity', 'EProperty')
       
   566     hm.register_hook(after_add_for_user, 'after_add_relation', 'for_user')
       
   567     hm.register_hook(before_del_for_user, 'before_delete_relation', 'for_user')