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 __docformat__ = "restructuredtext en" |
20 __docformat__ = "restructuredtext en" |
|
21 _ = unicode |
21 |
22 |
22 from datetime import datetime |
23 from datetime import datetime |
23 |
24 |
24 from yams.schema import role_name |
25 |
25 |
26 from cubicweb import RepositoryError, validation_error |
26 from cubicweb import RepositoryError, ValidationError |
|
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(session, x, oldstate, newstate): |
90 # transaction |
90 # transaction |
91 mainwf = iworkflowable.main_workflow |
91 mainwf = iworkflowable.main_workflow |
92 if mainwf.eid == self.wfeid: |
92 if mainwf.eid == self.wfeid: |
93 deststate = mainwf.initial |
93 deststate = mainwf.initial |
94 if not deststate: |
94 if not deststate: |
95 qname = role_name('custom_workflow', 'subject') |
95 msg = _('workflow has no initial state') |
96 msg = session._('workflow has no initial state') |
96 raise validation_error(entity, {('custom_workflow', 'subject'): msg}) |
97 raise ValidationError(entity.eid, {qname: msg}) |
|
98 if mainwf.state_by_eid(iworkflowable.current_state.eid): |
97 if mainwf.state_by_eid(iworkflowable.current_state.eid): |
99 # nothing to do |
98 # nothing to do |
100 return |
99 return |
101 # if there are no history, simply go to new workflow's initial state |
100 # if there are no history, simply go to new workflow's initial state |
102 if not iworkflowable.workflow_history: |
101 if not iworkflowable.workflow_history: |
117 def precommit_event(self): |
116 def precommit_event(self): |
118 tr = self.session.entity_from_eid(self.treid) |
117 tr = self.session.entity_from_eid(self.treid) |
119 outputs = set() |
118 outputs = set() |
120 for ep in tr.subworkflow_exit: |
119 for ep in tr.subworkflow_exit: |
121 if ep.subwf_state.eid in outputs: |
120 if ep.subwf_state.eid in outputs: |
122 qname = role_name('subworkflow_exit', 'subject') |
121 msg = _("can't have multiple exits on the same state") |
123 msg = self.session._("can't have multiple exits on the same state") |
122 raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg}) |
124 raise ValidationError(self.treid, {qname: msg}) |
|
125 outputs.add(ep.subwf_state.eid) |
123 outputs.add(ep.subwf_state.eid) |
126 |
124 |
127 |
125 |
128 class _SubWorkflowExitOp(hook.Operation): |
126 class _SubWorkflowExitOp(hook.Operation): |
129 forentity = trinfo = None # make pylint happy |
127 forentity = trinfo = None # make pylint happy |
135 trinfo = self.trinfo |
133 trinfo = self.trinfo |
136 # we're in a subworkflow, check if we've reached an exit point |
134 # we're in a subworkflow, check if we've reached an exit point |
137 wftr = iworkflowable.subworkflow_input_transition() |
135 wftr = iworkflowable.subworkflow_input_transition() |
138 if wftr is None: |
136 if wftr is None: |
139 # inconsistency detected |
137 # inconsistency detected |
140 qname = role_name('to_state', 'subject') |
138 msg = _("state doesn't belong to entity's current workflow") |
141 msg = session._("state doesn't belong to entity's current workflow") |
139 raise validation_error(self.trinfo, {('to_state', 'subject'): msg}) |
142 raise ValidationError(self.trinfo.eid, {'to_state': msg}) |
|
143 tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) |
140 tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) |
144 if tostate is not None: |
141 if tostate is not None: |
145 # reached an exit point |
142 # reached an exit point |
146 msg = session._('exiting from subworkflow %s') |
143 msg = _('exiting from subworkflow %s') |
147 msg %= session._(iworkflowable.current_workflow.name) |
144 msg %= session._(iworkflowable.current_workflow.name) |
148 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
145 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
149 iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) |
146 iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) |
150 |
147 |
151 |
148 |
184 entity = self.entity |
181 entity = self.entity |
185 # first retreive entity to which the state change apply |
182 # first retreive entity to which the state change apply |
186 try: |
183 try: |
187 foreid = entity.cw_attr_cache['wf_info_for'] |
184 foreid = entity.cw_attr_cache['wf_info_for'] |
188 except KeyError: |
185 except KeyError: |
189 qname = role_name('wf_info_for', 'subject') |
186 msg = _('mandatory relation') |
190 msg = session._('mandatory relation') |
187 raise validation_error(entity, {('wf_info_for', 'subject'): msg}) |
191 raise ValidationError(entity.eid, {qname: msg}) |
|
192 forentity = session.entity_from_eid(foreid) |
188 forentity = session.entity_from_eid(foreid) |
193 # see comment in the TrInfo entity definition |
189 # see comment in the TrInfo entity definition |
194 entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for) |
190 entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for) |
195 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
191 iworkflowable = forentity.cw_adapt_to('IWorkflowable') |
196 # then check it has a workflow set, unless we're in the process of changing |
192 # then check it has a workflow set, unless we're in the process of changing |
199 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
195 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
200 wf = session.entity_from_eid(wfeid) |
196 wf = session.entity_from_eid(wfeid) |
201 else: |
197 else: |
202 wf = iworkflowable.current_workflow |
198 wf = iworkflowable.current_workflow |
203 if wf is None: |
199 if wf is None: |
204 msg = session._('related entity has no workflow set') |
200 msg = _('related entity has no workflow set') |
205 raise ValidationError(entity.eid, {None: msg}) |
201 raise validation_error(entity, {None: msg}) |
206 # then check it has a state set |
202 # then check it has a state set |
207 fromstate = iworkflowable.current_state |
203 fromstate = iworkflowable.current_state |
208 if fromstate is None: |
204 if fromstate is None: |
209 msg = session._('related entity has no state') |
205 msg = _('related entity has no state') |
210 raise ValidationError(entity.eid, {None: msg}) |
206 raise validation_error(entity, {None: msg}) |
211 # True if we are coming back from subworkflow |
207 # True if we are coming back from subworkflow |
212 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
208 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
213 cowpowers = (session.user.is_in_group('managers') |
209 cowpowers = (session.user.is_in_group('managers') |
214 or not session.write_security) |
210 or not session.write_security) |
215 # no investigate the requested state change... |
211 # no investigate the requested state change... |
217 treid = entity.cw_attr_cache['by_transition'] |
213 treid = entity.cw_attr_cache['by_transition'] |
218 except KeyError: |
214 except KeyError: |
219 # no transition set, check user is a manager and destination state |
215 # no transition set, check user is a manager and destination state |
220 # is specified (and valid) |
216 # is specified (and valid) |
221 if not cowpowers: |
217 if not cowpowers: |
222 qname = role_name('by_transition', 'subject') |
218 msg = _('mandatory relation') |
223 msg = session._('mandatory relation') |
219 raise validation_error(entity, {('by_transition', 'subject'): msg}) |
224 raise ValidationError(entity.eid, {qname: msg}) |
|
225 deststateeid = entity.cw_attr_cache.get('to_state') |
220 deststateeid = entity.cw_attr_cache.get('to_state') |
226 if not deststateeid: |
221 if not deststateeid: |
227 qname = role_name('by_transition', 'subject') |
222 msg = _('mandatory relation') |
228 msg = session._('mandatory relation') |
223 raise validation_error(entity, {('by_transition', 'subject'): msg}) |
229 raise ValidationError(entity.eid, {qname: msg}) |
|
230 deststate = wf.state_by_eid(deststateeid) |
224 deststate = wf.state_by_eid(deststateeid) |
231 if deststate is None: |
225 if deststate is None: |
232 qname = role_name('to_state', 'subject') |
226 msg = _("state doesn't belong to entity's workflow") |
233 msg = session._("state doesn't belong to entity's workflow") |
227 raise validation_error(entity, {('to_state', 'subject'): msg}) |
234 raise ValidationError(entity.eid, {qname: msg}) |
|
235 else: |
228 else: |
236 # check transition is valid and allowed, unless we're coming back |
229 # check transition is valid and allowed, unless we're coming back |
237 # from subworkflow |
230 # from subworkflow |
238 tr = session.entity_from_eid(treid) |
231 tr = session.entity_from_eid(treid) |
239 if swtr is None: |
232 if swtr is None: |
240 qname = role_name('by_transition', 'subject') |
233 qname = ('by_transition', 'subject') |
241 if tr is None: |
234 if tr is None: |
242 msg = session._("transition doesn't belong to entity's workflow") |
235 msg = _("transition doesn't belong to entity's workflow") |
243 raise ValidationError(entity.eid, {qname: msg}) |
236 raise validation_error(entity, {qname: msg}) |
244 if not tr.has_input_state(fromstate): |
237 if not tr.has_input_state(fromstate): |
245 msg = session._("transition %(tr)s isn't allowed from %(st)s") % { |
238 msg = _("transition %(tr)s isn't allowed from %(st)s") |
246 'tr': session._(tr.name), 'st': session._(fromstate.name)} |
239 raise validation_error(entity, {qname: msg}, { |
247 raise ValidationError(entity.eid, {qname: msg}) |
240 'tr': tr.name, 'st': fromstate.name}, ['tr', 'st']) |
248 if not tr.may_be_fired(foreid): |
241 if not tr.may_be_fired(foreid): |
249 msg = session._("transition may not be fired") |
242 msg = _("transition may not be fired") |
250 raise ValidationError(entity.eid, {qname: msg}) |
243 raise validation_error(entity, {qname: msg}) |
251 deststateeid = entity.cw_attr_cache.get('to_state') |
244 deststateeid = entity.cw_attr_cache.get('to_state') |
252 if deststateeid is not None: |
245 if deststateeid is not None: |
253 if not cowpowers and deststateeid != tr.destination(forentity).eid: |
246 if not cowpowers and deststateeid != tr.destination(forentity).eid: |
254 qname = role_name('by_transition', 'subject') |
247 msg = _("transition isn't allowed") |
255 msg = session._("transition isn't allowed") |
248 raise validation_error(entity, {('by_transition', 'subject'): msg}) |
256 raise ValidationError(entity.eid, {qname: msg}) |
|
257 if swtr is None: |
249 if swtr is None: |
258 deststate = session.entity_from_eid(deststateeid) |
250 deststate = session.entity_from_eid(deststateeid) |
259 if not cowpowers and deststate is None: |
251 if not cowpowers and deststate is None: |
260 qname = role_name('to_state', 'subject') |
252 msg = _("state doesn't belong to entity's workflow") |
261 msg = session._("state doesn't belong to entity's workflow") |
253 raise validation_error(entity, {('to_state', 'subject'): msg}) |
262 raise ValidationError(entity.eid, {qname: msg}) |
|
263 else: |
254 else: |
264 deststateeid = tr.destination(forentity).eid |
255 deststateeid = tr.destination(forentity).eid |
265 # everything is ok, add missing information on the trinfo entity |
256 # everything is ok, add missing information on the trinfo entity |
266 entity.cw_edited['from_state'] = fromstate.eid |
257 entity.cw_edited['from_state'] = fromstate.eid |
267 entity.cw_edited['to_state'] = deststateeid |
258 entity.cw_edited['to_state'] = deststateeid |
305 return |
296 return |
306 entity = session.entity_from_eid(self.eidfrom) |
297 entity = session.entity_from_eid(self.eidfrom) |
307 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
298 iworkflowable = entity.cw_adapt_to('IWorkflowable') |
308 mainwf = iworkflowable.main_workflow |
299 mainwf = iworkflowable.main_workflow |
309 if mainwf is None: |
300 if mainwf is None: |
310 msg = session._('entity has no workflow set') |
301 msg = _('entity has no workflow set') |
311 raise ValidationError(entity.eid, {None: msg}) |
302 raise validation_error(entity, {None: msg}) |
312 for wf in mainwf.iter_workflows(): |
303 for wf in mainwf.iter_workflows(): |
313 if wf.state_by_eid(self.eidto): |
304 if wf.state_by_eid(self.eidto): |
314 break |
305 break |
315 else: |
306 else: |
316 qname = role_name('in_state', 'subject') |
307 msg = _("state doesn't belong to entity's workflow. You may " |
317 msg = session._("state doesn't belong to entity's workflow. You may " |
308 "want to set a custom workflow for this entity first.") |
318 "want to set a custom workflow for this entity first.") |
309 raise validation_error(self.eidfrom, {('in_state', 'subject'): msg}) |
319 raise ValidationError(self.eidfrom, {qname: msg}) |
|
320 if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid: |
310 if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid: |
321 qname = role_name('in_state', 'subject') |
311 msg = _("state doesn't belong to entity's current workflow") |
322 msg = session._("state doesn't belong to entity's current workflow") |
312 raise validation_error(self.eidfrom, {('in_state', 'subject'): msg}) |
323 raise ValidationError(self.eidfrom, {qname: msg}) |
|
324 |
313 |
325 |
314 |
326 class SetModificationDateOnStateChange(WorkflowHook): |
315 class SetModificationDateOnStateChange(WorkflowHook): |
327 """update entity's modification date after changing its state""" |
316 """update entity's modification date after changing its state""" |
328 __regid__ = 'wfsyncmdate' |
317 __regid__ = 'wfsyncmdate' |