server/sources/native.py
changeset 8265 9747ab9230ad
parent 8235 c2a91d6639d8
child 8349 fdb796435d7b
equal deleted inserted replaced
8262:272e10526679 8265:9747ab9230ad
    53 
    53 
    54 from yams import schema2sql as y2sql
    54 from yams import schema2sql as y2sql
    55 from yams.schema import role_name
    55 from yams.schema import role_name
    56 
    56 
    57 from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
    57 from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
    58                       UniqueTogetherError)
    58                       UniqueTogetherError, QueryError, UndoTransactionException)
    59 from cubicweb import transaction as tx, server, neg_role
    59 from cubicweb import transaction as tx, server, neg_role
    60 from cubicweb.utils import QueryCache
    60 from cubicweb.utils import QueryCache
    61 from cubicweb.schema import VIRTUAL_RTYPES
    61 from cubicweb.schema import VIRTUAL_RTYPES
    62 from cubicweb.cwconfig import CubicWebNoAppConfiguration
    62 from cubicweb.cwconfig import CubicWebNoAppConfiguration
    63 from cubicweb.server import hook
    63 from cubicweb.server import hook
    64 from cubicweb.server.utils import crypt_password, eschema_eid
    64 from cubicweb.server.utils import crypt_password, eschema_eid
    65 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
    65 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
    66 from cubicweb.server.rqlannotation import set_qdata
    66 from cubicweb.server.rqlannotation import set_qdata
    67 from cubicweb.server.hook import CleanupDeletedEidsCacheOp
    67 from cubicweb.server.hook import CleanupDeletedEidsCacheOp
    68 from cubicweb.server.session import hooks_control, security_enabled
       
    69 from cubicweb.server.edition import EditedEntity
    68 from cubicweb.server.edition import EditedEntity
    70 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
    69 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
    71 from cubicweb.server.sources.rql2sql import SQLGenerator
    70 from cubicweb.server.sources.rql2sql import SQLGenerator
    72 
    71 
    73 
    72 
   160     coltype = y2sql.type_from_constraints(dbhelper, ttype,
   159     coltype = y2sql.type_from_constraints(dbhelper, ttype,
   161                                           rdef.constraints, creating=False)
   160                                           rdef.constraints, creating=False)
   162     allownull = rdef.cardinality[0] != '1'
   161     allownull = rdef.cardinality[0] != '1'
   163     return coltype, allownull
   162     return coltype, allownull
   164 
   163 
   165 class UndoException(Exception):
   164 
       
   165 class _UndoException(Exception):
   166     """something went wrong during undoing"""
   166     """something went wrong during undoing"""
   167 
   167 
   168     def __unicode__(self):
   168     def __unicode__(self):
   169         """Called by the unicode builtin; should return a Unicode object
   169         """Called by the unicode builtin; should return a Unicode object
   170 
   170 
   171         Type of UndoException message must be `unicode` by design in CubicWeb.
   171         Type of _UndoException message must be `unicode` by design in CubicWeb.
   172 
   172         """
   173         .. warning::
   173         assert isinstance(self.args[0], unicode)
   174             This method is not available in python2.5"""
   174         return self.args[0]
   175         assert isinstance(self.message, unicode)
   175 
   176         return self.message
       
   177 
   176 
   178 def _undo_check_relation_target(tentity, rdef, role):
   177 def _undo_check_relation_target(tentity, rdef, role):
   179     """check linked entity has not been redirected for this relation"""
   178     """check linked entity has not been redirected for this relation"""
   180     card = rdef.role_cardinality(role)
   179     card = rdef.role_cardinality(role)
   181     if card in '?1' and tentity.related(rdef.rtype, role):
   180     if card in '?1' and tentity.related(rdef.rtype, role):
   182         raise UndoException(tentity._cw._(
   181         raise _UndoException(tentity._cw._(
   183             "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
   182             "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
   184             "is already linked using this relation.")
   183             "is already linked using this relation.")
   185                             % {'role': neg_role(role),
   184                             % {'role': neg_role(role),
   186                                'rtype': rdef.rtype,
   185                                'rtype': rdef.rtype,
   187                                'eid': tentity.eid})
   186                                'eid': tentity.eid})
   190     entities = []
   189     entities = []
   191     for role, eid in (('subject', subj), ('object', obj)):
   190     for role, eid in (('subject', subj), ('object', obj)):
   192         try:
   191         try:
   193             entities.append(session.entity_from_eid(eid))
   192             entities.append(session.entity_from_eid(eid))
   194         except UnknownEid:
   193         except UnknownEid:
   195             raise UndoException(session._(
   194             raise _UndoException(session._(
   196                 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
   195                 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
   197                 " doesn't exist anymore.")
   196                 " doesn't exist anymore.")
   198                                 % {'role': session._(role),
   197                                 % {'role': session._(role),
   199                                    'rtype': session._(rtype),
   198                                    'rtype': session._(rtype),
   200                                    'eid': eid})
   199                                    'eid': eid})
   201     sentity, oentity = entities
   200     sentity, oentity = entities
   202     try:
   201     try:
   203         rschema = session.vreg.schema.rschema(rtype)
   202         rschema = session.vreg.schema.rschema(rtype)
   204         rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
   203         rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
   205     except KeyError:
   204     except KeyError:
   206         raise UndoException(session._(
   205         raise _UndoException(session._(
   207             "Can't restore relation %(rtype)s between %(subj)s and "
   206             "Can't restore relation %(rtype)s between %(subj)s and "
   208             "%(obj)s, that relation does not exists anymore in the "
   207             "%(obj)s, that relation does not exists anymore in the "
   209             "schema.")
   208             "schema.")
   210                             % {'rtype': session._(rtype),
   209                             % {'rtype': session._(rtype),
   211                                'subj': subj,
   210                                'subj': subj,
   635         """add a new entity to the source"""
   634         """add a new entity to the source"""
   636         with self._storage_handler(entity, 'added'):
   635         with self._storage_handler(entity, 'added'):
   637             attrs = self.preprocess_entity(entity)
   636             attrs = self.preprocess_entity(entity)
   638             sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
   637             sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
   639             self.doexec(session, sql, attrs)
   638             self.doexec(session, sql, attrs)
   640             if session.undoable_action('C', entity.__regid__):
   639             if session.ertype_supports_undo(entity.__regid__):
   641                 self._record_tx_action(session, 'tx_entity_actions', 'C',
   640                 self._record_tx_action(session, 'tx_entity_actions', 'C',
   642                                        etype=entity.__regid__, eid=entity.eid)
   641                                        etype=entity.__regid__, eid=entity.eid)
   643 
   642 
   644     def update_entity(self, session, entity):
   643     def update_entity(self, session, entity):
   645         """replace an entity in the source"""
   644         """replace an entity in the source"""
   646         with self._storage_handler(entity, 'updated'):
   645         with self._storage_handler(entity, 'updated'):
   647             attrs = self.preprocess_entity(entity)
   646             attrs = self.preprocess_entity(entity)
   648             if session.undoable_action('U', entity.__regid__):
   647             if session.ertype_supports_undo(entity.__regid__):
   649                 changes = self._save_attrs(session, entity, attrs)
   648                 changes = self._save_attrs(session, entity, attrs)
   650                 self._record_tx_action(session, 'tx_entity_actions', 'U',
   649                 self._record_tx_action(session, 'tx_entity_actions', 'U',
   651                                        etype=entity.__regid__, eid=entity.eid,
   650                                        etype=entity.__regid__, eid=entity.eid,
   652                                        changes=self._binary(dumps(changes)))
   651                                        changes=self._binary(dumps(changes)))
   653             sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs,
   652             sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs,
   655             self.doexec(session, sql, attrs)
   654             self.doexec(session, sql, attrs)
   656 
   655 
   657     def delete_entity(self, session, entity):
   656     def delete_entity(self, session, entity):
   658         """delete an entity from the source"""
   657         """delete an entity from the source"""
   659         with self._storage_handler(entity, 'deleted'):
   658         with self._storage_handler(entity, 'deleted'):
   660             if session.undoable_action('D', entity.__regid__):
   659             if session.ertype_supports_undo(entity.__regid__):
   661                 attrs = [SQL_PREFIX + r.type
   660                 attrs = [SQL_PREFIX + r.type
   662                          for r in entity.e_schema.subject_relations()
   661                          for r in entity.e_schema.subject_relations()
   663                          if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
   662                          if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
   664                 changes = self._save_attrs(session, entity, attrs)
   663                 changes = self._save_attrs(session, entity, attrs)
   665                 self._record_tx_action(session, 'tx_entity_actions', 'D',
   664                 self._record_tx_action(session, 'tx_entity_actions', 'D',
   670             self.doexec(session, sql, attrs)
   669             self.doexec(session, sql, attrs)
   671 
   670 
   672     def add_relation(self, session, subject, rtype, object, inlined=False):
   671     def add_relation(self, session, subject, rtype, object, inlined=False):
   673         """add a relation to the source"""
   672         """add a relation to the source"""
   674         self._add_relations(session,  rtype, [(subject, object)], inlined)
   673         self._add_relations(session,  rtype, [(subject, object)], inlined)
   675         if session.undoable_action('A', rtype):
   674         if session.ertype_supports_undo(rtype):
   676             self._record_tx_action(session, 'tx_relation_actions', 'A',
   675             self._record_tx_action(session, 'tx_relation_actions', 'A',
   677                                    eid_from=subject, rtype=rtype, eid_to=object)
   676                                    eid_from=subject, rtype=rtype, eid_to=object)
   678 
   677 
   679     def add_relations(self, session,  rtype, subj_obj_list, inlined=False):
   678     def add_relations(self, session,  rtype, subj_obj_list, inlined=False):
   680         """add a relations to the source"""
   679         """add a relations to the source"""
   681         self._add_relations(session, rtype, subj_obj_list, inlined)
   680         self._add_relations(session, rtype, subj_obj_list, inlined)
   682         if session.undoable_action('A', rtype):
   681         if session.ertype_supports_undo(rtype):
   683             for subject, object in subj_obj_list:
   682             for subject, object in subj_obj_list:
   684                 self._record_tx_action(session, 'tx_relation_actions', 'A',
   683                 self._record_tx_action(session, 'tx_relation_actions', 'A',
   685                                        eid_from=subject, rtype=rtype, eid_to=object)
   684                                        eid_from=subject, rtype=rtype, eid_to=object)
   686 
   685 
   687     def _add_relations(self, session, rtype, subj_obj_list, inlined=False):
   686     def _add_relations(self, session, rtype, subj_obj_list, inlined=False):
   710 
   709 
   711     def delete_relation(self, session, subject, rtype, object):
   710     def delete_relation(self, session, subject, rtype, object):
   712         """delete a relation from the source"""
   711         """delete a relation from the source"""
   713         rschema = self.schema.rschema(rtype)
   712         rschema = self.schema.rschema(rtype)
   714         self._delete_relation(session, subject, rtype, object, rschema.inlined)
   713         self._delete_relation(session, subject, rtype, object, rschema.inlined)
   715         if session.undoable_action('R', rtype):
   714         if session.ertype_supports_undo(rtype):
   716             self._record_tx_action(session, 'tx_relation_actions', 'R',
   715             self._record_tx_action(session, 'tx_relation_actions', 'R',
   717                                    eid_from=subject, rtype=rtype, eid_to=object)
   716                                    eid_from=subject, rtype=rtype, eid_to=object)
   718 
   717 
   719     def _delete_relation(self, session, subject, rtype, object, inlined=False):
   718     def _delete_relation(self, session, subject, rtype, object, inlined=False):
   720         """delete a relation from the source"""
   719         """delete a relation from the source"""
  1155         """
  1154         """
  1156         # set mode so connections set isn't released subsquently until commit/rollback
  1155         # set mode so connections set isn't released subsquently until commit/rollback
  1157         session.mode = 'write'
  1156         session.mode = 'write'
  1158         errors = []
  1157         errors = []
  1159         session.transaction_data['undoing_uuid'] = txuuid
  1158         session.transaction_data['undoing_uuid'] = txuuid
  1160         with hooks_control(session, session.HOOKS_DENY_ALL,
  1159         with session.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'):
  1161                            'integrity', 'activeintegrity', 'undo'):
  1160             with session.security_enabled(read=False):
  1162             with security_enabled(session, read=False):
       
  1163                 for action in reversed(self.tx_actions(session, txuuid, False)):
  1161                 for action in reversed(self.tx_actions(session, txuuid, False)):
  1164                     undomethod = getattr(self, '_undo_%s' % action.action.lower())
  1162                     undomethod = getattr(self, '_undo_%s' % action.action.lower())
  1165                     errors += undomethod(session, action)
  1163                     errors += undomethod(session, action)
  1166         # remove the transactions record
  1164         # remove the transactions record
  1167         self.doexec(session,
  1165         self.doexec(session,
  1168                     "DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid)
  1166                     "DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid)
  1169         return errors
  1167         if errors:
       
  1168             raise UndoTransactionException(txuuid, errors)
       
  1169         else:
       
  1170             return
  1170 
  1171 
  1171     def start_undoable_transaction(self, session, uuid):
  1172     def start_undoable_transaction(self, session, uuid):
  1172         """session callback to insert a transaction record in the transactions
  1173         """session callback to insert a transaction record in the transactions
  1173         table when some undoable transaction is started
  1174         table when some undoable transaction is started
  1174         """
  1175         """
  1217         sql = self.sqlgen.select('transactions', restr, ('tx_time', 'tx_user'))
  1218         sql = self.sqlgen.select('transactions', restr, ('tx_time', 'tx_user'))
  1218         cu = self.doexec(session, sql, restr)
  1219         cu = self.doexec(session, sql, restr)
  1219         try:
  1220         try:
  1220             time, ueid = cu.fetchone()
  1221             time, ueid = cu.fetchone()
  1221         except TypeError:
  1222         except TypeError:
  1222             raise tx.NoSuchTransaction()
  1223             raise tx.NoSuchTransaction(txuuid)
  1223         if not (session.user.is_in_group('managers')
  1224         if not (session.user.is_in_group('managers')
  1224                 or session.user.eid == ueid):
  1225                 or session.user.eid == ueid):
  1225             raise tx.NoSuchTransaction()
  1226             raise tx.NoSuchTransaction(txuuid)
  1226         return time, ueid
  1227         return time, ueid
       
  1228 
       
  1229     def _reedit_entity(self, entity, changes, err):
       
  1230         session = entity._cw
       
  1231         eid = entity.eid
       
  1232         entity.cw_edited = edited = EditedEntity(entity)
       
  1233         # check for schema changes, entities linked through inlined relation
       
  1234         # still exists, rewrap binary values
       
  1235         eschema = entity.e_schema
       
  1236         getrschema = eschema.subjrels
       
  1237         for column, value in changes.items():
       
  1238             rtype = column[len(SQL_PREFIX):]
       
  1239             if rtype == "eid":
       
  1240                 continue # XXX should even `eid` be stored in action changes?
       
  1241             try:
       
  1242                 rschema = getrschema[rtype]
       
  1243             except KeyError:
       
  1244                 err(session._("can't restore relation %(rtype)s of entity %(eid)s, "
       
  1245                               "this relation does not exist in the schema anymore.")
       
  1246                     % {'rtype': rtype, 'eid': eid})
       
  1247             if not rschema.final:
       
  1248                 if not rschema.inlined:
       
  1249                     assert value is None
       
  1250                 # rschema is an inlined relation
       
  1251                 elif value is not None:
       
  1252                     # not a deletion: we must put something in edited
       
  1253                     try:
       
  1254                         entity._cw.entity_from_eid(value) # check target exists
       
  1255                         edited[rtype] = value
       
  1256                     except UnknownEid:
       
  1257                         err(session._("can't restore entity %(eid)s of type %(eschema)s, "
       
  1258                                       "target of %(rtype)s (eid %(value)s) does not exist any longer")
       
  1259                             % locals())
       
  1260             elif eschema.destination(rtype) in ('Bytes', 'Password'):
       
  1261                 changes[column] = self._binary(value)
       
  1262                 edited[rtype] = Binary(value)
       
  1263             elif isinstance(value, str):
       
  1264                 edited[rtype] = unicode(value, session.encoding, 'replace')
       
  1265             else:
       
  1266                 edited[rtype] = value
       
  1267         # This must only be done after init_entitiy_caches : defered in calling functions
       
  1268         # edited.check()
  1227 
  1269 
  1228     def _undo_d(self, session, action):
  1270     def _undo_d(self, session, action):
  1229         """undo an entity deletion"""
  1271         """undo an entity deletion"""
  1230         errors = []
  1272         errors = []
  1231         err = errors.append
  1273         err = errors.append
  1237             entity = self.repo.vreg['etypes'].etype_class(etype)(session)
  1279             entity = self.repo.vreg['etypes'].etype_class(etype)(session)
  1238         except Exception:
  1280         except Exception:
  1239             err("can't restore entity %s of type %s, type no more supported"
  1281             err("can't restore entity %s of type %s, type no more supported"
  1240                 % (eid, etype))
  1282                 % (eid, etype))
  1241             return errors
  1283             return errors
  1242         entity.cw_edited = edited = EditedEntity(entity)
  1284         self._reedit_entity(entity, action.changes, err)
  1243         # check for schema changes, entities linked through inlined relation
       
  1244         # still exists, rewrap binary values
       
  1245         eschema = entity.e_schema
       
  1246         getrschema = eschema.subjrels
       
  1247         for column, value in action.changes.items():
       
  1248             rtype = column[3:] # remove cw_ prefix
       
  1249             try:
       
  1250                 rschema = getrschema[rtype]
       
  1251             except KeyError:
       
  1252                 err(_("Can't restore relation %(rtype)s of entity %(eid)s, "
       
  1253                       "this relation does not exists anymore in the schema.")
       
  1254                     % {'rtype': rtype, 'eid': eid})
       
  1255             if not rschema.final:
       
  1256                 assert value is None
       
  1257             elif eschema.destination(rtype) in ('Bytes', 'Password'):
       
  1258                 action.changes[column] = self._binary(value)
       
  1259                 edited[rtype] = Binary(value)
       
  1260             elif isinstance(value, str):
       
  1261                 edited[rtype] = unicode(value, session.encoding, 'replace')
       
  1262             else:
       
  1263                 edited[rtype] = value
       
  1264         entity.eid = eid
  1285         entity.eid = eid
  1265         session.repo.init_entity_caches(session, entity, self)
  1286         session.repo.init_entity_caches(session, entity, self)
  1266         edited.check()
  1287         entity.cw_edited.check()
  1267         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
  1288         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
  1268         # restore the entity
  1289         # restore the entity
  1269         action.changes['cw_eid'] = eid
  1290         action.changes['cw_eid'] = eid
  1270         sql = self.sqlgen.insert(SQL_PREFIX + etype, action.changes)
  1291         sql = self.sqlgen.insert(SQL_PREFIX + etype, action.changes)
  1271         self.doexec(session, sql, action.changes)
  1292         self.doexec(session, sql, action.changes)
  1282         """undo a relation removal"""
  1303         """undo a relation removal"""
  1283         errors = []
  1304         errors = []
  1284         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
  1305         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
  1285         try:
  1306         try:
  1286             sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
  1307             sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
  1287         except UndoException, ex:
  1308         except _UndoException, ex:
  1288             errors.append(unicode(ex))
  1309             errors.append(unicode(ex))
  1289         else:
  1310         else:
  1290             for role, entity in (('subject', sentity),
  1311             for role, entity in (('subject', sentity),
  1291                                  ('object', oentity)):
  1312                                  ('object', oentity)):
  1292                 try:
  1313                 try:
  1293                     _undo_check_relation_target(entity, rdef, role)
  1314                     _undo_check_relation_target(entity, rdef, role)
  1294                 except UndoException, ex:
  1315                 except _UndoException, ex:
  1295                     errors.append(unicode(ex))
  1316                     errors.append(unicode(ex))
  1296                     continue
  1317                     continue
  1297         if not errors:
  1318         if not errors:
  1298             self.repo.hm.call_hooks('before_add_relation', session,
  1319             self.repo.hm.call_hooks('before_add_relation', session,
  1299                                     eidfrom=subj, rtype=rtype, eidto=obj)
  1320                                     eidfrom=subj, rtype=rtype, eidto=obj)
  1342         self.repo.hm.call_hooks('after_delete_entity', session, entity=entity)
  1363         self.repo.hm.call_hooks('after_delete_entity', session, entity=entity)
  1343         return ()
  1364         return ()
  1344 
  1365 
  1345     def _undo_u(self, session, action):
  1366     def _undo_u(self, session, action):
  1346         """undo an entity update"""
  1367         """undo an entity update"""
  1347         return ['undoing of entity updating not yet supported.']
  1368         errors = []
       
  1369         err = errors.append
       
  1370         try:
       
  1371             entity = session.entity_from_eid(action.eid)
       
  1372         except UnknownEid:
       
  1373             err(session._("can't restore state of entity %s, it has been "
       
  1374                           "deleted inbetween") % action.eid)
       
  1375             return errors
       
  1376         self._reedit_entity(entity, action.changes, err)
       
  1377         entity.cw_edited.check()
       
  1378         self.repo.hm.call_hooks('before_update_entity', session, entity=entity)
       
  1379         sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, action.changes,
       
  1380                                  ['cw_eid'])
       
  1381         self.doexec(session, sql, action.changes)
       
  1382         self.repo.hm.call_hooks('after_update_entity', session, entity=entity)
       
  1383         return errors
  1348 
  1384 
  1349     def _undo_a(self, session, action):
  1385     def _undo_a(self, session, action):
  1350         """undo a relation addition"""
  1386         """undo a relation addition"""
  1351         errors = []
  1387         errors = []
  1352         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
  1388         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
  1353         try:
  1389         try:
  1354             sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
  1390             sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
  1355         except UndoException, ex:
  1391         except _UndoException, ex:
  1356             errors.append(unicode(ex))
  1392             errors.append(unicode(ex))
  1357         else:
  1393         else:
  1358             rschema = rdef.rtype
  1394             rschema = rdef.rtype
  1359             if rschema.inlined:
  1395             if rschema.inlined:
  1360                 sql = 'SELECT 1 FROM cw_%s WHERE cw_eid=%s and cw_%s=%s'\
  1396                 sql = 'SELECT 1 FROM cw_%s WHERE cw_eid=%s and cw_%s=%s'\