hooks/workflow.py
changeset 8556 bbe0d6985e59
parent 8483 4ba11607d84a
child 8695 358d8bed9626
equal deleted inserted replaced
8555:c747242d22a6 8556:bbe0d6985e59
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Core hooks: workflow related hooks"""
    18 """Core hooks: workflow related hooks"""
    19 
    19 
    20 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
       
    21 _ = unicode
    21 
    22 
    22 from datetime import datetime
    23 from datetime import datetime
    23 
    24 
    24 from yams.schema import role_name
    25 
    25 
    26 from cubicweb import RepositoryError, validation_error
    26 from cubicweb import RepositoryError, ValidationError
       
    27 from cubicweb.predicates import is_instance, adaptable
    27 from cubicweb.predicates import is_instance, adaptable
    28 from cubicweb.server import hook
    28 from cubicweb.server import hook
    29 
    29 
    30 
    30 
    31 def _change_state(session, x, oldstate, newstate):
    31 def _change_state(session, x, oldstate, newstate):
    90         # transaction
    90         # transaction
    91         mainwf = iworkflowable.main_workflow
    91         mainwf = iworkflowable.main_workflow
    92         if mainwf.eid == self.wfeid:
    92         if mainwf.eid == self.wfeid:
    93             deststate = mainwf.initial
    93             deststate = mainwf.initial
    94             if not deststate:
    94             if not deststate:
    95                 qname = role_name('custom_workflow', 'subject')
    95                 msg = _('workflow has no initial state')
    96                 msg = session._('workflow has no initial state')
    96                 raise validation_error(entity, {('custom_workflow', 'subject'): msg})
    97                 raise ValidationError(entity.eid, {qname: msg})
       
    98             if mainwf.state_by_eid(iworkflowable.current_state.eid):
    97             if mainwf.state_by_eid(iworkflowable.current_state.eid):
    99                 # nothing to do
    98                 # nothing to do
   100                 return
    99                 return
   101             # if there are no history, simply go to new workflow's initial state
   100             # if there are no history, simply go to new workflow's initial state
   102             if not iworkflowable.workflow_history:
   101             if not iworkflowable.workflow_history:
   117     def precommit_event(self):
   116     def precommit_event(self):
   118         tr = self.session.entity_from_eid(self.treid)
   117         tr = self.session.entity_from_eid(self.treid)
   119         outputs = set()
   118         outputs = set()
   120         for ep in tr.subworkflow_exit:
   119         for ep in tr.subworkflow_exit:
   121             if ep.subwf_state.eid in outputs:
   120             if ep.subwf_state.eid in outputs:
   122                 qname = role_name('subworkflow_exit', 'subject')
   121                 msg = _("can't have multiple exits on the same state")
   123                 msg = self.session._("can't have multiple exits on the same state")
   122                 raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
   124                 raise ValidationError(self.treid, {qname: msg})
       
   125             outputs.add(ep.subwf_state.eid)
   123             outputs.add(ep.subwf_state.eid)
   126 
   124 
   127 
   125 
   128 class _SubWorkflowExitOp(hook.Operation):
   126 class _SubWorkflowExitOp(hook.Operation):
   129     forentity = trinfo = None # make pylint happy
   127     forentity = trinfo = None # make pylint happy
   135         trinfo = self.trinfo
   133         trinfo = self.trinfo
   136         # we're in a subworkflow, check if we've reached an exit point
   134         # we're in a subworkflow, check if we've reached an exit point
   137         wftr = iworkflowable.subworkflow_input_transition()
   135         wftr = iworkflowable.subworkflow_input_transition()
   138         if wftr is None:
   136         if wftr is None:
   139             # inconsistency detected
   137             # inconsistency detected
   140             qname = role_name('to_state', 'subject')
   138             msg = _("state doesn't belong to entity's current workflow")
   141             msg = session._("state doesn't belong to entity's current workflow")
   139             raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
   142             raise ValidationError(self.trinfo.eid, {'to_state': msg})
       
   143         tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
   140         tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
   144         if tostate is not None:
   141         if tostate is not None:
   145             # reached an exit point
   142             # reached an exit point
   146             msg = session._('exiting from subworkflow %s')
   143             msg = _('exiting from subworkflow %s')
   147             msg %= session._(iworkflowable.current_workflow.name)
   144             msg %= session._(iworkflowable.current_workflow.name)
   148             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
   145             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
   149             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
   146             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
   150 
   147 
   151 
   148 
   184         entity = self.entity
   181         entity = self.entity
   185         # first retreive entity to which the state change apply
   182         # first retreive entity to which the state change apply
   186         try:
   183         try:
   187             foreid = entity.cw_attr_cache['wf_info_for']
   184             foreid = entity.cw_attr_cache['wf_info_for']
   188         except KeyError:
   185         except KeyError:
   189             qname = role_name('wf_info_for', 'subject')
   186             msg = _('mandatory relation')
   190             msg = session._('mandatory relation')
   187             raise validation_error(entity, {('wf_info_for', 'subject'): msg})
   191             raise ValidationError(entity.eid, {qname: msg})
       
   192         forentity = session.entity_from_eid(foreid)
   188         forentity = session.entity_from_eid(foreid)
   193         # see comment in the TrInfo entity definition
   189         # see comment in the TrInfo entity definition
   194         entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
   190         entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
   195         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   191         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
   196         # then check it has a workflow set, unless we're in the process of changing
   192         # then check it has a workflow set, unless we're in the process of changing
   199             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
   195             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
   200             wf = session.entity_from_eid(wfeid)
   196             wf = session.entity_from_eid(wfeid)
   201         else:
   197         else:
   202             wf = iworkflowable.current_workflow
   198             wf = iworkflowable.current_workflow
   203         if wf is None:
   199         if wf is None:
   204             msg = session._('related entity has no workflow set')
   200             msg = _('related entity has no workflow set')
   205             raise ValidationError(entity.eid, {None: msg})
   201             raise validation_error(entity, {None: msg})
   206         # then check it has a state set
   202         # then check it has a state set
   207         fromstate = iworkflowable.current_state
   203         fromstate = iworkflowable.current_state
   208         if fromstate is None:
   204         if fromstate is None:
   209             msg = session._('related entity has no state')
   205             msg = _('related entity has no state')
   210             raise ValidationError(entity.eid, {None: msg})
   206             raise validation_error(entity, {None: msg})
   211         # True if we are coming back from subworkflow
   207         # True if we are coming back from subworkflow
   212         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
   208         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
   213         cowpowers = (session.user.is_in_group('managers')
   209         cowpowers = (session.user.is_in_group('managers')
   214                      or not session.write_security)
   210                      or not session.write_security)
   215         # no investigate the requested state change...
   211         # no investigate the requested state change...
   217             treid = entity.cw_attr_cache['by_transition']
   213             treid = entity.cw_attr_cache['by_transition']
   218         except KeyError:
   214         except KeyError:
   219             # no transition set, check user is a manager and destination state
   215             # no transition set, check user is a manager and destination state
   220             # is specified (and valid)
   216             # is specified (and valid)
   221             if not cowpowers:
   217             if not cowpowers:
   222                 qname = role_name('by_transition', 'subject')
   218                 msg = _('mandatory relation')
   223                 msg = session._('mandatory relation')
   219                 raise validation_error(entity, {('by_transition', 'subject'): msg})
   224                 raise ValidationError(entity.eid, {qname: msg})
       
   225             deststateeid = entity.cw_attr_cache.get('to_state')
   220             deststateeid = entity.cw_attr_cache.get('to_state')
   226             if not deststateeid:
   221             if not deststateeid:
   227                 qname = role_name('by_transition', 'subject')
   222                 msg = _('mandatory relation')
   228                 msg = session._('mandatory relation')
   223                 raise validation_error(entity, {('by_transition', 'subject'): msg})
   229                 raise ValidationError(entity.eid, {qname: msg})
       
   230             deststate = wf.state_by_eid(deststateeid)
   224             deststate = wf.state_by_eid(deststateeid)
   231             if deststate is None:
   225             if deststate is None:
   232                 qname = role_name('to_state', 'subject')
   226                 msg = _("state doesn't belong to entity's workflow")
   233                 msg = session._("state doesn't belong to entity's workflow")
   227                 raise validation_error(entity, {('to_state', 'subject'): msg})
   234                 raise ValidationError(entity.eid, {qname: msg})
       
   235         else:
   228         else:
   236             # check transition is valid and allowed, unless we're coming back
   229             # check transition is valid and allowed, unless we're coming back
   237             # from subworkflow
   230             # from subworkflow
   238             tr = session.entity_from_eid(treid)
   231             tr = session.entity_from_eid(treid)
   239             if swtr is None:
   232             if swtr is None:
   240                 qname = role_name('by_transition', 'subject')
   233                 qname = ('by_transition', 'subject')
   241                 if tr is None:
   234                 if tr is None:
   242                     msg = session._("transition doesn't belong to entity's workflow")
   235                     msg = _("transition doesn't belong to entity's workflow")
   243                     raise ValidationError(entity.eid, {qname: msg})
   236                     raise validation_error(entity, {qname: msg})
   244                 if not tr.has_input_state(fromstate):
   237                 if not tr.has_input_state(fromstate):
   245                     msg = session._("transition %(tr)s isn't allowed from %(st)s") % {
   238                     msg = _("transition %(tr)s isn't allowed from %(st)s")
   246                         'tr': session._(tr.name), 'st': session._(fromstate.name)}
   239                     raise validation_error(entity, {qname: msg}, {
   247                     raise ValidationError(entity.eid, {qname: msg})
   240                             'tr': tr.name, 'st': fromstate.name}, ['tr', 'st'])
   248                 if not tr.may_be_fired(foreid):
   241                 if not tr.may_be_fired(foreid):
   249                     msg = session._("transition may not be fired")
   242                     msg = _("transition may not be fired")
   250                     raise ValidationError(entity.eid, {qname: msg})
   243                     raise validation_error(entity, {qname: msg})
   251             deststateeid = entity.cw_attr_cache.get('to_state')
   244             deststateeid = entity.cw_attr_cache.get('to_state')
   252             if deststateeid is not None:
   245             if deststateeid is not None:
   253                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
   246                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
   254                     qname = role_name('by_transition', 'subject')
   247                     msg = _("transition isn't allowed")
   255                     msg = session._("transition isn't allowed")
   248                     raise validation_error(entity, {('by_transition', 'subject'): msg})
   256                     raise ValidationError(entity.eid, {qname: msg})
       
   257                 if swtr is None:
   249                 if swtr is None:
   258                     deststate = session.entity_from_eid(deststateeid)
   250                     deststate = session.entity_from_eid(deststateeid)
   259                     if not cowpowers and deststate is None:
   251                     if not cowpowers and deststate is None:
   260                         qname = role_name('to_state', 'subject')
   252                         msg = _("state doesn't belong to entity's workflow")
   261                         msg = session._("state doesn't belong to entity's workflow")
   253                         raise validation_error(entity, {('to_state', 'subject'): msg})
   262                         raise ValidationError(entity.eid, {qname: msg})
       
   263             else:
   254             else:
   264                 deststateeid = tr.destination(forentity).eid
   255                 deststateeid = tr.destination(forentity).eid
   265         # everything is ok, add missing information on the trinfo entity
   256         # everything is ok, add missing information on the trinfo entity
   266         entity.cw_edited['from_state'] = fromstate.eid
   257         entity.cw_edited['from_state'] = fromstate.eid
   267         entity.cw_edited['to_state'] = deststateeid
   258         entity.cw_edited['to_state'] = deststateeid
   305             return
   296             return
   306         entity = session.entity_from_eid(self.eidfrom)
   297         entity = session.entity_from_eid(self.eidfrom)
   307         iworkflowable = entity.cw_adapt_to('IWorkflowable')
   298         iworkflowable = entity.cw_adapt_to('IWorkflowable')
   308         mainwf = iworkflowable.main_workflow
   299         mainwf = iworkflowable.main_workflow
   309         if mainwf is None:
   300         if mainwf is None:
   310             msg = session._('entity has no workflow set')
   301             msg = _('entity has no workflow set')
   311             raise ValidationError(entity.eid, {None: msg})
   302             raise validation_error(entity, {None: msg})
   312         for wf in mainwf.iter_workflows():
   303         for wf in mainwf.iter_workflows():
   313             if wf.state_by_eid(self.eidto):
   304             if wf.state_by_eid(self.eidto):
   314                 break
   305                 break
   315         else:
   306         else:
   316             qname = role_name('in_state', 'subject')
   307             msg = _("state doesn't belong to entity's workflow. You may "
   317             msg = session._("state doesn't belong to entity's workflow. You may "
   308                     "want to set a custom workflow for this entity first.")
   318                             "want to set a custom workflow for this entity first.")
   309             raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
   319             raise ValidationError(self.eidfrom, {qname: msg})
       
   320         if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
   310         if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
   321             qname = role_name('in_state', 'subject')
   311             msg = _("state doesn't belong to entity's current workflow")
   322             msg = session._("state doesn't belong to entity's current workflow")
   312             raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
   323             raise ValidationError(self.eidfrom, {qname: msg})
       
   324 
   313 
   325 
   314 
   326 class SetModificationDateOnStateChange(WorkflowHook):
   315 class SetModificationDateOnStateChange(WorkflowHook):
   327     """update entity's modification date after changing its state"""
   316     """update entity's modification date after changing its state"""
   328     __regid__ = 'wfsyncmdate'
   317     __regid__ = 'wfsyncmdate'