hooks/integrity.py
changeset 2835 04034421b072
child 2841 107ba1c45227
equal deleted inserted replaced
2834:7df3494ae657 2835:04034421b072
       
     1 """Core hooks: check for data integrity according to the instance'schema
       
     2 validity
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2009 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 cubicweb import ValidationError
       
    12 from cubicweb.selectors import entity_implements
       
    13 from cubicweb.server.hook import Hook
       
    14 from cubicweb.server.pool import LateOperation, PreCommitOperation
       
    15 from cubicweb.server.hookhelper import rproperty
       
    16 
       
    17 # special relations that don't have to be checked for integrity, usually
       
    18 # because they are handled internally by hooks (so we trust ourselves)
       
    19 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
       
    20                                 'is', 'is_instance_of',
       
    21                                 'wf_info_for', 'from_state', 'to_state'))
       
    22 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
       
    23                                 'wf_info_for', 'from_state', 'to_state'))
       
    24 
       
    25 
       
    26 class _CheckRequiredRelationOperation(LateOperation):
       
    27     """checking relation cardinality has to be done after commit in
       
    28     case the relation is being replaced
       
    29     """
       
    30     eid, rtype = None, None
       
    31 
       
    32     def precommit_event(self):
       
    33         # recheck pending eids
       
    34         if self.eid in self.session.transaction_data.get('pendingeids', ()):
       
    35             return
       
    36         if self.session.unsafe_execute(*self._rql()).rowcount < 1:
       
    37             etype = self.session.describe(self.eid)[0]
       
    38             _ = self.session._
       
    39             msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
       
    40             msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
       
    41             raise ValidationError(self.eid, {self.rtype: msg})
       
    42 
       
    43     def commit_event(self):
       
    44         pass
       
    45 
       
    46     def _rql(self):
       
    47         raise NotImplementedError()
       
    48 
       
    49 
       
    50 class _CheckSRelationOp(_CheckRequiredRelationOperation):
       
    51     """check required subject relation"""
       
    52     def _rql(self):
       
    53         return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
       
    54 
       
    55 
       
    56 class _CheckORelationOp(_CheckRequiredRelationOperation):
       
    57     """check required object relation"""
       
    58     def _rql(self):
       
    59         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
       
    60 
       
    61 
       
    62 class CheckCardinalityHook(Hook):
       
    63     """check cardinalities are satisfied"""
       
    64     __id__ = 'checkcard'
       
    65     category = 'integrity'
       
    66     events = ('after_add_entity', 'before_delete_relation')
       
    67 
       
    68     def __call__(self):
       
    69         getattr(self, self.event)()
       
    70 
       
    71     def checkrel_if_necessary(self, opcls, rtype, eid):
       
    72         """check an equivalent operation has not already been added"""
       
    73         for op in self.cw_req.pending_operations:
       
    74             if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
       
    75                 break
       
    76         else:
       
    77             opcls(self.cw_req, rtype=rtype, eid=eid)
       
    78 
       
    79     def after_add_entity(self):
       
    80         eid = self.entity.eid
       
    81         eschema = self.entity.e_schema
       
    82         for rschema, targetschemas, x in eschema.relation_definitions():
       
    83             # skip automatically handled relations
       
    84             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
       
    85                 continue
       
    86             if x == 'subject':
       
    87                 subjtype = eschema
       
    88                 objtype = targetschemas[0].type
       
    89                 cardindex = 0
       
    90                 opcls = _CheckSRelationOp
       
    91             else:
       
    92                 subjtype = targetschemas[0].type
       
    93                 objtype = eschema
       
    94                 cardindex = 1
       
    95                 opcls = _CheckORelationOp
       
    96             card = rschema.rproperty(subjtype, objtype, 'cardinality')
       
    97             if card[cardindex] in '1+':
       
    98                 self.checkrel_if_necessary(opcls, rschema.type, eid)
       
    99 
       
   100     def before_delete_relation(self):
       
   101         rtype = self.rtype
       
   102         if rtype in DONT_CHECK_RTYPES_ON_DEL:
       
   103             return
       
   104         session = self.cw_req
       
   105         eidfrom, eidto = self.eidfrom, self.eidto
       
   106         card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
       
   107         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
       
   108         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
       
   109             return
       
   110         pendingeids = session.transaction_data.get('pendingeids', ())
       
   111         if card[0] in '1+' and not eidfrom in pendingeids:
       
   112             self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
       
   113         if card[1] in '1+' and not eidto in pendingeids:
       
   114             self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
       
   115 
       
   116 
       
   117 class _CheckConstraintsOp(LateOperation):
       
   118     """check a new relation satisfy its constraints
       
   119     """
       
   120     def precommit_event(self):
       
   121         eidfrom, rtype, eidto = self.rdef
       
   122         # first check related entities have not been deleted in the same
       
   123         # transaction
       
   124         pending = self.session.transaction_data.get('pendingeids', ())
       
   125         if eidfrom in pending:
       
   126             return
       
   127         if eidto in pending:
       
   128             return
       
   129         for constraint in self.constraints:
       
   130             try:
       
   131                 constraint.repo_check(self.session, eidfrom, rtype, eidto)
       
   132             except NotImplementedError:
       
   133                 self.critical('can\'t check constraint %s, not supported',
       
   134                               constraint)
       
   135 
       
   136     def commit_event(self):
       
   137         pass
       
   138 
       
   139 
       
   140 class CheckConstraintHook(Hook):
       
   141     """check the relation satisfy its constraints
       
   142 
       
   143     this is delayed to a precommit time operation since other relation which
       
   144     will make constraint satisfied may be added later.
       
   145     """
       
   146     __id__ = 'checkconstraint'
       
   147     category = 'integrity'
       
   148     events = ('after_add_relation',)
       
   149     def __call__(self):
       
   150         constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
       
   151                                 'constraints')
       
   152         if constraints:
       
   153             _CheckConstraintsOp(self.cw_req, constraints=constraints,
       
   154                                rdef=(self.eidfrom, self.rtype, self.eidto))
       
   155 
       
   156 class CheckUniqueHook(Hook):
       
   157     __id__ = 'checkunique'
       
   158     category = 'integrity'
       
   159     events = ('before_add_entity', 'before_update_entity')
       
   160 
       
   161     def __call__(self):
       
   162         entity = self.entity
       
   163         eschema = entity.e_schema
       
   164         for attr in entity.edited_attributes:
       
   165             val = entity[attr]
       
   166             if val is None:
       
   167                 continue
       
   168             if eschema.subject_relation(attr).is_final() and \
       
   169                    eschema.has_unique_values(attr):
       
   170                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
       
   171                 rset = self.cw_req.unsafe_execute(rql, {'val': val})
       
   172                 if rset and rset[0][0] != entity.eid:
       
   173                     msg = self.cw_req._('the value "%s" is already used, use another one')
       
   174                     raise ValidationError(entity.eid, {attr: msg % val})
       
   175 
       
   176 
       
   177 class _DelayedDeleteOp(PreCommitOperation):
       
   178     """delete the object of composite relation except if the relation
       
   179     has actually been redirected to another composite
       
   180     """
       
   181 
       
   182     def precommit_event(self):
       
   183         session = self.session
       
   184         # don't do anything if the entity is being created or deleted
       
   185         if not (self.eid in session.transaction_data.get('pendingeids', ()) or
       
   186                 self.eid in session.transaction_data.get('neweids', ())):
       
   187             etype = session.describe(self.eid)[0]
       
   188             session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
       
   189                                    % (etype, self.relation),
       
   190                                    {'x': self.eid}, 'x')
       
   191 
       
   192 
       
   193 class DeleteCompositeOrphanHook(Hook):
       
   194     """delete the composed of a composite relation when this relation is deleted
       
   195     """
       
   196     __id__ = 'deletecomposite'
       
   197     category = 'integrity'
       
   198     events = ('before_delete_relation',)
       
   199     def __call__(self):
       
   200         composite = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
       
   201                               'composite')
       
   202         if composite == 'subject':
       
   203             _DelayedDeleteOp(self.cw_req, eid=self.eidto,
       
   204                              relation='Y %s X' % self.rtype)
       
   205         elif composite == 'object':
       
   206             _DelayedDeleteOp(self.cw_req, eid=self.eidfrom,
       
   207                              relation='X %s Y' % self.rtype)
       
   208 
       
   209 
       
   210 class DontRemoveOwnersGroupHook(Hook):
       
   211     """delete the composed of a composite relation when this relation is deleted
       
   212     """
       
   213     __id__ = 'checkownersgroup'
       
   214     __select__ = Hook.__select__ & entity_implements('CWGroup')
       
   215     category = 'integrity'
       
   216     events = ('before_delete_entity', 'before_update_entity')
       
   217 
       
   218     def __call__(self):
       
   219         if self.event == 'before_delete_entity' and self.entity.name == 'owners':
       
   220             raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
       
   221         elif self.event == 'before_update_entity' and 'name' in self.entity.edited_attribute:
       
   222             newname = self.entity.pop('name')
       
   223             oldname = self.entity.name
       
   224             if oldname == 'owners' and newname != oldname:
       
   225                 raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')})
       
   226             self.entity['name'] = newname
       
   227 
       
   228