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'\ |