[web] Exposes the undo feature to user through a undo-history view (closes #893940)
--- 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"
--- 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 ""
--- 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"
--- 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 <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.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"
--- 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'<span class="undo">[<a href="%s">%s</a>]</span>' %(
- 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'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+ 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:
--- 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()
--- 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
--- 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)) )
+
--- /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 <http://www.gnu.org/licenses/>.
+
+__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"<ul class='undo-transactions'>")
+ for tx in txs:
+ self.cell_call(tx)
+ self.w(u"</ul>")
+
+ def cell_call(self, tx):
+ self.w(u'<li>')
+ 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'</li>\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"<span class='undo'>%s</span>" % 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"<ol class='undo-actions'>")
+ for action in tx_actions:
+ self.cell_call(action)
+ self.w(u"</ol>")
+
+ def cell_call(self, action):
+ self.w(u'<li>')
+ self.wview(self.item_vid, None, tx_action=action)
+ self.w(u'</li>\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))