1 """Core hooks: check schema validity, unsure we are not deleting necessary |
|
2 entities... |
|
3 |
|
4 :organization: Logilab |
|
5 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. |
|
6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
7 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses |
|
8 """ |
|
9 __docformat__ = "restructuredtext en" |
|
10 |
|
11 from threading import Lock |
|
12 from datetime import datetime |
|
13 |
|
14 from cubicweb import UnknownProperty, ValidationError, BadConnectionId |
|
15 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint |
|
16 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation |
|
17 from cubicweb.server.hookhelper import (check_internal_entity, |
|
18 get_user_sessions, rproperty) |
|
19 from cubicweb.server.repository import FTIndexEntityOp |
|
20 |
|
21 # special relations that don't have to be checked for integrity, usually |
|
22 # because they are handled internally by hooks (so we trust ourselves) |
|
23 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
|
24 'is', 'is_instance_of', |
|
25 'wf_info_for', 'from_state', 'to_state')) |
|
26 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of', |
|
27 'wf_info_for', 'from_state', 'to_state')) |
|
28 |
|
29 _UNIQUE_CONSTRAINTS_LOCK = Lock() |
|
30 _UNIQUE_CONSTRAINTS_HOLDER = None |
|
31 |
|
32 class _ReleaseUniqueConstraintsHook(Operation): |
|
33 def commit_event(self): |
|
34 pass |
|
35 def postcommit_event(self): |
|
36 _release_unique_cstr_lock(self.session) |
|
37 def rollback_event(self): |
|
38 _release_unique_cstr_lock(self.session) |
|
39 |
|
40 def _acquire_unique_cstr_lock(session): |
|
41 """acquire the _UNIQUE_CONSTRAINTS_LOCK for the session. |
|
42 |
|
43 This lock used to avoid potential integrity pb when checking |
|
44 RQLUniqueConstraint in two different transactions, as explained in |
|
45 http://intranet.logilab.fr/jpl/ticket/36564 |
|
46 """ |
|
47 global _UNIQUE_CONSTRAINTS_HOLDER |
|
48 asession = session.actual_session() |
|
49 if _UNIQUE_CONSTRAINTS_HOLDER is asession: |
|
50 return |
|
51 _UNIQUE_CONSTRAINTS_LOCK.acquire() |
|
52 _UNIQUE_CONSTRAINTS_HOLDER = asession |
|
53 # register operation responsible to release the lock on commit/rollback |
|
54 _ReleaseUniqueConstraintsHook(asession) |
|
55 |
|
56 def _release_unique_cstr_lock(session): |
|
57 global _UNIQUE_CONSTRAINTS_HOLDER |
|
58 if _UNIQUE_CONSTRAINTS_HOLDER is session: |
|
59 _UNIQUE_CONSTRAINTS_HOLDER = None |
|
60 _UNIQUE_CONSTRAINTS_LOCK.release() |
|
61 |
|
62 |
|
63 def relation_deleted(session, eidfrom, rtype, eidto): |
|
64 session.transaction_data.setdefault('pendingrelations', []).append( |
|
65 (eidfrom, rtype, eidto)) |
|
66 |
|
67 def eschema_type_eid(session, etype): |
|
68 """get eid of the CWEType entity for the given yams type""" |
|
69 eschema = session.repo.schema.eschema(etype) |
|
70 # eschema.eid is None if schema has been readen from the filesystem, not |
|
71 # from the database (eg during tests) |
|
72 if eschema.eid is None: |
|
73 eschema.eid = session.unsafe_execute( |
|
74 'Any X WHERE X is CWEType, X name %(name)s', |
|
75 {'name': str(etype)})[0][0] |
|
76 return eschema.eid |
|
77 |
|
78 |
|
79 # base meta-data handling ###################################################### |
|
80 |
|
81 def setctime_before_add_entity(session, entity): |
|
82 """before create a new entity -> set creation and modification date |
|
83 |
|
84 this is a conveniency hook, you shouldn't have to disable it |
|
85 """ |
|
86 timestamp = datetime.now() |
|
87 entity.setdefault('creation_date', timestamp) |
|
88 entity.setdefault('modification_date', timestamp) |
|
89 if not session.get_shared_data('do-not-insert-cwuri'): |
|
90 entity.setdefault('cwuri', u'%seid/%s' % (session.base_url(), entity.eid)) |
|
91 |
|
92 |
|
93 def setmtime_before_update_entity(session, entity): |
|
94 """update an entity -> set modification date""" |
|
95 entity.setdefault('modification_date', datetime.now()) |
|
96 |
|
97 |
|
98 class SetCreatorOp(PreCommitOperation): |
|
99 |
|
100 def precommit_event(self): |
|
101 session = self.session |
|
102 if self.entity.eid in session.transaction_data.get('pendingeids', ()): |
|
103 # entity have been created and deleted in the same transaction |
|
104 return |
|
105 if not self.entity.created_by: |
|
106 session.add_relation(self.entity.eid, 'created_by', session.user.eid) |
|
107 |
|
108 |
|
109 def setowner_after_add_entity(session, entity): |
|
110 """create a new entity -> set owner and creator metadata""" |
|
111 asession = session.actual_session() |
|
112 if not asession.is_internal_session: |
|
113 session.add_relation(entity.eid, 'owned_by', asession.user.eid) |
|
114 SetCreatorOp(asession, entity=entity) |
|
115 |
|
116 |
|
117 def setis_after_add_entity(session, entity): |
|
118 """create a new entity -> set is relation""" |
|
119 if hasattr(entity, '_cw_recreating'): |
|
120 return |
|
121 try: |
|
122 #session.add_relation(entity.eid, 'is', |
|
123 # eschema_type_eid(session, entity.id)) |
|
124 session.system_sql('INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)' |
|
125 % (entity.eid, eschema_type_eid(session, entity.id))) |
|
126 except IndexError: |
|
127 # during schema serialization, skip |
|
128 return |
|
129 for etype in entity.e_schema.ancestors() + [entity.e_schema]: |
|
130 #session.add_relation(entity.eid, 'is_instance_of', |
|
131 # eschema_type_eid(session, etype)) |
|
132 session.system_sql('INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)' |
|
133 % (entity.eid, eschema_type_eid(session, etype))) |
|
134 |
|
135 |
|
136 def setowner_after_add_user(session, entity): |
|
137 """when a user has been created, add owned_by relation on itself""" |
|
138 session.add_relation(entity.eid, 'owned_by', entity.eid) |
|
139 |
|
140 |
|
141 def fti_update_after_add_relation(session, eidfrom, rtype, eidto): |
|
142 """sync fulltext index when relevant relation is added. Reindexing the |
|
143 contained entity is enough since it will implicitly reindex the container |
|
144 entity. |
|
145 """ |
|
146 ftcontainer = session.repo.schema.rschema(rtype).fulltext_container |
|
147 if ftcontainer == 'subject': |
|
148 FTIndexEntityOp(session, entity=session.entity_from_eid(eidto)) |
|
149 elif ftcontainer == 'object': |
|
150 FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom)) |
|
151 |
|
152 |
|
153 def fti_update_after_delete_relation(session, eidfrom, rtype, eidto): |
|
154 """sync fulltext index when relevant relation is deleted. Reindexing both |
|
155 entities is necessary. |
|
156 """ |
|
157 if session.repo.schema.rschema(rtype).fulltext_container: |
|
158 FTIndexEntityOp(session, entity=session.entity_from_eid(eidto)) |
|
159 FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom)) |
|
160 |
|
161 |
|
162 class SyncOwnersOp(PreCommitOperation): |
|
163 |
|
164 def precommit_event(self): |
|
165 self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,' |
|
166 'NOT EXISTS(X owned_by U, X eid %(x)s)', |
|
167 {'c': self.compositeeid, 'x': self.composedeid}, |
|
168 ('c', 'x')) |
|
169 |
|
170 |
|
171 def sync_owner_after_add_composite_relation(session, eidfrom, rtype, eidto): |
|
172 """when adding composite relation, the composed should have the same owners |
|
173 has the composite |
|
174 """ |
|
175 if rtype == 'wf_info_for': |
|
176 # skip this special composite relation # XXX (syt) why? |
|
177 return |
|
178 composite = rproperty(session, rtype, eidfrom, eidto, 'composite') |
|
179 if composite == 'subject': |
|
180 SyncOwnersOp(session, compositeeid=eidfrom, composedeid=eidto) |
|
181 elif composite == 'object': |
|
182 SyncOwnersOp(session, compositeeid=eidto, composedeid=eidfrom) |
|
183 |
|
184 |
|
185 def _register_metadata_hooks(hm): |
|
186 """register meta-data related hooks on the hooks manager""" |
|
187 hm.register_hook(setctime_before_add_entity, 'before_add_entity', '') |
|
188 hm.register_hook(setmtime_before_update_entity, 'before_update_entity', '') |
|
189 hm.register_hook(setowner_after_add_entity, 'after_add_entity', '') |
|
190 hm.register_hook(sync_owner_after_add_composite_relation, 'after_add_relation', '') |
|
191 hm.register_hook(fti_update_after_add_relation, 'after_add_relation', '') |
|
192 hm.register_hook(fti_update_after_delete_relation, 'after_delete_relation', '') |
|
193 if 'is' in hm.schema: |
|
194 hm.register_hook(setis_after_add_entity, 'after_add_entity', '') |
|
195 if 'CWUser' in hm.schema: |
|
196 hm.register_hook(setowner_after_add_user, 'after_add_entity', 'CWUser') |
|
197 |
|
198 |
|
199 # core hooks ################################################################## |
|
200 |
|
201 class DelayedDeleteOp(PreCommitOperation): |
|
202 """delete the object of composite relation except if the relation |
|
203 has actually been redirected to another composite |
|
204 """ |
|
205 |
|
206 def precommit_event(self): |
|
207 session = self.session |
|
208 # don't do anything if the entity is being created or deleted |
|
209 if not (self.eid in session.transaction_data.get('pendingeids', ()) or |
|
210 self.eid in session.transaction_data.get('neweids', ())): |
|
211 etype = session.describe(self.eid)[0] |
|
212 if self.role == 'subject': |
|
213 rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y' |
|
214 else: # self.role == 'object': |
|
215 rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X' |
|
216 session.unsafe_execute(rql % (etype, self.rtype), {'x': self.eid}, 'x') |
|
217 |
|
218 |
|
219 def handle_composite_before_del_relation(session, eidfrom, rtype, eidto): |
|
220 """delete the object of composite relation""" |
|
221 # if the relation is being delete, don't delete composite's components |
|
222 # automatically |
|
223 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
|
224 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
|
225 return |
|
226 composite = rproperty(session, rtype, eidfrom, eidto, 'composite') |
|
227 if composite == 'subject': |
|
228 DelayedDeleteOp(session, eid=eidto, rtype=rtype, role='object') |
|
229 elif composite == 'object': |
|
230 DelayedDeleteOp(session, eid=eidfrom, rtype=rtype, role='subject') |
|
231 |
|
232 |
|
233 def before_del_group(session, eid): |
|
234 """check that we don't remove the owners group""" |
|
235 check_internal_entity(session, eid, ('owners',)) |
|
236 |
|
237 |
|
238 # schema validation hooks ##################################################### |
|
239 |
|
240 class CheckConstraintsOperation(LateOperation): |
|
241 """check a new relation satisfy its constraints |
|
242 """ |
|
243 def precommit_event(self): |
|
244 eidfrom, rtype, eidto = self.rdef |
|
245 # first check related entities have not been deleted in the same |
|
246 # transaction |
|
247 pending = self.session.transaction_data.get('pendingeids', ()) |
|
248 if eidfrom in pending: |
|
249 return |
|
250 if eidto in pending: |
|
251 return |
|
252 for constraint in self.constraints: |
|
253 # XXX |
|
254 # * lock RQLConstraint as well? |
|
255 # * use a constraint id to use per constraint lock and avoid |
|
256 # unnecessary commit serialization ? |
|
257 if isinstance(constraint, RQLUniqueConstraint): |
|
258 _acquire_unique_cstr_lock(self.session) |
|
259 try: |
|
260 constraint.repo_check(self.session, eidfrom, rtype, eidto) |
|
261 except NotImplementedError: |
|
262 self.critical('can\'t check constraint %s, not supported', |
|
263 constraint) |
|
264 |
|
265 def commit_event(self): |
|
266 pass |
|
267 |
|
268 |
|
269 def cstrcheck_after_add_relation(session, eidfrom, rtype, eidto): |
|
270 """check the relation satisfy its constraints |
|
271 |
|
272 this is delayed to a precommit time operation since other relation which |
|
273 will make constraint satisfied may be added later. |
|
274 """ |
|
275 if session.is_super_session: |
|
276 return |
|
277 constraints = rproperty(session, rtype, eidfrom, eidto, 'constraints') |
|
278 if constraints: |
|
279 # XXX get only RQL[Unique]Constraints? |
|
280 CheckConstraintsOperation(session, constraints=constraints, |
|
281 rdef=(eidfrom, rtype, eidto)) |
|
282 |
|
283 def uniquecstrcheck_before_modification(session, entity): |
|
284 if session.is_super_session: |
|
285 return |
|
286 eschema = entity.e_schema |
|
287 for attr in entity.edited_attributes: |
|
288 val = entity[attr] |
|
289 if val is None: |
|
290 continue |
|
291 if eschema.subjrels[attr].final and \ |
|
292 eschema.has_unique_values(attr): |
|
293 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) |
|
294 rset = session.unsafe_execute(rql, {'val': val}) |
|
295 if rset and rset[0][0] != entity.eid: |
|
296 msg = session._('the value "%s" is already used, use another one') |
|
297 raise ValidationError(entity.eid, {attr: msg % val}) |
|
298 |
|
299 |
|
300 def cstrcheck_after_update_attributes(session, entity): |
|
301 if session.is_super_session: |
|
302 return |
|
303 eschema = entity.e_schema |
|
304 for attr in entity.edited_attributes: |
|
305 if eschema.subjrels[attr].final: |
|
306 constraints = [c for c in entity.e_schema.constraints(attr) |
|
307 if isinstance(c, (RQLConstraint, RQLUniqueConstraint))] |
|
308 if constraints: |
|
309 CheckConstraintsOperation(session, rdef=(entity.eid, attr, None), |
|
310 constraints=constraints) |
|
311 |
|
312 |
|
313 class CheckRequiredRelationOperation(LateOperation): |
|
314 """checking relation cardinality has to be done after commit in |
|
315 case the relation is being replaced |
|
316 """ |
|
317 eid, rtype = None, None |
|
318 |
|
319 def precommit_event(self): |
|
320 # recheck pending eids |
|
321 if self.eid in self.session.transaction_data.get('pendingeids', ()): |
|
322 return |
|
323 if self.rtype in self.session.transaction_data.get('pendingrtypes', ()): |
|
324 return |
|
325 if self.session.unsafe_execute(*self._rql()).rowcount < 1: |
|
326 etype = self.session.describe(self.eid)[0] |
|
327 _ = self.session._ |
|
328 msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)') |
|
329 msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid} |
|
330 raise ValidationError(self.eid, {self.rtype: msg}) |
|
331 |
|
332 def commit_event(self): |
|
333 pass |
|
334 |
|
335 def _rql(self): |
|
336 raise NotImplementedError() |
|
337 |
|
338 |
|
339 class CheckSRelationOp(CheckRequiredRelationOperation): |
|
340 """check required subject relation""" |
|
341 def _rql(self): |
|
342 return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
|
343 |
|
344 |
|
345 class CheckORelationOp(CheckRequiredRelationOperation): |
|
346 """check required object relation""" |
|
347 def _rql(self): |
|
348 return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
|
349 |
|
350 |
|
351 def checkrel_if_necessary(session, opcls, rtype, eid): |
|
352 """check an equivalent operation has not already been added""" |
|
353 for op in session.pending_operations: |
|
354 if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid: |
|
355 break |
|
356 else: |
|
357 opcls(session, rtype=rtype, eid=eid) |
|
358 |
|
359 |
|
360 def cardinalitycheck_after_add_entity(session, entity): |
|
361 """check cardinalities are satisfied""" |
|
362 if session.is_super_session: |
|
363 return |
|
364 eid = entity.eid |
|
365 for rschema, targetschemas, x in entity.e_schema.relation_definitions(): |
|
366 # skip automatically handled relations |
|
367 if rschema.type in DONT_CHECK_RTYPES_ON_ADD: |
|
368 continue |
|
369 if x == 'subject': |
|
370 subjtype = entity.e_schema |
|
371 objtype = targetschemas[0].type |
|
372 cardindex = 0 |
|
373 opcls = CheckSRelationOp |
|
374 else: |
|
375 subjtype = targetschemas[0].type |
|
376 objtype = entity.e_schema |
|
377 cardindex = 1 |
|
378 opcls = CheckORelationOp |
|
379 card = rschema.rproperty(subjtype, objtype, 'cardinality') |
|
380 if card[cardindex] in '1+': |
|
381 checkrel_if_necessary(session, opcls, rschema.type, eid) |
|
382 |
|
383 def cardinalitycheck_before_del_relation(session, eidfrom, rtype, eidto): |
|
384 """check cardinalities are satisfied""" |
|
385 if session.is_super_session: |
|
386 return |
|
387 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
|
388 return |
|
389 card = rproperty(session, rtype, eidfrom, eidto, 'cardinality') |
|
390 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
|
391 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
|
392 return |
|
393 pendingeids = session.transaction_data.get('pendingeids', ()) |
|
394 if card[0] in '1+' and not eidfrom in pendingeids: |
|
395 checkrel_if_necessary(session, CheckSRelationOp, rtype, eidfrom) |
|
396 if card[1] in '1+' and not eidto in pendingeids: |
|
397 checkrel_if_necessary(session, CheckORelationOp, rtype, eidto) |
|
398 |
|
399 |
|
400 def _register_core_hooks(hm): |
|
401 hm.register_hook(handle_composite_before_del_relation, 'before_delete_relation', '') |
|
402 hm.register_hook(before_del_group, 'before_delete_entity', 'CWGroup') |
|
403 |
|
404 #hm.register_hook(cstrcheck_before_update_entity, 'before_update_entity', '') |
|
405 hm.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '') |
|
406 hm.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '') |
|
407 hm.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '') |
|
408 hm.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '') |
|
409 hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '') |
|
410 hm.register_hook(cstrcheck_after_update_attributes, 'after_add_entity', '') |
|
411 hm.register_hook(cstrcheck_after_update_attributes, 'after_update_entity', '') |
|
412 |
|
413 # user/groups synchronisation ################################################# |
|
414 |
|
415 class GroupOperation(Operation): |
|
416 """base class for group operation""" |
|
417 geid = None |
|
418 def __init__(self, session, *args, **kwargs): |
|
419 """override to get the group name before actual groups manipulation: |
|
420 |
|
421 we may temporarily loose right access during a commit event, so |
|
422 no query should be emitted while comitting |
|
423 """ |
|
424 rql = 'Any N WHERE G eid %(x)s, G name N' |
|
425 result = session.execute(rql, {'x': kwargs['geid']}, 'x', build_descr=False) |
|
426 Operation.__init__(self, session, *args, **kwargs) |
|
427 self.group = result[0][0] |
|
428 |
|
429 |
|
430 class DeleteGroupOp(GroupOperation): |
|
431 """synchronize user when a in_group relation has been deleted""" |
|
432 def commit_event(self): |
|
433 """the observed connections pool has been commited""" |
|
434 groups = self.cnxuser.groups |
|
435 try: |
|
436 groups.remove(self.group) |
|
437 except KeyError: |
|
438 self.error('user %s not in group %s', self.cnxuser, self.group) |
|
439 return |
|
440 |
|
441 |
|
442 def after_del_in_group(session, fromeid, rtype, toeid): |
|
443 """modify user permission, need to update users""" |
|
444 for session_ in get_user_sessions(session.repo, fromeid): |
|
445 DeleteGroupOp(session, cnxuser=session_.user, geid=toeid) |
|
446 |
|
447 |
|
448 class AddGroupOp(GroupOperation): |
|
449 """synchronize user when a in_group relation has been added""" |
|
450 def commit_event(self): |
|
451 """the observed connections pool has been commited""" |
|
452 groups = self.cnxuser.groups |
|
453 if self.group in groups: |
|
454 self.warning('user %s already in group %s', self.cnxuser, |
|
455 self.group) |
|
456 return |
|
457 groups.add(self.group) |
|
458 |
|
459 |
|
460 def after_add_in_group(session, fromeid, rtype, toeid): |
|
461 """modify user permission, need to update users""" |
|
462 for session_ in get_user_sessions(session.repo, fromeid): |
|
463 AddGroupOp(session, cnxuser=session_.user, geid=toeid) |
|
464 |
|
465 |
|
466 class DelUserOp(Operation): |
|
467 """synchronize user when a in_group relation has been added""" |
|
468 def __init__(self, session, cnxid): |
|
469 self.cnxid = cnxid |
|
470 Operation.__init__(self, session) |
|
471 |
|
472 def commit_event(self): |
|
473 """the observed connections pool has been commited""" |
|
474 try: |
|
475 self.repo.close(self.cnxid) |
|
476 except BadConnectionId: |
|
477 pass # already closed |
|
478 |
|
479 |
|
480 def after_del_user(session, eid): |
|
481 """modify user permission, need to update users""" |
|
482 for session_ in get_user_sessions(session.repo, eid): |
|
483 DelUserOp(session, session_.id) |
|
484 |
|
485 |
|
486 def _register_usergroup_hooks(hm): |
|
487 """register user/group related hooks on the hooks manager""" |
|
488 hm.register_hook(after_del_user, 'after_delete_entity', 'CWUser') |
|
489 hm.register_hook(after_add_in_group, 'after_add_relation', 'in_group') |
|
490 hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group') |
|
491 |
|
492 |
|
493 # workflow handling ########################################################### |
|
494 |
|
495 from cubicweb.entities.wfobjs import WorkflowTransition, WorkflowException |
|
496 |
|
497 def _change_state(session, x, oldstate, newstate): |
|
498 nocheck = session.transaction_data.setdefault('skip-security', set()) |
|
499 nocheck.add((x, 'in_state', oldstate)) |
|
500 nocheck.add((x, 'in_state', newstate)) |
|
501 # delete previous state first in case we're using a super session |
|
502 fromsource = session.describe(x)[1] |
|
503 # don't try to remove previous state if in_state isn't stored in the system |
|
504 # source |
|
505 if fromsource == 'system' or \ |
|
506 not session.repo.sources_by_uri[fromsource].support_relation('in_state'): |
|
507 session.delete_relation(x, 'in_state', oldstate) |
|
508 session.add_relation(x, 'in_state', newstate) |
|
509 |
|
510 |
|
511 class FireAutotransitionOp(PreCommitOperation): |
|
512 """try to fire auto transition after state changes""" |
|
513 |
|
514 def precommit_event(self): |
|
515 session = self.session |
|
516 entity = self.entity |
|
517 autotrs = list(entity.possible_transitions('auto')) |
|
518 if autotrs: |
|
519 assert len(autotrs) == 1 |
|
520 entity.fire_transition(autotrs[0]) |
|
521 |
|
522 |
|
523 def before_add_trinfo(session, entity): |
|
524 """check the transition is allowed, add missing information. Expect that: |
|
525 * wf_info_for inlined relation is set |
|
526 * by_transition or to_state (managers only) inlined relation is set |
|
527 """ |
|
528 # first retreive entity to which the state change apply |
|
529 try: |
|
530 foreid = entity['wf_info_for'] |
|
531 except KeyError: |
|
532 msg = session._('mandatory relation') |
|
533 raise ValidationError(entity.eid, {'wf_info_for': msg}) |
|
534 forentity = session.entity_from_eid(foreid) |
|
535 # then check it has a workflow set, unless we're in the process of changing |
|
536 # entity's workflow |
|
537 if session.transaction_data.get((forentity.eid, 'customwf')): |
|
538 wfeid = session.transaction_data[(forentity.eid, 'customwf')] |
|
539 wf = session.entity_from_eid(wfeid) |
|
540 else: |
|
541 wf = forentity.current_workflow |
|
542 if wf is None: |
|
543 msg = session._('related entity has no workflow set') |
|
544 raise ValidationError(entity.eid, {None: msg}) |
|
545 # then check it has a state set |
|
546 fromstate = forentity.current_state |
|
547 if fromstate is None: |
|
548 msg = session._('related entity has no state') |
|
549 raise ValidationError(entity.eid, {None: msg}) |
|
550 # True if we are coming back from subworkflow |
|
551 swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) |
|
552 cowpowers = session.is_super_session or 'managers' in session.user.groups |
|
553 # no investigate the requested state change... |
|
554 try: |
|
555 treid = entity['by_transition'] |
|
556 except KeyError: |
|
557 # no transition set, check user is a manager and destination state is |
|
558 # specified (and valid) |
|
559 if not cowpowers: |
|
560 msg = session._('mandatory relation') |
|
561 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
562 deststateeid = entity.get('to_state') |
|
563 if not deststateeid: |
|
564 msg = session._('mandatory relation') |
|
565 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
566 deststate = wf.state_by_eid(deststateeid) |
|
567 if deststate is None: |
|
568 msg = entity.req._("state doesn't belong to entity's current workflow") |
|
569 raise ValidationError(entity.eid, {'to_state': msg}) |
|
570 else: |
|
571 # check transition is valid and allowed, unless we're coming back from |
|
572 # subworkflow |
|
573 tr = session.entity_from_eid(treid) |
|
574 if swtr is None: |
|
575 if tr is None: |
|
576 msg = session._("transition doesn't belong to entity's workflow") |
|
577 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
578 if not tr.has_input_state(fromstate): |
|
579 _ = session._ |
|
580 msg = _("transition %(tr)s isn't allowed from %(st)s") % {'tr': _(tr.name), |
|
581 'st': _(fromstate.name), |
|
582 } |
|
583 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
584 if not tr.may_be_fired(foreid): |
|
585 msg = session._("transition may not be fired") |
|
586 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
587 if entity.get('to_state'): |
|
588 deststateeid = entity['to_state'] |
|
589 if not cowpowers and deststateeid != tr.destination().eid: |
|
590 msg = session._("transition isn't allowed") |
|
591 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
592 if swtr is None: |
|
593 deststate = session.entity_from_eid(deststateeid) |
|
594 if not cowpowers and deststate is None: |
|
595 msg = entity.req._("state doesn't belong to entity's workflow") |
|
596 raise ValidationError(entity.eid, {'to_state': msg}) |
|
597 else: |
|
598 deststateeid = tr.destination().eid |
|
599 # everything is ok, add missing information on the trinfo entity |
|
600 entity['from_state'] = fromstate.eid |
|
601 entity['to_state'] = deststateeid |
|
602 nocheck = session.transaction_data.setdefault('skip-security', set()) |
|
603 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
|
604 nocheck.add((entity.eid, 'to_state', deststateeid)) |
|
605 FireAutotransitionOp(session, entity=forentity) |
|
606 |
|
607 |
|
608 def after_add_trinfo(session, entity): |
|
609 """change related entity state""" |
|
610 _change_state(session, entity['wf_info_for'], |
|
611 entity['from_state'], entity['to_state']) |
|
612 forentity = session.entity_from_eid(entity['wf_info_for']) |
|
613 assert forentity.current_state.eid == entity['to_state'], ( |
|
614 forentity.eid, forentity.current_state.name) |
|
615 if forentity.main_workflow.eid != forentity.current_workflow.eid: |
|
616 SubWorkflowExitOp(session, forentity=forentity, trinfo=entity) |
|
617 |
|
618 class SubWorkflowExitOp(PreCommitOperation): |
|
619 def precommit_event(self): |
|
620 session = self.session |
|
621 forentity = self.forentity |
|
622 trinfo = self.trinfo |
|
623 # we're in a subworkflow, check if we've reached an exit point |
|
624 wftr = forentity.subworkflow_input_transition() |
|
625 if wftr is None: |
|
626 # inconsistency detected |
|
627 msg = session._("state doesn't belong to entity's current workflow") |
|
628 raise ValidationError(self.trinfo.eid, {'to_state': msg}) |
|
629 tostate = wftr.get_exit_point(forentity, trinfo['to_state']) |
|
630 if tostate is not None: |
|
631 # reached an exit point |
|
632 msg = session._('exiting from subworkflow %s') |
|
633 msg %= session._(forentity.current_workflow.name) |
|
634 session.transaction_data[(forentity.eid, 'subwfentrytr')] = True |
|
635 # XXX iirk |
|
636 req = forentity.req |
|
637 forentity.req = session.super_session |
|
638 try: |
|
639 trinfo = forentity.change_state(tostate, msg, u'text/plain', |
|
640 tr=wftr) |
|
641 finally: |
|
642 forentity.req = req |
|
643 |
|
644 |
|
645 class SetInitialStateOp(PreCommitOperation): |
|
646 """make initial state be a default state""" |
|
647 |
|
648 def precommit_event(self): |
|
649 session = self.session |
|
650 entity = self.entity |
|
651 # if there is an initial state and the entity's state is not set, |
|
652 # use the initial state as a default state |
|
653 pendingeids = session.transaction_data.get('pendingeids', ()) |
|
654 if not entity.eid in pendingeids and not entity.in_state and \ |
|
655 entity.main_workflow: |
|
656 state = entity.main_workflow.initial |
|
657 if state: |
|
658 # use super session to by-pass security checks |
|
659 session.super_session.add_relation(entity.eid, 'in_state', |
|
660 state.eid) |
|
661 |
|
662 |
|
663 def set_initial_state_after_add(session, entity): |
|
664 SetInitialStateOp(session, entity=entity) |
|
665 |
|
666 |
|
667 def before_add_in_state(session, eidfrom, rtype, eidto): |
|
668 """check state apply, in case of direct in_state change using unsafe_execute |
|
669 """ |
|
670 nocheck = session.transaction_data.setdefault('skip-security', set()) |
|
671 if (eidfrom, 'in_state', eidto) in nocheck: |
|
672 # state changed through TrInfo insertion, so we already know it's ok |
|
673 return |
|
674 entity = session.entity_from_eid(eidfrom) |
|
675 mainwf = entity.main_workflow |
|
676 if mainwf is None: |
|
677 msg = session._('entity has no workflow set') |
|
678 raise ValidationError(entity.eid, {None: msg}) |
|
679 for wf in mainwf.iter_workflows(): |
|
680 if wf.state_by_eid(eidto): |
|
681 break |
|
682 else: |
|
683 msg = session._("state doesn't belong to entity's workflow. You may " |
|
684 "want to set a custom workflow for this entity first.") |
|
685 raise ValidationError(eidfrom, {'in_state': msg}) |
|
686 if entity.current_workflow and wf.eid != entity.current_workflow.eid: |
|
687 msg = session._("state doesn't belong to entity's current workflow") |
|
688 raise ValidationError(eidfrom, {'in_state': msg}) |
|
689 |
|
690 |
|
691 class CheckTrExitPoint(PreCommitOperation): |
|
692 |
|
693 def precommit_event(self): |
|
694 tr = self.session.entity_from_eid(self.treid) |
|
695 outputs = set() |
|
696 for ep in tr.subworkflow_exit: |
|
697 if ep.subwf_state.eid in outputs: |
|
698 msg = self.session._("can't have multiple exits on the same state") |
|
699 raise ValidationError(self.treid, {'subworkflow_exit': msg}) |
|
700 outputs.add(ep.subwf_state.eid) |
|
701 |
|
702 |
|
703 def after_add_subworkflow_exit(session, eidfrom, rtype, eidto): |
|
704 CheckTrExitPoint(session, treid=eidfrom) |
|
705 |
|
706 |
|
707 class WorkflowChangedOp(PreCommitOperation): |
|
708 """fix entity current state when changing its workflow""" |
|
709 |
|
710 def precommit_event(self): |
|
711 # notice that enforcement that new workflow apply to the entity's type is |
|
712 # done by schema rule, no need to check it here |
|
713 session = self.session |
|
714 pendingeids = session.transaction_data.get('pendingeids', ()) |
|
715 if self.eid in pendingeids: |
|
716 return |
|
717 entity = session.entity_from_eid(self.eid) |
|
718 # check custom workflow has not been rechanged to another one in the same |
|
719 # transaction |
|
720 mainwf = entity.main_workflow |
|
721 if mainwf.eid == self.wfeid: |
|
722 deststate = mainwf.initial |
|
723 if not deststate: |
|
724 msg = session._('workflow has no initial state') |
|
725 raise ValidationError(entity.eid, {'custom_workflow': msg}) |
|
726 if mainwf.state_by_eid(entity.current_state.eid): |
|
727 # nothing to do |
|
728 return |
|
729 # if there are no history, simply go to new workflow's initial state |
|
730 if not entity.workflow_history: |
|
731 if entity.current_state.eid != deststate.eid: |
|
732 _change_state(session, entity.eid, |
|
733 entity.current_state.eid, deststate.eid) |
|
734 return |
|
735 msg = session._('workflow changed to "%s"') |
|
736 msg %= session._(mainwf.name) |
|
737 session.transaction_data[(entity.eid, 'customwf')] = self.wfeid |
|
738 entity.change_state(deststate, msg, u'text/plain') |
|
739 |
|
740 |
|
741 def set_custom_workflow(session, eidfrom, rtype, eidto): |
|
742 WorkflowChangedOp(session, eid=eidfrom, wfeid=eidto) |
|
743 |
|
744 |
|
745 def del_custom_workflow(session, eidfrom, rtype, eidto): |
|
746 entity = session.entity_from_eid(eidfrom) |
|
747 typewf = entity.cwetype_workflow() |
|
748 if typewf is not None: |
|
749 WorkflowChangedOp(session, eid=eidfrom, wfeid=typewf.eid) |
|
750 |
|
751 |
|
752 def after_del_workflow(session, eid): |
|
753 # workflow cleanup |
|
754 session.execute('DELETE State X WHERE NOT X state_of Y') |
|
755 session.execute('DELETE Transition X WHERE NOT X transition_of Y') |
|
756 |
|
757 |
|
758 def _register_wf_hooks(hm): |
|
759 """register workflow related hooks on the hooks manager""" |
|
760 if 'in_state' in hm.schema: |
|
761 hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo') |
|
762 hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo') |
|
763 #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') |
|
764 for eschema in hm.schema.entities(): |
|
765 if 'in_state' in eschema.subject_relations(): |
|
766 hm.register_hook(set_initial_state_after_add, 'after_add_entity', |
|
767 str(eschema)) |
|
768 hm.register_hook(set_custom_workflow, 'after_add_relation', 'custom_workflow') |
|
769 hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow') |
|
770 hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow') |
|
771 hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') |
|
772 hm.register_hook(after_add_subworkflow_exit, 'after_add_relation', 'subworkflow_exit') |
|
773 |
|
774 |
|
775 # CWProperty hooks ############################################################# |
|
776 |
|
777 |
|
778 class DelCWPropertyOp(Operation): |
|
779 """a user's custom properties has been deleted""" |
|
780 |
|
781 def commit_event(self): |
|
782 """the observed connections pool has been commited""" |
|
783 try: |
|
784 del self.epropdict[self.key] |
|
785 except KeyError: |
|
786 self.error('%s has no associated value', self.key) |
|
787 |
|
788 |
|
789 class ChangeCWPropertyOp(Operation): |
|
790 """a user's custom properties has been added/changed""" |
|
791 |
|
792 def commit_event(self): |
|
793 """the observed connections pool has been commited""" |
|
794 self.epropdict[self.key] = self.value |
|
795 |
|
796 |
|
797 class AddCWPropertyOp(Operation): |
|
798 """a user's custom properties has been added/changed""" |
|
799 |
|
800 def commit_event(self): |
|
801 """the observed connections pool has been commited""" |
|
802 eprop = self.eprop |
|
803 if not eprop.for_user: |
|
804 self.repo.vreg.eprop_values[eprop.pkey] = eprop.value |
|
805 # if for_user is set, update is handled by a ChangeCWPropertyOp operation |
|
806 |
|
807 |
|
808 def after_add_eproperty(session, entity): |
|
809 key, value = entity.pkey, entity.value |
|
810 try: |
|
811 value = session.vreg.typed_value(key, value) |
|
812 except UnknownProperty: |
|
813 raise ValidationError(entity.eid, {'pkey': session._('unknown property key')}) |
|
814 except ValueError, ex: |
|
815 raise ValidationError(entity.eid, {'value': session._(str(ex))}) |
|
816 if not session.user.matching_groups('managers'): |
|
817 session.add_relation(entity.eid, 'for_user', session.user.eid) |
|
818 else: |
|
819 AddCWPropertyOp(session, eprop=entity) |
|
820 |
|
821 |
|
822 def after_update_eproperty(session, entity): |
|
823 if not ('pkey' in entity.edited_attributes or |
|
824 'value' in entity.edited_attributes): |
|
825 return |
|
826 key, value = entity.pkey, entity.value |
|
827 try: |
|
828 value = session.vreg.typed_value(key, value) |
|
829 except UnknownProperty: |
|
830 return |
|
831 except ValueError, ex: |
|
832 raise ValidationError(entity.eid, {'value': session._(str(ex))}) |
|
833 if entity.for_user: |
|
834 for session_ in get_user_sessions(session.repo, entity.for_user[0].eid): |
|
835 ChangeCWPropertyOp(session, epropdict=session_.user.properties, |
|
836 key=key, value=value) |
|
837 else: |
|
838 # site wide properties |
|
839 ChangeCWPropertyOp(session, epropdict=session.vreg.eprop_values, |
|
840 key=key, value=value) |
|
841 |
|
842 |
|
843 def before_del_eproperty(session, eid): |
|
844 for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()): |
|
845 if rtype == 'for_user' and eidfrom == eid: |
|
846 # if for_user was set, delete has already been handled |
|
847 break |
|
848 else: |
|
849 key = session.execute('Any K WHERE P eid %(x)s, P pkey K', |
|
850 {'x': eid}, 'x')[0][0] |
|
851 DelCWPropertyOp(session, epropdict=session.vreg.eprop_values, key=key) |
|
852 |
|
853 |
|
854 def after_add_for_user(session, fromeid, rtype, toeid): |
|
855 if not session.describe(fromeid)[0] == 'CWProperty': |
|
856 return |
|
857 key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V', |
|
858 {'x': fromeid}, 'x')[0] |
|
859 if session.vreg.property_info(key)['sitewide']: |
|
860 raise ValidationError(fromeid, |
|
861 {'for_user': session._("site-wide property can't be set for user")}) |
|
862 for session_ in get_user_sessions(session.repo, toeid): |
|
863 ChangeCWPropertyOp(session, epropdict=session_.user.properties, |
|
864 key=key, value=value) |
|
865 |
|
866 |
|
867 def before_del_for_user(session, fromeid, rtype, toeid): |
|
868 key = session.execute('Any K WHERE P eid %(x)s, P pkey K', |
|
869 {'x': fromeid}, 'x')[0][0] |
|
870 relation_deleted(session, fromeid, rtype, toeid) |
|
871 for session_ in get_user_sessions(session.repo, toeid): |
|
872 DelCWPropertyOp(session, epropdict=session_.user.properties, key=key) |
|
873 |
|
874 |
|
875 def _register_eproperty_hooks(hm): |
|
876 """register workflow related hooks on the hooks manager""" |
|
877 hm.register_hook(after_add_eproperty, 'after_add_entity', 'CWProperty') |
|
878 hm.register_hook(after_update_eproperty, 'after_update_entity', 'CWProperty') |
|
879 hm.register_hook(before_del_eproperty, 'before_delete_entity', 'CWProperty') |
|
880 hm.register_hook(after_add_for_user, 'after_add_relation', 'for_user') |
|
881 hm.register_hook(before_del_for_user, 'before_delete_relation', 'for_user') |
|