26 from cubicweb import RepositoryError, validation_error |
26 from cubicweb import RepositoryError, validation_error |
27 from cubicweb.predicates import is_instance, adaptable |
27 from cubicweb.predicates import is_instance, adaptable |
28 from cubicweb.server import hook |
28 from cubicweb.server import hook |
29 |
29 |
30 |
30 |
31 def _change_state(session, x, oldstate, newstate): |
31 def _change_state(cnx, x, oldstate, newstate): |
32 nocheck = session.transaction_data.setdefault('skip-security', set()) |
32 nocheck = cnx.transaction_data.setdefault('skip-security', set()) |
33 nocheck.add((x, 'in_state', oldstate)) |
33 nocheck.add((x, 'in_state', oldstate)) |
34 nocheck.add((x, 'in_state', newstate)) |
34 nocheck.add((x, 'in_state', newstate)) |
35 # delete previous state first |
35 # delete previous state first |
36 session.delete_relation(x, 'in_state', oldstate) |
36 cnx.delete_relation(x, 'in_state', oldstate) |
37 session.add_relation(x, 'in_state', newstate) |
37 cnx.add_relation(x, 'in_state', newstate) |
38 |
38 |
39 |
39 |
40 # operations ################################################################### |
40 # operations ################################################################### |
41 |
41 |
42 class _SetInitialStateOp(hook.Operation): |
42 class _SetInitialStateOp(hook.Operation): |
43 """make initial state be a default state""" |
43 """make initial state be a default state""" |
44 entity = None # make pylint happy |
44 entity = None # make pylint happy |
45 |
45 |
46 def precommit_event(self): |
46 def precommit_event(self): |
47 session = self.session |
47 cnx = self.cnx |
48 entity = self.entity |
48 entity = self.entity |
49 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
49 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
50 # if there is an initial state and the entity's state is not set, |
50 # if there is an initial state and the entity's state is not set, |
51 # use the initial state as a default state |
51 # use the initial state as a default state |
52 if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ |
52 if not (cnx.deleted_in_transaction(entity.eid) or entity.in_state) \ |
53 and iworkflowable.current_workflow: |
53 and iworkflowable.current_workflow: |
54 state = iworkflowable.current_workflow.initial |
54 state = iworkflowable.current_workflow.initial |
55 if state: |
55 if state: |
56 session.add_relation(entity.eid, 'in_state', state.eid) |
56 cnx.add_relation(entity.eid, 'in_state', state.eid) |
57 _FireAutotransitionOp(session, entity=entity) |
57 _FireAutotransitionOp(cnx, entity=entity) |
58 |
58 |
59 class _FireAutotransitionOp(hook.Operation): |
59 class _FireAutotransitionOp(hook.Operation): |
60 """try to fire auto transition after state changes""" |
60 """try to fire auto transition after state changes""" |
61 entity = None # make pylint happy |
61 entity = None # make pylint happy |
62 |
62 |
74 eid = wfeid = None # make pylint happy |
74 eid = wfeid = None # make pylint happy |
75 |
75 |
76 def precommit_event(self): |
76 def precommit_event(self): |
77 # notice that enforcement that new workflow apply to the entity's type is |
77 # notice that enforcement that new workflow apply to the entity's type is |
78 # done by schema rule, no need to check it here |
78 # done by schema rule, no need to check it here |
79 session = self.session |
79 cnx = self.cnx |
80 pendingeids = session.transaction_data.get('pendingeids', ()) |
80 pendingeids = cnx.transaction_data.get('pendingeids', ()) |
81 if self.eid in pendingeids: |
81 if self.eid in pendingeids: |
82 return |
82 return |
83 entity = session.entity_from_eid(self.eid) |
83 entity = cnx.entity_from_eid(self.eid) |
84 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
84 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
85 # check custom workflow has not been rechanged to another one in the same |
85 # check custom workflow has not been rechanged to another one in the same |
86 # transaction |
86 # transaction |
87 mainwf = iworkflowable.main_workflow |
87 mainwf = iworkflowable.main_workflow |
88 if mainwf.eid == self.wfeid: |
88 if mainwf.eid == self.wfeid: |
94 # nothing to do |
94 # nothing to do |
95 return |
95 return |
96 # if there are no history, simply go to new workflow's initial state |
96 # if there are no history, simply go to new workflow's initial state |
97 if not iworkflowable.workflow_history: |
97 if not iworkflowable.workflow_history: |
98 if iworkflowable.current_state.eid != deststate.eid: |
98 if iworkflowable.current_state.eid != deststate.eid: |
99 _change_state(session, entity.eid, |
99 _change_state(cnx, entity.eid, |
100 iworkflowable.current_state.eid, deststate.eid) |
100 iworkflowable.current_state.eid, deststate.eid) |
101 _FireAutotransitionOp(session, entity=entity) |
101 _FireAutotransitionOp(cnx, entity=entity) |
102 return |
102 return |
103 msg = session._('workflow changed to "%s"') |
103 msg = cnx._('workflow changed to "%s"') |
104 msg %= session._(mainwf.name) |
104 msg %= cnx._(mainwf.name) |
105 session.transaction_data[(entity.eid, 'customwf')] = self.wfeid |
105 cnx.transaction_data[(entity.eid, 'customwf')] = self.wfeid |
106 iworkflowable.change_state(deststate, msg, u'text/plain') |
106 iworkflowable.change_state(deststate, msg, u'text/plain') |
107 |
107 |
108 |
108 |
109 class _CheckTrExitPoint(hook.Operation): |
109 class _CheckTrExitPoint(hook.Operation): |
110 treid = None # make pylint happy |
110 treid = None # make pylint happy |
111 |
111 |
112 def precommit_event(self): |
112 def precommit_event(self): |
113 tr = self.session.entity_from_eid(self.treid) |
113 tr = self.cnx.entity_from_eid(self.treid) |
114 outputs = set() |
114 outputs = set() |
115 for ep in tr.subworkflow_exit: |
115 for ep in tr.subworkflow_exit: |
116 if ep.subwf_state.eid in outputs: |
116 if ep.subwf_state.eid in outputs: |
117 msg = _("can't have multiple exits on the same state") |
117 msg = _("can't have multiple exits on the same state") |
118 raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg}) |
118 raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg}) |
121 |
121 |
122 class _SubWorkflowExitOp(hook.Operation): |
122 class _SubWorkflowExitOp(hook.Operation): |
123 forentity = trinfo = None # make pylint happy |
123 forentity = trinfo = None # make pylint happy |
124 |
124 |
125 def precommit_event(self): |
125 def precommit_event(self): |
126 session = self.session |
126 cnx = self.cnx |
127 forentity = self.forentity |
127 forentity = self.forentity |
128 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
128 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
129 trinfo = self.trinfo |
129 trinfo = self.trinfo |
130 # we're in a subworkflow, check if we've reached an exit point |
130 # we're in a subworkflow, check if we've reached an exit point |
131 wftr = iworkflowable.subworkflow_input_transition() |
131 wftr = iworkflowable.subworkflow_input_transition() |
135 raise validation_error(self.trinfo, {('to_state', 'subject'): msg}) |
135 raise validation_error(self.trinfo, {('to_state', 'subject'): msg}) |
136 tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) |
136 tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) |
137 if tostate is not None: |
137 if tostate is not None: |
138 # reached an exit point |
138 # reached an exit point |
139 msg = _('exiting from subworkflow %s') |
139 msg = _('exiting from subworkflow %s') |
140 msg %= session._(iworkflowable.current_workflow.name) |
140 msg %= cnx._(iworkflowable.current_workflow.name) |
141 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
141 cnx.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
142 iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) |
142 iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) |
143 |
143 |
144 |
144 |
145 # hooks ######################################################################## |
145 # hooks ######################################################################## |
146 |
146 |
171 __regid__ = 'wffiretransition' |
171 __regid__ = 'wffiretransition' |
172 __select__ = WorkflowHook.__select__ & is_instance('TrInfo') |
172 __select__ = WorkflowHook.__select__ & is_instance('TrInfo') |
173 events = ('before_add_entity',) |
173 events = ('before_add_entity',) |
174 |
174 |
175 def __call__(self): |
175 def __call__(self): |
176 session = self._cw |
176 cnx = self._cw |
177 entity = self.entity |
177 entity = self.entity |
178 # first retreive entity to which the state change apply |
178 # first retreive entity to which the state change apply |
179 try: |
179 try: |
180 foreid = entity.cw_attr_cache['wf_info_for'] |
180 foreid = entity.cw_attr_cache['wf_info_for'] |
181 except KeyError: |
181 except KeyError: |
182 msg = _('mandatory relation') |
182 msg = _('mandatory relation') |
183 raise validation_error(entity, {('wf_info_for', 'subject'): msg}) |
183 raise validation_error(entity, {('wf_info_for', 'subject'): msg}) |
184 forentity = session.entity_from_eid(foreid) |
184 forentity = cnx.entity_from_eid(foreid) |
185 # see comment in the TrInfo entity definition |
185 # see comment in the TrInfo entity definition |
186 entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for) |
186 entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for) |
187 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
187 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
188 # then check it has a workflow set, unless we're in the process of changing |
188 # then check it has a workflow set, unless we're in the process of changing |
189 # entity's workflow |
189 # entity's workflow |
190 if session.transaction_data.get((forentity.eid, 'customwf')): |
190 if cnx.transaction_data.get((forentity.eid, 'customwf')): |
191 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
191 wfeid = cnx.transaction_data[(forentity.eid, 'customwf')] |
192 wf = session.entity_from_eid(wfeid) |
192 wf = cnx.entity_from_eid(wfeid) |
193 else: |
193 else: |
194 wf = iworkflowable.current_workflow |
194 wf = iworkflowable.current_workflow |
195 if wf is None: |
195 if wf is None: |
196 msg = _('related entity has no workflow set') |
196 msg = _('related entity has no workflow set') |
197 raise validation_error(entity, {None: msg}) |
197 raise validation_error(entity, {None: msg}) |
199 fromstate = iworkflowable.current_state |
199 fromstate = iworkflowable.current_state |
200 if fromstate is None: |
200 if fromstate is None: |
201 msg = _('related entity has no state') |
201 msg = _('related entity has no state') |
202 raise validation_error(entity, {None: msg}) |
202 raise validation_error(entity, {None: msg}) |
203 # True if we are coming back from subworkflow |
203 # True if we are coming back from subworkflow |
204 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
204 swtr = cnx.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
205 cowpowers = (session.user.is_in_group('managers') |
205 cowpowers = (cnx.user.is_in_group('managers') |
206 or not session.write_security) |
206 or not cnx.write_security) |
207 # no investigate the requested state change... |
207 # no investigate the requested state change... |
208 try: |
208 try: |
209 treid = entity.cw_attr_cache['by_transition'] |
209 treid = entity.cw_attr_cache['by_transition'] |
210 except KeyError: |
210 except KeyError: |
211 # no transition set, check user is a manager and destination state |
211 # no transition set, check user is a manager and destination state |
222 msg = _("state doesn't belong to entity's workflow") |
222 msg = _("state doesn't belong to entity's workflow") |
223 raise validation_error(entity, {('to_state', 'subject'): msg}) |
223 raise validation_error(entity, {('to_state', 'subject'): msg}) |
224 else: |
224 else: |
225 # check transition is valid and allowed, unless we're coming back |
225 # check transition is valid and allowed, unless we're coming back |
226 # from subworkflow |
226 # from subworkflow |
227 tr = session.entity_from_eid(treid) |
227 tr = cnx.entity_from_eid(treid) |
228 if swtr is None: |
228 if swtr is None: |
229 qname = ('by_transition', 'subject') |
229 qname = ('by_transition', 'subject') |
230 if tr is None: |
230 if tr is None: |
231 msg = _("transition doesn't belong to entity's workflow") |
231 msg = _("transition doesn't belong to entity's workflow") |
232 raise validation_error(entity, {qname: msg}) |
232 raise validation_error(entity, {qname: msg}) |
241 if deststateeid is not None: |
241 if deststateeid is not None: |
242 if not cowpowers and deststateeid != tr.destination(forentity).eid: |
242 if not cowpowers and deststateeid != tr.destination(forentity).eid: |
243 msg = _("transition isn't allowed") |
243 msg = _("transition isn't allowed") |
244 raise validation_error(entity, {('by_transition', 'subject'): msg}) |
244 raise validation_error(entity, {('by_transition', 'subject'): msg}) |
245 if swtr is None: |
245 if swtr is None: |
246 deststate = session.entity_from_eid(deststateeid) |
246 deststate = cnx.entity_from_eid(deststateeid) |
247 if not cowpowers and deststate is None: |
247 if not cowpowers and deststate is None: |
248 msg = _("state doesn't belong to entity's workflow") |
248 msg = _("state doesn't belong to entity's workflow") |
249 raise validation_error(entity, {('to_state', 'subject'): msg}) |
249 raise validation_error(entity, {('to_state', 'subject'): msg}) |
250 else: |
250 else: |
251 deststateeid = tr.destination(forentity).eid |
251 deststateeid = tr.destination(forentity).eid |
252 # everything is ok, add missing information on the trinfo entity |
252 # everything is ok, add missing information on the trinfo entity |
253 entity.cw_edited['from_state'] = fromstate.eid |
253 entity.cw_edited['from_state'] = fromstate.eid |
254 entity.cw_edited['to_state'] = deststateeid |
254 entity.cw_edited['to_state'] = deststateeid |
255 nocheck = session.transaction_data.setdefault('skip-security', set()) |
255 nocheck = cnx.transaction_data.setdefault('skip-security', set()) |
256 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
256 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
257 nocheck.add((entity.eid, 'to_state', deststateeid)) |
257 nocheck.add((entity.eid, 'to_state', deststateeid)) |
258 _FireAutotransitionOp(session, entity=forentity) |
258 _FireAutotransitionOp(cnx, entity=forentity) |
259 |
259 |
260 |
260 |
261 class FiredTransitionHook(WorkflowHook): |
261 class FiredTransitionHook(WorkflowHook): |
262 """change related entity state and handle exit of subworkflow""" |
262 """change related entity state and handle exit of subworkflow""" |
263 __regid__ = 'wffiretransition' |
263 __regid__ = 'wffiretransition' |
283 __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state') |
283 __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state') |
284 events = ('before_add_relation',) |
284 events = ('before_add_relation',) |
285 category = 'integrity' |
285 category = 'integrity' |
286 |
286 |
287 def __call__(self): |
287 def __call__(self): |
288 session = self._cw |
288 cnx = self._cw |
289 nocheck = session.transaction_data.get('skip-security', ()) |
289 nocheck = cnx.transaction_data.get('skip-security', ()) |
290 if (self.eidfrom, 'in_state', self.eidto) in nocheck: |
290 if (self.eidfrom, 'in_state', self.eidto) in nocheck: |
291 # state changed through TrInfo insertion, so we already know it's ok |
291 # state changed through TrInfo insertion, so we already know it's ok |
292 return |
292 return |
293 entity = session.entity_from_eid(self.eidfrom) |
293 entity = cnx.entity_from_eid(self.eidfrom) |
294 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
294 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
295 mainwf = iworkflowable.main_workflow |
295 mainwf = iworkflowable.main_workflow |
296 if mainwf is None: |
296 if mainwf is None: |
297 msg = _('entity has no workflow set') |
297 msg = _('entity has no workflow set') |
298 raise validation_error(entity, {None: msg}) |
298 raise validation_error(entity, {None: msg}) |