[Zope3-checkins] CVS: Zope3/src/zope/app/undo - undo_all.pt:1.1
undo_macros.pt:1.1 undo_more.pt:1.1 __init__.py:1.3
browser.py:1.2 configure.zcml:1.4 interfaces.py:1.3 undo_log.pt:NONE
Philipp von Weitershausen
philikon at philikon.de
Sun Mar 21 12:21:02 EST 2004
Update of /cvs-repository/Zope3/src/zope/app/undo
In directory cvs.zope.org:/tmp/cvs-serv20580/src/zope/app/undo
Modified Files:
__init__.py browser.py configure.zcml interfaces.py
Added Files:
undo_all.pt undo_macros.pt undo_more.pt
Removed Files:
undo_log.pt
Log Message:
Implement steps 4 and 5 of
http://dev.zope.org/Zope3/SimplifyUndoModel:
- Provided new interfaces IUndo, IPrincipalUndo, IUndoManager
- Changed ZODBUndoManager to implement IUndoManager.
- Implemented a new UI for ZODBUndoManager
=== Added File Zope3/src/zope/app/undo/undo_all.pt ===
<html metal:use-macro="context/@@standard_macros/page">
<body>
<div metal:fill-slot="body">
<h2 i18n:translate="">Undo all</h2>
<p i18n:translate="">This form lets you undo all transactions
initiated by any user.</p>
<p i18n:translate="">Select one or more transactions from the list
below and click the button below. Please be aware that you may only
undo a transaction if the object has not been modified in a later
transaction by you or any other user.</p>
<tal:var define="global macros nocall:context/@@undo_macros" />
<div metal:use-macro="macros/global_vars" />
<div metal:use-macro="macros/location_link" />
<form action="@@undoAllTransactions.html" method="post">
<div metal:use-macro="macros/previous_batch" />
<div metal:use-macro="macros/undo_log">
<div metal:fill-slot="define_info">
<tal:var define="global undo_info python:view.getAllTransactions(
first=first, last=-batch_size, showall=showall)" />
</div>
</div>
<div metal:use-macro="macros/submit_button" />
<div metal:use-macro="macros/next_batch" />
</form>
</div>
</body>
</html>
=== Added File Zope3/src/zope/app/undo/undo_macros.pt ===
<html>
<body>
<!-- This file contains macros common to undo views. -->
<div metal:define-macro="global_vars">
<tal:var define="global batch_size python:10;
global first python:int(request.get('first', 0));
global showall python:bool(request.get('showall', False))"
/>
</div>
<div metal:define-macro="location_link">
<span tal:condition="showall">
<p>
<span i18n:translate="">You are looking at transactions
regardless of location.</span> <a href="?"
i18n:translate="">View only transactions in this location</a>.
</p>
</span>
<span tal:condition="not:showall">
<p>
<span i18n:translate="">You are looking only at transactions
from this location.</span> <a href="?showall=true"
i18n:translate="">View transactions regardless of location</a>.
</p>
</span>
</div>
<div metal:define-macro="undo_log">
<!-- We expect the list of undo information in the global
'undo_info' variable -->
<div metal:define-slot="define_info">
<tal:var define="global undo_info python:[]" />
</div>
<table style="width: 100%; border: none;">
<tr>
<th></th>
<th i18n:translate="heading-location">Location</th>
<th i18n:translate="heading-request-info">Request info</th>
<th i18n:translate="heading-principal">Principal</th>
<th i18n:translate="heading-date">Date</th>
<th i18n:translate="heading-description">Description</th>
</tr>
<tal:block repeat="item undo_info">
<tr tal:attributes="class python:repeat['item'].odd() and
'content odd' or 'content even'">
<td width="16">
<input type="checkbox" name="ids:list" value="-1"
tal:attributes="value item/id" />
</td>
<td tal:define="location item/location | nothing">
<tal:location replace="location" />
<tal:if condition="not:location"
i18n:translate="label-not-available">not available</tal:if>
</td>
<td>
<tal:request_info replace="item/request_type | nothing" /><br />
<tal:request_info replace="item/request_info | nothing" />
<tal:if condition="python:not item.get('request_type', '') and
not item.get('request_info', '')"
i18n:translate="label-not-available">not available</tal:if>
</td>
<td>
<tal:principal replace="item/principal/id | nothing" />
<tal:if condition="not:exists:item/principal/id"
i18n:translate="label-not-available">not available</tal:if>
</td>
<td tal:define="formatter python:request.locale.dates.getFormatter(
'dateTime', 'medium')"
tal:content="python:formatter.format(item['datetime'])">
</td>
<td>
<tal:description replace="item/description | nothing" />
<tal:if condition="not:item/description"
i18n:translate="label-not-available">not available</tal:if>
</td>
</tr>
</tal:block>
</table>
</div>
<p metal:define-macro="next_batch">
<a tal:define="showall python:showall and '&showall=true' or ''"
tal:attributes="href python:'?first=%s%s'%(first+batch_size, showall)">
<<<
<tal:text i18n:translate="">
View <tal:num replace="batch_size" i18n:name="number" />
earlier transactions
</tal:text>
</a>
</p>
<p metal:define-macro="previous_batch">
<a tal:define="showall python:showall and '&showall=true' or ''"
tal:condition="python:first >= batch_size"
tal:attributes="href python:'?first=%s%s'%(first-batch_size, showall)">
<tal:text i18n:translate="">
View <tal:num tal:replace="batch_size" i18n:name="number" />
later transactions
</tal:text>
>>>
</a>
</p>
<p metal:define-macro="submit_button">
<input type="submit" value="Undo"
i18n:attributes="value undo-button" />
</p>
</body>
</html>
=== Added File Zope3/src/zope/app/undo/undo_more.pt ===
<html metal:use-macro="context/@@standard_macros/page">
<body>
<div metal:fill-slot="body">
<h2 i18n:translate="">Undo more</h2>
<p i18n:translate="">This form lets you undo your last
transactions. You are only viewing transactions initiated by you.</p>
<p i18n:translate="">Select one or more transactions from the list
below and click the button below. Please be aware that you may only
undo a transaction if the object has not been modified in a later
transaction by you or any other user.</p>
<tal:var define="global macros nocall:context/@@undo_macros" />
<div metal:use-macro="macros/global_vars" />
<div metal:use-macro="macros/location_link" />
<form action="@@undoPrincipalTransactions.html" method="post">
<div metal:use-macro="macros/previous_batch" />
<div metal:use-macro="macros/undo_log">
<div metal:fill-slot="define_info">
<tal:var define="global undo_info python:view.getPrincipalTransactions(
first=first, last=-batch_size, showall=showall)" />
</div>
</div>
<div metal:use-macro="macros/submit_button" />
<div metal:use-macro="macros/next_batch" />
</form>
</div>
</body>
</html>
=== Zope3/src/zope/app/undo/__init__.py 1.2 => 1.3 ===
--- Zope3/src/zope/app/undo/__init__.py:1.2 Fri Mar 19 13:34:01 2004
+++ Zope3/src/zope/app/undo/__init__.py Sun Mar 21 12:20:28 2004
@@ -16,11 +16,15 @@
"""
from datetime import datetime
from zope.interface import implements
+from zope.exceptions import NotFoundError
from zope.app import zapi
from zope.app.event import function
-from zope.app.undo.interfaces import IUndoManager
+from zope.app.undo.interfaces import IUndoManager, UndoError
from zope.app.servicenames import Utilities
+from zope.app.traversing.interfaces import IPhysicallyLocatable
+from zope.app.security.principalregistry import principalRegistry
+from zope.app.security.interfaces import IPrincipal
def undoSetup(event):
# setup undo functionality
@@ -29,44 +33,153 @@
undoSetup = function.Subscriber(undoSetup)
-class ZODBUndoManager:
- """Implement the basic undo management api for a single ZODB database."""
+class Prefix(str):
+ """A prefix is equal to any string it is a prefix of.
+
+ This class can be compared to a string (or arbitrary sequence).
+ The comparison will return True if the prefix value is a prefix of
+ the string being compared.
+
+ Two prefixes cannot safely be compared.
+
+ It does not matter from which side you compare with a prefix:
+
+ >>> p = Prefix('str')
+ >>> p == 'string'
+ True
+ >>> 'string' == p
+ True
+
+ You can also test for inequality:
+
+ >>> p != 'string'
+ False
+ >>> 'string' != p
+ False
+
+ Unicode works, too:
+
+ >>> p == u'string'
+ True
+ >>> u'string' == p
+ True
+ >>> p != u'string'
+ False
+ >>> u'string' != p
+ False
+
+ >>> p = Prefix('foo')
+ >>> p == 'bar'
+ False
+ >>> 'bar' == p
+ False
+
+ >>> p != 'bar'
+ True
+ >>> 'bar' != p
+ True
+
+ >>> p == None
+ False
+ """
+ def __eq__(self, other):
+ if not other:
+ return False
+ length = len(self)
+ return str(other[:length]).__eq__(self)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+class ZODBUndoManager(object):
+ """Implement the basic undo management API for a single ZODB database."""
implements(IUndoManager)
def __init__(self, db):
self.__db = db
- def getUndoInfo(self, first=0, last=-20, user_name=None):
- """See zope.app.undo.interfaces.IUndoManager"""
+ def getTransactions(self, context=None, first=0, last=-20):
+ """See zope.app.undo.interfaces.IUndo"""
+ return self._getUndoInfo(context, None, first, last)
+
+ def getPrincipalTransactions(self, principal, context=None,
+ first=0, last=-20):
+ """See zope.app.undo.interfaces.IPrincipal"""
+ if not IPrincipal.providedBy(principal):
+ raise TypeError, "Invalid principal: %s" % principal
+ return self._getUndoInfo(context, principal, first, last)
+
+ def _getUndoInfo(self, context, principal, first, last):
+ specification = {}
+
+ if context is not None:
+ locatable = IPhysicallyLocatable(context, None)
+ if locatable is not None:
+ location = Prefix(locatable.getPath())
+ specification.update({'location': location})
- # Entries are a list of dictionaries, containing
- # id -> internal id for zodb
- # user_name -> name of user that last accessed the file
- # time -> unix timestamp of last access
- # description -> transaction description
-
- if user_name is not None:
+ if principal is not None:
# XXX The 'user' in the transactions is a concatenation of
# 'path' and 'user' (id). 'path' used to be the path of
# the user folder in Zope 2. ZopePublication currently
- # does not set a path, so it defaults to '/'. Maybe we can
- # find a new meaning for 'path' in Zope 3 (the principal
- # source?)
+ # does not set a path, so ZODB lets the path default to
+ # '/'. We should change ZODB3 to set no default path.
path = '/' # default for now
- specification = {'user_name': path + ' ' + user_name}
- else:
- specification = None
+ specification.update({'user_name': path + ' ' + principal.id})
+
entries = self.__db.undoInfo(first, last, specification)
# We walk through the entries, augmenting the dictionaries
- # with some additional items (at the moment, datetime, a useful
- # form of the unix timestamp).
- for e in entries:
- e['datetime'] = datetime.fromtimestamp(e['time'])
+ # with some additional items we have promised in the interface
+ for entry in entries:
+ entry['datetime'] = datetime.fromtimestamp(entry['time'])
+ entry['principal'] = None
+
+ user_name = entry['user_name']
+ if user_name:
+ # XXX this is because of ZODB3/Zope2 cruft regarding
+ # the user path (see comment above). This 'if' block
+ # will hopefully go away.
+ split = user_name.split()
+ if len(split) == 2:
+ user_name = split[1]
+ if user_name:
+ try:
+ entry['principal'] = principalRegistry.getPrincipal(
+ user_name)
+ except NotFoundError:
+ # principals might have passed away
+ pass
return entries
- def undoTransaction(self, id_list):
- '''See zope.app.undo.interfaces.IUndoManager'''
+ def undoTransactions(self, ids):
+ """See zope.app.undo.interfaces.IUndo"""
+ self._undo(ids)
+
+ def undoPrincipalTransactions(self, principal, ids):
+ """See zope.app.undo.interfaces.IPrincipal"""
+ if not IPrincipal.providedBy(principal):
+ raise TypeError, "Invalid principal: %s" % principal
+
+ # Make sure we only undo the transactions initiated by our
+ # principal
+ left_overs = list(ids)
+ first = 0
+ batch_size = 20
+ txns = self._getUndoInfo(None, principal, first, -batch_size)
+ while txns and left_overs:
+ for info in txns:
+ if info['id'] in left_overs and info['principal'] is principal:
+ left_overs.remove(info['id'])
+ first += batch_size
+ txns = self._getUndoInfo(None, principal, first, -batch_size)
+ if left_overs:
+ raise UndoError, ("You are trying to undo a transaction that "
+ "either does not exist or was not initiated "
+ "by the principal.")
+ self._undo(ids)
- for id in id_list:
+ def _undo(self, ids):
+ for id in ids:
self.__db.undo(id)
+ get_transaction().setExtendedInfo('undo', True)
=== Zope3/src/zope/app/undo/browser.py 1.1 => 1.2 ===
--- Zope3/src/zope/app/undo/browser.py:1.1 Mon Mar 1 09:16:56 2004
+++ Zope3/src/zope/app/undo/browser.py Sun Mar 21 12:20:28 2004
@@ -15,23 +15,66 @@
$Id$
"""
+from zope.exceptions import ForbiddenAttribute
+
from zope.app import zapi
+from zope.app.publisher.browser import BrowserView
from zope.app.undo.interfaces import IUndoManager
-class UndoView:
- """Undo view
- """
-
- def action(self, id_list):
- """processes undo form and redirects to form again (if possible)"""
- utility = zapi.getUtility(self.context, IUndoManager)
- utility.undoTransaction(id_list)
- self.request.response.redirect('index.html')
-
- def getUndoInfo(self, first=0, last=-20, user_name=None):
- utility = zapi.getUtility(self.context, IUndoManager)
- info = utility.getUndoInfo(first, last, user_name)
- formatter = self.request.locale.dates.getFormatter('dateTime', 'medium')
- for entry in info:
- entry['datetime'] = formatter.format(entry['datetime'])
- return info
+class UndoView(BrowserView):
+ """Undo view"""
+
+ def principalLastTransactionIsUndo(self):
+ """Return True if the authenticated principal's last
+ transaction is an undo transaction.
+ """
+ request = self.request
+ undo = zapi.getUtility(self.context, IUndoManager)
+ txn_info = undo.getPrincipalTransactions(request.user, first=0, last=1)
+ if txn_info:
+ return txn_info[0].get('undo', False)
+ return False
+
+ def undoPrincipalLastTransaction(self):
+ """Undo the authenticated principal's last transaction and
+ return where he/she came from"""
+ request = self.request
+ undo = zapi.getUtility(self.context, IUndoManager)
+ txn_info = undo.getPrincipalTransactions(request.user, first=0, last=1)
+ if txn_info:
+ id = txn_info[0]['id']
+ undo.undoPrincipalTransactions(request.user, [id])
+ target = request.get('HTTP_REFERER', '@@SelectedManagementView.html')
+ request.response.redirect(target)
+
+ def undoAllTransactions(self, ids):
+ """Undo transactions specified in 'ids'."""
+ undo = zapi.getUtility(self.context, IUndoManager)
+ undo.undoTransactions(ids)
+ self._redirect()
+
+ def undoPrincipalTransactions(self, ids):
+ """Undo transactions that were issued by the authenticated
+ user specified in 'ids'."""
+ undo = zapi.getUtility(self.context, IUndoManager)
+ undo.undoPrincipalTransactions(self.request.user, ids)
+ self._redirect()
+
+ def _redirect(self):
+ target = "@@SelectedManagementView.html"
+ self.request.response.redirect(target)
+
+ def getAllTransactions(self, first=0, last=-20, showall=False):
+ context = None
+ if not showall:
+ context = self.context
+ undo = zapi.getUtility(self.context, IUndoManager)
+ return undo.getTransactions(context, first, last)
+
+ def getPrincipalTransactions(self, first=0, last=-20, showall=False):
+ context = None
+ if not showall:
+ context = self.context
+ undo = zapi.getUtility(self.context, IUndoManager)
+ return undo.getPrincipalTransactions(self.request.user, context,
+ first, last)
=== Zope3/src/zope/app/undo/configure.zcml 1.3 => 1.4 ===
--- Zope3/src/zope/app/undo/configure.zcml:1.3 Fri Mar 19 13:37:38 2004
+++ Zope3/src/zope/app/undo/configure.zcml Sun Mar 21 12:20:28 2004
@@ -26,23 +26,109 @@
/>
+ <content class="zope.app.undo.ZODBUndoManager">
+ <require
+ permission="zope.UndoOwnTransactions"
+ interface="zope.app.undo.interfaces.IPrincipalUndo"
+ />
+ <require
+ permission="zope.UndoAllTransactions"
+ interface="zope.app.undo.interfaces.IUndo"
+ />
+ </content>
+
<!-- Browser directives -->
<browser:pages
for="*"
- permission="zope.ManageContent"
+ permission="zope.UndoOwnTransactions"
class="zope.app.undo.browser.UndoView"
>
- <browser:page name="undoForm.html" template="undo_log.pt" />
- <browser:page name="undo.html" attribute="action" />
+ <browser:page
+ name="principalLastTransactionIsUndo"
+ attribute="principalLastTransactionIsUndo"
+ />
+
+ <browser:page
+ name="undo.html"
+ attribute="undoPrincipalLastTransaction"
+ />
+
+ <browser:page
+ name="undoPrincipalTransactions.html"
+ attribute="undoPrincipalTransactions"
+ />
+
+ <browser:page
+ name="undoMore.html"
+ template="undo_more.pt"
+ />
+
</browser:pages>
+ <browser:pages
+ for="*"
+ permission="zope.UndoAllTransactions"
+ class="zope.app.undo.browser.UndoView"
+ >
+
+ <browser:page
+ name="undoTransactions.html"
+ attribute="undoPrincipalTransactions"
+ />
+
+ <browser:page
+ name="undoAll.html"
+ template="undo_all.pt"
+ />
+
+ </browser:pages>
+
+ <!-- We hereby imply that users having zope.UndoAllTransactions also
+ have zope.UndoOwnTransactions -->
+ <browser:page
+ for="*"
+ name="undo_macros"
+ permission="zope.UndoOwnTransactions"
+ template="undo_macros.pt"
+ />
+
+
+ <!-- menu items -->
+
+ <browser:menuItem
+ for="*"
+ menu="zmi_actions"
+ title="Undo!"
+ action="@@undo.html"
+ permission="zope.UndoOwnTransactions"
+ filter="not:context/@@principalLastTransactionIsUndo"
+ />
+
+ <browser:menuItem
+ for="*"
+ menu="zmi_actions"
+ title="Redo!"
+ action="@@undo.html"
+ permission="zope.UndoOwnTransactions"
+ filter="context/@@principalLastTransactionIsUndo"
+ />
+
+ <browser:menuItem
+ for="*"
+ menu="zmi_actions"
+ title="Undo more"
+ action="@@undoMore.html"
+ permission="zope.UndoOwnTransactions"
+ />
+
<browser:menuItem
for="*"
menu="zmi_actions"
- title="Undo"
- action="@@undoForm.html"
+ title="Undo all"
+ action="@@undoAll.html"
+ permission="zope.UndoAllTransactions"
/>
</configure>
=== Zope3/src/zope/app/undo/interfaces.py 1.2 => 1.3 ===
--- Zope3/src/zope/app/undo/interfaces.py:1.2 Thu Mar 18 09:31:49 2004
+++ Zope3/src/zope/app/undo/interfaces.py Sun Mar 21 12:20:28 2004
@@ -17,38 +17,62 @@
from zope.interface import Interface
-class IUndoManager(Interface):
- """Interface for the Undo Manager"""
+class UndoError(Exception):
+ pass
- def getUndoInfo(first=0, last=-20, user_name=None):
- """
- Gets some undo information. It skips the 'first' most
- recent transactions; i.e. if first is N, then the first
- transaction returned will be the Nth transaction.
+class IUndo(Interface):
+ """Undo functionality"""
+
+ def getTransactions(context=None, first=0, last=-20):
+ """Return a sequence of mapping objects describing
+ transactions, ordered by date, descending:
+
+ Keys of mapping objects:
+
+ id -> internal id for zodb
+ principal -> principal that invoked the transaction
+ datetime -> datetime object of time
+ description -> description/note (string)
+
+ Extended information (not necessarily available):
+ request_type -> type of request that caused transaction
+ request_info -> request information, e.g. URL, method
+ location -> location of the object that was modified
+ undo -> boolean value, indicated an undo transaction
+
+ If 'context' is None, all transactions will be listed,
+ otherwise only transactions for that location.
+
+ It skips the 'first' most recent transactions; i.e. if first
+ is N, then the first transaction returned will be the Nth
+ transaction.
If last is less than zero, then its absolute value is the
maximum number of transactions to return. Otherwise if last
is N, then only the N most recent transactions following start
are considered.
+ """
- If user_name is not None, only transactions from the given
- user_name are returned.
+ def undoTransactions(ids):
+ """Undo the transactions specified in the sequence 'ids'.
+ """
- Note: at the moment, doesnt care where called from
+class IPrincipalUndo(Interface):
+ """Undo functionality for one specific principal"""
- returns sequence of mapping objects by date desc
+ def getPrincipalTransactions(principal, context=None, first=0, last=-20):
+ """Returns transactions invoked by the given principal.
- keys of mapping objects:
- id -> internal id for zodb
- user_name -> name of user that last accessed the file
- time -> unix timestamp of last access
- datetime -> datetime object of time
- description -> transaction description
+ See IUndo.getTransactions() for more information
"""
- def undoTransaction(id_list):
- """
- id_list will be a list of transaction ids.
- iterate over each id in list, and undo
- the transaction item.
+ def undoPrincipalTransactions(principal, ids):
+ """Undo the transactions invoked by 'principal' with the given
+ 'ids'. Raise UndoError if a transaction is listed among 'ids'
+ that does not belong to 'principal'.
"""
+
+class IUndoManager(IUndo, IPrincipalUndo):
+ """Utility to provide both global and principal-specific undo
+ functionality
+ """
=== Removed File Zope3/src/zope/app/undo/undo_log.pt ===
More information about the Zope3-Checkins
mailing list