[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)">
    &lt;&lt;&lt;
    <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>
    &gt;&gt;&gt;
  </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