hooks/integrity.py
changeset 6426 541659c39f6a
parent 6376 f8662240ed4d
child 6838 6c7adf825b3c
equal deleted inserted replaced
6425:8d7c2fd2ac66 6426:541659c39f6a
    29 from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
    29 from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
    30                              RQLConstraint, RQLUniqueConstraint)
    30                              RQLConstraint, RQLUniqueConstraint)
    31 from cubicweb.selectors import is_instance
    31 from cubicweb.selectors import is_instance
    32 from cubicweb.uilib import soup2xhtml
    32 from cubicweb.uilib import soup2xhtml
    33 from cubicweb.server import hook
    33 from cubicweb.server import hook
    34 from cubicweb.server.hook import set_operation
       
    35 
    34 
    36 # special relations that don't have to be checked for integrity, usually
    35 # special relations that don't have to be checked for integrity, usually
    37 # because they are handled internally by hooks (so we trust ourselves)
    36 # because they are handled internally by hooks (so we trust ourselves)
    38 DONT_CHECK_RTYPES_ON_ADD = META_RTYPES | WORKFLOW_RTYPES
    37 DONT_CHECK_RTYPES_ON_ADD = META_RTYPES | WORKFLOW_RTYPES
    39 DONT_CHECK_RTYPES_ON_DEL = META_RTYPES | WORKFLOW_RTYPES
    38 DONT_CHECK_RTYPES_ON_DEL = META_RTYPES | WORKFLOW_RTYPES
    66         _release_unique_cstr_lock(self.session)
    65         _release_unique_cstr_lock(self.session)
    67     def rollback_event(self):
    66     def rollback_event(self):
    68         _release_unique_cstr_lock(self.session)
    67         _release_unique_cstr_lock(self.session)
    69 
    68 
    70 
    69 
    71 class _CheckRequiredRelationOperation(hook.LateOperation):
    70 class _CheckRequiredRelationOperation(hook.DataOperationMixIn,
    72     """checking relation cardinality has to be done after commit in
    71                                       hook.LateOperation):
    73     case the relation is being replaced
    72     """checking relation cardinality has to be done after commit in case the
    74     """
    73     relation is being replaced
       
    74     """
       
    75     containercls = list
    75     role = key = base_rql = None
    76     role = key = base_rql = None
    76 
    77 
    77     def precommit_event(self):
    78     def precommit_event(self):
    78         session =self.session
    79         session = self.session
    79         pendingeids = session.transaction_data.get('pendingeids', ())
    80         pendingeids = session.transaction_data.get('pendingeids', ())
    80         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
    81         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
    81         # poping key is not optional: if further operation trigger new deletion
    82         # poping key is not optional: if further operation trigger new deletion
    82         # of relation, we'll need a new operation
    83         # of relation, we'll need a new operation
    83         for eid, rtype in session.transaction_data.pop(self.key):
    84         for eid, rtype in self.get_data():
    84             # recheck pending eids / relation types
    85             # recheck pending eids / relation types
    85             if eid in pendingeids:
    86             if eid in pendingeids:
    86                 continue
    87                 continue
    87             if rtype in pendingrtypes:
    88             if rtype in pendingrtypes:
    88                 continue
    89                 continue
    96 
    97 
    97 
    98 
    98 class _CheckSRelationOp(_CheckRequiredRelationOperation):
    99 class _CheckSRelationOp(_CheckRequiredRelationOperation):
    99     """check required subject relation"""
   100     """check required subject relation"""
   100     role = 'subject'
   101     role = 'subject'
   101     key = '_cwisrel'
       
   102     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
   102     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
   103 
   103 
   104 class _CheckORelationOp(_CheckRequiredRelationOperation):
   104 class _CheckORelationOp(_CheckRequiredRelationOperation):
   105     """check required object relation"""
   105     """check required object relation"""
   106     role = 'object'
   106     role = 'object'
   107     key = '_cwiorel'
       
   108     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
   107     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
   109 
   108 
   110 
   109 
   111 class IntegrityHook(hook.Hook):
   110 class IntegrityHook(hook.Hook):
   112     __abstract__ = True
   111     __abstract__ = True
   129             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
   128             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
   130                 continue
   129                 continue
   131             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
   130             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
   132             if rdef.role_cardinality(role) in '1+':
   131             if rdef.role_cardinality(role) in '1+':
   133                 if role == 'subject':
   132                 if role == 'subject':
   134                     set_operation(self._cw, '_cwisrel', (eid, rschema.type),
   133                     op = _CheckSRelationOp.get_instance(self._cw)
   135                                   _CheckSRelationOp, list)
       
   136                 else:
   134                 else:
   137                     set_operation(self._cw, '_cwiorel', (eid, rschema.type),
   135                     op = _CheckORelationOp.get_instance(self._cw)
   138                                   _CheckORelationOp, list)
   136                 op.add_data((eid, rschema.type))
   139 
   137 
   140     def before_delete_relation(self):
   138     def before_delete_relation(self):
   141         rtype = self.rtype
   139         rtype = self.rtype
   142         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   140         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   143             return
   141             return
   146         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   144         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   147         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   145         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   148             return
   146             return
   149         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
   147         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
   150         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   148         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   151             set_operation(self._cw, '_cwisrel', (eidfrom, rtype),
   149             _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
   152                           _CheckSRelationOp, list)
       
   153         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   150         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   154             set_operation(self._cw, '_cwiorel', (eidto, rtype),
   151             _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
   155                           _CheckORelationOp, list)
   152 
   156 
   153 
   157 
   154 class _CheckConstraintsOp(hook.DataOperationMixIn, hook.LateOperation):
   158 class _CheckConstraintsOp(hook.LateOperation):
       
   159     """ check a new relation satisfy its constraints """
   155     """ check a new relation satisfy its constraints """
   160 
   156     containercls = list
   161     def precommit_event(self):
   157     def precommit_event(self):
   162         session = self.session
   158         session = self.session
   163         for values in session.transaction_data.pop('check_constraints_op'):
   159         for values in self.get_data():
   164             eidfrom, rtype, eidto, constraints = values
   160             eidfrom, rtype, eidto, constraints = values
   165             # first check related entities have not been deleted in the same
   161             # first check related entities have not been deleted in the same
   166             # transaction
   162             # transaction
   167             if session.deleted_in_transaction(eidfrom):
   163             if session.deleted_in_transaction(eidfrom):
   168                 return
   164                 return
   194     def __call__(self):
   190     def __call__(self):
   195         # XXX get only RQL[Unique]Constraints?
   191         # XXX get only RQL[Unique]Constraints?
   196         constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   192         constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   197                                                 'constraints')
   193                                                 'constraints')
   198         if constraints:
   194         if constraints:
   199             hook.set_operation(self._cw, 'check_constraints_op',
   195             _CheckConstraintsOp.get_instance(self._cw).add_data(
   200                                (self.eidfrom, self.rtype, self.eidto, tuple(constraints)),
   196                 (self.eidfrom, self.rtype, self.eidto, constraints))
   201                                _CheckConstraintsOp, list)
       
   202 
   197 
   203 
   198 
   204 class CheckAttributeConstraintHook(IntegrityHook):
   199 class CheckAttributeConstraintHook(IntegrityHook):
   205     """check the attribute relation satisfy its constraints
   200     """check the attribute relation satisfy its constraints
   206 
   201 
   215         for attr in self.entity.cw_edited:
   210         for attr in self.entity.cw_edited:
   216             if eschema.subjrels[attr].final:
   211             if eschema.subjrels[attr].final:
   217                 constraints = [c for c in eschema.rdef(attr).constraints
   212                 constraints = [c for c in eschema.rdef(attr).constraints
   218                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
   213                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
   219                 if constraints:
   214                 if constraints:
   220                     hook.set_operation(self._cw, 'check_constraints_op',
   215                     _CheckConstraintsOp.get_instance(self._cw).add_data(
   221                                        (self.entity.eid, attr, None, tuple(constraints)),
   216                         (self.entity.eid, attr, None, constraints))
   222                                        _CheckConstraintsOp, list)
       
   223 
   217 
   224 
   218 
   225 class CheckUniqueHook(IntegrityHook):
   219 class CheckUniqueHook(IntegrityHook):
   226     __regid__ = 'checkunique'
   220     __regid__ = 'checkunique'
   227     events = ('before_add_entity', 'before_update_entity')
   221     events = ('before_add_entity', 'before_update_entity')
   295 
   289 
   296 
   290 
   297 # 'active' integrity hooks: you usually don't want to deactivate them, they are
   291 # 'active' integrity hooks: you usually don't want to deactivate them, they are
   298 # not really integrity check, they maintain consistency on changes
   292 # not really integrity check, they maintain consistency on changes
   299 
   293 
   300 class _DelayedDeleteOp(hook.Operation):
   294 class _DelayedDeleteOp(hook.DataOperationMixIn, hook.Operation):
   301     """delete the object of composite relation except if the relation has
   295     """delete the object of composite relation except if the relation has
   302     actually been redirected to another composite
   296     actually been redirected to another composite
   303     """
   297     """
   304     key = base_rql = None
   298     base_rql = None
   305 
   299 
   306     def precommit_event(self):
   300     def precommit_event(self):
   307         session = self.session
   301         session = self.session
   308         pendingeids = session.transaction_data.get('pendingeids', ())
   302         pendingeids = session.transaction_data.get('pendingeids', ())
   309         neweids = session.transaction_data.get('neweids', ())
   303         neweids = session.transaction_data.get('neweids', ())
   310         # poping key is not optional: if further operation trigger new deletion
   304         # poping key is not optional: if further operation trigger new deletion
   311         # of composite relation, we'll need a new operation
   305         # of composite relation, we'll need a new operation
   312         for eid, rtype in session.transaction_data.pop(self.key):
   306         for eid, rtype in self.get_data():
   313             # don't do anything if the entity is being created or deleted
   307             # don't do anything if the entity is being created or deleted
   314             if not (eid in pendingeids or eid in neweids):
   308             if not (eid in pendingeids or eid in neweids):
   315                 etype = session.describe(eid)[0]
   309                 etype = session.describe(eid)[0]
   316                 session.execute(self.base_rql % (etype, rtype), {'x': eid})
   310                 session.execute(self.base_rql % (etype, rtype), {'x': eid})
   317 
   311 
   318 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
   312 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
   319     """delete orphan subject entity of a composite relation"""
   313     """delete orphan subject entity of a composite relation"""
   320     key = '_cwiscomp'
       
   321     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
   314     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
   322 
   315 
   323 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
   316 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
   324     """check required object relation"""
   317     """check required object relation"""
   325     key = '_cwiocomp'
       
   326     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
   318     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
   327 
   319 
   328 
   320 
   329 class DeleteCompositeOrphanHook(hook.Hook):
   321 class DeleteCompositeOrphanHook(hook.Hook):
   330     """delete the composed of a composite relation when this relation is deleted
   322     """delete the composed of a composite relation when this relation is deleted
   341             self._cw.describe(self.eidto)[0]) in pendingrdefs:
   333             self._cw.describe(self.eidto)[0]) in pendingrdefs:
   342             return
   334             return
   343         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   335         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   344                                               'composite')
   336                                               'composite')
   345         if composite == 'subject':
   337         if composite == 'subject':
   346             set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype),
   338             _DelayedDeleteOEntityOp.get_instance(self._cw).add_data(
   347                           _DelayedDeleteOEntityOp)
   339                 (self.eidto, self.rtype))
   348         elif composite == 'object':
   340         elif composite == 'object':
   349             set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype),
   341             _DelayedDeleteSEntityOp.get_instance(self._cw).add_data(
   350                           _DelayedDeleteSEntityOp)
   342                 (self.eidfrom, self.rtype))