server/hooks.py
brancholdstable
changeset 4985 02b52bf9f5f8
parent 4563 c25da7573ebd
parent 4982 4247066fd3de
child 5422 0865e1e90674
equal deleted inserted replaced
4563:c25da7573ebd 4985:02b52bf9f5f8
     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')