13 from cubicweb.interfaces import IWorkflowable |
13 from cubicweb.interfaces import IWorkflowable |
14 from cubicweb.selectors import entity_implements |
14 from cubicweb.selectors import entity_implements |
15 from cubicweb.server import hook |
15 from cubicweb.server import hook |
16 |
16 |
17 |
17 |
18 |
18 def _change_state(session, x, oldstate, newstate): |
19 def previous_state(session, eid): |
19 nocheck = session.transaction_data.setdefault('skip-security', set()) |
20 """return the state of the entity with the given eid, |
20 nocheck.add((x, 'in_state', oldstate)) |
21 usually since it's changing in the current transaction. Due to internal |
21 nocheck.add((x, 'in_state', newstate)) |
22 relation hooks, the relation may has been deleted at this point, so |
22 # delete previous state first in case we're using a super session |
23 we have handle that |
23 session.delete_relation(x, 'in_state', oldstate) |
24 """ |
24 session.add_relation(x, 'in_state', newstate) |
25 # don't check eid has been added in the current transaction, we don't want |
|
26 # to miss previous state of entity whose state change in the same |
|
27 # transaction as it's being created |
|
28 pending = session.transaction_data.get('pendingrelations', ()) |
|
29 for eidfrom, rtype, eidto in reversed(pending): |
|
30 if rtype == 'in_state' and eidfrom == eid: |
|
31 rset = session.execute('Any S,N WHERE S eid %(x)s, S name N', |
|
32 {'x': eidto}, 'x') |
|
33 return rset.get_entity(0, 0) |
|
34 rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N', |
|
35 {'x': eid}, 'x') |
|
36 if rset: |
|
37 return rset.get_entity(0, 0) |
|
38 |
|
39 |
|
40 def relation_deleted(session, eidfrom, rtype, eidto): |
|
41 session.transaction_data.setdefault('pendingrelations', []).append( |
|
42 (eidfrom, rtype, eidto)) |
|
43 |
25 |
44 |
26 |
45 class _SetInitialStateOp(hook.Operation): |
27 class _SetInitialStateOp(hook.Operation): |
46 """make initial state be a default state""" |
28 """make initial state be a default state""" |
47 |
29 |
48 def precommit_event(self): |
30 def precommit_event(self): |
49 session = self.session |
31 session = self.session |
50 entity = self.entity |
32 entity = self.entity |
51 # if there is an initial state and the entity's state is not set, |
33 # if there is an initial state and the entity's state is not set, |
52 # use the initial state as a default state |
34 # use the initial state as a default state |
53 if not session.deleted_in_transaction(entity.eid) and not entity.in_state: |
35 pendingeids = session.transaction_data.get('pendingeids', ()) |
54 rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s', |
36 if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ |
55 {'name': entity.id}) |
37 and entity.current_workflow: |
56 if rset: |
38 state = entity.current_workflow.initial |
57 session.add_relation(entity.eid, 'in_state', rset[0][0]) |
39 if state: |
|
40 # use super session to by-pass security checks |
|
41 session.super_session.add_relation(entity.eid, 'in_state', |
|
42 state.eid) |
|
43 |
|
44 class _WorkflowChangedOp(hook.Operation): |
|
45 """fix entity current state when changing its workflow""" |
|
46 |
|
47 def precommit_event(self): |
|
48 session = self.session |
|
49 if session.deleted_in_transaction(self.eid): |
|
50 return |
|
51 entity = session.entity_from_eid(self.eid) |
|
52 # notice that enforcment that new workflow apply to the entity's type is |
|
53 # done by schema rule, no need to check it here |
|
54 if entity.current_workflow.eid == self.wfeid: |
|
55 deststate = entity.current_workflow.initial |
|
56 if not deststate: |
|
57 msg = session._('workflow has no initial state') |
|
58 raise ValidationError(entity.eid, {'custom_workflow': msg}) |
|
59 if entity.current_workflow.state_by_eid(entity.current_state.eid): |
|
60 # nothing to do |
|
61 return |
|
62 # if there are no history, simply go to new workflow's initial state |
|
63 if not entity.workflow_history: |
|
64 if entity.current_state.eid != deststate.eid: |
|
65 _change_state(session, entity.eid, |
|
66 entity.current_state.eid, deststate.eid) |
|
67 return |
|
68 msg = session._('workflow changed to "%s"') |
|
69 msg %= entity.current_workflow.name |
|
70 entity.change_state(deststate.name, msg) |
|
71 |
|
72 |
58 |
73 |
59 class WorkflowHook(hook.Hook): |
74 class WorkflowHook(hook.Hook): |
60 __abstract__ = True |
75 __abstract__ = True |
61 category = 'worfklow' |
76 category = 'worfklow' |
62 |
77 |
79 def __call__(self): |
94 def __call__(self): |
80 self._cw.transaction_data.setdefault('pendingrelations', []).append( |
95 self._cw.transaction_data.setdefault('pendingrelations', []).append( |
81 (self.eidfrom, self.rtype, self.eidto)) |
96 (self.eidfrom, self.rtype, self.eidto)) |
82 |
97 |
83 |
98 |
84 class FireTransitionHook(PrepareStateChangeHook): |
99 class FireTransitionHook(WorkflowHook): |
85 """check the transition is allowed and record transition information""" |
100 """check the transition is allowed, add missing information. Expect that: |
|
101 * wf_info_for inlined relation is set |
|
102 * by_transition or to_state (managers only) inlined relation is set |
|
103 """ |
86 __id__ = 'wffiretransition' |
104 __id__ = 'wffiretransition' |
87 events = ('before_add_relation',) |
105 __select__ = WorkflowHook.__select__ & entity_implements('TrInfo') |
|
106 events = ('before_add_entity',) |
88 |
107 |
89 def __call__(self): |
108 def __call__(self): |
90 session = self._cw |
109 session = self._cw |
91 eidfrom = self.eidfrom |
110 entity = self.entity |
92 eidto = self.eidto |
111 # first retreive entity to which the state change apply |
93 state = previous_state(session, eidfrom) |
112 try: |
94 etype = session.describe(eidfrom)[0] |
113 foreid = entity['wf_info_for'] |
95 if not (session.is_super_session or 'managers' in session.user.groups): |
114 except KeyError: |
96 if not state is None: |
115 msg = session._('mandatory relation') |
97 entity = session.entity_from_eid(eidfrom) |
116 raise ValidationError(entity.eid, {'wf_info_for': msg}) |
98 # we should find at least one transition going to this state |
117 forentity = session.entity_from_eid(foreid) |
99 try: |
118 # then check it has a workflow set |
100 iter(state.transitions(entity, eidto)).next() |
119 wf = forentity.current_workflow |
101 except StopIteration: |
120 if wf is None: |
102 msg = session._('transition is not allowed') |
121 msg = session._('related entity has no workflow set') |
103 raise ValidationError(eidfrom, {'in_state': msg}) |
122 raise ValidationError(entity.eid, {None: msg}) |
104 else: |
123 # then check it has a state set |
105 # not a transition |
124 fromstate = forentity.current_state |
106 # check state is initial state if the workflow defines one |
125 if fromstate is None: |
107 isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s', |
126 msg = session._('related entity has no state') |
108 {'etype': etype}) |
127 raise ValidationError(entity.eid, {None: msg}) |
109 if isrset and not eidto == isrset[0][0]: |
128 # no investigate the requested state change... |
110 msg = session._('not the initial state for this entity') |
129 try: |
111 raise ValidationError(eidfrom, {'in_state': msg}) |
130 treid = entity['by_transition'] |
112 eschema = session.repo.schema[etype] |
131 except KeyError: |
113 if not 'wf_info_for' in eschema.object_relations(): |
132 # no transition set, check user is a manager and destination state is |
114 # workflow history not activated for this entity type |
133 # specified (and valid) |
115 return |
134 if not (session.is_super_session or 'managers' in session.user.groups): |
116 rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s' |
135 msg = session._('mandatory relation') |
117 args = {'comment': session.get_shared_data('trcomment', None, pop=True), |
136 raise ValidationError(entity.eid, {'by_transition': msg}) |
118 'e': eidfrom, 'ds': eidto} |
137 deststateeid = entity.get('to_state') |
119 cformat = session.get_shared_data('trcommentformat', None, pop=True) |
138 if not deststateeid: |
120 if cformat is not None: |
139 msg = session._('mandatory relation') |
121 args['comment_format'] = cformat |
140 raise ValidationError(entity.eid, {'by_transition': msg}) |
122 rql += ', T comment_format %(comment_format)s' |
141 deststate = wf.state_by_eid(deststateeid) |
123 restriction = ['DS eid %(ds)s, E eid %(e)s'] |
142 if deststate is None: |
124 if not state is None: # not a transition |
143 msg = session._("state doesn't belong to entity's workflow") |
125 rql += ', T from_state FS' |
144 raise ValidationError(entity.eid, {'to_state': msg}) |
126 restriction.append('FS eid %(fs)s') |
145 else: |
127 args['fs'] = state.eid |
146 # check transition is valid and allowed |
128 rql = '%s WHERE %s' % (rql, ', '.join(restriction)) |
147 tr = wf.transition_by_eid(treid) |
129 session.unsafe_execute(rql, args, 'e') |
148 if tr is None: |
|
149 msg = session._("transition doesn't belong to entity's workflow") |
|
150 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
151 if not tr.has_input_state(fromstate): |
|
152 msg = session._("transition isn't allowed") |
|
153 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
154 if not tr.may_be_fired(foreid): |
|
155 msg = session._("transition may not be fired") |
|
156 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
157 deststateeid = tr.destination().eid |
|
158 # everything is ok, add missing information on the trinfo entity |
|
159 entity['from_state'] = fromstate.eid |
|
160 entity['to_state'] = deststateeid |
|
161 nocheck = session.transaction_data.setdefault('skip-security', set()) |
|
162 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
|
163 nocheck.add((entity.eid, 'to_state', deststateeid)) |
|
164 |
|
165 |
|
166 class FiredTransitionHook(WorkflowHook): |
|
167 """change related entity state""" |
|
168 __id__ = 'wffiretransition' |
|
169 __select__ = WorkflowHook.__select__ & entity_implements('TrInfo') |
|
170 events = ('after_add_entity',) |
|
171 |
|
172 def __call__(self): |
|
173 _change_state(self._cw, self.entity['wf_info_for'], |
|
174 self.entity['from_state'], self.entity['to_state']) |
130 |
175 |
131 |
176 |
132 class SetModificationDateOnStateChange(WorkflowHook): |
177 class SetModificationDateOnStateChange(WorkflowHook): |
133 """update entity's modification date after changing its state""" |
178 """update entity's modification date after changing its state""" |
134 __id__ = 'wfsyncmdate' |
179 __id__ = 'wfsyncmdate' |
145 _cw_unsafe=True) |
190 _cw_unsafe=True) |
146 except RepositoryError, ex: |
191 except RepositoryError, ex: |
147 # usually occurs if entity is coming from a read-only source |
192 # usually occurs if entity is coming from a read-only source |
148 # (eg ldap user) |
193 # (eg ldap user) |
149 self.warning('cant change modification date for %s: %s', entity, ex) |
194 self.warning('cant change modification date for %s: %s', entity, ex) |
|
195 |
|
196 |
|
197 class SetCustomWorkflow(WorkflowHook): |
|
198 __id__ = 'wfsetcustom' |
|
199 __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow') |
|
200 events = ('after_add_relation',) |
|
201 |
|
202 def __call__(self): |
|
203 _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=self.eidto) |
|
204 |
|
205 |
|
206 class DelCustomWorkflow(SetCustomWorkflow): |
|
207 __id__ = 'wfdelcustom' |
|
208 events = ('after_delete_relation',) |
|
209 |
|
210 def __call__(self): |
|
211 entity = self._cw.entity_from_eid(self.eidfrom) |
|
212 typewf = entity.cwetype_workflow() |
|
213 if typewf is not None: |
|
214 _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid) |
|
215 |
|
216 |
|
217 |
|
218 class DelWorkflowHook(WorkflowHook): |
|
219 __id__ = 'wfdel' |
|
220 __select__ = WorkflowHook.__select__ & entity_implements('Workflow') |
|
221 events = ('after_delete_entity',) |
|
222 |
|
223 def __call__(self): |
|
224 # cleanup unused state and transition |
|
225 self._cw.execute('DELETE State X WHERE NOT X state_of Y') |
|
226 self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y') |
|
227 |