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

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

36
37
38
39
40
41
42
43
44
45

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


46
class WorkflowException(Exception): pass
47
48

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

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

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

63
64
65
66
67
68
69
70
71
    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)
72
        for tr in self._cw.execute('Any T WHERE T is WorkflowTransition, '
73
74
75
76
77
78
79
                                   '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

80
81
82
    # state / transitions accessors ############################################

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

    def state_by_eid(self, eid):
91
        rset = self._cw.execute('Any S, SN WHERE S name SN, S eid %(s)s, '
92
                                'S state_of WF, WF eid %(wf)s',
93
                                {'s': eid, 'wf': self.eid})
94
95
96
97
98
        if rset:
            return rset.get_entity(0, 0)
        return None

    def transition_by_name(self, trname):
99
        rset = self._cw.execute('Any T, TN WHERE T name TN, T name %(n)s, '
100
                                'T transition_of WF, WF eid %(wf)s',
101
                                {'n': text_type(trname), 'wf': self.eid})
102
103
104
105
106
        if rset:
            return rset.get_entity(0, 0)
        return None

    def transition_by_eid(self, eid):
107
        rset = self._cw.execute('Any T, TN WHERE T name TN, T eid %(t)s, '
108
                                'T transition_of WF, WF eid %(wf)s',
109
                                {'t': eid, 'wf': self.eid})
110
111
112
113
114
        if rset:
            return rset.get_entity(0, 0)
        return None

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

116
    def add_state(self, name, initial=False, **kwargs):
117
        """add a state to this workflow"""
118
        state = self._cw.create_entity('State', name=text_type(name), **kwargs)
119
        self._cw.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
120
                         {'s': state.eid, 'wf': self.eid})
121
        if initial:
122
            assert not self.initial, "Initial state already defined as %s" % self.initial
123
            self._cw.execute('SET WF initial_state S '
124
                             'WHERE S eid %(s)s, WF eid %(wf)s',
125
                             {'s': state.eid, 'wf': self.eid})
126
127
        return state

128
129
    def _add_transition(self, trtype, name, fromstates,
                        requiredgroups=(), conditions=(), **kwargs):
130
        tr = self._cw.create_entity(trtype, name=text_type(name), **kwargs)
131
        self._cw.execute('SET T transition_of WF '
132
                         'WHERE T eid %(t)s, WF eid %(wf)s',
133
                         {'t': tr.eid, 'wf': self.eid})
134
135
136
        assert fromstates, fromstates
        if not isinstance(fromstates, (tuple, list)):
            fromstates = (fromstates,)
137
138
139
        for state in fromstates:
            if hasattr(state, 'eid'):
                state = state.eid
140
            self._cw.execute('SET S allowed_transition T '
141
                             'WHERE S eid %(s)s, T eid %(t)s',
142
                             {'s': state, 't': tr.eid})
143
        tr.set_permissions(requiredgroups, conditions, reset=False)
144
145
        return tr

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

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

172
173
174
175
176
177
    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)
178
        args = {'os': todelstate.eid, 'ns': replacement.eid}
179
        execute = self._cw.execute
180
181
182
183
184
185
        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)
186
        todelstate.cw_delete()
187

Adrien Di Mascio's avatar
Adrien Di Mascio committed
188

189
190
191
192
193
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
194
    """
195
    __regid__ = 'BaseTransition'
196
    fetch_attrs, cw_fetch_order = fetch_config(['name', 'type'])
197

198
    def __init__(self, *args, **kwargs):
199
        if self.cw_etype == 'BaseTransition':
200
            raise WorkflowException('should not be instantiated')
201
        super(BaseTransition, self).__init__(*args, **kwargs)
202

203
204
205
206
    @property
    def workflow(self):
        return self.transition_of[0]

Sylvain Thénault's avatar
cleanup    
Sylvain Thénault committed
207
208
209
210
211
    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)

212
213
    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
214

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

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
277

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

367
368
369

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

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

377
378
    @property
    def workflow(self):
379
        # take care, may be missing in multi-sources configuration
380
        return self.state_of and self.state_of[0] or None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
381

382

Adrien Di Mascio's avatar
Adrien Di Mascio committed
383
384
385
class TrInfo(AnyEntity):
    """customized class for Transition information entities
    """
386
    __regid__ = 'TrInfo'
387
388
    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
389
390
    @property
    def for_entity(self):
391
392
        return self.wf_info_for[0]

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

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

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


406

Aurelien Campeas's avatar
Aurelien Campeas committed
407
class IWorkflowableAdapter(EntityAdapter):
408
409
410
411
412
413
414
415
416
417
    """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, '
418
                                  'ET name %(et)s', {'et': text_type(self.entity.cw_etype)})
419
420
        if wfrset:
            return wfrset.get_entity(0, 0)
421
        self.warning("can't find any workflow for %s", self.entity.cw_etype)
422
        return None
423
424

    @property
425
    def main_workflow(self):
426
        """return current workflow applied to this entity"""
427
428
        if self.entity.custom_workflow:
            return self.entity.custom_workflow[0]
429
        return self.cwetype_workflow()
430

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

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

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

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

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

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

472
    def possible_transitions(self, type='normal'):
473
474
        """generates transition that MAY be fired for the given entity,
        expected to be in this state
475
        used only by the UI
476
477
478
        """
        if self.current_state is None or self.current_workflow is None:
            return
479
        rset = self._cw.execute(
480
481
482
            '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',
483
            {'x': self.current_state.eid, 'type': text_type(type),
484
             'wfeid': self.current_workflow.eid})
485
        for tr in rset.entities():
486
            if tr.may_be_fired(self.entity.eid):
487
488
                yield tr

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

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

530
    def _get_transition(self, tr):
531
        assert self.current_workflow
532
        if isinstance(tr, string_types):
533
            _tr = self.current_workflow.transition_by_name(tr)
Sylvain Thénault's avatar
Sylvain Thénault committed
534
535
            assert _tr is not None, 'not a %s transition: %s' % (
                self.__regid__, tr)
536
            tr = _tr
537
538
539
540
541
542
543
        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)
544
        return self._add_trinfo(comment, commentformat, tr.eid)
545

546
547
548
549
550
551
552
    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):
553
            self.fire_transition(tr, comment, commentformat)
554

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

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