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