wfobjs.py 22.6 KB
Newer Older
1
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
11
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
12
13
14
15
16
17
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
18
"""workflow handling:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
19

20
21
22
* entity types defining workflow (Workflow, State, Transition...)
* workflow history (TrInfo)
* adapter for workflowable entities (IWorkflowableAdapter)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
23
"""
Samuel Trégouët's avatar
Samuel Trégouët committed
24
from __future__ import print_function
25

Sylvain Thénault's avatar
Sylvain Thénault committed
26

Adrien Di Mascio's avatar
Adrien Di Mascio committed
27

28
from six import text_type, string_types
29

30
from logilab.common.decorators import cached, clear_cache
31
32
from logilab.common.deprecation import deprecated

Adrien Di Mascio's avatar
Adrien Di Mascio committed
33
from cubicweb.entities import AnyEntity, fetch_config
34
from cubicweb.view import EntityAdapter
35
from cubicweb.predicates import relation_possible
36

37
38
39
40
41
42
43
44
45
46

try:
    from cubicweb import server
except ImportError:
    # We need to lookup DEBUG from there,
    # however a pure dbapi client may not have it.
    class server(object): pass
    server.DEBUG = False


47
class WorkflowException(Exception): pass
48
49

class Workflow(AnyEntity):
50
    __regid__ = 'Workflow'
51
52
53
54

    @property
    def initial(self):
        """return the initial state for this workflow"""
55
        return self.initial_state and self.initial_state[0] or None
56
57
58
59
60

    def is_default_workflow_of(self, etype):
        """return True if this workflow is the default workflow for the given
        entity type
        """
61
62
        return any(et for et in self.reverse_default_workflow
                   if et.name == etype)
63

64
65
66
67
68
69
70
71
72
    def iter_workflows(self, _done=None):
        """return an iterator on actual workflows, eg this workflow and its
        subworkflows
        """
        # infinite loop safety belt
        if _done is None:
            _done = set()
        yield self
        _done.add(self.eid)
73
        for tr in self._cw.execute('Any T WHERE T is WorkflowTransition, '
74
75
76
77
78
79
80
                                   'T transition_of WF, WF eid %(wf)s',
                                   {'wf': self.eid}).entities():
            if tr.subwf.eid in _done:
                continue
            for subwf in tr.subwf.iter_workflows(_done):
                yield subwf

81
82
83
    # state / transitions accessors ############################################

    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
88
89
90
91
        if rset:
            return rset.get_entity(0, 0)
        return None

    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
96
97
98
99
        if rset:
            return rset.get_entity(0, 0)
        return None

    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
104
105
106
107
        if rset:
            return rset.get_entity(0, 0)
        return None

    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
112
113
114
115
        if rset:
            return rset.get_entity(0, 0)
        return None

    # wf construction methods ##################################################
Adrien Di Mascio's avatar
Adrien Di Mascio committed
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
128
        return state

129
130
    def _add_transition(self, trtype, name, fromstates,
                        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
136
137
        assert fromstates, fromstates
        if not isinstance(fromstates, (tuple, list)):
            fromstates = (fromstates,)
138
139
140
        for state in fromstates:
            if hasattr(state, 'eid'):
                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
146
        return tr

147
    def add_transition(self, name, fromstates, tostate=None,
148
149
150
151
                       requiredgroups=(), conditions=(), **kwargs):
        """add a transition to this workflow from some state(s) to another"""
        tr = self._add_transition('Transition', name, fromstates,
                                  requiredgroups, conditions, **kwargs)
152
153
154
        if tostate is not None:
            if hasattr(tostate, 'eid'):
                tostate = tostate.eid
Sylvain Thénault's avatar
Sylvain Thénault committed
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
159
        return tr

160
    def add_wftransition(self, name, subworkflow, fromstates, exitpoints=(),
Aurelien Campeas's avatar
Aurelien Campeas committed
161
                         requiredgroups=(), conditions=(), **kwargs):
162
163
164
165
166
        """add a workflow transition to this workflow"""
        tr = self._add_transition('WorkflowTransition', name, fromstates,
                                  requiredgroups, conditions, **kwargs)
        if hasattr(subworkflow, 'eid'):
            subworkflow = subworkflow.eid
Sylvain Thénault's avatar
Sylvain Thénault committed
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
170
        for fromstate, tostate in exitpoints:
            tr.add_exit_point(fromstate, tostate)
171
        return tr
172

173
174
175
176
177
178
    def replace_state(self, todelstate, replacement):
        """migration convenience method"""
        if not hasattr(todelstate, 'eid'):
            todelstate = self.state_by_name(todelstate)
        if not hasattr(replacement, 'eid'):
            replacement = self.state_by_name(replacement)
179
        args = {'os': todelstate.eid, 'ns': replacement.eid}
180
        execute = self._cw.execute
181
182
183
184
185
186
        execute('SET X in_state NS WHERE X in_state OS, '
                'NS eid %(ns)s, OS eid %(os)s', args)
        execute('SET X from_state NS WHERE X from_state OS, '
                'OS eid %(os)s, NS eid %(ns)s', args)
        execute('SET X to_state NS WHERE X to_state OS, '
                'OS eid %(os)s, NS eid %(ns)s', args)
187
        todelstate.cw_delete()
188

Adrien Di Mascio's avatar
Adrien Di Mascio committed
189

190
191
192
193
194
class BaseTransition(AnyEntity):
    """customized class for abstract transition

    provides a specific may_be_fired method to check if the relation may be
    fired by the logged user
Adrien Di Mascio's avatar
Adrien Di Mascio committed
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
205
206
207
    @property
    def workflow(self):
        return self.transition_of[0]

Sylvain Thénault's avatar
cleanup    
Sylvain Thénault committed
208
209
210
211
212
    def has_input_state(self, state):
        if hasattr(state, 'eid'):
            state = state.eid
        return any(s for s in self.reverse_allowed_transition if s.eid == state)

213
214
    def may_be_fired(self, eid):
        """return true if the logged user may fire this transition
Adrien Di Mascio's avatar
Adrien Di Mascio committed
215

216
        `eid` is the eid of the object on which we may fire the transition
Adrien Di Mascio's avatar
Adrien Di Mascio committed
217
        """
218
219
220
221
        DBG = False
        if server.DEBUG & server.DBG_SEC:
            if 'transition' in server._SECURITY_CAPS:
                DBG = True
222
        user = self._cw.user
Adrien Di Mascio's avatar
Adrien Di Mascio committed
223
224
225
226
227
        # check user is at least in one of the required groups if any
        groups = frozenset(g.name for g in self.require_group)
        if groups:
            matches = user.matching_groups(groups)
            if matches:
228
                if DBG:
Samuel Trégouët's avatar
Samuel Trégouët committed
229
                    print('may_be_fired: %r may fire: user matches %s' % (self.name, groups))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
230
231
                return matches
            if 'owners' in groups and user.owns(eid):
232
                if DBG:
Samuel Trégouët's avatar
Samuel Trégouët committed
233
                    print('may_be_fired: %r may fire: user is owner' % self.name)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
234
235
236
                return True
        # check one of the rql expression conditions matches if any
        if self.condition:
237
            if DBG:
Samuel Trégouët's avatar
Samuel Trégouët committed
238
239
                print('my_be_fired: %r: %s' %
                      (self.name, [(rqlexpr.expression,
240
                                    rqlexpr.check_expression(self._cw, eid))
Samuel Trégouët's avatar
Samuel Trégouët committed
241
                                    for rqlexpr in self.condition]))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
242
            for rqlexpr in self.condition:
243
                if rqlexpr.check_expression(self._cw, eid):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
244
245
246
247
248
                    return True
        if self.condition or groups:
            return False
        return True

249
    def set_permissions(self, requiredgroups=(), conditions=(), reset=True):
250
251
252
253
        """set or add (if `reset` is False) groups and conditions for this
        transition
        """
        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
265
            conditions = (conditions,)
        for expr in conditions:
266
            if isinstance(expr, string_types):
267
                kwargs = {'expr': text_type(expr)}
Sylvain Thénault's avatar
oops    
Sylvain Thénault committed
268
269
            else:
                assert isinstance(expr, dict)
270
271
272
                kwargs = expr
            kwargs['x'] = self.eid
            kwargs.setdefault('mainvars', u'X')
273
            self._cw.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
Sylvain Thénault's avatar
Sylvain Thénault committed
274
                             'X expression %(expr)s, X mainvars %(mainvars)s, '
275
                             'T condition X WHERE T eid %(x)s', kwargs)
276
        # XXX clear caches?
277

Adrien Di Mascio's avatar
Adrien Di Mascio committed
278

279
280
class Transition(BaseTransition):
    """customized class for Transition entities"""
281
    __regid__ = 'Transition'
282

283
284
285
    def dc_long_title(self):
        return '%s (%s)' % (self.name, self._cw._(self.name))

286
287
288
289
    def destination(self, entity):
        try:
            return self.destination_state[0]
        except IndexError:
290
            return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state
291

292
293
294
295
    def potential_destinations(self):
        try:
            yield self.destination_state[0]
        except IndexError:
296
297
298
299
            for incomingstate in self.reverse_allowed_transition:
                for tr in incomingstate.reverse_destination_state:
                    for previousstate in tr.reverse_allowed_transition:
                        yield previousstate
300
301
302
303


class WorkflowTransition(BaseTransition):
    """customized class for WorkflowTransition entities"""
304
    __regid__ = 'WorkflowTransition'
305
306
307
308
309

    @property
    def subwf(self):
        return self.subworkflow[0]

310
    def destination(self, entity):
311
312
        return self.subwf.initial

313
314
315
    def potential_destinations(self):
        yield self.subwf.initial

316
317
318
    def add_exit_point(self, fromstate, tostate):
        if hasattr(fromstate, 'eid'):
            fromstate = fromstate.eid
319
        if tostate is None:
Sylvain Thénault's avatar
Sylvain Thénault committed
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
324
325
        else:
            if hasattr(tostate, 'eid'):
                tostate = tostate.eid
Sylvain Thénault's avatar
Sylvain Thénault committed
326
            self._cw.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
327
328
                             'X subworkflow_state FS, X destination_state TS '
                             '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
334
335
336
337
338
339
340
        if hasattr(stateeid, 'eid'):
            stateeid = stateeid.eid
        try:
            tostateeid = self.exit_points()[stateeid]
        except KeyError:
            return None
        if tostateeid is None:
            # go back to state from which we've entered the subworkflow
341
            return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state
Sylvain Thénault's avatar
Sylvain Thénault committed
342
        return self._cw.entity_from_eid(tostateeid)
343
344
345
346
347

    @cached
    def exit_points(self):
        result = {}
        for ep in self.subworkflow_exit:
348
            result[ep.subwf_state.eid] = ep.destination and ep.destination.eid
349
350
        return result

351
352
    def cw_clear_all_caches(self):
        super(WorkflowTransition, self).cw_clear_all_caches()
353
354
355
356
357
        clear_cache(self, 'exit_points')


class SubWorkflowExitPoint(AnyEntity):
    """customized class for SubWorkflowExitPoint entities"""
358
    __regid__ = 'SubWorkflowExitPoint'
359
360
361
362
363
364
365

    @property
    def subwf_state(self):
        return self.subworkflow_state[0]

    @property
    def destination(self):
366
        return self.destination_state and self.destination_state[0] or None
367

368
369
370

class State(AnyEntity):
    """customized class for State entities"""
371
    __regid__ = 'State'
372
    fetch_attrs, cw_fetch_order = fetch_config(['name'])
Adrien Di Mascio's avatar
Adrien Di Mascio committed
373
    rest_attr = 'eid'
374

375
376
377
    def dc_long_title(self):
        return '%s (%s)' % (self.name, self._cw._(self.name))

378
379
    @property
    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
Adrien Di Mascio's avatar
Adrien Di Mascio committed
382

383

Adrien Di Mascio's avatar
Adrien Di Mascio committed
384
385
386
class TrInfo(AnyEntity):
    """customized class for Transition information entities
    """
387
    __regid__ = 'TrInfo'
388
389
    fetch_attrs, cw_fetch_order = fetch_config(['creation_date', 'comment'],
                                               pclass=None) # don't want modification_date
Adrien Di Mascio's avatar
Adrien Di Mascio committed
390
391
    @property
    def for_entity(self):
392
393
        return self.wf_info_for[0]

Adrien Di Mascio's avatar
Adrien Di Mascio committed
394
395
    @property
    def previous_state(self):
396
        return self.from_state[0]
397

Adrien Di Mascio's avatar
Adrien Di Mascio committed
398
399
400
401
    @property
    def new_state(self):
        return self.to_state[0]

402
403
404
405
406
    @property
    def transition(self):
        return self.by_transition and self.by_transition[0] or None


407

Aurelien Campeas's avatar
Aurelien Campeas committed
408
class IWorkflowableAdapter(EntityAdapter):
409
410
411
412
413
414
415
416
417
418
    """base adapter providing workflow helper methods for workflowable entities.
    """
    __regid__ = 'IWorkflowable'
    __select__ = relation_possible('in_state')

    @cached
    def cwetype_workflow(self):
        """return the default workflow for entities of this type"""
        # XXX CWEType method
        wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
419
                                  'ET name %(et)s', {'et': text_type(self.entity.cw_etype)})
420
421
        if wfrset:
            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
429
        if self.entity.custom_workflow:
            return self.entity.custom_workflow[0]
430
        return self.cwetype_workflow()
431

432
433
434
    @property
    def current_workflow(self):
        """return current workflow applied to this entity"""
Sylvain Thénault's avatar
cleanup    
Sylvain Thénault committed
435
        return self.current_state and self.current_state.workflow or self.main_workflow
436

437
438
439
    @property
    def current_state(self):
        """return current state entity"""
440
        return self.entity.in_state and self.entity.in_state[0] or None
441
442
443
444
445

    @property
    def state(self):
        """return current state name"""
        try:
446
447
            return self.current_state.name
        except AttributeError:
448
            self.warning('entity %s has no state', self.entity)
449
450
451
452
453
454
455
            return None

    @property
    def printable_state(self):
        """return current state name translated to context's language"""
        state = self.current_state
        if state:
456
            return self._cw._(state.name)
457
458
        return u''

459
460
461
462
463
    @property
    def workflow_history(self):
        """return the workflow history for this entity (eg ordered list of
        TrInfo entities)
        """
464
        return self.entity.reverse_wf_info_for
465

466
467
    def latest_trinfo(self):
        """return the latest transition information for this entity"""
468
        try:
469
            return self.workflow_history[-1]
470
471
        except IndexError:
            return None
472

473
    def possible_transitions(self, type='normal'):
474
475
        """generates transition that MAY be fired for the given entity,
        expected to be in this state
476
        used only by the UI
477
478
479
        """
        if self.current_state is None or self.current_workflow is None:
            return
480
        rset = self._cw.execute(
481
482
483
            'Any T,TT, TN WHERE S allowed_transition T, S eid %(x)s, '
            'T type TT, T type %(type)s, '
            '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
489
                yield tr

490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
    def subworkflow_input_trinfo(self):
        """return the TrInfo which has be recorded when this entity went into
        the current sub-workflow
        """
        if self.main_workflow.eid == self.current_workflow.eid:
            return # doesn't make sense
        subwfentries = []
        for trinfo in self.workflow_history:
            if (trinfo.transition and
                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
                # entering or leaving a subworkflow
                if (subwfentries and
                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
                    subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
                    # leave
                    del subwfentries[-1]
                else:
                    # enter
                    subwfentries.append(trinfo)
        if not subwfentries:
            return None
        return subwfentries[-1]

    def subworkflow_input_transition(self):
        """return the transition which has went through the current sub-workflow
        """
        return getattr(self.subworkflow_input_trinfo(), 'transition', None)

518
    def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
519
520
521
522
523
        kwargs = {}
        if comment is not None:
            kwargs['comment'] = comment
            if commentformat is not None:
                kwargs['comment_format'] = commentformat
524
        kwargs['wf_info_for'] = self.entity
525
        if treid is not None:
Sylvain Thénault's avatar
Sylvain Thénault committed
526
            kwargs['by_transition'] = self._cw.entity_from_eid(treid)
527
        if tseid is not None:
Sylvain Thénault's avatar
Sylvain Thénault committed
528
            kwargs['to_state'] = self._cw.entity_from_eid(tseid)
Sylvain Thénault's avatar
Sylvain Thénault committed
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)
Sylvain Thénault's avatar
Sylvain Thénault committed
535
536
            assert _tr is not None, 'not a %s transition: %s' % (
                self.__regid__, tr)
537
            tr = _tr
538
539
540
541
542
543
544
        return tr

    def fire_transition(self, tr, comment=None, commentformat=None):
        """change the entity's state by firing given transition (name or entity)
        in entity's workflow
        """
        tr = self._get_transition(tr)
545
        return self._add_trinfo(comment, commentformat, tr.eid)
546

547
548
549
550
551
552
553
    def fire_transition_if_possible(self, tr, comment=None, commentformat=None):
        """change the entity's state by firing given transition (name or entity)
        in entity's workflow if this transition is possible
        """
        tr = self._get_transition(tr)
        if any(tr_ for tr_ in self.possible_transitions()
               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
558
559
560
        """change the entity's state to the given state (name or entity) in
        entity's workflow. This method should only by used by manager to fix an
        entity's state when their is no matching transition, otherwise
        fire_transition should be used.
561
562
        """
        assert self.current_workflow
563
564
        if hasattr(statename, 'eid'):
            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
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589

    def set_initial_state(self, statename):
        """set a newly created entity's state to the given state (name or entity)
        in entity's workflow. This is useful if you don't want it to be the
        workflow's initial state.
        """
        assert self.current_workflow
        if hasattr(statename, 'eid'):
            stateeid = statename.eid
        else:
            state = self.current_workflow.state_by_name(statename)
            if state is None:
                raise WorkflowException('not a %s state: %s' % (self.__regid__,
                                                                statename))
            stateeid = state.eid
        self._cw.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
                         {'x': self.entity.eid, 's': stateeid})