server/hooks.py
branch3.5
changeset 2920 64322aa83a1d
parent 2909 e695b7b0359d
child 2949 a2aa2c51f3be
equal deleted inserted replaced
2919:662f35236d1c 2920:64322aa83a1d
    11 from datetime import datetime
    11 from datetime import datetime
    12 
    12 
    13 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
    13 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
    14 
    14 
    15 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
    15 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
    16 from cubicweb.server.hookhelper import (check_internal_entity, previous_state,
    16 from cubicweb.server.hookhelper import (check_internal_entity, 
    17                                      get_user_sessions, rproperty)
    17                                         get_user_sessions, rproperty)
    18 from cubicweb.server.repository import FTIndexEntityOp
    18 from cubicweb.server.repository import FTIndexEntityOp
    19 
    19 
    20 # special relations that don't have to be checked for integrity, usually
    20 # special relations that don't have to be checked for integrity, usually
    21 # because they are handled internally by hooks (so we trust ourselves)
    21 # because they are handled internally by hooks (so we trust ourselves)
    22 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
    22 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
    35     eschema = session.repo.schema.eschema(etype)
    35     eschema = session.repo.schema.eschema(etype)
    36     # eschema.eid is None if schema has been readen from the filesystem, not
    36     # eschema.eid is None if schema has been readen from the filesystem, not
    37     # from the database (eg during tests)
    37     # from the database (eg during tests)
    38     if eschema.eid is None:
    38     if eschema.eid is None:
    39         eschema.eid = session.unsafe_execute(
    39         eschema.eid = session.unsafe_execute(
    40             'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0]
    40             'Any X WHERE X is CWEType, X name %(name)s',
       
    41             {'name': str(etype)})[0][0]
    41     return eschema.eid
    42     return eschema.eid
    42 
    43 
    43 
    44 
    44 # base meta-data handling ######################################################
    45 # base meta-data handling ######################################################
    45 
    46 
   415     hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group')
   416     hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group')
   416 
   417 
   417 
   418 
   418 # workflow handling ###########################################################
   419 # workflow handling ###########################################################
   419 
   420 
   420 def before_add_in_state(session, fromeid, rtype, toeid):
   421 def before_add_trinfo(session, entity):
   421     """check the transition is allowed and record transition information
   422     """check the transition is allowed, add missing information. Expect that:
   422     """
   423     * wf_info_for inlined relation is set
   423     assert rtype == 'in_state'
   424     * by_transition or to_state (managers only) inlined relation is set
   424     state = previous_state(session, fromeid)
   425     """
   425     etype = session.describe(fromeid)[0]
   426     # first retreive entity to which the state change apply
   426     if not (session.is_super_session or 'managers' in session.user.groups):
   427     try:
   427         if not state is None:
   428         foreid = entity['wf_info_for']
   428             entity = session.entity_from_eid(fromeid)
   429     except KeyError:
   429             # we should find at least one transition going to this state
   430         msg = session._('mandatory relation')
   430             try:
   431         raise ValidationError(entity.eid, {'wf_info_for': msg})
   431                 iter(state.transitions(entity, toeid)).next()
   432     forentity = session.entity_from_eid(foreid)
   432             except StopIteration:
   433     # then check it has a workflow set
   433                 _ = session._
   434     wf = forentity.current_workflow
   434                 msg = _('transition from %s to %s does not exist or is not allowed') % (
   435     if wf is None:
   435                     _(state.name), _(session.entity_from_eid(toeid).name))
   436         msg = session._('related entity has no workflow set')
   436                 raise ValidationError(fromeid, {'in_state': msg})
   437         raise ValidationError(entity.eid, {None: msg})
   437         else:
   438     # then check it has a state set
   438             # not a transition
   439     fromstate = forentity.current_state
   439             # check state is initial state if the workflow defines one
   440     if fromstate is None:
   440             isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
   441         msg = session._('related entity has no state')
   441                                             {'etype': etype})
   442         raise ValidationError(entity.eid, {None: msg})
   442             if isrset and not toeid == isrset[0][0]:
   443     # no investigate the requested state change...
   443                 _ = session._
   444     try:
   444                 msg = _('%s is not the initial state (%s) for this entity') % (
   445         treid = entity['by_transition']
   445                     _(session.entity_from_eid(toeid).name), _(isrset.get_entity(0,0).name))
   446     except KeyError:
   446                 raise ValidationError(fromeid, {'in_state': msg})
   447         # no transition set, check user is a manager and destination state is
   447     eschema = session.repo.schema[etype]
   448         # specified (and valid)
   448     if not 'wf_info_for' in eschema.object_relations():
   449         if not (session.is_super_session or 'managers' in session.user.groups):
   449         # workflow history not activated for this entity type
   450             msg = session._('mandatory relation')
   450         return
   451             raise ValidationError(entity.eid, {'by_transition': msg})
   451     rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
   452         deststateeid = entity.get('to_state')
   452     args = {'comment': session.get_shared_data('trcomment', None, pop=True),
   453         if not deststateeid:
   453             'e': fromeid, 'ds': toeid}
   454             msg = session._('mandatory relation')
   454     cformat = session.get_shared_data('trcommentformat', None, pop=True)
   455             raise ValidationError(entity.eid, {'by_transition': msg})
   455     if cformat is not None:
   456         deststate = wf.state_by_eid(deststateeid)
   456         args['comment_format'] = cformat
   457         if deststate is None:
   457         rql += ', T comment_format %(comment_format)s'
   458             msg = session._("state doesn't belong to entity's workflow")
   458     restriction = ['DS eid %(ds)s, E eid %(e)s']
   459             raise ValidationError(entity.eid, {'to_state': msg})
   459     if not state is None: # not a transition
   460     else:
   460         rql += ', T from_state FS'
   461         # check transition is valid and allowed
   461         restriction.append('FS eid %(fs)s')
   462         tr = wf.transition_by_eid(treid)
   462         args['fs'] = state.eid
   463         if tr is None:
   463     rql = '%s WHERE %s' % (rql, ', '.join(restriction))
   464             msg = session._("transition doesn't belong to entity's workflow")
   464     session.unsafe_execute(rql, args, 'e')
   465             raise ValidationError(entity.eid, {'by_transition': msg})
       
   466         if not tr.has_input_state(fromstate):
       
   467             msg = session._("transition isn't allowed")
       
   468             raise ValidationError(entity.eid, {'by_transition': msg})
       
   469         if not tr.may_be_fired(foreid):
       
   470             msg = session._("transition may not be fired")
       
   471             raise ValidationError(entity.eid, {'by_transition': msg})
       
   472         deststateeid = tr.destination().eid
       
   473     # everything is ok, add missing information on the trinfo entity
       
   474     entity['from_state'] = fromstate.eid
       
   475     entity['to_state'] = deststateeid
       
   476     nocheck = session.transaction_data.setdefault('skip-security', set())
       
   477     nocheck.add((entity.eid, 'from_state', fromstate.eid))
       
   478     nocheck.add((entity.eid, 'to_state', deststateeid))
       
   479 
       
   480 def after_add_trinfo(session, entity):
       
   481     """change related entity state"""
       
   482     # need to delete previous state first, not done automatically since
       
   483     # we're using a super session
       
   484     session.unsafe_execute('DELETE X in_state S WHERE X eid %(x)s, S eid %(s)s',
       
   485                            {'x': entity['wf_info_for'], 's': entity['from_state']},
       
   486                            ('x', 's'))
       
   487     session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
       
   488                            {'x': entity['wf_info_for'], 's': entity['to_state']},
       
   489                            ('x', 's'))
   465 
   490 
   466 
   491 
   467 class SetInitialStateOp(PreCommitOperation):
   492 class SetInitialStateOp(PreCommitOperation):
   468     """make initial state be a default state"""
   493     """make initial state be a default state"""
   469 
   494 
   471         session = self.session
   496         session = self.session
   472         entity = self.entity
   497         entity = self.entity
   473         # if there is an initial state and the entity's state is not set,
   498         # if there is an initial state and the entity's state is not set,
   474         # use the initial state as a default state
   499         # use the initial state as a default state
   475         pendingeids = session.transaction_data.get('pendingeids', ())
   500         pendingeids = session.transaction_data.get('pendingeids', ())
   476         if not entity.eid in pendingeids and not entity.in_state:
   501         if not entity.eid in pendingeids and not entity.in_state and \
   477             rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
   502                entity.current_workflow:
   478                                    {'name': entity.id})
   503             state = entity.current_workflow.initial
   479             if rset:
   504             if state:
   480                 session.add_relation(entity.eid, 'in_state', rset[0][0])
   505                 # use super session to by-pass security checks
       
   506                 session.super_session.add_relation(entity.eid, 'in_state',
       
   507                                                    state.eid)
   481 
   508 
   482 
   509 
   483 def set_initial_state_after_add(session, entity):
   510 def set_initial_state_after_add(session, entity):
   484     SetInitialStateOp(session, entity=entity)
   511     SetInitialStateOp(session, entity=entity)
       
   512 
       
   513 def after_del_workflow(session, eid):
       
   514     # workflow cleanup
       
   515     session.execute('DELETE State X WHERE NOT X state_of Y')
       
   516     session.execute('DELETE Transition X WHERE NOT X transition_of Y')
   485 
   517 
   486 
   518 
   487 def _register_wf_hooks(hm):
   519 def _register_wf_hooks(hm):
   488     """register workflow related hooks on the hooks manager"""
   520     """register workflow related hooks on the hooks manager"""
   489     if 'in_state' in hm.schema:
   521     if 'in_state' in hm.schema:
   490         hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
   522         hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo')
   491         hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
   523         hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo')
       
   524         #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
   492         for eschema in hm.schema.entities():
   525         for eschema in hm.schema.entities():
   493             if 'in_state' in eschema.subject_relations():
   526             if 'in_state' in eschema.subject_relations():
   494                 hm.register_hook(set_initial_state_after_add, 'after_add_entity',
   527                 hm.register_hook(set_initial_state_after_add, 'after_add_entity',
   495                                  str(eschema))
   528                                  str(eschema))
       
   529         hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow')
   496 
   530 
   497 
   531 
   498 # CWProperty hooks #############################################################
   532 # CWProperty hooks #############################################################
   499 
   533 
   500 
   534