# HG changeset patch # User Anthony Truchet # Date 1330333411 -3600 # Node ID 486386d9f8362c54f7874c3b512191fc3593d7dd # Parent 704dda5c07f746822c696bb85fa726423573c881 [web] Exposes the undo feature to user through a undo-history view (closes #893940) diff -r 704dda5c07f7 -r 486386d9f836 i18n/de.po --- a/i18n/de.po Mon Feb 27 09:45:48 2012 +0100 +++ b/i18n/de.po Mon Feb 27 10:03:31 2012 +0100 @@ -145,6 +145,10 @@ msgid "(UNEXISTANT EID)" msgstr "(EID nicht gefunden)" +#, python-format +msgid "(suppressed) entity #%d" +msgstr "" + msgid "**" msgstr "0..n 0..n" @@ -218,6 +222,13 @@ msgid "About this site" msgstr "Über diese Seite" +msgid "Action" +msgstr "" + +#, python-format +msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "" + msgid "Any" msgstr "irgendein" @@ -415,6 +426,10 @@ msgid "Click to sort on this column" msgstr "" +#, python-format +msgid "Created %(etype)s : %(entity)s" +msgstr "" + msgid "DEBUG" msgstr "" @@ -440,6 +455,14 @@ msgid "Decimal_plural" msgstr "Dezimalzahlen" +#, python-format +msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "" + +#, python-format +msgid "Deleted %(etype)s : %(entity)s" +msgstr "" + msgid "Detected problems" msgstr "" @@ -751,6 +774,9 @@ msgid "TZTime_plural" msgstr "" +msgid "Target" +msgstr "" + #, python-format msgid "The view %s can not be applied to this query" msgstr "Die Ansicht %s ist auf diese Anfrage nicht anwendbar." @@ -864,15 +890,29 @@ msgid "URLs from which content will be imported. You can put one url per line" msgstr "" +msgid "Undoable actions" +msgstr "" + +msgid "Undoing" +msgstr "" + msgid "UniqueConstraint" msgstr "eindeutige Einschränkung" msgid "Unreachable objects" msgstr "unzugängliche Objekte" +#, python-format +msgid "Updated %(etype)s : %(entity)s" +msgstr "" + msgid "Used by:" msgstr "benutzt von:" +#, python-format +msgid "User %(user_eid)s on %(dt)s [%(undo_link)s] \n" +msgstr "" + msgid "Users and groups management" msgstr "" @@ -2044,6 +2084,9 @@ msgid "default" msgstr "Standardwert" +msgid "default /base view for (transaction) actions " +msgstr "" + msgid "default text format for rich text fields." msgstr "Standardformat für Textfelder" diff -r 704dda5c07f7 -r 486386d9f836 i18n/en.po --- a/i18n/en.po Mon Feb 27 09:45:48 2012 +0100 +++ b/i18n/en.po Mon Feb 27 10:03:31 2012 +0100 @@ -137,6 +137,10 @@ msgid "(UNEXISTANT EID)" msgstr "" +#, python-format +msgid "(suppressed) entity #%d" +msgstr "" + msgid "**" msgstr "0..n 0..n" @@ -207,6 +211,13 @@ msgid "About this site" msgstr "" +msgid "Action" +msgstr "" + +#, python-format +msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "" + msgid "Any" msgstr "" @@ -393,6 +404,10 @@ msgid "Click to sort on this column" msgstr "" +#, python-format +msgid "Created %(etype)s : %(entity)s" +msgstr "" + msgid "DEBUG" msgstr "" @@ -418,6 +433,14 @@ msgid "Decimal_plural" msgstr "Decimal numbers" +#, python-format +msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "" + +#, python-format +msgid "Deleted %(etype)s : %(entity)s" +msgstr "" + msgid "Detected problems" msgstr "" @@ -727,6 +750,9 @@ msgid "TZTime_plural" msgstr "International times" +msgid "Target" +msgstr "" + #, python-format msgid "The view %s can not be applied to this query" msgstr "" @@ -840,15 +866,29 @@ msgid "URLs from which content will be imported. You can put one url per line" msgstr "" +msgid "Undoable actions" +msgstr "" + +msgid "Undoing" +msgstr "" + msgid "UniqueConstraint" msgstr "unique constraint" msgid "Unreachable objects" msgstr "" +#, python-format +msgid "Updated %(etype)s : %(entity)s" +msgstr "" + msgid "Used by:" msgstr "" +#, python-format +msgid "User %(user_eid)s on %(dt)s [%(undo_link)s] \n" +msgstr "" + msgid "Users and groups management" msgstr "" @@ -2001,6 +2041,9 @@ msgid "default" msgstr "" +msgid "default /base view for (transaction) actions " +msgstr "" + msgid "default text format for rich text fields." msgstr "" diff -r 704dda5c07f7 -r 486386d9f836 i18n/es.po --- a/i18n/es.po Mon Feb 27 09:45:48 2012 +0100 +++ b/i18n/es.po Mon Feb 27 10:03:31 2012 +0100 @@ -146,6 +146,10 @@ msgid "(UNEXISTANT EID)" msgstr "(EID INEXISTENTE" +#, python-format +msgid "(suppressed) entity #%d" +msgstr "" + msgid "**" msgstr "0..n 0..n" @@ -219,6 +223,13 @@ msgid "About this site" msgstr "Información del Sistema" +msgid "Action" +msgstr "" + +#, python-format +msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "" + msgid "Any" msgstr "Cualquiera" @@ -415,6 +426,10 @@ msgid "Click to sort on this column" msgstr "" +#, python-format +msgid "Created %(etype)s : %(entity)s" +msgstr "" + msgid "DEBUG" msgstr "" @@ -440,6 +455,14 @@ msgid "Decimal_plural" msgstr "Decimales" +#, python-format +msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "" + +#, python-format +msgid "Deleted %(etype)s : %(entity)s" +msgstr "" + msgid "Detected problems" msgstr "Problemas detectados" @@ -752,6 +775,9 @@ msgid "TZTime_plural" msgstr "Horas internacionales" +msgid "Target" +msgstr "" + #, python-format msgid "The view %s can not be applied to this query" msgstr "La vista %s no puede ser aplicada a esta búsqueda" @@ -867,15 +893,29 @@ "URLs desde el cual el contenido sera importado. Usted puede incluir un URL " "por línea." +msgid "Undoable actions" +msgstr "" + +msgid "Undoing" +msgstr "" + msgid "UniqueConstraint" msgstr "Restricción de Unicidad" msgid "Unreachable objects" msgstr "Objetos inaccesibles" +#, python-format +msgid "Updated %(etype)s : %(entity)s" +msgstr "" + msgid "Used by:" msgstr "Utilizado por :" +#, python-format +msgid "User %(user_eid)s on %(dt)s [%(undo_link)s] \n" +msgstr "" + msgid "Users and groups management" msgstr "Usuarios y grupos de administradores" @@ -2073,6 +2113,9 @@ msgid "default" msgstr "Valor por defecto" +msgid "default /base view for (transaction) actions " +msgstr "" + msgid "default text format for rich text fields." msgstr "" "Formato de texto que se utilizará por defecto para los campos de tipo texto" diff -r 704dda5c07f7 -r 486386d9f836 i18n/fr.po --- a/i18n/fr.po Mon Feb 27 09:45:48 2012 +0100 +++ b/i18n/fr.po Mon Feb 27 10:03:31 2012 +0100 @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: cubicweb 2.46.0\n" -"PO-Revision-Date: 2012-02-08 17:43+0100\n" +"PO-Revision-Date: 2012-02-15 16:08+0100\n" "Last-Translator: Logilab Team \n" "Language-Team: fr \n" "Language: \n" @@ -147,6 +147,10 @@ msgid "(UNEXISTANT EID)" msgstr "(EID INTROUVABLE)" +#, python-format +msgid "(suppressed) entity #%d" +msgstr "entité #%d (supprimée)" + msgid "**" msgstr "0..n 0..n" @@ -219,6 +223,13 @@ msgid "About this site" msgstr "À propos de ce site" +msgid "Action" +msgstr "Action" + +#, python-format +msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s" + msgid "Any" msgstr "Tous" @@ -415,6 +426,10 @@ msgid "Click to sort on this column" msgstr "Cliquer pour trier sur cette colonne" +#, python-format +msgid "Created %(etype)s : %(entity)s" +msgstr "Entité %(etype)s crée : %(entity)s" + msgid "DEBUG" msgstr "DEBUG" @@ -440,6 +455,14 @@ msgid "Decimal_plural" msgstr "Nombres décimaux" +#, python-format +msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s" +msgstr "Relation supprimée : %(entity_from)s %(rtype)s %(entity_to)s" + +#, python-format +msgid "Deleted %(etype)s : %(entity)s" +msgstr "Entité %(etype)s supprimée : %(entity)s" + msgid "Detected problems" msgstr "Problèmes détectés" @@ -752,6 +775,9 @@ msgid "TZTime_plural" msgstr "Heures internationales" +msgid "Target" +msgstr "" + #, python-format msgid "The view %s can not be applied to this query" msgstr "La vue %s ne peut être appliquée à cette requête" @@ -867,15 +893,29 @@ "URLs depuis lesquelles le contenu sera importé. Vous pouvez mettre une URL " "par ligne." +msgid "Undoable actions" +msgstr "Action annulables" + +msgid "Undoing" +msgstr "Annuler" + msgid "UniqueConstraint" msgstr "contrainte d'unicité" msgid "Unreachable objects" msgstr "Objets inaccessibles" +#, python-format +msgid "Updated %(etype)s : %(entity)s" +msgstr "Entité %(etype)s mise à jour : %(entity)s" + msgid "Used by:" msgstr "Utilisé par :" +#, python-format +msgid "By %(user)s on %(dt)s [%(undo_link)s]" +msgstr "Par %(user)s le %(dt)s [%(undo_link)s] " + msgid "Users and groups management" msgstr "Gestion des utilisateurs et groupes" @@ -1398,17 +1438,23 @@ 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" +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." +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" +msgstr "" +"impossible de rétablir l'état de l'entité %s, elle a été supprimée entre-" +"temps" #, python-format msgid "" @@ -2079,6 +2125,9 @@ msgid "default" msgstr "valeur par défaut" +msgid "default /base view for (transaction) actions " +msgstr "" + msgid "default text format for rich text fields." msgstr "format de texte par défaut pour les champs textes" diff -r 704dda5c07f7 -r 486386d9f836 web/application.py --- a/web/application.py Mon Feb 27 09:45:48 2012 +0100 +++ b/web/application.py Mon Feb 27 10:03:31 2012 +0100 @@ -371,7 +371,9 @@ # no req.cnx if anonymous aren't allowed and we are # displaying some anonymous enabled view such as the cookie # authentication form - req.cnx.commit() + txuuid = req.cnx.commit() + if txuuid is not None: + req.data['last_undoable_transaction'] = txuuid commited = True except (StatusResponse, DirectResponse): if req.cnx: @@ -386,9 +388,7 @@ if req.cnx: txuuid = req.cnx.commit() if txuuid is not None: - msg = u'[%s]' %( - req.build_url('undo', txuuid=txuuid), req._('undo')) - req.append_to_redirect_message(msg) + req.data['last_undoable_transaction'] = txuuid except ValidationError, ex: self.validation_error_handler(req, ex) except Unauthorized, ex: @@ -398,6 +398,7 @@ except Exception, ex: self.error_handler(req, ex, tb=True) else: + self.add_undo_link_to_msg(req) # delete validation errors which may have been previously set if '__errorurl' in req.form: req.session.data.pop(req.form['__errorurl'], None) @@ -425,9 +426,17 @@ req.cnx.rollback() except Exception: pass # ignore rollback error at this point + self.add_undo_link_to_msg(req) self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart) return result + def add_undo_link_to_msg(self, req): + txuuid = req.data.get('last_undoable_transaction') + if txuuid is not None: + msg = u'[%s]' %( + req.build_url('undo', txuuid=txuuid), req._('undo')) + req.append_to_redirect_message(msg) + def validation_error_handler(self, req, ex): ex.errors = dict((k, v) for k, v in ex.errors.items()) if '__errorurl' in req.form: diff -r 704dda5c07f7 -r 486386d9f836 web/test/unittest_views_basecontrollers.py --- a/web/test/unittest_views_basecontrollers.py Mon Feb 27 09:45:48 2012 +0100 +++ b/web/test/unittest_views_basecontrollers.py Mon Feb 27 10:03:31 2012 +0100 @@ -19,6 +19,12 @@ from __future__ import with_statement +from urlparse import urlsplit, urlunsplit, urljoin +# parse_qs is deprecated in cgi and has been moved to urlparse in Python 2.6 +try: + from urlparse import parse_qs as url_parse_query +except ImportError: + from cgi import parse_qs as url_parse_query from logilab.common.testlib import unittest_main, mock_object from logilab.common.decorators import monkeypatch @@ -32,6 +38,7 @@ from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction +import cubicweb.transaction as tx u = unicode @@ -768,5 +775,66 @@ res, req = self.remote_call('foo') self.assertEqual(res, '12') + + + + +class UndoControllerTC(CubicWebTC): + + def setup_database(self): + req = self.request() + self.session.undo_actions = True + self.toto = self.create_user(req, 'toto', password='toto', groups=('users',), + commit=False) + self.txuuid_toto = self.commit() + self.toto_email = self.session.create_entity('EmailAddress', + address=u'toto@logilab.org', + reverse_use_email=self.toto) + self.txuuid_toto_email = self.commit() + + def test_no_such_transaction(self): + req = self.request() + txuuid = u"12345acbd" + req.form['txuuid'] = txuuid + controller = self.vreg['controllers'].select('undo', req) + with self.assertRaises(tx.NoSuchTransaction) as cm: + result = controller.publish(rset=None) + self.assertEqual(cm.exception.txuuid, txuuid) + + def assertURLPath(self, url, expected_path, expected_params=None): + """ This assert that the path part of `url` matches expected path + + TODO : implement assertion on the expected_params too + """ + req = self.request() + scheme, netloc, path, query, fragment = urlsplit(url) + query_dict = url_parse_query(query) + expected_url = urljoin(req.base_url(), expected_path) + self.assertEqual( urlunsplit((scheme, netloc, path, None, None)), expected_url) + + def test_redirect_redirectpath(self): + "Check that the potential __redirectpath is honored" + req = self.request() + txuuid = self.txuuid_toto_email + req.form['txuuid'] = txuuid + rpath = "toto" + req.form['__redirectpath'] = rpath + controller = self.vreg['controllers'].select('undo', req) + with self.assertRaises(Redirect) as cm: + result = controller.publish(rset=None) + self.assertURLPath(cm.exception.location, rpath) + + def test_redirect_default(self): + req = self.request() + txuuid = self.txuuid_toto_email + req.form['txuuid'] = txuuid + req.session.data['breadcrumbs'] = [ urljoin(req.base_url(), path) + for path in ('tata', 'toto',)] + controller = self.vreg['controllers'].select('undo', req) + with self.assertRaises(Redirect) as cm: + result = controller.publish(rset=None) + self.assertURLPath(cm.exception.location, 'toto') + + if __name__ == '__main__': unittest_main() diff -r 704dda5c07f7 -r 486386d9f836 web/views/actions.py --- a/web/views/actions.py Mon Feb 27 09:45:48 2012 +0100 +++ b/web/views/actions.py Mon Feb 27 10:03:31 2012 +0100 @@ -82,6 +82,18 @@ return 1 return 0 +class has_undoable_transactions(EntityPredicate): + "Select entities having public (i.e. end-user) undoable transactions." + + def score_entity(self, entity): + if not entity._cw.vreg.config['undo-support']: + return 0 + if entity._cw.cnx.undoable_transactions(eid=entity.eid): + return 1 + else: + return 0 + + # generic 'main' actions ####################################################### class SelectAction(action.Action): @@ -420,6 +432,7 @@ self._cw.add_js('cubicweb.rhythm.js') return 'rhythm' + ## default actions ui configuration ########################################### addmenu = uicfg.actionbox_appearsin_addmenu diff -r 704dda5c07f7 -r 486386d9f836 web/views/basecontrollers.py --- a/web/views/basecontrollers.py Mon Feb 27 09:45:48 2012 +0100 +++ b/web/views/basecontrollers.py Mon Feb 27 10:03:31 2012 +0100 @@ -27,14 +27,14 @@ from logilab.common.deprecation import deprecated from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, - AuthenticationError, typed_eid) + AuthenticationError, typed_eid, UndoTransactionException) from cubicweb.utils import json_dumps from cubicweb.predicates import (authenticated_user, anonymous_user, match_form_params) from cubicweb.web import Redirect, RemoteCallFailed -from cubicweb.web.controller import Controller +from cubicweb.web.controller import Controller, append_url_params from cubicweb.web.views import vid_from_rset - +import cubicweb.transaction as tx @deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead') def jsonize(func): @@ -203,7 +203,7 @@ return (False, _validation_error(req, ex), ctrl._edited_entity) except Redirect, ex: try: - req.cnx.commit() # ValidationError may be raise on commit + txuuid = req.cnx.commit() # ValidationError may be raised on commit except ValidationError, ex: return (False, _validation_error(req, ex), ctrl._edited_entity) except Exception, ex: @@ -211,6 +211,8 @@ req.exception('unexpected error while validating form') return (False, str(ex).decode('utf-8'), ctrl._edited_entity) else: + if txuuid is not None: + req.data['last_undoable_transaction'] = txuuid # complete entity: it can be used in js callbacks where we might # want every possible information if ctrl._edited_entity: @@ -275,17 +277,17 @@ def publish(self, rset=None): txuuid = self._cw.form['txuuid'] - errors = self._cw.cnx.undo_transaction(txuuid) - if not errors: - self.redirect() - raise ValidationError(None, {None: '\n'.join(errors)}) + try: + self._cw.cnx.undo_transaction(txuuid) + except UndoTransactionException, exc: + errors = exc.errors + #This will cause a rollback in main_publish + raise ValidationError(None, {None: '\n'.join(errors)}) + else : + self.redirect() # Will raise Redirect def redirect(self, msg=None): req = self._cw msg = msg or req._("transaction undone") - breadcrumbs = req.session.data.get('breadcrumbs', None) - if breadcrumbs is not None and len(breadcrumbs) > 1: - url = req.rebuild_url(breadcrumbs[-2], __message=msg) - else: - url = req.build_url(__message=msg) - raise Redirect(url) + self._return_to_lastpage( dict(_cwmsgid= req.set_redirect_message(msg)) ) + diff -r 704dda5c07f7 -r 486386d9f836 web/views/undohistory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/views/undohistory.py Mon Feb 27 10:03:31 2012 +0100 @@ -0,0 +1,225 @@ +# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . + +__docformat__ = "restructuredtext en" +_ = unicode + + +from logilab.common.registry import Predicate, yes + +from cubicweb import UnknownEid, tags, transaction as tx +from cubicweb.view import View, StartupView +from cubicweb.predicates import match_kwargs, ExpectedValuePredicate +from cubicweb.schema import display_name + + +class undoable_action(Predicate): + """Select only undoable actions depending on filters provided. Undo Action + is expected to be specified by the `tx_action` argument. + + Currently the only implemented filter is: + + :param action_type: chars among CUDAR (standing for Create, Update, Delete, + Add, Remove) + """ + + # XXX FIXME : this selector should be completed to allow selection on the + # entity or relation types and public / private. + def __init__(self, action_type='CUDAR'): + assert not set(action_type) & set('CUDAR') + self.action_type = action_type + + def __str__(self): + return '%s(%s)' % (self.__class__.__name__, ', '.join( + "%s=%v" % (str(k), str(v)) for k, v in kwargs.iteritems() )) + + def __call__(self, cls, req, tx_action=None, **kwargs): + # tx_action is expected to be a transaction.AbstractAction + if not isinstance(tx_action, tx.AbstractAction): + return 0 + # Filter according to action type + return int(tx_action.action in self.action_type) + + +class UndoHistoryView(StartupView): + __regid__ = 'undohistory' + __select__ = yes() + title = _('Undoing') + item_vid = 'undoable-transaction-view' + cache_max_age = 0 + + redirect_path = 'view' #TODO + redirect_params = dict(vid='undohistory') #TODO + public_actions_only = True + + # TODO Allow to choose if if want all actions or only the public ones + # (default) + + def call(self, **kwargs): + txs = self._cw.cnx.undoable_transactions() + if txs : + self.w(u"
    ") + for tx in txs: + self.cell_call(tx) + self.w(u"
") + + def cell_call(self, tx): + self.w(u'
  • ') + self.wview(self.item_vid, None, txuuid=tx.uuid, + public=self.public_actions_only, + redirect_path=self.redirect_path, + redirect_params=self.redirect_params) + self.w(u'
  • \n') + + +class UndoableTransactionView(View): + __regid__ = 'undoable-transaction-view' + __select__ = View.__select__ & match_kwargs('txuuid') + + item_vid = 'undoable-action-list-view' + cache_max_age = 0 + + def build_undo_link(self, txuuid, + redirect_path=None, redirect_params=None): + """ the kwargs are passed to build_url""" + _ = self._cw._ + redirect = {} + if redirect_path: + redirect['__redirectpath'] = redirect_path + if redirect_params: + if isinstance(redirect_params, dict): + redirect['__redirectparams'] = self._cw.build_url_params(**redirect_params) + else: + redirect['__redirectparams'] = redirect_params + link_url = self._cw.build_url('undo', txuuid=txuuid, **redirect) + msg = u"%s" % tags.a( _('undo'), href=link_url) + return msg + + def call(self, txuuid, public=True, + redirect_path=None, redirect_params=None): + _ = self._cw._ + txinfo = self._cw.cnx.transaction_info(txuuid) + try: + #XXX Under some unknown circumstances txinfo.user_eid=-1 + user = self._cw.entity_from_eid(txinfo.user_eid) + except UnknownEid: + user = None + undo_url = self.build_undo_link(txuuid, + redirect_path=redirect_path, + redirect_params=redirect_params) + txinfo_dict = dict( dt = self._cw.format_date(txinfo.datetime, time=True), + user_eid = txinfo.user_eid, + user = user and user.view('outofcontext') or _("undefined user"), + txuuid = txuuid, + undo_link = undo_url) + self.w( _("By %(user)s on %(dt)s [%(undo_link)s]") % txinfo_dict) + + tx_actions = txinfo.actions_list(public=public) + if tx_actions : + self.wview(self.item_vid, None, tx_actions=tx_actions) + + +class UndoableActionListView(View): + __regid__ = 'undoable-action-list-view' + __select__ = View.__select__ & match_kwargs('tx_actions') + title = _('Undoable actions') + item_vid = 'undoable-action-view' + cache_max_age = 0 + + def call(self, tx_actions): + if tx_actions : + self.w(u"
      ") + for action in tx_actions: + self.cell_call(action) + self.w(u"
    ") + + def cell_call(self, action): + self.w(u'
  • ') + self.wview(self.item_vid, None, tx_action=action) + self.w(u'
  • \n') + + +class UndoableActionBaseView(View): + __regid__ = 'undoable-action-view' + __abstract__ = True + + def call(self, tx_action): + raise NotImplementedError(self) + + def _build_entity_link(self, eid): + try: + entity = self._cw.entity_from_eid(eid) + return entity.view('outofcontext') + except UnknownEid: + return _("(suppressed) entity #%d") % eid + + def _build_relation_info(self, rtype, eid_from, eid_to): + return dict( rtype=display_name(self._cw, rtype), + entity_from=self._build_entity_link(eid_from), + entity_to=self._build_entity_link(eid_to) ) + + def _build_entity_info(self, etype, eid, changes): + return dict( etype=display_name(self._cw, etype), + entity=self._build_entity_link(eid), + eid=eid, + changes=changes) + + +class UndoableAddActionView(UndoableActionBaseView): + __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='A') + + def call(self, tx_action): + _ = self._cw._ + self.w(_("Added relation : %(entity_from)s %(rtype)s %(entity_to)s") % + self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to)) + + +class UndoableRemoveActionView(UndoableActionBaseView): + __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='R') + + def call(self, tx_action): + _ = self._cw._ + self.w(_("Delete relation : %(entity_from)s %(rtype)s %(entity_to)s") % + self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to)) + + +class UndoableCreateActionView(UndoableActionBaseView): + __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='C') + + def call(self, tx_action): + _ = self._cw._ + self.w(_("Created %(etype)s : %(entity)s") % # : %(changes)s + self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes) ) + + +class UndoableDeleteActionView(UndoableActionBaseView): + __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='D') + + def call(self, tx_action): + _ = self._cw._ + self.w(_("Deleted %(etype)s : %(entity)s") % + self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes)) + + +class UndoableUpdateActionView(UndoableActionBaseView): + __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='U') + + def call(self, tx_action): + _ = self._cw._ + self.w(_("Updated %(etype)s : %(entity)s") % + self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))