wfobjs.py 22.4 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
"""
24
from logilab.common.decorators import cached, clear_cache
25

Adrien Di Mascio's avatar
Adrien Di Mascio committed
26
from cubicweb.entities import AnyEntity, fetch_config
27
from cubicweb.entity import EntityAdapter
28
from cubicweb.predicates import relation_possible
29

30
31
32
33
34
35
36
37
38
39

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


40
class WorkflowException(Exception): pass
41
42

class Workflow(AnyEntity):
43
    __regid__ = 'Workflow'
44
45
46
47

    @property
    def initial(self):
        """return the initial state for this workflow"""
48
        return self.initial_state and self.initial_state[0] or None
49
50
51
52
53

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

57
58
59
60
61
62
63
64
65
    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)
66
        for tr in self._cw.execute('Any T WHERE T is WorkflowTransition, '
67
68
69
70
71
72
73
                                   '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

74
75
76
    # state / transitions accessors ############################################

    def state_by_name(self, statename):
77
        rset = self._cw.execute('Any S, SN WHERE S name SN, S name %(n)s, '
78
                                'S state_of WF, WF eid %(wf)s',
79
                                {'n': statename, 'wf': self.eid})
80
81
82
83
84
        if rset:
            return rset.get_entity(0, 0)
        return None

    def state_by_eid(self, eid):
85
        rset = self._cw.execute('Any S, SN WHERE S name SN, S eid %(s)s, '
86
                                'S state_of WF, WF eid %(wf)s',
87
                                {'s': eid, 'wf': self.eid})
88
89
90
91
92
        if rset:
            return rset.get_entity(0, 0)
        return None

    def transition_by_name(self, trname):
93
        rset = self._cw.execute('Any T, TN WHERE T name TN, T name %(n)s, '
94
                                'T transition_of WF, WF eid %(wf)s',
Denis Laxalde's avatar
Denis Laxalde committed
95
                                {'n': trname, 'wf': self.eid})
96
97
98
99
100
        if rset:
            return rset.get_entity(0, 0)
        return None

    def transition_by_eid(self, eid):
101
        rset = self._cw.execute('Any T, TN WHERE T name TN, T eid %(t)s, '
102
                                'T transition_of WF, WF eid %(wf)s',
103
                                {'t': eid, 'wf': self.eid})
104
105
106
107
108
        if rset:
            return rset.get_entity(0, 0)
        return None

    # wf construction methods ##################################################
Adrien Di Mascio's avatar
Adrien Di Mascio committed
109

110
    def add_state(self, name, initial=False, **kwargs):
111
        """add a state to this workflow"""
Denis Laxalde's avatar
Denis Laxalde committed
112
        state = self._cw.create_entity('State', name=name, **kwargs)
113
        self._cw.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
114
                         {'s': state.eid, 'wf': self.eid})
115
        if initial:
116
            assert not self.initial, "Initial state already defined as %s" % self.initial
117
            self._cw.execute('SET WF initial_state S '
118
                             'WHERE S eid %(s)s, WF eid %(wf)s',
119
                             {'s': state.eid, 'wf': self.eid})
120
121
        return state

122
123
    def _add_transition(self, trtype, name, fromstates,
                        requiredgroups=(), conditions=(), **kwargs):
Denis Laxalde's avatar
Denis Laxalde committed
124
        tr = self._cw.create_entity(trtype, name=name, **kwargs)
125
        self._cw.execute('SET T transition_of WF '
126
                         'WHERE T eid %(t)s, WF eid %(wf)s',
127
                         {'t': tr.eid, 'wf': self.eid})
128
129
130
        assert fromstates, fromstates
        if not isinstance(fromstates, (tuple, list)):
            fromstates = (fromstates,)
131
132
133
        for state in fromstates:
            if hasattr(state, 'eid'):
                state = state.eid
134
            self._cw.execute('SET S allowed_transition T '
135
                             'WHERE S eid %(s)s, T eid %(t)s',
136
                             {'s': state, 't': tr.eid})
137
        tr.set_permissions(requiredgroups, conditions, reset=False)
138
139
        return tr

140
    def add_transition(self, name, fromstates, tostate=None,
141
142
143
144
                       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)
145
146
147
        if tostate is not None:
            if hasattr(tostate, 'eid'):
                tostate = tostate.eid
Sylvain Thénault's avatar
Sylvain Thénault committed
148
            self._cw.execute('SET T destination_state S '
149
                             'WHERE S eid %(s)s, T eid %(t)s',
150
                             {'t': tr.eid, 's': tostate})
151
152
        return tr

153
    def add_wftransition(self, name, subworkflow, fromstates, exitpoints=(),
Aurelien Campeas's avatar
Aurelien Campeas committed
154
                         requiredgroups=(), conditions=(), **kwargs):
155
156
157
158
159
        """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
160
        assert self._cw.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s',
161
                                {'t': tr.eid, 'wf': subworkflow})
162
163
        for fromstate, tostate in exitpoints:
            tr.add_exit_point(fromstate, tostate)
164
        return tr
165

166
167
168
169
170
171
    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)
172
        args = {'os': todelstate.eid, 'ns': replacement.eid}
173
        execute = self._cw.execute
174
175
176
177
178
179
        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)
180
        todelstate.cw_delete()
181

Adrien Di Mascio's avatar
Adrien Di Mascio committed
182

183
184
185
186
187
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
188
    """
189
    __regid__ = 'BaseTransition'
190
    fetch_attrs, cw_fetch_order = fetch_config(['name', 'type'])
191

192
    def __init__(self, *args, **kwargs):
193
        if self.cw_etype == 'BaseTransition':
194
            raise WorkflowException('should not be instantiated')
195
        super(BaseTransition, self).__init__(*args, **kwargs)
196

197
198
199
200
    @property
    def workflow(self):
        return self.transition_of[0]

Sylvain Thénault's avatar
cleanup    
Sylvain Thénault committed
201
202
203
204
205
    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)

206
207
    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
208

209
        `eid` is the eid of the object on which we may fire the transition
Adrien Di Mascio's avatar
Adrien Di Mascio committed
210
        """
211
212
213
214
        DBG = False
        if server.DEBUG & server.DBG_SEC:
            if 'transition' in server._SECURITY_CAPS:
                DBG = True
215
        user = self._cw.user
Adrien Di Mascio's avatar
Adrien Di Mascio committed
216
217
218
219
220
        # 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:
221
                if DBG:
Samuel Trégouët's avatar
Samuel Trégouët committed
222
                    print('may_be_fired: %r may fire: user matches %s' % (self.name, groups))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
223
224
                return matches
            if 'owners' in groups and user.owns(eid):
225
                if DBG:
Samuel Trégouët's avatar
Samuel Trégouët committed
226
                    print('may_be_fired: %r may fire: user is owner' % self.name)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
227
228
229
                return True
        # check one of the rql expression conditions matches if any
        if self.condition:
230
            if DBG:
231
                print('may_be_fired: %r: %s' %
Samuel Trégouët's avatar
Samuel Trégouët committed
232
                      (self.name, [(rqlexpr.expression,
233
                                    rqlexpr.check_expression(self._cw, eid))
Samuel Trégouët's avatar
Samuel Trégouët committed
234
                                    for rqlexpr in self.condition]))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
235
            for rqlexpr in self.condition:
236
                if rqlexpr.check_expression(self._cw, eid):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
237
238
239
240
241
                    return True
        if self.condition or groups:
            return False
        return True

242
    def set_permissions(self, requiredgroups=(), conditions=(), reset=True):
243
244
245
246
        """set or add (if `reset` is False) groups and conditions for this
        transition
        """
        if reset:
247
            self._cw.execute('DELETE T require_group G WHERE T eid %(x)s',
248
                             {'x': self.eid})
249
            self._cw.execute('DELETE T condition R WHERE T eid %(x)s',
250
                             {'x': self.eid})
251
        for gname in requiredgroups:
252
            rset = self._cw.execute('SET T require_group G '
253
                                    'WHERE T eid %(x)s, G name %(gn)s',
Denis Laxalde's avatar
Denis Laxalde committed
254
                                    {'x': self.eid, 'gn': gname})
255
            assert rset, '%s is not a known group' % gname
Denis Laxalde's avatar
Denis Laxalde committed
256
        if isinstance(conditions, str):
257
258
            conditions = (conditions,)
        for expr in conditions:
Denis Laxalde's avatar
Denis Laxalde committed
259
260
            if isinstance(expr, str):
                kwargs = {'expr': expr}
Sylvain Thénault's avatar
oops    
Sylvain Thénault committed
261
262
            else:
                assert isinstance(expr, dict)
263
264
265
                kwargs = expr
            kwargs['x'] = self.eid
            kwargs.setdefault('mainvars', u'X')
266
            self._cw.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
Sylvain Thénault's avatar
Sylvain Thénault committed
267
                             'X expression %(expr)s, X mainvars %(mainvars)s, '
268
                             'T condition X WHERE T eid %(x)s', kwargs)
269
        # XXX clear caches?
270

Adrien Di Mascio's avatar
Adrien Di Mascio committed
271

272
273
class Transition(BaseTransition):
    """customized class for Transition entities"""
274
    __regid__ = 'Transition'
275

276
277
278
    def dc_long_title(self):
        return '%s (%s)' % (self.name, self._cw._(self.name))

279
280
281
282
    def destination(self, entity):
        try:
            return self.destination_state[0]
        except IndexError:
283
            return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state
284

285
286
287
288
    def potential_destinations(self):
        try:
            yield self.destination_state[0]
        except IndexError:
289
290
291
292
            for incomingstate in self.reverse_allowed_transition:
                for tr in incomingstate.reverse_destination_state:
                    for previousstate in tr.reverse_allowed_transition:
                        yield previousstate
293
294
295
296


class WorkflowTransition(BaseTransition):
    """customized class for WorkflowTransition entities"""
297
    __regid__ = 'WorkflowTransition'
298
299
300
301
302

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

303
    def destination(self, entity):
304
305
        return self.subwf.initial

306
307
308
    def potential_destinations(self):
        yield self.subwf.initial

309
310
311
    def add_exit_point(self, fromstate, tostate):
        if hasattr(fromstate, 'eid'):
            fromstate = fromstate.eid
312
        if tostate is None:
Sylvain Thénault's avatar
Sylvain Thénault committed
313
            self._cw.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
314
                             'X subworkflow_state FS WHERE T eid %(t)s, FS eid %(fs)s',
315
                             {'t': self.eid, 'fs': fromstate})
316
317
318
        else:
            if hasattr(tostate, 'eid'):
                tostate = tostate.eid
Sylvain Thénault's avatar
Sylvain Thénault committed
319
            self._cw.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
320
321
                             'X subworkflow_state FS, X destination_state TS '
                             'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s',
322
                             {'t': self.eid, 'fs': fromstate, 'ts': tostate})
323
324

    def get_exit_point(self, entity, stateeid):
325
        """if state is an exit point, return its associated destination state"""
326
327
328
329
330
331
332
333
        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
334
            return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state
Sylvain Thénault's avatar
Sylvain Thénault committed
335
        return self._cw.entity_from_eid(tostateeid)
336
337
338
339
340

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

344
345
    def cw_clear_all_caches(self):
        super(WorkflowTransition, self).cw_clear_all_caches()
346
347
348
349
350
        clear_cache(self, 'exit_points')


class SubWorkflowExitPoint(AnyEntity):
    """customized class for SubWorkflowExitPoint entities"""
351
    __regid__ = 'SubWorkflowExitPoint'
352
353
354
355
356
357
358

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

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

361
362
363

class State(AnyEntity):
    """customized class for State entities"""
364
    __regid__ = 'State'
365
    fetch_attrs, cw_fetch_order = fetch_config(['name'])
Adrien Di Mascio's avatar
Adrien Di Mascio committed
366
    rest_attr = 'eid'
367

368
369
370
    def dc_long_title(self):
        return '%s (%s)' % (self.name, self._cw._(self.name))

371
372
    @property
    def workflow(self):
373
        # take care, may be missing in multi-sources configuration
374
        return self.state_of and self.state_of[0] or None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
375

376

Adrien Di Mascio's avatar
Adrien Di Mascio committed
377
378
379
class TrInfo(AnyEntity):
    """customized class for Transition information entities
    """
380
    __regid__ = 'TrInfo'
381
382
    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
383
384
    @property
    def for_entity(self):
385
386
        return self.wf_info_for[0]

Adrien Di Mascio's avatar
Adrien Di Mascio committed
387
388
    @property
    def previous_state(self):
389
        return self.from_state[0]
390

Adrien Di Mascio's avatar
Adrien Di Mascio committed
391
392
393
394
    @property
    def new_state(self):
        return self.to_state[0]

395
396
397
398
399
    @property
    def transition(self):
        return self.by_transition and self.by_transition[0] or None


400

Aurelien Campeas's avatar
Aurelien Campeas committed
401
class IWorkflowableAdapter(EntityAdapter):
402
403
404
405
406
407
408
409
410
411
    """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, '
Denis Laxalde's avatar
Denis Laxalde committed
412
                                  'ET name %(et)s', {'et': self.entity.cw_etype})
413
414
        if wfrset:
            return wfrset.get_entity(0, 0)
415
        self.warning("can't find any workflow for %s", self.entity.cw_etype)
416
        return None
417
418

    @property
419
    def main_workflow(self):
420
        """return current workflow applied to this entity"""
421
422
        if self.entity.custom_workflow:
            return self.entity.custom_workflow[0]
423
        return self.cwetype_workflow()
424

425
426
427
    @property
    def current_workflow(self):
        """return current workflow applied to this entity"""
Sylvain Thénault's avatar
cleanup    
Sylvain Thénault committed
428
        return self.current_state and self.current_state.workflow or self.main_workflow
429

430
431
432
    @property
    def current_state(self):
        """return current state entity"""
433
        return self.entity.in_state and self.entity.in_state[0] or None
434
435
436
437
438

    @property
    def state(self):
        """return current state name"""
        try:
439
440
            return self.current_state.name
        except AttributeError:
441
            self.warning('entity %s has no state', self.entity)
442
443
444
445
446
447
448
            return None

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

452
453
454
455
456
    @property
    def workflow_history(self):
        """return the workflow history for this entity (eg ordered list of
        TrInfo entities)
        """
457
        return self.entity.reverse_wf_info_for
458

459
460
    def latest_trinfo(self):
        """return the latest transition information for this entity"""
461
        try:
462
            return self.workflow_history[-1]
463
464
        except IndexError:
            return None
465

466
    def possible_transitions(self, type='normal'):
467
468
        """generates transition that MAY be fired for the given entity,
        expected to be in this state
469
        used only by the UI
470
471
472
        """
        if self.current_state is None or self.current_workflow is None:
            return
473
        rset = self._cw.execute(
474
475
476
            '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',
Denis Laxalde's avatar
Denis Laxalde committed
477
            {'x': self.current_state.eid, 'type': type,
478
             'wfeid': self.current_workflow.eid})
479
        for tr in rset.entities():
480
            if tr.may_be_fired(self.entity.eid):
481
482
                yield tr

483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
    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)

511
    def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
512
513
514
515
516
        kwargs = {}
        if comment is not None:
            kwargs['comment'] = comment
            if commentformat is not None:
                kwargs['comment_format'] = commentformat
517
        kwargs['wf_info_for'] = self.entity
518
        if treid is not None:
Sylvain Thénault's avatar
Sylvain Thénault committed
519
            kwargs['by_transition'] = self._cw.entity_from_eid(treid)
520
        if tseid is not None:
Sylvain Thénault's avatar
Sylvain Thénault committed
521
            kwargs['to_state'] = self._cw.entity_from_eid(tseid)
Sylvain Thénault's avatar
Sylvain Thénault committed
522
        return self._cw.create_entity('TrInfo', **kwargs)
523

524
    def _get_transition(self, tr):
525
        assert self.current_workflow
Denis Laxalde's avatar
Denis Laxalde committed
526
        if isinstance(tr, str):
527
            _tr = self.current_workflow.transition_by_name(tr)
Sylvain Thénault's avatar
Sylvain Thénault committed
528
529
            assert _tr is not None, 'not a %s transition: %s' % (
                self.__regid__, tr)
530
            tr = _tr
531
532
533
534
535
536
537
        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)
538
        return self._add_trinfo(comment, commentformat, tr.eid)
539

540
541
542
543
544
545
546
    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):
547
            self.fire_transition(tr, comment, commentformat)
548

549
    def change_state(self, statename, comment=None, commentformat=None, tr=None):
550
551
552
553
        """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.
554
555
        """
        assert self.current_workflow
556
557
        if hasattr(statename, 'eid'):
            stateeid = statename.eid
558
        else:
559
            state = self.current_workflow.state_by_name(statename)
560
            if state is None:
561
                raise WorkflowException('not a %s state: %s' % (self.__regid__,
562
                                                                statename))
563
            stateeid = state.eid
564
        # XXX try to find matching transition?
565
        return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582

    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})