hooks/integrity.py
changeset 2841 107ba1c45227
parent 2835 04034421b072
child 2847 c2ee28f4d4b1
equal deleted inserted replaced
2840:06daf13195d4 2841:107ba1c45227
     8 """
     8 """
     9 __docformat__ = "restructuredtext en"
     9 __docformat__ = "restructuredtext en"
    10 
    10 
    11 from cubicweb import ValidationError
    11 from cubicweb import ValidationError
    12 from cubicweb.selectors import entity_implements
    12 from cubicweb.selectors import entity_implements
    13 from cubicweb.server.hook import Hook
    13 from cubicweb.common.uilib import soup2xhtml
       
    14 from cubicweb.server import hook
    14 from cubicweb.server.pool import LateOperation, PreCommitOperation
    15 from cubicweb.server.pool import LateOperation, PreCommitOperation
    15 from cubicweb.server.hookhelper import rproperty
       
    16 
    16 
    17 # special relations that don't have to be checked for integrity, usually
    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)
    18 # because they are handled internally by hooks (so we trust ourselves)
    19 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
    19 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
    20                                 'is', 'is_instance_of',
    20                                 'is', 'is_instance_of',
    21                                 'wf_info_for', 'from_state', 'to_state'))
    21                                 'wf_info_for', 'from_state', 'to_state'))
    22 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
    22 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
    23                                 'wf_info_for', 'from_state', 'to_state'))
    23                                 'wf_info_for', 'from_state', 'to_state'))
    24 
    24 
    25 
    25 
    26 class _CheckRequiredRelationOperation(LateOperation):
    26 class _CheckRequiredRelationOperation(hook.LateOperation):
    27     """checking relation cardinality has to be done after commit in
    27     """checking relation cardinality has to be done after commit in
    28     case the relation is being replaced
    28     case the relation is being replaced
    29     """
    29     """
    30     eid, rtype = None, None
    30     eid, rtype = None, None
    31 
    31 
    32     def precommit_event(self):
    32     def precommit_event(self):
    33         # recheck pending eids
    33         # recheck pending eids
    34         if self.eid in self.session.transaction_data.get('pendingeids', ()):
    34         if self.session.deleted_in_transaction(self.eid):
    35             return
    35             return
    36         if self.session.unsafe_execute(*self._rql()).rowcount < 1:
    36         if self.session.unsafe_execute(*self._rql()).rowcount < 1:
    37             etype = self.session.describe(self.eid)[0]
    37             etype = self.session.describe(self.eid)[0]
    38             _ = self.session._
    38             _ = self.session._
    39             msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
    39             msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
    57     """check required object relation"""
    57     """check required object relation"""
    58     def _rql(self):
    58     def _rql(self):
    59         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
    59         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
    60 
    60 
    61 
    61 
    62 class CheckCardinalityHook(Hook):
    62 class IntegrityHook(hook.Hook):
       
    63     __abstract__ = True
       
    64     category = 'integrity'
       
    65 
       
    66 
       
    67 class CheckCardinalityHook(IntegrityHook):
    63     """check cardinalities are satisfied"""
    68     """check cardinalities are satisfied"""
    64     __id__ = 'checkcard'
    69     __id__ = 'checkcard'
    65     category = 'integrity'
       
    66     events = ('after_add_entity', 'before_delete_relation')
    70     events = ('after_add_entity', 'before_delete_relation')
    67 
    71 
    68     def __call__(self):
    72     def __call__(self):
    69         getattr(self, self.event)()
    73         getattr(self, self.event)()
    70 
    74 
   101         rtype = self.rtype
   105         rtype = self.rtype
   102         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   106         if rtype in DONT_CHECK_RTYPES_ON_DEL:
   103             return
   107             return
   104         session = self.cw_req
   108         session = self.cw_req
   105         eidfrom, eidto = self.eidfrom, self.eidto
   109         eidfrom, eidto = self.eidfrom, self.eidto
   106         card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
   110         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
   107         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   111         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
   108         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   112         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
   109             return
   113             return
   110         pendingeids = session.transaction_data.get('pendingeids', ())
   114         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
   111         if card[0] in '1+' and not eidfrom in pendingeids:
       
   112             self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
   115             self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
   113         if card[1] in '1+' and not eidto in pendingeids:
   116         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
   114             self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
   117             self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
   115 
   118 
   116 
   119 
   117 class _CheckConstraintsOp(LateOperation):
   120 class _CheckConstraintsOp(hook.LateOperation):
   118     """check a new relation satisfy its constraints
   121     """check a new relation satisfy its constraints
   119     """
   122     """
   120     def precommit_event(self):
   123     def precommit_event(self):
   121         eidfrom, rtype, eidto = self.rdef
   124         eidfrom, rtype, eidto = self.rdef
   122         # first check related entities have not been deleted in the same
   125         # first check related entities have not been deleted in the same
   123         # transaction
   126         # transaction
   124         pending = self.session.transaction_data.get('pendingeids', ())
   127         if self.session.deleted_in_transaction(eidfrom):
   125         if eidfrom in pending:
   128             return
   126             return
   129         if self.session.deleted_in_transaction(eidto):
   127         if eidto in pending:
       
   128             return
   130             return
   129         for constraint in self.constraints:
   131         for constraint in self.constraints:
   130             try:
   132             try:
   131                 constraint.repo_check(self.session, eidfrom, rtype, eidto)
   133                 constraint.repo_check(self.session, eidfrom, rtype, eidto)
   132             except NotImplementedError:
   134             except NotImplementedError:
   135 
   137 
   136     def commit_event(self):
   138     def commit_event(self):
   137         pass
   139         pass
   138 
   140 
   139 
   141 
   140 class CheckConstraintHook(Hook):
   142 class CheckConstraintHook(IntegrityHook):
   141     """check the relation satisfy its constraints
   143     """check the relation satisfy its constraints
   142 
   144 
   143     this is delayed to a precommit time operation since other relation which
   145     this is delayed to a precommit time operation since other relation which
   144     will make constraint satisfied may be added later.
   146     will make constraint satisfied may be added later.
   145     """
   147     """
   146     __id__ = 'checkconstraint'
   148     __id__ = 'checkconstraint'
   147     category = 'integrity'
       
   148     events = ('after_add_relation',)
   149     events = ('after_add_relation',)
   149     def __call__(self):
   150 
   150         constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
   151     def __call__(self):
       
   152         constraints = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
   151                                 'constraints')
   153                                 'constraints')
   152         if constraints:
   154         if constraints:
   153             _CheckConstraintsOp(self.cw_req, constraints=constraints,
   155             _CheckConstraintsOp(self.cw_req, constraints=constraints,
   154                                rdef=(self.eidfrom, self.rtype, self.eidto))
   156                                rdef=(self.eidfrom, self.rtype, self.eidto))
   155 
   157 
   156 class CheckUniqueHook(Hook):
   158 
       
   159 class CheckUniqueHook(IntegrityHook):
   157     __id__ = 'checkunique'
   160     __id__ = 'checkunique'
   158     category = 'integrity'
       
   159     events = ('before_add_entity', 'before_update_entity')
   161     events = ('before_add_entity', 'before_update_entity')
   160 
   162 
   161     def __call__(self):
   163     def __call__(self):
   162         entity = self.entity
   164         entity = self.entity
   163         eschema = entity.e_schema
   165         eschema = entity.e_schema
   172                 if rset and rset[0][0] != entity.eid:
   174                 if rset and rset[0][0] != entity.eid:
   173                     msg = self.cw_req._('the value "%s" is already used, use another one')
   175                     msg = self.cw_req._('the value "%s" is already used, use another one')
   174                     raise ValidationError(entity.eid, {attr: msg % val})
   176                     raise ValidationError(entity.eid, {attr: msg % val})
   175 
   177 
   176 
   178 
   177 class _DelayedDeleteOp(PreCommitOperation):
   179 class _DelayedDeleteOp(hook.Operation):
   178     """delete the object of composite relation except if the relation
   180     """delete the object of composite relation except if the relation
   179     has actually been redirected to another composite
   181     has actually been redirected to another composite
   180     """
   182     """
   181 
   183 
   182     def precommit_event(self):
   184     def precommit_event(self):
   183         session = self.session
   185         session = self.session
   184         # don't do anything if the entity is being created or deleted
   186         # don't do anything if the entity is being created or deleted
   185         if not (self.eid in session.transaction_data.get('pendingeids', ()) or
   187         if not (session.deleted_in_transaction(self.eid) or
   186                 self.eid in session.transaction_data.get('neweids', ())):
   188                 session.added_in_transaction(self.eid)):
   187             etype = session.describe(self.eid)[0]
   189             etype = session.describe(self.eid)[0]
   188             session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
   190             session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
   189                                    % (etype, self.relation),
   191                                    % (etype, self.relation),
   190                                    {'x': self.eid}, 'x')
   192                                    {'x': self.eid}, 'x')
   191 
   193 
   192 
   194 
   193 class DeleteCompositeOrphanHook(Hook):
   195 class DeleteCompositeOrphanHook(IntegrityHook):
   194     """delete the composed of a composite relation when this relation is deleted
   196     """delete the composed of a composite relation when this relation is deleted
   195     """
   197     """
   196     __id__ = 'deletecomposite'
   198     __id__ = 'deletecomposite'
   197     category = 'integrity'
       
   198     events = ('before_delete_relation',)
   199     events = ('before_delete_relation',)
   199     def __call__(self):
   200 
   200         composite = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
   201     def __call__(self):
   201                               'composite')
   202         composite = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
       
   203                                                  'composite')
   202         if composite == 'subject':
   204         if composite == 'subject':
   203             _DelayedDeleteOp(self.cw_req, eid=self.eidto,
   205             _DelayedDeleteOp(self.cw_req, eid=self.eidto,
   204                              relation='Y %s X' % self.rtype)
   206                              relation='Y %s X' % self.rtype)
   205         elif composite == 'object':
   207         elif composite == 'object':
   206             _DelayedDeleteOp(self.cw_req, eid=self.eidfrom,
   208             _DelayedDeleteOp(self.cw_req, eid=self.eidfrom,
   207                              relation='X %s Y' % self.rtype)
   209                              relation='X %s Y' % self.rtype)
   208 
   210 
   209 
   211 
   210 class DontRemoveOwnersGroupHook(Hook):
   212 class DontRemoveOwnersGroupHook(IntegrityHook):
   211     """delete the composed of a composite relation when this relation is deleted
   213     """delete the composed of a composite relation when this relation is deleted
   212     """
   214     """
   213     __id__ = 'checkownersgroup'
   215     __id__ = 'checkownersgroup'
   214     __select__ = Hook.__select__ & entity_implements('CWGroup')
   216     __select__ = IntegrityHook.__select__ & entity_implements('CWGroup')
   215     category = 'integrity'
       
   216     events = ('before_delete_entity', 'before_update_entity')
   217     events = ('before_delete_entity', 'before_update_entity')
   217 
   218 
   218     def __call__(self):
   219     def __call__(self):
   219         if self.event == 'before_delete_entity' and self.entity.name == 'owners':
   220         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             raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
   224             if oldname == 'owners' and newname != oldname:
   225             if oldname == 'owners' and newname != oldname:
   225                 raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')})
   226                 raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')})
   226             self.entity['name'] = newname
   227             self.entity['name'] = newname
   227 
   228 
   228 
   229 
       
   230 class TidyHtmlFields(IntegrityHook):
       
   231     """tidy HTML in rich text strings"""
       
   232     __id__ = 'htmltidy'
       
   233     events = ('before_add_entity', 'before_update_entity')
       
   234 
       
   235     def __call__(self):
       
   236         entity = self.entity
       
   237         metaattrs = entity.e_schema.meta_attributes()
       
   238         for metaattr, (metadata, attr) in metaattrs.iteritems():
       
   239             if metadata == 'format' and attr in entity.edited_attributes:
       
   240                 try:
       
   241                     value = entity[attr]
       
   242                 except KeyError:
       
   243                     continue # no text to tidy
       
   244                 if isinstance(value, unicode): # filter out None and Binary
       
   245                     if getattr(entity, str(metaattr)) == 'text/html':
       
   246                         entity[attr] = soup2xhtml(value, self.cw_req.encoding)
       
   247 
       
   248 
       
   249 class StripCWUserLoginHook(IntegrityHook):
       
   250     """ensure user logins are stripped"""
       
   251     __id__ = 'stripuserlogin'
       
   252     __select__ = IntegrityHook.__select__ & entity_implements('CWUser')
       
   253     events = ('before_add_entity', 'before_update_entity',)
       
   254 
       
   255     def call(self, session, entity):
       
   256         if 'login' in entity.edited_attributes and entity['login']:
       
   257             entity['login'] = entity['login'].strip()