entities/wfobjs.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    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/>.
       
    18 """workflow handling:
       
    19 
       
    20 * entity types defining workflow (Workflow, State, Transition...)
       
    21 * workflow history (TrInfo)
       
    22 * adapter for workflowable entities (IWorkflowableAdapter)
       
    23 """
       
    24 from __future__ import print_function
       
    25 
       
    26 __docformat__ = "restructuredtext en"
       
    27 
       
    28 from six import text_type, string_types
       
    29 
       
    30 from logilab.common.decorators import cached, clear_cache
       
    31 from logilab.common.deprecation import deprecated
       
    32 
       
    33 from cubicweb.entities import AnyEntity, fetch_config
       
    34 from cubicweb.view import EntityAdapter
       
    35 from cubicweb.predicates import relation_possible
       
    36 
       
    37 
       
    38 try:
       
    39     from cubicweb import server
       
    40 except ImportError:
       
    41     # We need to lookup DEBUG from there,
       
    42     # however a pure dbapi client may not have it.
       
    43     class server(object): pass
       
    44     server.DEBUG = False
       
    45 
       
    46 
       
    47 class WorkflowException(Exception): pass
       
    48 
       
    49 class Workflow(AnyEntity):
       
    50     __regid__ = 'Workflow'
       
    51 
       
    52     @property
       
    53     def initial(self):
       
    54         """return the initial state for this workflow"""
       
    55         return self.initial_state and self.initial_state[0] or None
       
    56 
       
    57     def is_default_workflow_of(self, etype):
       
    58         """return True if this workflow is the default workflow for the given
       
    59         entity type
       
    60         """
       
    61         return any(et for et in self.reverse_default_workflow
       
    62                    if et.name == etype)
       
    63 
       
    64     def iter_workflows(self, _done=None):
       
    65         """return an iterator on actual workflows, eg this workflow and its
       
    66         subworkflows
       
    67         """
       
    68         # infinite loop safety belt
       
    69         if _done is None:
       
    70             _done = set()
       
    71         yield self
       
    72         _done.add(self.eid)
       
    73         for tr in self._cw.execute('Any T WHERE T is WorkflowTransition, '
       
    74                                    'T transition_of WF, WF eid %(wf)s',
       
    75                                    {'wf': self.eid}).entities():
       
    76             if tr.subwf.eid in _done:
       
    77                 continue
       
    78             for subwf in tr.subwf.iter_workflows(_done):
       
    79                 yield subwf
       
    80 
       
    81     # state / transitions accessors ############################################
       
    82 
       
    83     def state_by_name(self, statename):
       
    84         rset = self._cw.execute('Any S, SN WHERE S name SN, S name %(n)s, '
       
    85                                 'S state_of WF, WF eid %(wf)s',
       
    86                                 {'n': statename, 'wf': self.eid})
       
    87         if rset:
       
    88             return rset.get_entity(0, 0)
       
    89         return None
       
    90 
       
    91     def state_by_eid(self, eid):
       
    92         rset = self._cw.execute('Any S, SN WHERE S name SN, S eid %(s)s, '
       
    93                                 'S state_of WF, WF eid %(wf)s',
       
    94                                 {'s': eid, 'wf': self.eid})
       
    95         if rset:
       
    96             return rset.get_entity(0, 0)
       
    97         return None
       
    98 
       
    99     def transition_by_name(self, trname):
       
   100         rset = self._cw.execute('Any T, TN WHERE T name TN, T name %(n)s, '
       
   101                                 'T transition_of WF, WF eid %(wf)s',
       
   102                                 {'n': text_type(trname), 'wf': self.eid})
       
   103         if rset:
       
   104             return rset.get_entity(0, 0)
       
   105         return None
       
   106 
       
   107     def transition_by_eid(self, eid):
       
   108         rset = self._cw.execute('Any T, TN WHERE T name TN, T eid %(t)s, '
       
   109                                 'T transition_of WF, WF eid %(wf)s',
       
   110                                 {'t': eid, 'wf': self.eid})
       
   111         if rset:
       
   112             return rset.get_entity(0, 0)
       
   113         return None
       
   114 
       
   115     # wf construction methods ##################################################
       
   116 
       
   117     def add_state(self, name, initial=False, **kwargs):
       
   118         """add a state to this workflow"""
       
   119         state = self._cw.create_entity('State', name=text_type(name), **kwargs)
       
   120         self._cw.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
       
   121                          {'s': state.eid, 'wf': self.eid})
       
   122         if initial:
       
   123             assert not self.initial, "Initial state already defined as %s" % self.initial
       
   124             self._cw.execute('SET WF initial_state S '
       
   125                              'WHERE S eid %(s)s, WF eid %(wf)s',
       
   126                              {'s': state.eid, 'wf': self.eid})
       
   127         return state
       
   128 
       
   129     def _add_transition(self, trtype, name, fromstates,
       
   130                         requiredgroups=(), conditions=(), **kwargs):
       
   131         tr = self._cw.create_entity(trtype, name=text_type(name), **kwargs)
       
   132         self._cw.execute('SET T transition_of WF '
       
   133                          'WHERE T eid %(t)s, WF eid %(wf)s',
       
   134                          {'t': tr.eid, 'wf': self.eid})
       
   135         assert fromstates, fromstates
       
   136         if not isinstance(fromstates, (tuple, list)):
       
   137             fromstates = (fromstates,)
       
   138         for state in fromstates:
       
   139             if hasattr(state, 'eid'):
       
   140                 state = state.eid
       
   141             self._cw.execute('SET S allowed_transition T '
       
   142                              'WHERE S eid %(s)s, T eid %(t)s',
       
   143                              {'s': state, 't': tr.eid})
       
   144         tr.set_permissions(requiredgroups, conditions, reset=False)
       
   145         return tr
       
   146 
       
   147     def add_transition(self, name, fromstates, tostate=None,
       
   148                        requiredgroups=(), conditions=(), **kwargs):
       
   149         """add a transition to this workflow from some state(s) to another"""
       
   150         tr = self._add_transition('Transition', name, fromstates,
       
   151                                   requiredgroups, conditions, **kwargs)
       
   152         if tostate is not None:
       
   153             if hasattr(tostate, 'eid'):
       
   154                 tostate = tostate.eid
       
   155             self._cw.execute('SET T destination_state S '
       
   156                              'WHERE S eid %(s)s, T eid %(t)s',
       
   157                              {'t': tr.eid, 's': tostate})
       
   158         return tr
       
   159 
       
   160     def add_wftransition(self, name, subworkflow, fromstates, exitpoints=(),
       
   161                          requiredgroups=(), conditions=(), **kwargs):
       
   162         """add a workflow transition to this workflow"""
       
   163         tr = self._add_transition('WorkflowTransition', name, fromstates,
       
   164                                   requiredgroups, conditions, **kwargs)
       
   165         if hasattr(subworkflow, 'eid'):
       
   166             subworkflow = subworkflow.eid
       
   167         assert self._cw.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s',
       
   168                                 {'t': tr.eid, 'wf': subworkflow})
       
   169         for fromstate, tostate in exitpoints:
       
   170             tr.add_exit_point(fromstate, tostate)
       
   171         return tr
       
   172 
       
   173     def replace_state(self, todelstate, replacement):
       
   174         """migration convenience method"""
       
   175         if not hasattr(todelstate, 'eid'):
       
   176             todelstate = self.state_by_name(todelstate)
       
   177         if not hasattr(replacement, 'eid'):
       
   178             replacement = self.state_by_name(replacement)
       
   179         args = {'os': todelstate.eid, 'ns': replacement.eid}
       
   180         execute = self._cw.execute
       
   181         execute('SET X in_state NS WHERE X in_state OS, '
       
   182                 'NS eid %(ns)s, OS eid %(os)s', args)
       
   183         execute('SET X from_state NS WHERE X from_state OS, '
       
   184                 'OS eid %(os)s, NS eid %(ns)s', args)
       
   185         execute('SET X to_state NS WHERE X to_state OS, '
       
   186                 'OS eid %(os)s, NS eid %(ns)s', args)
       
   187         todelstate.cw_delete()
       
   188 
       
   189 
       
   190 class BaseTransition(AnyEntity):
       
   191     """customized class for abstract transition
       
   192 
       
   193     provides a specific may_be_fired method to check if the relation may be
       
   194     fired by the logged user
       
   195     """
       
   196     __regid__ = 'BaseTransition'
       
   197     fetch_attrs, cw_fetch_order = fetch_config(['name', 'type'])
       
   198 
       
   199     def __init__(self, *args, **kwargs):
       
   200         if self.cw_etype == 'BaseTransition':
       
   201             raise WorkflowException('should not be instantiated')
       
   202         super(BaseTransition, self).__init__(*args, **kwargs)
       
   203 
       
   204     @property
       
   205     def workflow(self):
       
   206         return self.transition_of[0]
       
   207 
       
   208     def has_input_state(self, state):
       
   209         if hasattr(state, 'eid'):
       
   210             state = state.eid
       
   211         return any(s for s in self.reverse_allowed_transition if s.eid == state)
       
   212 
       
   213     def may_be_fired(self, eid):
       
   214         """return true if the logged user may fire this transition
       
   215 
       
   216         `eid` is the eid of the object on which we may fire the transition
       
   217         """
       
   218         DBG = False
       
   219         if server.DEBUG & server.DBG_SEC:
       
   220             if 'transition' in server._SECURITY_CAPS:
       
   221                 DBG = True
       
   222         user = self._cw.user
       
   223         # check user is at least in one of the required groups if any
       
   224         groups = frozenset(g.name for g in self.require_group)
       
   225         if groups:
       
   226             matches = user.matching_groups(groups)
       
   227             if matches:
       
   228                 if DBG:
       
   229                     print('may_be_fired: %r may fire: user matches %s' % (self.name, groups))
       
   230                 return matches
       
   231             if 'owners' in groups and user.owns(eid):
       
   232                 if DBG:
       
   233                     print('may_be_fired: %r may fire: user is owner' % self.name)
       
   234                 return True
       
   235         # check one of the rql expression conditions matches if any
       
   236         if self.condition:
       
   237             if DBG:
       
   238                 print('my_be_fired: %r: %s' %
       
   239                       (self.name, [(rqlexpr.expression,
       
   240                                     rqlexpr.check_expression(self._cw, eid))
       
   241                                     for rqlexpr in self.condition]))
       
   242             for rqlexpr in self.condition:
       
   243                 if rqlexpr.check_expression(self._cw, eid):
       
   244                     return True
       
   245         if self.condition or groups:
       
   246             return False
       
   247         return True
       
   248 
       
   249     def set_permissions(self, requiredgroups=(), conditions=(), reset=True):
       
   250         """set or add (if `reset` is False) groups and conditions for this
       
   251         transition
       
   252         """
       
   253         if reset:
       
   254             self._cw.execute('DELETE T require_group G WHERE T eid %(x)s',
       
   255                              {'x': self.eid})
       
   256             self._cw.execute('DELETE T condition R WHERE T eid %(x)s',
       
   257                              {'x': self.eid})
       
   258         for gname in requiredgroups:
       
   259             rset = self._cw.execute('SET T require_group G '
       
   260                                     'WHERE T eid %(x)s, G name %(gn)s',
       
   261                                     {'x': self.eid, 'gn': text_type(gname)})
       
   262             assert rset, '%s is not a known group' % gname
       
   263         if isinstance(conditions, string_types):
       
   264             conditions = (conditions,)
       
   265         for expr in conditions:
       
   266             if isinstance(expr, string_types):
       
   267                 kwargs = {'expr': text_type(expr)}
       
   268             else:
       
   269                 assert isinstance(expr, dict)
       
   270                 kwargs = expr
       
   271             kwargs['x'] = self.eid
       
   272             kwargs.setdefault('mainvars', u'X')
       
   273             self._cw.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
       
   274                              'X expression %(expr)s, X mainvars %(mainvars)s, '
       
   275                              'T condition X WHERE T eid %(x)s', kwargs)
       
   276         # XXX clear caches?
       
   277 
       
   278 
       
   279 class Transition(BaseTransition):
       
   280     """customized class for Transition entities"""
       
   281     __regid__ = 'Transition'
       
   282 
       
   283     def dc_long_title(self):
       
   284         return '%s (%s)' % (self.name, self._cw._(self.name))
       
   285 
       
   286     def destination(self, entity):
       
   287         try:
       
   288             return self.destination_state[0]
       
   289         except IndexError:
       
   290             return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state
       
   291 
       
   292     def potential_destinations(self):
       
   293         try:
       
   294             yield self.destination_state[0]
       
   295         except IndexError:
       
   296             for incomingstate in self.reverse_allowed_transition:
       
   297                 for tr in incomingstate.reverse_destination_state:
       
   298                     for previousstate in tr.reverse_allowed_transition:
       
   299                         yield previousstate
       
   300 
       
   301 
       
   302 class WorkflowTransition(BaseTransition):
       
   303     """customized class for WorkflowTransition entities"""
       
   304     __regid__ = 'WorkflowTransition'
       
   305 
       
   306     @property
       
   307     def subwf(self):
       
   308         return self.subworkflow[0]
       
   309 
       
   310     def destination(self, entity):
       
   311         return self.subwf.initial
       
   312 
       
   313     def potential_destinations(self):
       
   314         yield self.subwf.initial
       
   315 
       
   316     def add_exit_point(self, fromstate, tostate):
       
   317         if hasattr(fromstate, 'eid'):
       
   318             fromstate = fromstate.eid
       
   319         if tostate is None:
       
   320             self._cw.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
       
   321                              'X subworkflow_state FS WHERE T eid %(t)s, FS eid %(fs)s',
       
   322                              {'t': self.eid, 'fs': fromstate})
       
   323         else:
       
   324             if hasattr(tostate, 'eid'):
       
   325                 tostate = tostate.eid
       
   326             self._cw.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
       
   327                              'X subworkflow_state FS, X destination_state TS '
       
   328                              'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s',
       
   329                              {'t': self.eid, 'fs': fromstate, 'ts': tostate})
       
   330 
       
   331     def get_exit_point(self, entity, stateeid):
       
   332         """if state is an exit point, return its associated destination state"""
       
   333         if hasattr(stateeid, 'eid'):
       
   334             stateeid = stateeid.eid
       
   335         try:
       
   336             tostateeid = self.exit_points()[stateeid]
       
   337         except KeyError:
       
   338             return None
       
   339         if tostateeid is None:
       
   340             # go back to state from which we've entered the subworkflow
       
   341             return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state
       
   342         return self._cw.entity_from_eid(tostateeid)
       
   343 
       
   344     @cached
       
   345     def exit_points(self):
       
   346         result = {}
       
   347         for ep in self.subworkflow_exit:
       
   348             result[ep.subwf_state.eid] = ep.destination and ep.destination.eid
       
   349         return result
       
   350 
       
   351     def cw_clear_all_caches(self):
       
   352         super(WorkflowTransition, self).cw_clear_all_caches()
       
   353         clear_cache(self, 'exit_points')
       
   354 
       
   355 
       
   356 class SubWorkflowExitPoint(AnyEntity):
       
   357     """customized class for SubWorkflowExitPoint entities"""
       
   358     __regid__ = 'SubWorkflowExitPoint'
       
   359 
       
   360     @property
       
   361     def subwf_state(self):
       
   362         return self.subworkflow_state[0]
       
   363 
       
   364     @property
       
   365     def destination(self):
       
   366         return self.destination_state and self.destination_state[0] or None
       
   367 
       
   368 
       
   369 class State(AnyEntity):
       
   370     """customized class for State entities"""
       
   371     __regid__ = 'State'
       
   372     fetch_attrs, cw_fetch_order = fetch_config(['name'])
       
   373     rest_attr = 'eid'
       
   374 
       
   375     def dc_long_title(self):
       
   376         return '%s (%s)' % (self.name, self._cw._(self.name))
       
   377 
       
   378     @property
       
   379     def workflow(self):
       
   380         # take care, may be missing in multi-sources configuration
       
   381         return self.state_of and self.state_of[0] or None
       
   382 
       
   383 
       
   384 class TrInfo(AnyEntity):
       
   385     """customized class for Transition information entities
       
   386     """
       
   387     __regid__ = 'TrInfo'
       
   388     fetch_attrs, cw_fetch_order = fetch_config(['creation_date', 'comment'],
       
   389                                                pclass=None) # don't want modification_date
       
   390     @property
       
   391     def for_entity(self):
       
   392         return self.wf_info_for[0]
       
   393 
       
   394     @property
       
   395     def previous_state(self):
       
   396         return self.from_state[0]
       
   397 
       
   398     @property
       
   399     def new_state(self):
       
   400         return self.to_state[0]
       
   401 
       
   402     @property
       
   403     def transition(self):
       
   404         return self.by_transition and self.by_transition[0] or None
       
   405 
       
   406 
       
   407 
       
   408 class IWorkflowableAdapter(EntityAdapter):
       
   409     """base adapter providing workflow helper methods for workflowable entities.
       
   410     """
       
   411     __regid__ = 'IWorkflowable'
       
   412     __select__ = relation_possible('in_state')
       
   413 
       
   414     @cached
       
   415     def cwetype_workflow(self):
       
   416         """return the default workflow for entities of this type"""
       
   417         # XXX CWEType method
       
   418         wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
       
   419                                   'ET name %(et)s', {'et': text_type(self.entity.cw_etype)})
       
   420         if wfrset:
       
   421             return wfrset.get_entity(0, 0)
       
   422         self.warning("can't find any workflow for %s", self.entity.cw_etype)
       
   423         return None
       
   424 
       
   425     @property
       
   426     def main_workflow(self):
       
   427         """return current workflow applied to this entity"""
       
   428         if self.entity.custom_workflow:
       
   429             return self.entity.custom_workflow[0]
       
   430         return self.cwetype_workflow()
       
   431 
       
   432     @property
       
   433     def current_workflow(self):
       
   434         """return current workflow applied to this entity"""
       
   435         return self.current_state and self.current_state.workflow or self.main_workflow
       
   436 
       
   437     @property
       
   438     def current_state(self):
       
   439         """return current state entity"""
       
   440         return self.entity.in_state and self.entity.in_state[0] or None
       
   441 
       
   442     @property
       
   443     def state(self):
       
   444         """return current state name"""
       
   445         try:
       
   446             return self.current_state.name
       
   447         except AttributeError:
       
   448             self.warning('entity %s has no state', self.entity)
       
   449             return None
       
   450 
       
   451     @property
       
   452     def printable_state(self):
       
   453         """return current state name translated to context's language"""
       
   454         state = self.current_state
       
   455         if state:
       
   456             return self._cw._(state.name)
       
   457         return u''
       
   458 
       
   459     @property
       
   460     def workflow_history(self):
       
   461         """return the workflow history for this entity (eg ordered list of
       
   462         TrInfo entities)
       
   463         """
       
   464         return self.entity.reverse_wf_info_for
       
   465 
       
   466     def latest_trinfo(self):
       
   467         """return the latest transition information for this entity"""
       
   468         try:
       
   469             return self.workflow_history[-1]
       
   470         except IndexError:
       
   471             return None
       
   472 
       
   473     def possible_transitions(self, type='normal'):
       
   474         """generates transition that MAY be fired for the given entity,
       
   475         expected to be in this state
       
   476         used only by the UI
       
   477         """
       
   478         if self.current_state is None or self.current_workflow is None:
       
   479             return
       
   480         rset = self._cw.execute(
       
   481             'Any T,TT, TN WHERE S allowed_transition T, S eid %(x)s, '
       
   482             'T type TT, T type %(type)s, '
       
   483             'T name TN, T transition_of WF, WF eid %(wfeid)s',
       
   484             {'x': self.current_state.eid, 'type': text_type(type),
       
   485              'wfeid': self.current_workflow.eid})
       
   486         for tr in rset.entities():
       
   487             if tr.may_be_fired(self.entity.eid):
       
   488                 yield tr
       
   489 
       
   490     def subworkflow_input_trinfo(self):
       
   491         """return the TrInfo which has be recorded when this entity went into
       
   492         the current sub-workflow
       
   493         """
       
   494         if self.main_workflow.eid == self.current_workflow.eid:
       
   495             return # doesn't make sense
       
   496         subwfentries = []
       
   497         for trinfo in self.workflow_history:
       
   498             if (trinfo.transition and
       
   499                 trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
       
   500                 # entering or leaving a subworkflow
       
   501                 if (subwfentries and
       
   502                     subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
       
   503                     subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
       
   504                     # leave
       
   505                     del subwfentries[-1]
       
   506                 else:
       
   507                     # enter
       
   508                     subwfentries.append(trinfo)
       
   509         if not subwfentries:
       
   510             return None
       
   511         return subwfentries[-1]
       
   512 
       
   513     def subworkflow_input_transition(self):
       
   514         """return the transition which has went through the current sub-workflow
       
   515         """
       
   516         return getattr(self.subworkflow_input_trinfo(), 'transition', None)
       
   517 
       
   518     def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
       
   519         kwargs = {}
       
   520         if comment is not None:
       
   521             kwargs['comment'] = comment
       
   522             if commentformat is not None:
       
   523                 kwargs['comment_format'] = commentformat
       
   524         kwargs['wf_info_for'] = self.entity
       
   525         if treid is not None:
       
   526             kwargs['by_transition'] = self._cw.entity_from_eid(treid)
       
   527         if tseid is not None:
       
   528             kwargs['to_state'] = self._cw.entity_from_eid(tseid)
       
   529         return self._cw.create_entity('TrInfo', **kwargs)
       
   530 
       
   531     def _get_transition(self, tr):
       
   532         assert self.current_workflow
       
   533         if isinstance(tr, string_types):
       
   534             _tr = self.current_workflow.transition_by_name(tr)
       
   535             assert _tr is not None, 'not a %s transition: %s' % (
       
   536                 self.__regid__, tr)
       
   537             tr = _tr
       
   538         return tr
       
   539 
       
   540     def fire_transition(self, tr, comment=None, commentformat=None):
       
   541         """change the entity's state by firing given transition (name or entity)
       
   542         in entity's workflow
       
   543         """
       
   544         tr = self._get_transition(tr)
       
   545         return self._add_trinfo(comment, commentformat, tr.eid)
       
   546 
       
   547     def fire_transition_if_possible(self, tr, comment=None, commentformat=None):
       
   548         """change the entity's state by firing given transition (name or entity)
       
   549         in entity's workflow if this transition is possible
       
   550         """
       
   551         tr = self._get_transition(tr)
       
   552         if any(tr_ for tr_ in self.possible_transitions()
       
   553                if tr_.eid == tr.eid):
       
   554             self.fire_transition(tr, comment, commentformat)
       
   555 
       
   556     def change_state(self, statename, comment=None, commentformat=None, tr=None):
       
   557         """change the entity's state to the given state (name or entity) in
       
   558         entity's workflow. This method should only by used by manager to fix an
       
   559         entity's state when their is no matching transition, otherwise
       
   560         fire_transition should be used.
       
   561         """
       
   562         assert self.current_workflow
       
   563         if hasattr(statename, 'eid'):
       
   564             stateeid = statename.eid
       
   565         else:
       
   566             state = self.current_workflow.state_by_name(statename)
       
   567             if state is None:
       
   568                 raise WorkflowException('not a %s state: %s' % (self.__regid__,
       
   569                                                                 statename))
       
   570             stateeid = state.eid
       
   571         # XXX try to find matching transition?
       
   572         return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
       
   573 
       
   574     def set_initial_state(self, statename):
       
   575         """set a newly created entity's state to the given state (name or entity)
       
   576         in entity's workflow. This is useful if you don't want it to be the
       
   577         workflow's initial state.
       
   578         """
       
   579         assert self.current_workflow
       
   580         if hasattr(statename, 'eid'):
       
   581             stateeid = statename.eid
       
   582         else:
       
   583             state = self.current_workflow.state_by_name(statename)
       
   584             if state is None:
       
   585                 raise WorkflowException('not a %s state: %s' % (self.__regid__,
       
   586                                                                 statename))
       
   587             stateeid = state.eid
       
   588         self._cw.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
       
   589                          {'x': self.entity.eid, 's': stateeid})