hooks/integrity.py
brancholdstable
changeset 7074 e4580e5f0703
parent 6894 ba3f7e655414
child 6957 ffda12be2e9f
equal deleted inserted replaced
6749:48f468f33704 7074:e4580e5f0703
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Core hooks: check for data integrity according to the instance'schema
    18 """Core hooks: check for data integrity according to the instance'schema
    19 validity
    19 validity
    20 
       
    21 """
    20 """
       
    21 
    22 __docformat__ = "restructuredtext en"
    22 __docformat__ = "restructuredtext en"
    23 
    23 
    24 from threading import Lock
    24 from threading import Lock
    25 
    25 
    26 from yams.schema import role_name
    26 from yams.schema import role_name
    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
    60     if 'uniquecstrholder' in session.transaction_data:
    59     if 'uniquecstrholder' in session.transaction_data:
    61         del session.transaction_data['uniquecstrholder']
    60         del session.transaction_data['uniquecstrholder']
    62         _UNIQUE_CONSTRAINTS_LOCK.release()
    61         _UNIQUE_CONSTRAINTS_LOCK.release()
    63 
    62 
    64 class _ReleaseUniqueConstraintsOperation(hook.Operation):
    63 class _ReleaseUniqueConstraintsOperation(hook.Operation):
    65     def commit_event(self):
       
    66         pass
       
    67     def postcommit_event(self):
    64     def postcommit_event(self):
    68         _release_unique_cstr_lock(self.session)
    65         _release_unique_cstr_lock(self.session)
    69     def rollback_event(self):
    66     def rollback_event(self):
    70         _release_unique_cstr_lock(self.session)
    67         _release_unique_cstr_lock(self.session)
    71 
    68 
    72 
    69 
    73 class _CheckRequiredRelationOperation(hook.LateOperation):
    70 class _CheckRequiredRelationOperation(hook.DataOperationMixIn,
    74     """checking relation cardinality has to be done after commit in
    71                                       hook.LateOperation):
    75     case the relation is being replaced
    72     """checking relation cardinality has to be done after commit in case the
    76     """
    73     relation is being replaced
       
    74     """
       
    75     containercls = list
    77     role = key = base_rql = None
    76     role = key = base_rql = None
    78 
    77 
    79     def precommit_event(self):
    78     def precommit_event(self):
    80         session =self.session
    79         session = self.session
    81         pendingeids = session.transaction_data.get('pendingeids', ())
    80         pendingeids = session.transaction_data.get('pendingeids', ())
    82         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
    81         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
    83         # poping key is not optional: if further operation trigger new deletion
    82         for eid, rtype in self.get_data():
    84         # of relation, we'll need a new operation
       
    85         for eid, rtype in session.transaction_data.pop(self.key):
       
    86             # recheck pending eids / relation types
    83             # recheck pending eids / relation types
    87             if eid in pendingeids:
    84             if eid in pendingeids:
    88                 continue
    85                 continue
    89             if rtype in pendingrtypes:
    86             if rtype in pendingrtypes:
    90                 continue
    87                 continue
    98 
    95 
    99 
    96 
   100 class _CheckSRelationOp(_CheckRequiredRelationOperation):
    97 class _CheckSRelationOp(_CheckRequiredRelationOperation):
   101     """check required subject relation"""
    98     """check required subject relation"""
   102     role = 'subject'
    99     role = 'subject'
   103     key = '_cwisrel'
       
   104     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
   100     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
   105 
   101 
   106 class _CheckORelationOp(_CheckRequiredRelationOperation):
   102 class _CheckORelationOp(_CheckRequiredRelationOperation):
   107     """check required object relation"""
   103     """check required object relation"""
   108     role = 'object'
   104     role = 'object'
   109     key = '_cwiorel'
       
   110     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
   105     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
   111 
   106 
   112 
   107 
   113 class IntegrityHook(hook.Hook):
   108 class IntegrityHook(hook.Hook):
   114     __abstract__ = True
   109     __abstract__ = True
   115     category = 'integrity'
   110     category = 'integrity'
   116 
   111 
   117 
   112 
   118 class CheckCardinalityHook(IntegrityHook):
   113 class CheckCardinalityHookBeforeDeleteRelation(IntegrityHook):
   119     """check cardinalities are satisfied"""
   114     """check cardinalities are satisfied"""
   120     __regid__ = 'checkcard'
   115     __regid__ = 'checkcard_before_delete_relation'
   121     events = ('after_add_entity', 'before_delete_relation')
   116     events = ('before_delete_relation',)
   122 
   117 
   123     def __call__(self):
   118     def __call__(self):
   124         getattr(self, self.event)()
   119         rtype = self.rtype
   125 
   120         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   126     def after_add_entity(self):
   121             return
       
   122         session = self._cw
       
   123         eidfrom, eidto = self.eidfrom, self.eidto
       
   124         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
       
   125         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
       
   126             return
       
   127         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
       
   128         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
       
   129             _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
       
   130         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
       
   131             _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
       
   132 
       
   133 class CheckCardinalityHookAfterAddEntity(IntegrityHook):
       
   134     """check cardinalities are satisfied"""
       
   135     __regid__ = 'checkcard_after_add_entity'
       
   136     events = ('after_add_entity',)
       
   137 
       
   138     def __call__(self):
   127         eid = self.entity.eid
   139         eid = self.entity.eid
   128         eschema = self.entity.e_schema
   140         eschema = self.entity.e_schema
   129         for rschema, targetschemas, role in eschema.relation_definitions():
   141         for rschema, targetschemas, role in eschema.relation_definitions():
   130             # skip automatically handled relations
   142             # skip automatically handled relations
   131             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
   143             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
   132                 continue
   144                 continue
   133             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
   145             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
   134             if rdef.role_cardinality(role) in '1+':
   146             if rdef.role_cardinality(role) in '1+':
   135                 if role == 'subject':
   147                 if role == 'subject':
   136                     set_operation(self._cw, '_cwisrel', (eid, rschema.type),
   148                     op = _CheckSRelationOp.get_instance(self._cw)
   137                                   _CheckSRelationOp, list)
       
   138                 else:
   149                 else:
   139                     set_operation(self._cw, '_cwiorel', (eid, rschema.type),
   150                     op = _CheckORelationOp.get_instance(self._cw)
   140                                   _CheckORelationOp, list)
   151                 op.add_data((eid, rschema.type))
   141 
   152 
   142     def before_delete_relation(self):
   153     def before_delete_relation(self):
   143         rtype = self.rtype
   154         rtype = self.rtype
   144         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   155         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   145             return
   156             return
   148         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   159         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   149         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   160         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   150             return
   161             return
   151         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
   162         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
   152         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   163         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   153             set_operation(self._cw, '_cwisrel', (eidfrom, rtype),
   164             _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
   154                           _CheckSRelationOp, list)
       
   155         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   165         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   156             set_operation(self._cw, '_cwiorel', (eidto, rtype),
   166             _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
   157                           _CheckORelationOp, list)
   167 
   158 
   168 
   159 
   169 class _CheckConstraintsOp(hook.DataOperationMixIn, hook.LateOperation):
   160 class _CheckConstraintsOp(hook.LateOperation):
       
   161     """ check a new relation satisfy its constraints """
   170     """ check a new relation satisfy its constraints """
   162 
   171     containercls = list
   163     def precommit_event(self):
   172     def precommit_event(self):
   164         session = self.session
   173         session = self.session
   165         for values in session.transaction_data.pop('check_constraints_op'):
   174         for values in self.get_data():
   166             eidfrom, rtype, eidto, constraints = values
   175             eidfrom, rtype, eidto, constraints = values
   167             # first check related entities have not been deleted in the same
   176             # first check related entities have not been deleted in the same
   168             # transaction
   177             # transaction
   169             if session.deleted_in_transaction(eidfrom):
   178             if session.deleted_in_transaction(eidfrom):
   170                 return
   179                 continue
   171             if session.deleted_in_transaction(eidto):
   180             if session.deleted_in_transaction(eidto):
   172                 return
   181                 continue
   173             for constraint in constraints:
   182             for constraint in constraints:
   174                 # XXX
   183                 # XXX
   175                 # * lock RQLConstraint as well?
   184                 # * lock RQLConstraint as well?
   176                 # * use a constraint id to use per constraint lock and avoid
   185                 # * use a constraint id to use per constraint lock and avoid
   177                 #   unnecessary commit serialization ?
   186                 #   unnecessary commit serialization ?
   181                     constraint.repo_check(session, eidfrom, rtype, eidto)
   190                     constraint.repo_check(session, eidfrom, rtype, eidto)
   182                 except NotImplementedError:
   191                 except NotImplementedError:
   183                     self.critical('can\'t check constraint %s, not supported',
   192                     self.critical('can\'t check constraint %s, not supported',
   184                                   constraint)
   193                                   constraint)
   185 
   194 
   186     def commit_event(self):
       
   187         pass
       
   188 
       
   189 
   195 
   190 class CheckConstraintHook(IntegrityHook):
   196 class CheckConstraintHook(IntegrityHook):
   191     """check the relation satisfy its constraints
   197     """check the relation satisfy its constraints
   192 
   198 
   193     this is delayed to a precommit time operation since other relation which
   199     this is delayed to a precommit time operation since other relation which
   199     def __call__(self):
   205     def __call__(self):
   200         # XXX get only RQL[Unique]Constraints?
   206         # XXX get only RQL[Unique]Constraints?
   201         constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   207         constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   202                                                 'constraints')
   208                                                 'constraints')
   203         if constraints:
   209         if constraints:
   204             hook.set_operation(self._cw, 'check_constraints_op',
   210             _CheckConstraintsOp.get_instance(self._cw).add_data(
   205                                (self.eidfrom, self.rtype, self.eidto, tuple(constraints)),
   211                 (self.eidfrom, self.rtype, self.eidto, constraints))
   206                                _CheckConstraintsOp, list)
       
   207 
   212 
   208 
   213 
   209 class CheckAttributeConstraintHook(IntegrityHook):
   214 class CheckAttributeConstraintHook(IntegrityHook):
   210     """check the attribute relation satisfy its constraints
   215     """check the attribute relation satisfy its constraints
   211 
   216 
   215     __regid__ = 'checkattrconstraint'
   220     __regid__ = 'checkattrconstraint'
   216     events = ('after_add_entity', 'after_update_entity')
   221     events = ('after_add_entity', 'after_update_entity')
   217 
   222 
   218     def __call__(self):
   223     def __call__(self):
   219         eschema = self.entity.e_schema
   224         eschema = self.entity.e_schema
   220         for attr in self.entity.edited_attributes:
   225         for attr in self.entity.cw_edited:
   221             if eschema.subjrels[attr].final:
   226             if eschema.subjrels[attr].final:
   222                 constraints = [c for c in eschema.rdef(attr).constraints
   227                 constraints = [c for c in eschema.rdef(attr).constraints
   223                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
   228                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
   224                 if constraints:
   229                 if constraints:
   225                     hook.set_operation(self._cw, 'check_constraints_op',
   230                     _CheckConstraintsOp.get_instance(self._cw).add_data(
   226                                        (self.entity.eid, attr, None, tuple(constraints)),
   231                         (self.entity.eid, attr, None, constraints))
   227                                        _CheckConstraintsOp, list)
       
   228 
   232 
   229 
   233 
   230 class CheckUniqueHook(IntegrityHook):
   234 class CheckUniqueHook(IntegrityHook):
   231     __regid__ = 'checkunique'
   235     __regid__ = 'checkunique'
   232     events = ('before_add_entity', 'before_update_entity')
   236     events = ('before_add_entity', 'before_update_entity')
   233 
   237 
   234     def __call__(self):
   238     def __call__(self):
   235         entity = self.entity
   239         entity = self.entity
   236         eschema = entity.e_schema
   240         eschema = entity.e_schema
   237         for attr in entity.edited_attributes:
   241         for attr, val in entity.cw_edited.iteritems():
   238             if eschema.subjrels[attr].final and eschema.has_unique_values(attr):
   242             if eschema.subjrels[attr].final and eschema.has_unique_values(attr):
   239                 val = entity[attr]
       
   240                 if val is None:
   243                 if val is None:
   241                     continue
   244                     continue
   242                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
   245                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
   243                 rset = self._cw.execute(rql, {'val': val})
   246                 rset = self._cw.execute(rql, {'val': val})
   244                 if rset and rset[0][0] != entity.eid:
   247                 if rset and rset[0][0] != entity.eid:
   253     __regid__ = 'checkownersgroup'
   256     __regid__ = 'checkownersgroup'
   254     __select__ = IntegrityHook.__select__ & is_instance('CWGroup')
   257     __select__ = IntegrityHook.__select__ & is_instance('CWGroup')
   255     events = ('before_delete_entity', 'before_update_entity')
   258     events = ('before_delete_entity', 'before_update_entity')
   256 
   259 
   257     def __call__(self):
   260     def __call__(self):
   258         if self.event == 'before_delete_entity' and self.entity.name == 'owners':
   261         entity = self.entity
       
   262         if self.event == 'before_delete_entity' and entity.name == 'owners':
   259             msg = self._cw._('can\'t be deleted')
   263             msg = self._cw._('can\'t be deleted')
   260             raise ValidationError(self.entity.eid, {None: msg})
   264             raise ValidationError(entity.eid, {None: msg})
   261         elif self.event == 'before_update_entity' and \
   265         elif self.event == 'before_update_entity' \
   262                  'name' in self.entity.edited_attributes:
   266                  and 'name' in entity.cw_edited:
   263             newname = self.entity.pop('name')
   267             oldname, newname = entity.cw_edited.oldnewvalue('name')
   264             oldname = self.entity.name
       
   265             if oldname == 'owners' and newname != oldname:
   268             if oldname == 'owners' and newname != oldname:
   266                 qname = role_name('name', 'subject')
   269                 qname = role_name('name', 'subject')
   267                 msg = self._cw._('can\'t be changed')
   270                 msg = self._cw._('can\'t be changed')
   268                 raise ValidationError(self.entity.eid, {qname: msg})
   271                 raise ValidationError(entity.eid, {qname: msg})
   269             self.entity['name'] = newname
       
   270 
   272 
   271 
   273 
   272 class TidyHtmlFields(IntegrityHook):
   274 class TidyHtmlFields(IntegrityHook):
   273     """tidy HTML in rich text strings"""
   275     """tidy HTML in rich text strings"""
   274     __regid__ = 'htmltidy'
   276     __regid__ = 'htmltidy'
   275     events = ('before_add_entity', 'before_update_entity')
   277     events = ('before_add_entity', 'before_update_entity')
   276 
   278 
   277     def __call__(self):
   279     def __call__(self):
   278         entity = self.entity
   280         entity = self.entity
   279         metaattrs = entity.e_schema.meta_attributes()
   281         metaattrs = entity.e_schema.meta_attributes()
       
   282         edited = entity.cw_edited
   280         for metaattr, (metadata, attr) in metaattrs.iteritems():
   283         for metaattr, (metadata, attr) in metaattrs.iteritems():
   281             if metadata == 'format' and attr in entity.edited_attributes:
   284             if metadata == 'format' and attr in edited:
   282                 try:
   285                 try:
   283                     value = entity[attr]
   286                     value = edited[attr]
   284                 except KeyError:
   287                 except KeyError:
   285                     continue # no text to tidy
   288                     continue # no text to tidy
   286                 if isinstance(value, unicode): # filter out None and Binary
   289                 if isinstance(value, unicode): # filter out None and Binary
   287                     if getattr(entity, str(metaattr)) == 'text/html':
   290                     if getattr(entity, str(metaattr)) == 'text/html':
   288                         entity[attr] = soup2xhtml(value, self._cw.encoding)
   291                         edited[attr] = soup2xhtml(value, self._cw.encoding)
   289 
   292 
   290 
   293 
   291 class StripCWUserLoginHook(IntegrityHook):
   294 class StripCWUserLoginHook(IntegrityHook):
   292     """ensure user logins are stripped"""
   295     """ensure user logins are stripped"""
   293     __regid__ = 'stripuserlogin'
   296     __regid__ = 'stripuserlogin'
   294     __select__ = IntegrityHook.__select__ & is_instance('CWUser')
   297     __select__ = IntegrityHook.__select__ & is_instance('CWUser')
   295     events = ('before_add_entity', 'before_update_entity',)
   298     events = ('before_add_entity', 'before_update_entity',)
   296 
   299 
   297     def __call__(self):
   300     def __call__(self):
   298         user = self.entity
   301         login = self.entity.cw_edited.get('login')
   299         if 'login' in user.edited_attributes and user.login:
   302         if login:
   300             user.login = user.login.strip()
   303             self.entity.cw_edited['login'] = login.strip()
   301 
   304 
   302 
   305 
   303 # 'active' integrity hooks: you usually don't want to deactivate them, they are
   306 # 'active' integrity hooks: you usually don't want to deactivate them, they are
   304 # not really integrity check, they maintain consistency on changes
   307 # not really integrity check, they maintain consistency on changes
   305 
   308 
   306 class _DelayedDeleteOp(hook.Operation):
   309 class _DelayedDeleteOp(hook.DataOperationMixIn, hook.Operation):
   307     """delete the object of composite relation except if the relation has
   310     """delete the object of composite relation except if the relation has
   308     actually been redirected to another composite
   311     actually been redirected to another composite
   309     """
   312     """
   310     key = base_rql = None
   313     base_rql = None
   311 
   314 
   312     def precommit_event(self):
   315     def precommit_event(self):
   313         session = self.session
   316         session = self.session
   314         pendingeids = session.transaction_data.get('pendingeids', ())
   317         pendingeids = session.transaction_data.get('pendingeids', ())
   315         neweids = session.transaction_data.get('neweids', ())
   318         neweids = session.transaction_data.get('neweids', ())
   316         # poping key is not optional: if further operation trigger new deletion
   319         eids_by_etype_rtype = {}
   317         # of composite relation, we'll need a new operation
   320         for eid, rtype in self.get_data():
   318         for eid, rtype in session.transaction_data.pop(self.key):
       
   319             # don't do anything if the entity is being created or deleted
   321             # don't do anything if the entity is being created or deleted
   320             if not (eid in pendingeids or eid in neweids):
   322             if not (eid in pendingeids or eid in neweids):
   321                 etype = session.describe(eid)[0]
   323                 etype = session.describe(eid)[0]
   322                 session.execute(self.base_rql % (etype, rtype), {'x': eid})
   324                 key = (etype, rtype)
       
   325                 if key not in eids_by_etype_rtype:
       
   326                     eids_by_etype_rtype[key] = [str(eid)]
       
   327                 else:
       
   328                     eids_by_etype_rtype[key].append(str(eid))
       
   329         for (etype, rtype), eids in eids_by_etype_rtype.iteritems():
       
   330             # quite unexpectedly, not deleting too many entities at a time in
       
   331             # this operation benefits to the exec speed (possibly on the RQL
       
   332             # parsing side)
       
   333             start = 0
       
   334             incr = 500
       
   335             while start < len(eids):
       
   336                 session.execute(self.base_rql % (etype, ','.join(eids[start:start+incr]), rtype))
       
   337                 start += incr
   323 
   338 
   324 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
   339 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
   325     """delete orphan subject entity of a composite relation"""
   340     """delete orphan subject entity of a composite relation"""
   326     key = '_cwiscomp'
   341     base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT X %s Y'
   327     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
       
   328 
   342 
   329 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
   343 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
   330     """check required object relation"""
   344     """check required object relation"""
   331     key = '_cwiocomp'
   345     base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT Y %s X'
   332     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
       
   333 
   346 
   334 
   347 
   335 class DeleteCompositeOrphanHook(hook.Hook):
   348 class DeleteCompositeOrphanHook(hook.Hook):
   336     """delete the composed of a composite relation when this relation is deleted
   349     """delete the composed of a composite relation when this relation is deleted
   337     """
   350     """
   347             self._cw.describe(self.eidto)[0]) in pendingrdefs:
   360             self._cw.describe(self.eidto)[0]) in pendingrdefs:
   348             return
   361             return
   349         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   362         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   350                                               'composite')
   363                                               'composite')
   351         if composite == 'subject':
   364         if composite == 'subject':
   352             set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype),
   365             _DelayedDeleteOEntityOp.get_instance(self._cw).add_data(
   353                           _DelayedDeleteOEntityOp)
   366                 (self.eidto, self.rtype))
   354         elif composite == 'object':
   367         elif composite == 'object':
   355             set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype),
   368             _DelayedDeleteSEntityOp.get_instance(self._cw).add_data(
   356                           _DelayedDeleteSEntityOp)
   369                 (self.eidfrom, self.rtype))