hooks/integrity.py
branchstable
changeset 5060 ee3b856e1406
parent 5030 5238d9a8dfee
child 5174 78438ad513ca
child 5421 8167de96c523
equal deleted inserted replaced
5059:1d5c81588144 5060:ee3b856e1406
    15 from cubicweb import ValidationError
    15 from cubicweb import ValidationError
    16 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
    16 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
    17 from cubicweb.selectors import implements
    17 from cubicweb.selectors import implements
    18 from cubicweb.uilib import soup2xhtml
    18 from cubicweb.uilib import soup2xhtml
    19 from cubicweb.server import hook
    19 from cubicweb.server import hook
       
    20 from cubicweb.server.hook import set_operation
    20 
    21 
    21 # special relations that don't have to be checked for integrity, usually
    22 # 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 # because they are handled internally by hooks (so we trust ourselves)
    23 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
    24 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
    24                                 'is', 'is_instance_of',
    25                                 'is', 'is_instance_of',
    60 
    61 
    61 class _CheckRequiredRelationOperation(hook.LateOperation):
    62 class _CheckRequiredRelationOperation(hook.LateOperation):
    62     """checking relation cardinality has to be done after commit in
    63     """checking relation cardinality has to be done after commit in
    63     case the relation is being replaced
    64     case the relation is being replaced
    64     """
    65     """
    65     eid, rtype = None, None
    66     role = key = base_rql = None
    66 
    67 
    67     def precommit_event(self):
    68     def precommit_event(self):
    68         # recheck pending eids
    69         session =self.session
    69         if self.session.deleted_in_transaction(self.eid):
    70         pendingeids = session.transaction_data.get('pendingeids', ())
    70             return
    71         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
    71         if self.rtype in self.session.transaction_data.get('pendingrtypes', ()):
    72         # poping key is not optional: if further operation trigger new deletion
    72             return
    73         # of relation, we'll need a new operation
    73         if self.session.execute(*self._rql()).rowcount < 1:
    74         for eid, rtype in session.transaction_data.pop(self.key):
    74             etype = self.session.describe(self.eid)[0]
    75             # recheck pending eids / relation types
    75             _ = self.session._
    76             if eid in pendingeids:
    76             msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
    77                 continue
    77             msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
    78             if rtype in pendingrtypes:
    78             qname = role_name(self.rtype, self.role)
    79                 continue
    79             raise ValidationError(self.eid, {qname: msg})
    80             if not session.execute(self.base_rql % rtype, {'x': eid}, 'x'):
    80 
    81                 etype = session.describe(eid)[0]
    81     def commit_event(self):
    82                 _ = session._
    82         pass
    83                 msg = _('at least one relation %(rtype)s is required on '
    83 
    84                         '%(etype)s (%(eid)s)')
    84     def _rql(self):
    85                 msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid}
    85         raise NotImplementedError()
    86                 raise ValidationError(eid, {role_name(rtype, self.role): msg})
    86 
    87 
    87 
    88 
    88 class _CheckSRelationOp(_CheckRequiredRelationOperation):
    89 class _CheckSRelationOp(_CheckRequiredRelationOperation):
    89     """check required subject relation"""
    90     """check required subject relation"""
    90     role = 'subject'
    91     role = 'subject'
    91     def _rql(self):
    92     key = '_cwisrel'
    92         return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
    93     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
    93 
       
    94 
    94 
    95 class _CheckORelationOp(_CheckRequiredRelationOperation):
    95 class _CheckORelationOp(_CheckRequiredRelationOperation):
    96     """check required object relation"""
    96     """check required object relation"""
    97     role = 'object'
    97     role = 'object'
    98     def _rql(self):
    98     key = '_cwiorel'
    99         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
    99     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
   100 
   100 
   101 
   101 
   102 class IntegrityHook(hook.Hook):
   102 class IntegrityHook(hook.Hook):
   103     __abstract__ = True
   103     __abstract__ = True
   104     category = 'integrity'
   104     category = 'integrity'
   109     __regid__ = 'checkcard'
   109     __regid__ = 'checkcard'
   110     events = ('after_add_entity', 'before_delete_relation')
   110     events = ('after_add_entity', 'before_delete_relation')
   111 
   111 
   112     def __call__(self):
   112     def __call__(self):
   113         getattr(self, self.event)()
   113         getattr(self, self.event)()
   114 
       
   115     def checkrel_if_necessary(self, opcls, rtype, eid):
       
   116         """check an equivalent operation has not already been added"""
       
   117         for op in self._cw.pending_operations:
       
   118             if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
       
   119                 break
       
   120         else:
       
   121             opcls(self._cw, rtype=rtype, eid=eid)
       
   122 
   114 
   123     def after_add_entity(self):
   115     def after_add_entity(self):
   124         eid = self.entity.eid
   116         eid = self.entity.eid
   125         eschema = self.entity.e_schema
   117         eschema = self.entity.e_schema
   126         for rschema, targetschemas, role in eschema.relation_definitions():
   118         for rschema, targetschemas, role in eschema.relation_definitions():
   127             # skip automatically handled relations
   119             # skip automatically handled relations
   128             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
   120             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
   129                 continue
   121                 continue
   130             opcls = role == 'subject' and _CheckSRelationOp or _CheckORelationOp
       
   131             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
   122             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
   132             if rdef.role_cardinality(role) in '1+':
   123             if rdef.role_cardinality(role) in '1+':
   133                 self.checkrel_if_necessary(opcls, rschema.type, eid)
   124                 if role == 'subject':
       
   125                     set_operation(self._cw, '_cwisrel', (eid, rschema.type),
       
   126                                   _CheckSRelationOp)
       
   127                 else:
       
   128                     set_operation(self._cw, '_cwiorel', (eid, rschema.type),
       
   129                                   _CheckORelationOp)
   134 
   130 
   135     def before_delete_relation(self):
   131     def before_delete_relation(self):
   136         rtype = self.rtype
   132         rtype = self.rtype
   137         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   133         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   138             return
   134             return
   139         session = self._cw
   135         session = self._cw
   140         eidfrom, eidto = self.eidfrom, self.eidto
   136         eidfrom, eidto = self.eidfrom, self.eidto
   141         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
       
   142         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   137         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   143         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   138         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   144             return
   139             return
       
   140         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
   145         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   141         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   146             self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
   142             set_operation(self._cw, '_cwisrel', (eidfrom, rtype),
       
   143                           _CheckSRelationOp)
   147         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   144         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   148             self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
   145             set_operation(self._cw, '_cwiorel', (eidto, rtype),
       
   146                           _CheckORelationOp)
   149 
   147 
   150 
   148 
   151 class _CheckConstraintsOp(hook.LateOperation):
   149 class _CheckConstraintsOp(hook.LateOperation):
   152     """check a new relation satisfy its constraints
   150     """check a new relation satisfy its constraints
   153     """
   151     """
   289 
   287 
   290 # 'active' integrity hooks: you usually don't want to deactivate them, they are
   288 # 'active' integrity hooks: you usually don't want to deactivate them, they are
   291 # not really integrity check, they maintain consistency on changes
   289 # not really integrity check, they maintain consistency on changes
   292 
   290 
   293 class _DelayedDeleteOp(hook.Operation):
   291 class _DelayedDeleteOp(hook.Operation):
   294     """delete the object of composite relation except if the relation
   292     """delete the object of composite relation except if the relation has
   295     has actually been redirected to another composite
   293     actually been redirected to another composite
   296     """
   294     """
       
   295     key = base_rql = None
   297 
   296 
   298     def precommit_event(self):
   297     def precommit_event(self):
   299         session = self.session
   298         session = self.session
   300         # don't do anything if the entity is being created or deleted
   299         pendingeids = session.transaction_data.get('pendingeids', ())
   301         if not (session.deleted_in_transaction(self.eid) or
   300         neweids = session.transaction_data.get('neweids', ())
   302                 session.added_in_transaction(self.eid)):
   301         # poping key is not optional: if further operation trigger new deletion
   303             etype = session.describe(self.eid)[0]
   302         # of composite relation, we'll need a new operation
   304             session.execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
   303         for eid, rtype in session.transaction_data.pop(self.key):
   305                             % (etype, self.relation),
   304             # don't do anything if the entity is being created or deleted
   306                             {'x': self.eid}, 'x')
   305             if not (eid in pendingeids or eid in neweids):
       
   306                 etype = session.describe(eid)[0]
       
   307                 session.execute(self.base_rql % (etype, rtype), {'x': eid}, 'x')
       
   308 
       
   309 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
       
   310     """delete orphan subject entity of a composite relation"""
       
   311     key = '_cwiscomp'
       
   312     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
       
   313 
       
   314 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
       
   315     """check required object relation"""
       
   316     key = '_cwiocomp'
       
   317     base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
   307 
   318 
   308 
   319 
   309 class DeleteCompositeOrphanHook(hook.Hook):
   320 class DeleteCompositeOrphanHook(hook.Hook):
   310     """delete the composed of a composite relation when this relation is deleted
   321     """delete the composed of a composite relation when this relation is deleted
   311     """
   322     """
   321             self._cw.describe(self.eidto)[0]) in pendingrdefs:
   332             self._cw.describe(self.eidto)[0]) in pendingrdefs:
   322             return
   333             return
   323         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   334         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   324                                               'composite')
   335                                               'composite')
   325         if composite == 'subject':
   336         if composite == 'subject':
   326             _DelayedDeleteOp(self._cw, eid=self.eidto,
   337             set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype),
   327                              relation='Y %s X' % self.rtype)
   338                           _DelayedDeleteOEntityOp)
   328         elif composite == 'object':
   339         elif composite == 'object':
   329             _DelayedDeleteOp(self._cw, eid=self.eidfrom,
   340             set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype),
   330                              relation='X %s Y' % self.rtype)
   341                           _DelayedDeleteSEntityOp)