# HG changeset patch # User Anthony Truchet # Date 1330332195 -3600 # Node ID 9747ab9230ad3d435218a99b099cc167809704a5 # Parent 272e10526679af2b14a6aa3042c1d7b6bc8e46c4 [repo, undo] Finish repository-side implementation of the undo feature (closes #893940) 1) Changes API for undo_transaction in order to raise an exception: the previous API returned a sequence of error, the user of the API needed to be careful to check explicitly. We change this for it to raise an UndoException with the list of errors as an attribute. 2) The server-side support for undoing update actions is finished 3) The configuration for `undo-support` is now a simple boolean Beware of the costs, both in DB space and in number of requests on the RDBMS backend, once this feture is activated. 4) Adds a txuuid param/attribute to the existing NoSuchTransaction exception. diff -r 272e10526679 -r 9747ab9230ad _exceptions.py --- a/_exceptions.py Thu Feb 23 19:06:14 2012 +0100 +++ b/_exceptions.py Mon Feb 27 09:43:15 2012 +0100 @@ -30,9 +30,10 @@ if self.msg: if self.args: return self.msg % tuple(self.args) - return self.msg - return ' '.join(unicode(arg) for arg in self.args) - + else: + return self.msg + else: + return u' '.join(unicode(arg) for arg in self.args) class ConfigurationError(CubicWebException): """a misconfiguration error""" @@ -81,6 +82,7 @@ class UniqueTogetherError(RepositoryError): """raised when a unique_together constraint caused an IntegrityError""" + # security exceptions ######################################################### class Unauthorized(SecurityError): @@ -128,6 +130,35 @@ a non final entity """ +class UndoTransactionException(QueryError): + """Raised when undoing a transaction could not be performed completely. + + Note that : + 1) the partial undo operation might be acceptable + depending upon the final application + + 2) the undo operation can also fail with a `ValidationError` in + cases where the undoing breaks integrity constraints checked + immediately. + + 3) It might be that neither of those exception is raised but a + subsequent `commit` might raise a `ValidationError` in cases + where the undoing breaks integrity constraints checked at + commit time. + + :type txuuix: int + :param txuuid: Unique identifier of the partialy undone transaction + + :type errors: list + :param errors: List of errors occured during undoing + """ + msg = u"The following error(s) occured while undoing transaction #%d : %s" + + def __init__(self, txuuid, errors): + super(UndoTransactionException, self).__init__(txuuid, errors) + self.txuuid = txuuid + self.errors = errors + # tools exceptions ############################################################ class ExecutionError(Exception): diff -r 272e10526679 -r 9747ab9230ad devtools/__init__.py --- a/devtools/__init__.py Thu Feb 23 19:06:14 2012 +0100 +++ b/devtools/__init__.py Mon Feb 27 09:43:15 2012 +0100 @@ -168,7 +168,7 @@ def load_configuration(self): super(TestServerConfiguration, self).load_configuration() # no undo support in tests - self.global_set_option('undo-support', '') + self.global_set_option('undo-support', 'n') def main_config_file(self): """return instance's control configuration file""" diff -r 272e10526679 -r 9747ab9230ad i18n/de.po --- a/i18n/de.po Thu Feb 23 19:06:14 2012 +0100 +++ b/i18n/de.po Mon Feb 27 09:43:15 2012 +0100 @@ -390,14 +390,6 @@ #, python-format msgid "" -"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not " -"exists anymore in the schema." -msgstr "" -"Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, " -"diese Relation existiert nicht mehr in dem Schema." - -#, python-format -msgid "" "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist " "anymore." msgstr "" @@ -1265,6 +1257,9 @@ msgid "bad value" msgstr "Unzulässiger Wert" +msgid "badly formatted url" +msgstr "" + msgid "base url" msgstr "Basis-URL" @@ -1349,6 +1344,9 @@ msgid "can not resolve entity types:" msgstr "Die Typen konnten nicht ermittelt werden:" +msgid "can only have one url" +msgstr "" + msgid "can't be changed" msgstr "kann nicht geändert werden" @@ -1386,6 +1384,22 @@ #, python-format msgid "" +"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid " +"%(value)s) does not exist any longer" +msgstr "" + +#, python-format +msgid "" +"can't restore relation %(rtype)s of entity %(eid)s, this relation does not " +"exist in the schema anymore." +msgstr "" + +#, python-format +msgid "can't restore state of entity %s, it has been deleted inbetween" +msgstr "" + +#, python-format +msgid "" "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality=" "%(card)s" msgstr "" @@ -4241,6 +4255,9 @@ msgid "undo" msgstr "rückgängig machen" +msgid "undo last change" +msgstr "" + msgid "unique identifier used to connect to the application" msgstr "eindeutiger Bezeichner zur Verbindung mit der Anwendung" @@ -4265,6 +4282,9 @@ msgid "unknown vocabulary:" msgstr "Unbekanntes Wörterbuch : " +msgid "unsupported protocol" +msgstr "" + msgid "upassword" msgstr "Passwort" @@ -4573,45 +4593,9 @@ msgid "you should un-inline relation %s which is supported and may be crossed " msgstr "" -#~ msgid "(loading ...)" -#~ msgstr "(laden...)" - -#~ msgid "Schema of the data model" -#~ msgstr "Schema des Datenmodells" - -#~ msgid "csv entities export" -#~ msgstr "CSV-Export von Entitäten" - -#~ msgid "follow this link if javascript is deactivated" -#~ msgstr "Folgen Sie diesem Link, falls Javascript deaktiviert ist." - #~ msgid "" -#~ "how to format date and time in the ui (\"man strftime\" for format " -#~ "description)" +#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does " +#~ "not exists anymore in the schema." #~ msgstr "" -#~ "Wie formatiert man das Datum Interface im (\"man strftime\" für die " -#~ "Beschreibung des neuen Formats" - -#~ msgid "" -#~ "how to format date in the ui (\"man strftime\" for format description)" -#~ msgstr "" -#~ "Wie formatiert man das Datum im Interface (\"man strftime\" für die " -#~ "Beschreibung des Formats)" - -#~ msgid "" -#~ "how to format time in the ui (\"man strftime\" for format description)" -#~ msgstr "" -#~ "Wie man die Uhrzeit im Interface (\"man strftime\" für die " -#~ "Formatbeschreibung)" - -#~ msgid "instance schema" -#~ msgstr "Schema der Instanz" - -#~ msgid "rss" -#~ msgstr "RSS" - -#~ msgid "xbel" -#~ msgstr "XBEL" - -#~ msgid "xml" -#~ msgstr "XML" +#~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, " +#~ "diese Relation existiert nicht mehr in dem Schema." diff -r 272e10526679 -r 9747ab9230ad i18n/en.po --- a/i18n/en.po Thu Feb 23 19:06:14 2012 +0100 +++ b/i18n/en.po Mon Feb 27 09:43:15 2012 +0100 @@ -374,12 +374,6 @@ #, python-format msgid "" -"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not " -"exists anymore in the schema." -msgstr "" - -#, python-format -msgid "" "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist " "anymore." msgstr "" @@ -1220,6 +1214,9 @@ msgid "bad value" msgstr "" +msgid "badly formatted url" +msgstr "" + msgid "base url" msgstr "" @@ -1304,6 +1301,9 @@ msgid "can not resolve entity types:" msgstr "" +msgid "can only have one url" +msgstr "" + msgid "can't be changed" msgstr "" @@ -1340,6 +1340,22 @@ #, python-format msgid "" +"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid " +"%(value)s) does not exist any longer" +msgstr "" + +#, python-format +msgid "" +"can't restore relation %(rtype)s of entity %(eid)s, this relation does not " +"exist in the schema anymore." +msgstr "" + +#, python-format +msgid "can't restore state of entity %s, it has been deleted inbetween" +msgstr "" + +#, python-format +msgid "" "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality=" "%(card)s" msgstr "" @@ -4139,6 +4155,9 @@ msgid "undo" msgstr "" +msgid "undo last change" +msgstr "" + msgid "unique identifier used to connect to the application" msgstr "" @@ -4163,6 +4182,9 @@ msgid "unknown vocabulary:" msgstr "" +msgid "unsupported protocol" +msgstr "" + msgid "upassword" msgstr "password" diff -r 272e10526679 -r 9747ab9230ad i18n/es.po --- a/i18n/es.po Thu Feb 23 19:06:14 2012 +0100 +++ b/i18n/es.po Mon Feb 27 09:43:15 2012 +0100 @@ -390,14 +390,6 @@ #, python-format msgid "" -"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not " -"exists anymore in the schema." -msgstr "" -"No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta " -"relación ya no existe en el esquema." - -#, python-format -msgid "" "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist " "anymore." msgstr "" @@ -1276,6 +1268,9 @@ msgid "bad value" msgstr "Valor erróneo" +msgid "badly formatted url" +msgstr "" + msgid "base url" msgstr "Url de base" @@ -1360,6 +1355,9 @@ msgid "can not resolve entity types:" msgstr "Imposible de interpretar los tipos de entidades:" +msgid "can only have one url" +msgstr "" + msgid "can't be changed" msgstr "No puede ser modificado" @@ -1396,6 +1394,22 @@ #, python-format msgid "" +"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid " +"%(value)s) does not exist any longer" +msgstr "" + +#, python-format +msgid "" +"can't restore relation %(rtype)s of entity %(eid)s, this relation does not " +"exist in the schema anymore." +msgstr "" + +#, python-format +msgid "can't restore state of entity %s, it has been deleted inbetween" +msgstr "" + +#, python-format +msgid "" "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality=" "%(card)s" msgstr "" @@ -4291,6 +4305,9 @@ msgid "undo" msgstr "Anular" +msgid "undo last change" +msgstr "" + msgid "unique identifier used to connect to the application" msgstr "Identificador único utilizado para conectarse al Sistema" @@ -4315,6 +4332,9 @@ msgid "unknown vocabulary:" msgstr "Vocabulario desconocido: " +msgid "unsupported protocol" +msgstr "" + msgid "upassword" msgstr "Contraseña" @@ -4624,54 +4644,9 @@ "usted debe quitar la puesta en línea de la relación %s que es aceptada y " "puede ser cruzada" -#~ msgid "(loading ...)" -#~ msgstr "(Cargando ...)" - -#~ msgid "Schema of the data model" -#~ msgstr "Esquema del modelo de datos" - -#~ msgid "add a CWSourceSchemaConfig" -#~ msgstr "agregar una parte de mapeo" - -#~ msgid "csv entities export" -#~ msgstr "Exportar entidades en csv" - -#~ msgid "follow this link if javascript is deactivated" -#~ msgstr "Seleccione esta liga si javascript esta desactivado" - #~ msgid "" -#~ "how to format date and time in the ui (\"man strftime\" for format " -#~ "description)" -#~ msgstr "" -#~ "Formato de fecha y hora que se utilizará por defecto en la interfaz " -#~ "(\"man strftime\" para mayor información del formato)" - -#~ msgid "" -#~ "how to format date in the ui (\"man strftime\" for format description)" +#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does " +#~ "not exists anymore in the schema." #~ msgstr "" -#~ "Formato de fecha que se utilizará por defecto en la interfaz (\"man " -#~ "strftime\" para mayor información del formato)" - -#~ msgid "" -#~ "how to format time in the ui (\"man strftime\" for format description)" -#~ msgstr "" -#~ "Formato de hora que se utilizará por defecto en la interfaz (\"man " -#~ "strftime\" para mayor información del formato)" - -#~ msgid "instance schema" -#~ msgstr "Esquema de la Instancia" - -#~ msgid "rdf" -#~ msgstr "rdf" - -#~ msgid "rss" -#~ msgstr "RSS" - -#~ msgid "siteinfo" -#~ msgstr "información" - -#~ msgid "xbel" -#~ msgstr "xbel" - -#~ msgid "xml" -#~ msgstr "xml" +#~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta " +#~ "relación ya no existe en el esquema." diff -r 272e10526679 -r 9747ab9230ad i18n/fr.po --- a/i18n/fr.po Thu Feb 23 19:06:14 2012 +0100 +++ b/i18n/fr.po Mon Feb 27 09:43:15 2012 +0100 @@ -390,14 +390,6 @@ #, python-format msgid "" -"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not " -"exists anymore in the schema." -msgstr "" -"Ne peut restaurer la relation %(rtype)s de l'entité %(eid)s, cette relation " -"n'existe plus dans le schéma" - -#, python-format -msgid "" "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist " "anymore." msgstr "" @@ -1277,6 +1269,9 @@ msgid "bad value" msgstr "mauvaise valeur" +msgid "badly formatted url" +msgstr "" + msgid "base url" msgstr "url de base" @@ -1362,6 +1357,9 @@ msgid "can not resolve entity types:" msgstr "impossible d'interpréter les types d'entités :" +msgid "can only have one url" +msgstr "" + msgid "can't be changed" msgstr "ne peut-être modifié" @@ -1398,6 +1396,22 @@ #, python-format msgid "" +"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid " +"%(value)s) does not exist any longer" +msgstr "impossible de rétablir l'entité %(eid)s de type %(eschema)s, cible de la relation %(rtype)s (eid %(value)s) n'existe plus" + +#, python-format +msgid "" +"can't restore relation %(rtype)s of entity %(eid)s, this relation does not " +"exist in the schema anymore." +msgstr "impossible de rétablir la relation %(rtype)s sur l'entité %(eid)s, cette relation n'existe plus dans le schéma." + +#, python-format +msgid "can't restore state of entity %s, it has been deleted inbetween" +msgstr "impossible de rétablir l'état de l'entité %s, elle a été supprimée entre-temps" + +#, python-format +msgid "" "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality=" "%(card)s" msgstr "" @@ -4296,6 +4310,9 @@ msgid "undo" msgstr "annuler" +msgid "undo last change" +msgstr "annuler dernier changement" + msgid "unique identifier used to connect to the application" msgstr "identifiant unique utilisé pour se connecter à l'application" @@ -4320,6 +4337,9 @@ msgid "unknown vocabulary:" msgstr "vocabulaire inconnu : " +msgid "unsupported protocol" +msgstr "" + msgid "upassword" msgstr "mot de passe" @@ -4627,28 +4647,3 @@ msgstr "" "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et " "peut-être croisée" - -#~ msgid "(loading ...)" -#~ msgstr "(chargement ...)" - -#~ msgid "follow this link if javascript is deactivated" -#~ msgstr "suivez ce lien si javascript est désactivé" - -#~ msgid "" -#~ "how to format date and time in the ui (\"man strftime\" for format " -#~ "description)" -#~ msgstr "" -#~ "comment formater la date dans l'interface (\"man strftime\" pour la " -#~ "description du format)" - -#~ msgid "" -#~ "how to format date in the ui (\"man strftime\" for format description)" -#~ msgstr "" -#~ "comment formater la date dans l'interface (\"man strftime\" pour la " -#~ "description du format)" - -#~ msgid "" -#~ "how to format time in the ui (\"man strftime\" for format description)" -#~ msgstr "" -#~ "comment formater l'heure dans l'interface (\"man strftime\" pour la " -#~ "description du format)" diff -r 272e10526679 -r 9747ab9230ad server/serverconfig.py --- a/server/serverconfig.py Thu Feb 23 19:06:14 2012 +0100 +++ b/server/serverconfig.py Mon Feb 27 09:43:15 2012 +0100 @@ -141,11 +141,8 @@ 'group': 'main', 'level': 3, }), ('undo-support', - {'type' : 'string', 'default': '', - 'help': 'string defining actions that will have undo support: \ -[C]reate [U]pdate [D]elete entities / [A]dd [R]emove relation. Leave it empty \ -for no undo support, set it to CUDAR for full undo support, or to DR for \ -support undoing of deletion only.', + {'type' : 'yn', 'default': False, + 'help': 'enable undo support', 'group': 'main', 'level': 3, }), ('keep-transaction-lifetime', diff -r 272e10526679 -r 9747ab9230ad server/session.py --- a/server/session.py Thu Feb 23 19:06:14 2012 +0100 +++ b/server/session.py Mon Feb 27 09:43:15 2012 +0100 @@ -252,13 +252,11 @@ self.cnxtype = cnxprops.cnxtype self.timestamp = time() self.default_mode = 'read' - # support undo for Create Update Delete entity / Add Remove relation + # undo support if repo.config.creating or repo.config.repairing or self.is_internal_session: - self.undo_actions = () + self.undo_actions = False else: - self.undo_actions = set(repo.config['undo-support'].upper()) - if self.undo_actions - set('CUDAR'): - raise Exception('bad undo-support string in configuration') + self.undo_actions = repo.config['undo-support'] # short cut to querier .execute method self._execute = repo.querier.execute # shared data, used to communicate extra information between the client @@ -1118,9 +1116,8 @@ # undo support ############################################################ - def undoable_action(self, action, ertype): - return action in self.undo_actions and not ertype in NO_UNDO_TYPES - # XXX elif transaction on mark it partial + def ertype_supports_undo(self, ertype): + return self.undo_actions and ertype not in NO_UNDO_TYPES def transaction_uuid(self, set=True): try: diff -r 272e10526679 -r 9747ab9230ad server/sources/native.py --- a/server/sources/native.py Thu Feb 23 19:06:14 2012 +0100 +++ b/server/sources/native.py Mon Feb 27 09:43:15 2012 +0100 @@ -55,7 +55,7 @@ from yams.schema import role_name from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary, - UniqueTogetherError) + UniqueTogetherError, QueryError, UndoTransactionException) from cubicweb import transaction as tx, server, neg_role from cubicweb.utils import QueryCache from cubicweb.schema import VIRTUAL_RTYPES @@ -65,7 +65,6 @@ from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn from cubicweb.server.rqlannotation import set_qdata from cubicweb.server.hook import CleanupDeletedEidsCacheOp -from cubicweb.server.session import hooks_control, security_enabled from cubicweb.server.edition import EditedEntity from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results from cubicweb.server.sources.rql2sql import SQLGenerator @@ -162,24 +161,24 @@ allownull = rdef.cardinality[0] != '1' return coltype, allownull -class UndoException(Exception): + +class _UndoException(Exception): """something went wrong during undoing""" def __unicode__(self): """Called by the unicode builtin; should return a Unicode object - Type of UndoException message must be `unicode` by design in CubicWeb. + Type of _UndoException message must be `unicode` by design in CubicWeb. + """ + assert isinstance(self.args[0], unicode) + return self.args[0] - .. warning:: - This method is not available in python2.5""" - assert isinstance(self.message, unicode) - return self.message def _undo_check_relation_target(tentity, rdef, role): """check linked entity has not been redirected for this relation""" card = rdef.role_cardinality(role) if card in '?1' and tentity.related(rdef.rtype, role): - raise UndoException(tentity._cw._( + raise _UndoException(tentity._cw._( "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which " "is already linked using this relation.") % {'role': neg_role(role), @@ -192,7 +191,7 @@ try: entities.append(session.entity_from_eid(eid)) except UnknownEid: - raise UndoException(session._( + raise _UndoException(session._( "Can't restore relation %(rtype)s, %(role)s entity %(eid)s" " doesn't exist anymore.") % {'role': session._(role), @@ -203,7 +202,7 @@ rschema = session.vreg.schema.rschema(rtype) rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)] except KeyError: - raise UndoException(session._( + raise _UndoException(session._( "Can't restore relation %(rtype)s between %(subj)s and " "%(obj)s, that relation does not exists anymore in the " "schema.") @@ -637,7 +636,7 @@ attrs = self.preprocess_entity(entity) sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs) self.doexec(session, sql, attrs) - if session.undoable_action('C', entity.__regid__): + if session.ertype_supports_undo(entity.__regid__): self._record_tx_action(session, 'tx_entity_actions', 'C', etype=entity.__regid__, eid=entity.eid) @@ -645,7 +644,7 @@ """replace an entity in the source""" with self._storage_handler(entity, 'updated'): attrs = self.preprocess_entity(entity) - if session.undoable_action('U', entity.__regid__): + if session.ertype_supports_undo(entity.__regid__): changes = self._save_attrs(session, entity, attrs) self._record_tx_action(session, 'tx_entity_actions', 'U', etype=entity.__regid__, eid=entity.eid, @@ -657,7 +656,7 @@ def delete_entity(self, session, entity): """delete an entity from the source""" with self._storage_handler(entity, 'deleted'): - if session.undoable_action('D', entity.__regid__): + if session.ertype_supports_undo(entity.__regid__): attrs = [SQL_PREFIX + r.type for r in entity.e_schema.subject_relations() if (r.final or r.inlined) and not r in VIRTUAL_RTYPES] @@ -672,14 +671,14 @@ def add_relation(self, session, subject, rtype, object, inlined=False): """add a relation to the source""" self._add_relations(session, rtype, [(subject, object)], inlined) - if session.undoable_action('A', rtype): + if session.ertype_supports_undo(rtype): self._record_tx_action(session, 'tx_relation_actions', 'A', eid_from=subject, rtype=rtype, eid_to=object) def add_relations(self, session, rtype, subj_obj_list, inlined=False): """add a relations to the source""" self._add_relations(session, rtype, subj_obj_list, inlined) - if session.undoable_action('A', rtype): + if session.ertype_supports_undo(rtype): for subject, object in subj_obj_list: self._record_tx_action(session, 'tx_relation_actions', 'A', eid_from=subject, rtype=rtype, eid_to=object) @@ -712,7 +711,7 @@ """delete a relation from the source""" rschema = self.schema.rschema(rtype) self._delete_relation(session, subject, rtype, object, rschema.inlined) - if session.undoable_action('R', rtype): + if session.ertype_supports_undo(rtype): self._record_tx_action(session, 'tx_relation_actions', 'R', eid_from=subject, rtype=rtype, eid_to=object) @@ -1157,16 +1156,18 @@ session.mode = 'write' errors = [] session.transaction_data['undoing_uuid'] = txuuid - with hooks_control(session, session.HOOKS_DENY_ALL, - 'integrity', 'activeintegrity', 'undo'): - with security_enabled(session, read=False): + with session.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'): + with session.security_enabled(read=False): for action in reversed(self.tx_actions(session, txuuid, False)): undomethod = getattr(self, '_undo_%s' % action.action.lower()) errors += undomethod(session, action) # remove the transactions record self.doexec(session, "DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid) - return errors + if errors: + raise UndoTransactionException(txuuid, errors) + else: + return def start_undoable_transaction(self, session, uuid): """session callback to insert a transaction record in the transactions @@ -1219,12 +1220,53 @@ try: time, ueid = cu.fetchone() except TypeError: - raise tx.NoSuchTransaction() + raise tx.NoSuchTransaction(txuuid) if not (session.user.is_in_group('managers') or session.user.eid == ueid): - raise tx.NoSuchTransaction() + raise tx.NoSuchTransaction(txuuid) return time, ueid + def _reedit_entity(self, entity, changes, err): + session = entity._cw + eid = entity.eid + entity.cw_edited = edited = EditedEntity(entity) + # check for schema changes, entities linked through inlined relation + # still exists, rewrap binary values + eschema = entity.e_schema + getrschema = eschema.subjrels + for column, value in changes.items(): + rtype = column[len(SQL_PREFIX):] + if rtype == "eid": + continue # XXX should even `eid` be stored in action changes? + try: + rschema = getrschema[rtype] + except KeyError: + err(session._("can't restore relation %(rtype)s of entity %(eid)s, " + "this relation does not exist in the schema anymore.") + % {'rtype': rtype, 'eid': eid}) + if not rschema.final: + if not rschema.inlined: + assert value is None + # rschema is an inlined relation + elif value is not None: + # not a deletion: we must put something in edited + try: + entity._cw.entity_from_eid(value) # check target exists + edited[rtype] = value + except UnknownEid: + err(session._("can't restore entity %(eid)s of type %(eschema)s, " + "target of %(rtype)s (eid %(value)s) does not exist any longer") + % locals()) + elif eschema.destination(rtype) in ('Bytes', 'Password'): + changes[column] = self._binary(value) + edited[rtype] = Binary(value) + elif isinstance(value, str): + edited[rtype] = unicode(value, session.encoding, 'replace') + else: + edited[rtype] = value + # This must only be done after init_entitiy_caches : defered in calling functions + # edited.check() + def _undo_d(self, session, action): """undo an entity deletion""" errors = [] @@ -1239,31 +1281,10 @@ err("can't restore entity %s of type %s, type no more supported" % (eid, etype)) return errors - entity.cw_edited = edited = EditedEntity(entity) - # check for schema changes, entities linked through inlined relation - # still exists, rewrap binary values - eschema = entity.e_schema - getrschema = eschema.subjrels - for column, value in action.changes.items(): - rtype = column[3:] # remove cw_ prefix - try: - rschema = getrschema[rtype] - except KeyError: - err(_("Can't restore relation %(rtype)s of entity %(eid)s, " - "this relation does not exists anymore in the schema.") - % {'rtype': rtype, 'eid': eid}) - if not rschema.final: - assert value is None - elif eschema.destination(rtype) in ('Bytes', 'Password'): - action.changes[column] = self._binary(value) - edited[rtype] = Binary(value) - elif isinstance(value, str): - edited[rtype] = unicode(value, session.encoding, 'replace') - else: - edited[rtype] = value + self._reedit_entity(entity, action.changes, err) entity.eid = eid session.repo.init_entity_caches(session, entity, self) - edited.check() + entity.cw_edited.check() self.repo.hm.call_hooks('before_add_entity', session, entity=entity) # restore the entity action.changes['cw_eid'] = eid @@ -1284,14 +1305,14 @@ subj, rtype, obj = action.eid_from, action.rtype, action.eid_to try: sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj) - except UndoException, ex: + except _UndoException, ex: errors.append(unicode(ex)) else: for role, entity in (('subject', sentity), ('object', oentity)): try: _undo_check_relation_target(entity, rdef, role) - except UndoException, ex: + except _UndoException, ex: errors.append(unicode(ex)) continue if not errors: @@ -1344,7 +1365,22 @@ def _undo_u(self, session, action): """undo an entity update""" - return ['undoing of entity updating not yet supported.'] + errors = [] + err = errors.append + try: + entity = session.entity_from_eid(action.eid) + except UnknownEid: + err(session._("can't restore state of entity %s, it has been " + "deleted inbetween") % action.eid) + return errors + self._reedit_entity(entity, action.changes, err) + entity.cw_edited.check() + self.repo.hm.call_hooks('before_update_entity', session, entity=entity) + sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, action.changes, + ['cw_eid']) + self.doexec(session, sql, action.changes) + self.repo.hm.call_hooks('after_update_entity', session, entity=entity) + return errors def _undo_a(self, session, action): """undo a relation addition""" @@ -1352,7 +1388,7 @@ subj, rtype, obj = action.eid_from, action.rtype, action.eid_to try: sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj) - except UndoException, ex: + except _UndoException, ex: errors.append(unicode(ex)) else: rschema = rdef.rtype diff -r 272e10526679 -r 9747ab9230ad server/test/unittest_undo.py --- a/server/test/unittest_undo.py Thu Feb 23 19:06:14 2012 +0100 +++ b/server/test/unittest_undo.py Mon Feb 27 09:43:15 2012 +0100 @@ -22,14 +22,14 @@ from cubicweb.devtools.testlib import CubicWebTC from cubicweb.transaction import * -from cubicweb.server.sources.native import UndoException +from cubicweb.server.sources.native import UndoTransactionException, _UndoException class UndoableTransactionTC(CubicWebTC): def setup_database(self): req = self.request() - self.session.undo_actions = set('CUDAR') + self.session.undo_actions = True self.toto = self.create_user(req, 'toto', password='toto', groups=('users',), commit=False) self.txuuid = self.commit() @@ -48,6 +48,17 @@ "SELECT * from tx_relation_actions WHERE tx_uuid='%s'" % txuuid) self.assertFalse(cu.fetchall()) + def assertUndoTransaction(self, txuuid, expected_errors=None): + if expected_errors is None : + expected_errors = [] + try: + self.cnx.undo_transaction(txuuid) + except UndoTransactionException, exn: + errors = exn.errors + else: + errors = [] + self.assertEqual(errors, expected_errors) + def test_undo_api(self): self.assertTrue(self.txuuid) # test transaction api @@ -155,10 +166,9 @@ self.assertEqual(len(actions), 1) toto.cw_clear_all_caches() e.cw_clear_all_caches() - errors = self.cnx.undo_transaction(txuuid) + self.assertUndoTransaction(txuuid) undotxuuid = self.commit() self.assertEqual(undotxuuid, None) # undo not undoable - self.assertEqual(errors, []) self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid})) self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid})) self.assertTrue(self.execute('Any X WHERE X has_text "toto@logilab"')) @@ -193,14 +203,12 @@ c2 = session.create_entity('Card', title=u'hip', content=u'hip') p.set_relations(fiche=c2) self.commit() - errors = self.cnx.undo_transaction(txuuid) + self.assertUndoTransaction(txuuid, [ + "Can't restore object relation fiche to entity " + "%s which is already linked using this relation." % p.eid]) self.commit() p.cw_clear_all_caches() self.assertEqual(p.fiche[0].eid, c2.eid) - self.assertEqual(len(errors), 1) - self.assertEqual(errors[0], - "Can't restore object relation fiche to entity " - "%s which is already linked using this relation." % p.eid) def test_undo_deletion_integrity_2(self): # test validation error raised if we can't restore a required relation @@ -213,10 +221,9 @@ txuuid = self.commit() g.cw_delete() self.commit() - errors = self.cnx.undo_transaction(txuuid) - self.assertEqual(errors, - [u"Can't restore relation in_group, object entity " - "%s doesn't exist anymore." % g.eid]) + self.assertUndoTransaction(txuuid, [ + u"Can't restore relation in_group, object entity " + "%s doesn't exist anymore." % g.eid]) with self.assertRaises(ValidationError) as cm: self.commit() self.assertEqual(cm.exception.entity, self.toto.eid) @@ -229,9 +236,8 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis', fiche=c) txuuid = self.commit() - errors = self.cnx.undo_transaction(txuuid) + self.assertUndoTransaction(txuuid) self.commit() - self.assertFalse(errors) self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': c.eid})) self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': p.eid})) self.assertFalse(self.execute('Any X,Y WHERE X fiche Y')) @@ -288,12 +294,135 @@ # test implicit 'replacement' of an inlined relation + def test_undo_inline_rel_remove_ok(self): + """Undo remove relation Personne (?) fiche (?) Card + + NB: processed by `_undo_r` as expected""" + session = self.session + c = session.create_entity('Card', title=u'hop', content=u'hop') + p = session.create_entity('Personne', nom=u'louis', fiche=c) + self.commit() + p.set_relations(fiche=None) + txuuid = self.commit() + self.assertUndoTransaction(txuuid) + self.commit() + p.cw_clear_all_caches() + self.assertEqual(p.fiche[0].eid, c.eid) + + def test_undo_inline_rel_remove_ko(self): + """Restore an inlined relation to a deleted entity, with an error. + + NB: processed by `_undo_r` as expected""" + session = self.session + c = session.create_entity('Card', title=u'hop', content=u'hop') + p = session.create_entity('Personne', nom=u'louis', fiche=c) + self.commit() + p.set_relations(fiche=None) + txuuid = self.commit() + c.cw_delete() + self.commit() + self.assertUndoTransaction(txuuid, [ + "Can't restore relation fiche, object entity %d doesn't exist anymore." % c.eid]) + self.commit() + p.cw_clear_all_caches() + self.assertFalse(p.fiche) + self.assertIsNone(session.system_sql( + 'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0]) + + def test_undo_inline_rel_add_ok(self): + """Undo add relation Personne (?) fiche (?) Card + + Caution processed by `_undo_u`, not `_undo_a` !""" + session = self.session + c = session.create_entity('Card', title=u'hop', content=u'hop') + p = session.create_entity('Personne', nom=u'louis') + self.commit() + p.set_relations(fiche=c) + txuuid = self.commit() + self.assertUndoTransaction(txuuid) + self.commit() + p.cw_clear_all_caches() + self.assertFalse(p.fiche) + + def test_undo_inline_rel_add_ko(self): + """Undo add relation Personne (?) fiche (?) Card + + Caution processed by `_undo_u`, not `_undo_a` !""" + session = self.session + c = session.create_entity('Card', title=u'hop', content=u'hop') + p = session.create_entity('Personne', nom=u'louis') + self.commit() + p.set_relations(fiche=c) + txuuid = self.commit() + c.cw_delete() + self.commit() + self.assertUndoTransaction(txuuid) + + def test_undo_inline_rel_replace_ok(self): + """Undo changing relation Personne (?) fiche (?) Card + + Caution processed by `_undo_u` """ + session = self.session + c1 = session.create_entity('Card', title=u'hop', content=u'hop') + c2 = session.create_entity('Card', title=u'hip', content=u'hip') + p = session.create_entity('Personne', nom=u'louis', fiche=c1) + self.commit() + p.set_relations(fiche=c2) + txuuid = self.commit() + self.assertUndoTransaction(txuuid) + self.commit() + p.cw_clear_all_caches() + self.assertEqual(p.fiche[0].eid, c1.eid) + + def test_undo_inline_rel_replace_ko(self): + """Undo changing relation Personne (?) fiche (?) Card, with an error + + Caution processed by `_undo_u` """ + session = self.session + c1 = session.create_entity('Card', title=u'hop', content=u'hop') + c2 = session.create_entity('Card', title=u'hip', content=u'hip') + p = session.create_entity('Personne', nom=u'louis', fiche=c1) + self.commit() + p.set_relations(fiche=c2) + txuuid = self.commit() + c1.cw_delete() + self.commit() + self.assertUndoTransaction(txuuid, [ + "can't restore entity %s of type Personne, target of fiche (eid %s)" + " does not exist any longer" % (p.eid, c1.eid)]) + self.commit() + p.cw_clear_all_caches() + self.assertFalse(p.fiche) + + def test_undo_attr_update_ok(self): + session = self.session + p = session.create_entity('Personne', nom=u'toto') + session.commit() + self.session.set_cnxset() + p.set_attributes(nom=u'titi') + txuuid = self.commit() + self.assertUndoTransaction(txuuid) + p.cw_clear_all_caches() + self.assertEqual(p.nom, u'toto') + + def test_undo_attr_update_ko(self): + session = self.session + p = session.create_entity('Personne', nom=u'toto') + session.commit() + self.session.set_cnxset() + p.set_attributes(nom=u'titi') + txuuid = self.commit() + p.cw_delete() + self.commit() + self.assertUndoTransaction(txuuid, [ + u"can't restore state of entity %s, it has been deleted inbetween" % p.eid]) + class UndoExceptionInUnicode(CubicWebTC): # problem occurs in string manipulation for python < 2.6 def test___unicode__method(self): - u = UndoException(u"voilà") + u = _UndoException(u"voilà") self.assertIsInstance(unicode(u), unicode) diff -r 272e10526679 -r 9747ab9230ad transaction.py --- a/transaction.py Thu Feb 23 19:06:14 2012 +0100 +++ b/transaction.py Mon Feb 27 09:43:15 2012 +0100 @@ -39,8 +39,12 @@ class NoSuchTransaction(RepositoryError): - pass + # Used by CubicWebException + msg = _("there is no transaction #%s") + def __init__(self, txuuid): + super(RepositoryError, self).__init__(txuuid) + self.txuuid = txuuid class Transaction(object): """an undoable transaction"""