22 # delete previous state first in case we're using a super session |
23 # delete previous state first in case we're using a super session |
23 session.delete_relation(x, 'in_state', oldstate) |
24 session.delete_relation(x, 'in_state', oldstate) |
24 session.add_relation(x, 'in_state', newstate) |
25 session.add_relation(x, 'in_state', newstate) |
25 |
26 |
26 |
27 |
|
28 # operations ################################################################### |
|
29 |
27 class _SetInitialStateOp(hook.Operation): |
30 class _SetInitialStateOp(hook.Operation): |
28 """make initial state be a default state""" |
31 """make initial state be a default state""" |
29 |
32 |
30 def precommit_event(self): |
33 def precommit_event(self): |
31 session = self.session |
34 session = self.session |
32 entity = self.entity |
35 entity = self.entity |
33 # if there is an initial state and the entity's state is not set, |
36 # if there is an initial state and the entity's state is not set, |
34 # use the initial state as a default state |
37 # use the initial state as a default state |
35 pendingeids = session.transaction_data.get('pendingeids', ()) |
|
36 if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ |
38 if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ |
37 and entity.current_workflow: |
39 and entity.current_workflow: |
38 state = entity.current_workflow.initial |
40 state = entity.current_workflow.initial |
39 if state: |
41 if state: |
40 # use super session to by-pass security checks |
42 # use super session to by-pass security checks |
43 |
45 |
44 class _WorkflowChangedOp(hook.Operation): |
46 class _WorkflowChangedOp(hook.Operation): |
45 """fix entity current state when changing its workflow""" |
47 """fix entity current state when changing its workflow""" |
46 |
48 |
47 def precommit_event(self): |
49 def precommit_event(self): |
|
50 # notice that enforcement that new workflow apply to the entity's type is |
|
51 # done by schema rule, no need to check it here |
48 session = self.session |
52 session = self.session |
49 if session.deleted_in_transaction(self.eid): |
53 pendingeids = session.transaction_data.get('pendingeids', ()) |
|
54 if self.eid in pendingeids: |
50 return |
55 return |
51 entity = session.entity_from_eid(self.eid) |
56 entity = session.entity_from_eid(self.eid) |
52 # notice that enforcment that new workflow apply to the entity's type is |
57 # check custom workflow has not been rechanged to another one in the same |
53 # done by schema rule, no need to check it here |
58 # transaction |
54 if entity.current_workflow.eid == self.wfeid: |
59 mainwf = entity.main_workflow |
55 deststate = entity.current_workflow.initial |
60 if mainwf.eid == self.wfeid: |
|
61 deststate = mainwf.initial |
56 if not deststate: |
62 if not deststate: |
57 msg = session._('workflow has no initial state') |
63 msg = session._('workflow has no initial state') |
58 raise ValidationError(entity.eid, {'custom_workflow': msg}) |
64 raise ValidationError(entity.eid, {'custom_workflow': msg}) |
59 if entity.current_workflow.state_by_eid(entity.current_state.eid): |
65 if mainwf.state_by_eid(entity.current_state.eid): |
60 # nothing to do |
66 # nothing to do |
61 return |
67 return |
62 # if there are no history, simply go to new workflow's initial state |
68 # if there are no history, simply go to new workflow's initial state |
63 if not entity.workflow_history: |
69 if not entity.workflow_history: |
64 if entity.current_state.eid != deststate.eid: |
70 if entity.current_state.eid != deststate.eid: |
65 _change_state(session, entity.eid, |
71 _change_state(session, entity.eid, |
66 entity.current_state.eid, deststate.eid) |
72 entity.current_state.eid, deststate.eid) |
67 return |
73 return |
68 msg = session._('workflow changed to "%s"') |
74 msg = session._('workflow changed to "%s"') |
69 msg %= entity.current_workflow.name |
75 msg %= session._(mainwf.name) |
70 entity.change_state(deststate.name, msg) |
76 session.transaction_data[(entity.eid, 'customwf')] = self.wfeid |
71 |
77 entity.change_state(deststate, msg, u'text/plain') |
72 |
78 |
|
79 |
|
80 class _CheckTrExitPoint(hook.Operation): |
|
81 |
|
82 def precommit_event(self): |
|
83 tr = self.session.entity_from_eid(self.treid) |
|
84 outputs = set() |
|
85 for ep in tr.subworkflow_exit: |
|
86 if ep.subwf_state.eid in outputs: |
|
87 msg = self.session._("can't have multiple exits on the same state") |
|
88 raise ValidationError(self.treid, {'subworkflow_exit': msg}) |
|
89 outputs.add(ep.subwf_state.eid) |
|
90 |
|
91 |
|
92 # hooks ######################################################################## |
73 |
93 |
74 class WorkflowHook(hook.Hook): |
94 class WorkflowHook(hook.Hook): |
75 __abstract__ = True |
95 __abstract__ = True |
76 category = 'worfklow' |
96 category = 'worfklow' |
77 |
97 |
113 foreid = entity['wf_info_for'] |
133 foreid = entity['wf_info_for'] |
114 except KeyError: |
134 except KeyError: |
115 msg = session._('mandatory relation') |
135 msg = session._('mandatory relation') |
116 raise ValidationError(entity.eid, {'wf_info_for': msg}) |
136 raise ValidationError(entity.eid, {'wf_info_for': msg}) |
117 forentity = session.entity_from_eid(foreid) |
137 forentity = session.entity_from_eid(foreid) |
118 # then check it has a workflow set |
138 # then check it has a workflow set, unless we're in the process of changing |
119 wf = forentity.current_workflow |
139 # entity's workflow |
|
140 if session.transaction_data.get((forentity.eid, 'customwf')): |
|
141 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
|
142 wf = session.entity_from_eid(wfeid) |
|
143 else: |
|
144 wf = forentity.current_workflow |
120 if wf is None: |
145 if wf is None: |
121 msg = session._('related entity has no workflow set') |
146 msg = session._('related entity has no workflow set') |
122 raise ValidationError(entity.eid, {None: msg}) |
147 raise ValidationError(entity.eid, {None: msg}) |
123 # then check it has a state set |
148 # then check it has a state set |
124 fromstate = forentity.current_state |
149 fromstate = forentity.current_state |
125 if fromstate is None: |
150 if fromstate is None: |
126 msg = session._('related entity has no state') |
151 msg = session._('related entity has no state') |
127 raise ValidationError(entity.eid, {None: msg}) |
152 raise ValidationError(entity.eid, {None: msg}) |
|
153 # True if we are coming back from subworkflow |
|
154 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
|
155 cowpowers = session.is_super_session or 'managers' in session.user.groups |
128 # no investigate the requested state change... |
156 # no investigate the requested state change... |
129 try: |
157 try: |
130 treid = entity['by_transition'] |
158 treid = entity['by_transition'] |
131 except KeyError: |
159 except KeyError: |
132 # no transition set, check user is a manager and destination state is |
160 # no transition set, check user is a manager and destination state is |
133 # specified (and valid) |
161 # specified (and valid) |
134 if not (session.is_super_session or 'managers' in session.user.groups): |
162 if not cowpowers: |
135 msg = session._('mandatory relation') |
163 msg = session._('mandatory relation') |
136 raise ValidationError(entity.eid, {'by_transition': msg}) |
164 raise ValidationError(entity.eid, {'by_transition': msg}) |
137 deststateeid = entity.get('to_state') |
165 deststateeid = entity.get('to_state') |
138 if not deststateeid: |
166 if not deststateeid: |
139 msg = session._('mandatory relation') |
167 msg = session._('mandatory relation') |
140 raise ValidationError(entity.eid, {'by_transition': msg}) |
168 raise ValidationError(entity.eid, {'by_transition': msg}) |
141 deststate = wf.state_by_eid(deststateeid) |
169 deststate = wf.state_by_eid(deststateeid) |
142 if deststate is None: |
170 if not cowpowers and deststate is None: |
143 msg = session._("state doesn't belong to entity's workflow") |
171 msg = entity.req._("state doesn't belong to entity's workflow") |
144 raise ValidationError(entity.eid, {'to_state': msg}) |
172 raise ValidationError(entity.eid, {'to_state': msg}) |
145 else: |
173 else: |
146 # check transition is valid and allowed |
174 # check transition is valid and allowed, unless we're coming back from |
147 tr = wf.transition_by_eid(treid) |
175 # subworkflow |
148 if tr is None: |
176 tr = session.entity_from_eid(treid) |
149 msg = session._("transition doesn't belong to entity's workflow") |
177 if swtr is None: |
150 raise ValidationError(entity.eid, {'by_transition': msg}) |
178 if tr is None: |
151 if not tr.has_input_state(fromstate): |
179 msg = session._("transition doesn't belong to entity's workflow") |
152 msg = session._("transition isn't allowed") |
180 raise ValidationError(entity.eid, {'by_transition': msg}) |
153 raise ValidationError(entity.eid, {'by_transition': msg}) |
181 if not tr.has_input_state(fromstate): |
154 if not tr.may_be_fired(foreid): |
182 msg = session._("transition isn't allowed") |
155 msg = session._("transition may not be fired") |
183 raise ValidationError(entity.eid, {'by_transition': msg}) |
156 raise ValidationError(entity.eid, {'by_transition': msg}) |
184 if not tr.may_be_fired(foreid): |
157 deststateeid = tr.destination().eid |
185 msg = session._("transition may not be fired") |
|
186 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
187 if entity.get('to_state'): |
|
188 deststateeid = entity['to_state'] |
|
189 if not cowpowers and deststateeid != tr.destination().eid: |
|
190 msg = session._("transition isn't allowed") |
|
191 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
192 if swtr is None: |
|
193 deststate = session.entity_from_eid(deststateeid) |
|
194 if not cowpowers and deststate is None: |
|
195 msg = entity.req._("state doesn't belong to entity's workflow") |
|
196 raise ValidationError(entity.eid, {'to_state': msg}) |
|
197 else: |
|
198 deststateeid = tr.destination().eid |
158 # everything is ok, add missing information on the trinfo entity |
199 # everything is ok, add missing information on the trinfo entity |
159 entity['from_state'] = fromstate.eid |
200 entity['from_state'] = fromstate.eid |
160 entity['to_state'] = deststateeid |
201 entity['to_state'] = deststateeid |
161 nocheck = session.transaction_data.setdefault('skip-security', set()) |
202 nocheck = session.transaction_data.setdefault('skip-security', set()) |
162 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
203 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
168 __id__ = 'wffiretransition' |
209 __id__ = 'wffiretransition' |
169 __select__ = WorkflowHook.__select__ & entity_implements('TrInfo') |
210 __select__ = WorkflowHook.__select__ & entity_implements('TrInfo') |
170 events = ('after_add_entity',) |
211 events = ('after_add_entity',) |
171 |
212 |
172 def __call__(self): |
213 def __call__(self): |
173 _change_state(self._cw, self.entity['wf_info_for'], |
214 session = self._cw |
174 self.entity['from_state'], self.entity['to_state']) |
215 entity = self.entity |
|
216 _change_state(session, entity['wf_info_for'], |
|
217 entity['from_state'], entity['to_state']) |
|
218 forentity = session.entity_from_eid(entity['wf_info_for']) |
|
219 assert forentity.current_state.eid == entity['to_state'] |
|
220 if forentity.main_workflow.eid != forentity.current_workflow.eid: |
|
221 # we're in a subworkflow, check if we've reached an exit point |
|
222 wftr = forentity.subworkflow_input_transition() |
|
223 if wftr is None: |
|
224 # inconsistency detected |
|
225 msg = entity.req._("state doesn't belong to entity's current workflow") |
|
226 raise ValidationError(entity.eid, {'to_state': msg}) |
|
227 tostate = wftr.get_exit_point(entity['to_state']) |
|
228 if tostate is not None: |
|
229 # reached an exit point |
|
230 msg = session._('exiting from subworkflow %s') |
|
231 msg %= session._(forentity.current_workflow.name) |
|
232 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
|
233 # XXX iirk |
|
234 req = forentity._cw |
|
235 forentity._cw = session.super_session |
|
236 try: |
|
237 trinfo = forentity.change_state(tostate, msg, u'text/plain', |
|
238 tr=wftr) |
|
239 finally: |
|
240 forentity._cw = req |
|
241 |
|
242 |
|
243 class CheckInStateChangeAllowed(WorkflowHook): |
|
244 """check state apply, in case of direct in_state change using unsafe_execute |
|
245 """ |
|
246 __id__ = 'wfcheckinstate' |
|
247 __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state') |
|
248 events = ('before_add_relation',) |
|
249 |
|
250 def __call__(self): |
|
251 session = self._cw |
|
252 nocheck = session.transaction_data.setdefault('skip-security', ()) |
|
253 if (self.eidfrom, 'in_state', self.eidto) in nocheck: |
|
254 # state changed through TrInfo insertion, so we already know it's ok |
|
255 return |
|
256 entity = session.entity_from_eid(self.eidfrom) |
|
257 mainwf = entity.main_workflow |
|
258 if mainwf is None: |
|
259 msg = session._('entity has no workflow set') |
|
260 raise ValidationError(entity.eid, {None: msg}) |
|
261 for wf in mainwf.iter_workflows(): |
|
262 if wf.state_by_eid(self.eidto): |
|
263 break |
|
264 else: |
|
265 msg = session._("state doesn't belong to entity's workflow. You may " |
|
266 "want to set a custom workflow for this entity first.") |
|
267 raise ValidationError(self.eidfrom, {'in_state': msg}) |
|
268 if entity.current_workflow and wf.eid != entity.current_workflow.eid: |
|
269 msg = session._("state doesn't belong to entity's current workflow") |
|
270 raise ValidationError(self.eidfrom, {'in_state': msg}) |
175 |
271 |
176 |
272 |
177 class SetModificationDateOnStateChange(WorkflowHook): |
273 class SetModificationDateOnStateChange(WorkflowHook): |
178 """update entity's modification date after changing its state""" |
274 """update entity's modification date after changing its state""" |
179 __id__ = 'wfsyncmdate' |
275 __id__ = 'wfsyncmdate' |