13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. |
14 # details. |
15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
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/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """Core hooks: workflow related hooks |
18 """Core hooks: workflow related hooks""" |
19 |
19 |
20 """ |
|
21 __docformat__ = "restructuredtext en" |
20 __docformat__ = "restructuredtext en" |
22 |
21 |
23 from datetime import datetime |
22 from datetime import datetime |
24 |
23 |
25 from yams.schema import role_name |
24 from yams.schema import role_name |
26 |
25 |
27 from cubicweb import RepositoryError, ValidationError |
26 from cubicweb import RepositoryError, ValidationError |
28 from cubicweb.interfaces import IWorkflowable |
27 from cubicweb.selectors import implements, adaptable |
29 from cubicweb.selectors import implements |
|
30 from cubicweb.server import hook |
28 from cubicweb.server import hook |
31 |
29 |
32 |
30 |
33 def _change_state(session, x, oldstate, newstate): |
31 def _change_state(session, x, oldstate, newstate): |
34 nocheck = session.transaction_data.setdefault('skip-security', set()) |
32 nocheck = session.transaction_data.setdefault('skip-security', set()) |
49 """make initial state be a default state""" |
47 """make initial state be a default state""" |
50 |
48 |
51 def precommit_event(self): |
49 def precommit_event(self): |
52 session = self.session |
50 session = self.session |
53 entity = self.entity |
51 entity = self.entity |
|
52 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
54 # if there is an initial state and the entity's state is not set, |
53 # if there is an initial state and the entity's state is not set, |
55 # use the initial state as a default state |
54 # use the initial state as a default state |
56 if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ |
55 if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ |
57 and entity.current_workflow: |
56 and iworkflowable.current_workflow: |
58 state = entity.current_workflow.initial |
57 state = iworkflowable.current_workflow.initial |
59 if state: |
58 if state: |
60 session.add_relation(entity.eid, 'in_state', state.eid) |
59 session.add_relation(entity.eid, 'in_state', state.eid) |
61 _FireAutotransitionOp(session, entity=entity) |
60 _FireAutotransitionOp(session, entity=entity) |
62 |
61 |
63 class _FireAutotransitionOp(hook.Operation): |
62 class _FireAutotransitionOp(hook.Operation): |
64 """try to fire auto transition after state changes""" |
63 """try to fire auto transition after state changes""" |
65 |
64 |
66 def precommit_event(self): |
65 def precommit_event(self): |
67 entity = self.entity |
66 entity = self.entity |
68 autotrs = list(entity.possible_transitions('auto')) |
67 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
|
68 autotrs = list(iworkflowable.possible_transitions('auto')) |
69 if autotrs: |
69 if autotrs: |
70 assert len(autotrs) == 1 |
70 assert len(autotrs) == 1 |
71 entity.fire_transition(autotrs[0]) |
71 iworkflowable.fire_transition(autotrs[0]) |
72 |
72 |
73 |
73 |
74 class _WorkflowChangedOp(hook.Operation): |
74 class _WorkflowChangedOp(hook.Operation): |
75 """fix entity current state when changing its workflow""" |
75 """fix entity current state when changing its workflow""" |
76 |
76 |
80 session = self.session |
80 session = self.session |
81 pendingeids = session.transaction_data.get('pendingeids', ()) |
81 pendingeids = session.transaction_data.get('pendingeids', ()) |
82 if self.eid in pendingeids: |
82 if self.eid in pendingeids: |
83 return |
83 return |
84 entity = session.entity_from_eid(self.eid) |
84 entity = session.entity_from_eid(self.eid) |
|
85 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
85 # check custom workflow has not been rechanged to another one in the same |
86 # check custom workflow has not been rechanged to another one in the same |
86 # transaction |
87 # transaction |
87 mainwf = entity.main_workflow |
88 mainwf = iworkflowable.main_workflow |
88 if mainwf.eid == self.wfeid: |
89 if mainwf.eid == self.wfeid: |
89 deststate = mainwf.initial |
90 deststate = mainwf.initial |
90 if not deststate: |
91 if not deststate: |
91 qname = role_name('custom_workflow', 'subject') |
92 qname = role_name('custom_workflow', 'subject') |
92 msg = session._('workflow has no initial state') |
93 msg = session._('workflow has no initial state') |
93 raise ValidationError(entity.eid, {qname: msg}) |
94 raise ValidationError(entity.eid, {qname: msg}) |
94 if mainwf.state_by_eid(entity.current_state.eid): |
95 if mainwf.state_by_eid(iworkflowable.current_state.eid): |
95 # nothing to do |
96 # nothing to do |
96 return |
97 return |
97 # if there are no history, simply go to new workflow's initial state |
98 # if there are no history, simply go to new workflow's initial state |
98 if not entity.workflow_history: |
99 if not iworkflowable.workflow_history: |
99 if entity.current_state.eid != deststate.eid: |
100 if iworkflowable.current_state.eid != deststate.eid: |
100 _change_state(session, entity.eid, |
101 _change_state(session, entity.eid, |
101 entity.current_state.eid, deststate.eid) |
102 iworkflowable.current_state.eid, deststate.eid) |
102 _FireAutotransitionOp(session, entity=entity) |
103 _FireAutotransitionOp(session, entity=entity) |
103 return |
104 return |
104 msg = session._('workflow changed to "%s"') |
105 msg = session._('workflow changed to "%s"') |
105 msg %= session._(mainwf.name) |
106 msg %= session._(mainwf.name) |
106 session.transaction_data[(entity.eid, 'customwf')] = self.wfeid |
107 session.transaction_data[(entity.eid, 'customwf')] = self.wfeid |
107 entity.change_state(deststate, msg, u'text/plain') |
108 iworkflowable.change_state(deststate, msg, u'text/plain') |
108 |
109 |
109 |
110 |
110 class _CheckTrExitPoint(hook.Operation): |
111 class _CheckTrExitPoint(hook.Operation): |
111 |
112 |
112 def precommit_event(self): |
113 def precommit_event(self): |
123 class _SubWorkflowExitOp(hook.Operation): |
124 class _SubWorkflowExitOp(hook.Operation): |
124 |
125 |
125 def precommit_event(self): |
126 def precommit_event(self): |
126 session = self.session |
127 session = self.session |
127 forentity = self.forentity |
128 forentity = self.forentity |
|
129 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
128 trinfo = self.trinfo |
130 trinfo = self.trinfo |
129 # we're in a subworkflow, check if we've reached an exit point |
131 # we're in a subworkflow, check if we've reached an exit point |
130 wftr = forentity.subworkflow_input_transition() |
132 wftr = iworkflowable.subworkflow_input_transition() |
131 if wftr is None: |
133 if wftr is None: |
132 # inconsistency detected |
134 # inconsistency detected |
133 qname = role_name('to_state', 'subject') |
135 qname = role_name('to_state', 'subject') |
134 msg = session._("state doesn't belong to entity's current workflow") |
136 msg = session._("state doesn't belong to entity's current workflow") |
135 raise ValidationError(self.trinfo.eid, {'to_state': msg}) |
137 raise ValidationError(self.trinfo.eid, {'to_state': msg}) |
136 tostate = wftr.get_exit_point(forentity, trinfo['to_state']) |
138 tostate = wftr.get_exit_point(forentity, trinfo['to_state']) |
137 if tostate is not None: |
139 if tostate is not None: |
138 # reached an exit point |
140 # reached an exit point |
139 msg = session._('exiting from subworkflow %s') |
141 msg = session._('exiting from subworkflow %s') |
140 msg %= session._(forentity.current_workflow.name) |
142 msg %= session._(iworkflowable.current_workflow.name) |
141 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
143 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
142 forentity.change_state(tostate, msg, u'text/plain', tr=wftr) |
144 iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) |
143 |
145 |
144 |
146 |
145 # hooks ######################################################################## |
147 # hooks ######################################################################## |
146 |
148 |
147 class WorkflowHook(hook.Hook): |
149 class WorkflowHook(hook.Hook): |
149 category = 'worfklow' |
151 category = 'worfklow' |
150 |
152 |
151 |
153 |
152 class SetInitialStateHook(WorkflowHook): |
154 class SetInitialStateHook(WorkflowHook): |
153 __regid__ = 'wfsetinitial' |
155 __regid__ = 'wfsetinitial' |
154 __select__ = WorkflowHook.__select__ & implements(IWorkflowable) |
156 __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable') |
155 events = ('after_add_entity',) |
157 events = ('after_add_entity',) |
156 |
158 |
157 def __call__(self): |
159 def __call__(self): |
158 _SetInitialStateOp(self._cw, entity=self.entity) |
160 _SetInitialStateOp(self._cw, entity=self.entity) |
159 |
161 |
187 except KeyError: |
189 except KeyError: |
188 qname = role_name('wf_info_for', 'subject') |
190 qname = role_name('wf_info_for', 'subject') |
189 msg = session._('mandatory relation') |
191 msg = session._('mandatory relation') |
190 raise ValidationError(entity.eid, {qname: msg}) |
192 raise ValidationError(entity.eid, {qname: msg}) |
191 forentity = session.entity_from_eid(foreid) |
193 forentity = session.entity_from_eid(foreid) |
|
194 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
192 # then check it has a workflow set, unless we're in the process of changing |
195 # then check it has a workflow set, unless we're in the process of changing |
193 # entity's workflow |
196 # entity's workflow |
194 if session.transaction_data.get((forentity.eid, 'customwf')): |
197 if session.transaction_data.get((forentity.eid, 'customwf')): |
195 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
198 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
196 wf = session.entity_from_eid(wfeid) |
199 wf = session.entity_from_eid(wfeid) |
197 else: |
200 else: |
198 wf = forentity.current_workflow |
201 wf = iworkflowable.current_workflow |
199 if wf is None: |
202 if wf is None: |
200 msg = session._('related entity has no workflow set') |
203 msg = session._('related entity has no workflow set') |
201 raise ValidationError(entity.eid, {None: msg}) |
204 raise ValidationError(entity.eid, {None: msg}) |
202 # then check it has a state set |
205 # then check it has a state set |
203 fromstate = forentity.current_state |
206 fromstate = iworkflowable.current_state |
204 if fromstate is None: |
207 if fromstate is None: |
205 msg = session._('related entity has no state') |
208 msg = session._('related entity has no state') |
206 raise ValidationError(entity.eid, {None: msg}) |
209 raise ValidationError(entity.eid, {None: msg}) |
207 # True if we are coming back from subworkflow |
210 # True if we are coming back from subworkflow |
208 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
211 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
276 def __call__(self): |
279 def __call__(self): |
277 trinfo = self.entity |
280 trinfo = self.entity |
278 _change_state(self._cw, trinfo['wf_info_for'], |
281 _change_state(self._cw, trinfo['wf_info_for'], |
279 trinfo['from_state'], trinfo['to_state']) |
282 trinfo['from_state'], trinfo['to_state']) |
280 forentity = self._cw.entity_from_eid(trinfo['wf_info_for']) |
283 forentity = self._cw.entity_from_eid(trinfo['wf_info_for']) |
281 assert forentity.current_state.eid == trinfo['to_state'] |
284 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
282 if forentity.main_workflow.eid != forentity.current_workflow.eid: |
285 assert iworkflowable.current_state.eid == trinfo['to_state'] |
|
286 if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid: |
283 _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo) |
287 _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo) |
284 |
288 |
285 |
289 |
286 class CheckInStateChangeAllowed(WorkflowHook): |
290 class CheckInStateChangeAllowed(WorkflowHook): |
287 """check state apply, in case of direct in_state change using unsafe execute |
291 """check state apply, in case of direct in_state change using unsafe execute |
295 nocheck = session.transaction_data.get('skip-security', ()) |
299 nocheck = session.transaction_data.get('skip-security', ()) |
296 if (self.eidfrom, 'in_state', self.eidto) in nocheck: |
300 if (self.eidfrom, 'in_state', self.eidto) in nocheck: |
297 # state changed through TrInfo insertion, so we already know it's ok |
301 # state changed through TrInfo insertion, so we already know it's ok |
298 return |
302 return |
299 entity = session.entity_from_eid(self.eidfrom) |
303 entity = session.entity_from_eid(self.eidfrom) |
300 mainwf = entity.main_workflow |
304 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
|
305 mainwf = iworkflowable.main_workflow |
301 if mainwf is None: |
306 if mainwf is None: |
302 msg = session._('entity has no workflow set') |
307 msg = session._('entity has no workflow set') |
303 raise ValidationError(entity.eid, {None: msg}) |
308 raise ValidationError(entity.eid, {None: msg}) |
304 for wf in mainwf.iter_workflows(): |
309 for wf in mainwf.iter_workflows(): |
305 if wf.state_by_eid(self.eidto): |
310 if wf.state_by_eid(self.eidto): |
307 else: |
312 else: |
308 qname = role_name('in_state', 'subject') |
313 qname = role_name('in_state', 'subject') |
309 msg = session._("state doesn't belong to entity's workflow. You may " |
314 msg = session._("state doesn't belong to entity's workflow. You may " |
310 "want to set a custom workflow for this entity first.") |
315 "want to set a custom workflow for this entity first.") |
311 raise ValidationError(self.eidfrom, {qname: msg}) |
316 raise ValidationError(self.eidfrom, {qname: msg}) |
312 if entity.current_workflow and wf.eid != entity.current_workflow.eid: |
317 if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid: |
313 qname = role_name('in_state', 'subject') |
318 qname = role_name('in_state', 'subject') |
314 msg = session._("state doesn't belong to entity's current workflow") |
319 msg = session._("state doesn't belong to entity's current workflow") |
315 raise ValidationError(self.eidfrom, {qname: msg}) |
320 raise ValidationError(self.eidfrom, {qname: msg}) |
316 |
321 |
317 |
322 |
357 __regid__ = 'wfdelcustom' |
362 __regid__ = 'wfdelcustom' |
358 events = ('after_delete_relation',) |
363 events = ('after_delete_relation',) |
359 |
364 |
360 def __call__(self): |
365 def __call__(self): |
361 entity = self._cw.entity_from_eid(self.eidfrom) |
366 entity = self._cw.entity_from_eid(self.eidfrom) |
362 typewf = entity.cwetype_workflow() |
367 typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow() |
363 if typewf is not None: |
368 if typewf is not None: |
364 _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid) |
369 _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid) |
365 |
370 |