11 from datetime import datetime |
11 from datetime import datetime |
12 |
12 |
13 from cubicweb import UnknownProperty, ValidationError, BadConnectionId |
13 from cubicweb import UnknownProperty, ValidationError, BadConnectionId |
14 |
14 |
15 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation |
15 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation |
16 from cubicweb.server.hookhelper import (check_internal_entity, previous_state, |
16 from cubicweb.server.hookhelper import (check_internal_entity, |
17 get_user_sessions, rproperty) |
17 get_user_sessions, rproperty) |
18 from cubicweb.server.repository import FTIndexEntityOp |
18 from cubicweb.server.repository import FTIndexEntityOp |
19 |
19 |
20 # special relations that don't have to be checked for integrity, usually |
20 # special relations that don't have to be checked for integrity, usually |
21 # because they are handled internally by hooks (so we trust ourselves) |
21 # because they are handled internally by hooks (so we trust ourselves) |
22 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
22 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
415 hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group') |
416 hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group') |
416 |
417 |
417 |
418 |
418 # workflow handling ########################################################### |
419 # workflow handling ########################################################### |
419 |
420 |
420 def before_add_in_state(session, fromeid, rtype, toeid): |
421 def before_add_trinfo(session, entity): |
421 """check the transition is allowed and record transition information |
422 """check the transition is allowed, add missing information. Expect that: |
422 """ |
423 * wf_info_for inlined relation is set |
423 assert rtype == 'in_state' |
424 * by_transition or to_state (managers only) inlined relation is set |
424 state = previous_state(session, fromeid) |
425 """ |
425 etype = session.describe(fromeid)[0] |
426 # first retreive entity to which the state change apply |
426 if not (session.is_super_session or 'managers' in session.user.groups): |
427 try: |
427 if not state is None: |
428 foreid = entity['wf_info_for'] |
428 entity = session.entity_from_eid(fromeid) |
429 except KeyError: |
429 # we should find at least one transition going to this state |
430 msg = session._('mandatory relation') |
430 try: |
431 raise ValidationError(entity.eid, {'wf_info_for': msg}) |
431 iter(state.transitions(entity, toeid)).next() |
432 forentity = session.entity_from_eid(foreid) |
432 except StopIteration: |
433 # then check it has a workflow set |
433 _ = session._ |
434 wf = forentity.current_workflow |
434 msg = _('transition from %s to %s does not exist or is not allowed') % ( |
435 if wf is None: |
435 _(state.name), _(session.entity_from_eid(toeid).name)) |
436 msg = session._('related entity has no workflow set') |
436 raise ValidationError(fromeid, {'in_state': msg}) |
437 raise ValidationError(entity.eid, {None: msg}) |
437 else: |
438 # then check it has a state set |
438 # not a transition |
439 fromstate = forentity.current_state |
439 # check state is initial state if the workflow defines one |
440 if fromstate is None: |
440 isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s', |
441 msg = session._('related entity has no state') |
441 {'etype': etype}) |
442 raise ValidationError(entity.eid, {None: msg}) |
442 if isrset and not toeid == isrset[0][0]: |
443 # no investigate the requested state change... |
443 _ = session._ |
444 try: |
444 msg = _('%s is not the initial state (%s) for this entity') % ( |
445 treid = entity['by_transition'] |
445 _(session.entity_from_eid(toeid).name), _(isrset.get_entity(0,0).name)) |
446 except KeyError: |
446 raise ValidationError(fromeid, {'in_state': msg}) |
447 # no transition set, check user is a manager and destination state is |
447 eschema = session.repo.schema[etype] |
448 # specified (and valid) |
448 if not 'wf_info_for' in eschema.object_relations(): |
449 if not (session.is_super_session or 'managers' in session.user.groups): |
449 # workflow history not activated for this entity type |
450 msg = session._('mandatory relation') |
450 return |
451 raise ValidationError(entity.eid, {'by_transition': msg}) |
451 rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s' |
452 deststateeid = entity.get('to_state') |
452 args = {'comment': session.get_shared_data('trcomment', None, pop=True), |
453 if not deststateeid: |
453 'e': fromeid, 'ds': toeid} |
454 msg = session._('mandatory relation') |
454 cformat = session.get_shared_data('trcommentformat', None, pop=True) |
455 raise ValidationError(entity.eid, {'by_transition': msg}) |
455 if cformat is not None: |
456 deststate = wf.state_by_eid(deststateeid) |
456 args['comment_format'] = cformat |
457 if deststate is None: |
457 rql += ', T comment_format %(comment_format)s' |
458 msg = session._("state doesn't belong to entity's workflow") |
458 restriction = ['DS eid %(ds)s, E eid %(e)s'] |
459 raise ValidationError(entity.eid, {'to_state': msg}) |
459 if not state is None: # not a transition |
460 else: |
460 rql += ', T from_state FS' |
461 # check transition is valid and allowed |
461 restriction.append('FS eid %(fs)s') |
462 tr = wf.transition_by_eid(treid) |
462 args['fs'] = state.eid |
463 if tr is None: |
463 rql = '%s WHERE %s' % (rql, ', '.join(restriction)) |
464 msg = session._("transition doesn't belong to entity's workflow") |
464 session.unsafe_execute(rql, args, 'e') |
465 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
466 if not tr.has_input_state(fromstate): |
|
467 msg = session._("transition isn't allowed") |
|
468 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
469 if not tr.may_be_fired(foreid): |
|
470 msg = session._("transition may not be fired") |
|
471 raise ValidationError(entity.eid, {'by_transition': msg}) |
|
472 deststateeid = tr.destination().eid |
|
473 # everything is ok, add missing information on the trinfo entity |
|
474 entity['from_state'] = fromstate.eid |
|
475 entity['to_state'] = deststateeid |
|
476 nocheck = session.transaction_data.setdefault('skip-security', set()) |
|
477 nocheck.add((entity.eid, 'from_state', fromstate.eid)) |
|
478 nocheck.add((entity.eid, 'to_state', deststateeid)) |
|
479 |
|
480 def after_add_trinfo(session, entity): |
|
481 """change related entity state""" |
|
482 # need to delete previous state first, not done automatically since |
|
483 # we're using a super session |
|
484 session.unsafe_execute('DELETE X in_state S WHERE X eid %(x)s, S eid %(s)s', |
|
485 {'x': entity['wf_info_for'], 's': entity['from_state']}, |
|
486 ('x', 's')) |
|
487 session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', |
|
488 {'x': entity['wf_info_for'], 's': entity['to_state']}, |
|
489 ('x', 's')) |
465 |
490 |
466 |
491 |
467 class SetInitialStateOp(PreCommitOperation): |
492 class SetInitialStateOp(PreCommitOperation): |
468 """make initial state be a default state""" |
493 """make initial state be a default state""" |
469 |
494 |
471 session = self.session |
496 session = self.session |
472 entity = self.entity |
497 entity = self.entity |
473 # if there is an initial state and the entity's state is not set, |
498 # if there is an initial state and the entity's state is not set, |
474 # use the initial state as a default state |
499 # use the initial state as a default state |
475 pendingeids = session.transaction_data.get('pendingeids', ()) |
500 pendingeids = session.transaction_data.get('pendingeids', ()) |
476 if not entity.eid in pendingeids and not entity.in_state: |
501 if not entity.eid in pendingeids and not entity.in_state and \ |
477 rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s', |
502 entity.current_workflow: |
478 {'name': entity.id}) |
503 state = entity.current_workflow.initial |
479 if rset: |
504 if state: |
480 session.add_relation(entity.eid, 'in_state', rset[0][0]) |
505 # use super session to by-pass security checks |
|
506 session.super_session.add_relation(entity.eid, 'in_state', |
|
507 state.eid) |
481 |
508 |
482 |
509 |
483 def set_initial_state_after_add(session, entity): |
510 def set_initial_state_after_add(session, entity): |
484 SetInitialStateOp(session, entity=entity) |
511 SetInitialStateOp(session, entity=entity) |
|
512 |
|
513 def after_del_workflow(session, eid): |
|
514 # workflow cleanup |
|
515 session.execute('DELETE State X WHERE NOT X state_of Y') |
|
516 session.execute('DELETE Transition X WHERE NOT X transition_of Y') |
485 |
517 |
486 |
518 |
487 def _register_wf_hooks(hm): |
519 def _register_wf_hooks(hm): |
488 """register workflow related hooks on the hooks manager""" |
520 """register workflow related hooks on the hooks manager""" |
489 if 'in_state' in hm.schema: |
521 if 'in_state' in hm.schema: |
490 hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') |
522 hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo') |
491 hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') |
523 hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo') |
|
524 #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') |
492 for eschema in hm.schema.entities(): |
525 for eschema in hm.schema.entities(): |
493 if 'in_state' in eschema.subject_relations(): |
526 if 'in_state' in eschema.subject_relations(): |
494 hm.register_hook(set_initial_state_after_add, 'after_add_entity', |
527 hm.register_hook(set_initial_state_after_add, 'after_add_entity', |
495 str(eschema)) |
528 str(eschema)) |
|
529 hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow') |
496 |
530 |
497 |
531 |
498 # CWProperty hooks ############################################################# |
532 # CWProperty hooks ############################################################# |
499 |
533 |
500 |
534 |