[CMF-checkins] CVS: CMF - Document.py:1.18

Ken Manheimer klm@digicool.com
Wed, 30 May 2001 17:07:11 -0400 (EDT)


Update of /cvs-repository/CMF/CMFDefault
In directory korak.digicool.com:/tmp/cvs-serv15685

Modified Files:
	Document.py 
Log Message:
Implemented a concurrent-edit safety belt for Document objects, like
the one jim added to ZWiki, to prevent edits that accidentlly stomp
concurrently occurring edits.  This implementation has a refinement so
a user can save a document repeatedly using the same seat belt (eg,
without revisiting the document after every save).  This allows for
common webdav or emacs-based ftp editing (or use of the web browser
back button to reedit) usage patterns, while still providing conflict
prevention.

It's possible we'll eventually be handling metadata headers in FTP and
webdav differently, which may mean taking a different tack on this
functionality (or discarding it).

._last_safety_belt_editor, ._last_safety_belt, ._safety_belt: Class
instance vars to track the safety belt state.

._edit(): Added safety_belt parameter, for web case where the safety
belt is not passed in the headers.  Extracting the safety belt info
from the headers or param, and using ._safety_belt_update() to
validate - if it fails, then raise an 'EditingConflict' exception with
an informative message.

.getMetadataHeaders(): Provide our own version of this method,
appending the safety belt header to the generic
(DefaultDublinCoreImpl) one.

.SafetyBelt(): Expose the safety belt value for consumption from the
web edit form.

._safety_belt_update(): Enforce safety belt rules, returning 1 when
satisfied, 0 when not.

.PUT(): Catch 'EditingConflict' exception that would be caused by
safety belt conflict trigger, aborting the transaction and returning a
450 FTP failure.



--- Updated File Document.py in package CMF --
--- Document.py	2001/05/30 02:53:17	1.17
+++ Document.py	2001/05/30 21:07:10	1.18
@@ -89,7 +89,7 @@
 import Globals, StructuredText, string, utils
 from StructuredText.HTMLWithImages import HTMLWithImages
 from Globals import DTMLFile, InitializeClass
-from AccessControl import ClassSecurityInfo
+from AccessControl import ClassSecurityInfo, getSecurityManager
 from Products.CMFCore.PortalContent import PortalContent
 from DublinCore import DefaultDublinCoreImpl
 
@@ -154,6 +154,10 @@
     effective_date = expiration_date = None
     _isDiscussable = 1
 
+    _last_safety_belt_editor = ''
+    _last_safety_belt = ''
+    _safety_belt = ''
+
     # Declarative security (replaces __ac_permissions__)
     security = ClassSecurityInfo()
 
@@ -182,7 +186,7 @@
                 + '?manage_tabs_message=Document+updated'
                 )
 
-    def _edit(self, text_format, text, file=''):
+    def _edit(self, text_format, text, file='', safety_belt=''):
         """ Edit the Document - Parses headers and cooks the body"""
         self.text = text
         headers = {}
@@ -192,6 +196,17 @@
                 text = self.text = contents
 
         headers, body, cooked, format = self.handleText(text, text_format)
+
+        if not safety_belt:
+            safety_belt = headers.get('SafetyBelt', '')
+        if not self._safety_belt_update(safety_belt=safety_belt):
+            msg = ("Intervening changes from elsewhere detected."
+                   " Please refetch the document and reapply your changes."
+                   " (You may be able to recover your version using the"
+                   " browser 'back' button, but will have to apply them"
+                   " to a freshly fetched copy.)")
+            raise 'EditingConflict', msg
+
         self.text_format = format
         self.cooked_text = cooked
         self.text = body
@@ -247,6 +262,57 @@
 
         return headers, body, cooked, format
             
+    security.declarePublic( 'getMetadataHeaders' )
+    def getMetadataHeaders(self):
+        """Return RFC-822-style header spec."""
+        hdrlist = DefaultDublinCoreImpl.getMetadataHeaders(self)
+        hdrlist.append( ('SafetyBelt', self._safety_belt) )
+        return hdrlist
+
+    security.declarePublic( 'SafetyBelt' )
+    def SafetyBelt(self):
+        """Return the current safety belt setting.
+        For web form hidden button."""
+        return self._safety_belt
+
+    def _safety_belt_update(self, safety_belt=''):
+        """Check validity of safety belt and update tracking if valid.
+
+        Return 0 if safety belt is invalid, 1 otherwise.
+
+        Note that the policy is deliberately lax if no safety belt value is
+        present - "you're on your own if you don't use your safety belt".
+
+        When present, either the safety belt token:
+         - ... is the same as the current one given out, or
+         - ... is the same as the last one given out, and the person doing the
+           edit is the same as the last editor."""
+
+        this_belt = safety_belt
+        this_user = getSecurityManager().getUser().getUserName()
+
+        if (# we have a safety belt value:
+            this_belt
+            # and the safety belt doesn't match the current one:
+            and (this_belt != self._safety_belt)
+            # and safety belt and user don't match last safety belt and user:
+            and not ((this_belt == self._last_safety_belt)
+                     and (this_user == self._last_safety_belt_editor))):
+            # Fail.
+            return 0
+
+        # We qualified - either:
+        #  - the edit was submitted with safety belt stripped, or
+        #  - the current safety belt was used, or
+        #  - the last one was reused by the last person who did the last edit.
+        # In any case, update the tracking.
+
+        self._last_safety_belt_editor = this_user
+        self._last_safety_belt = this_belt
+        self._safety_belt = str(self._p_mtime)
+
+        return 1
+
     security.declareProtected(CMFCorePermissions.View, 'SearchableText')
     def SearchableText(self):
         """ Used by the catalog for basic full text indexing """
@@ -288,7 +354,15 @@
         if ishtml: self.setFormat('text/html')
         else: self.setFormat('text/plain')
 
-        self.edit(text_format=self.text_format, text=body)
+        try:
+            self.edit(text_format=self.text_format, text=body)
+        except 'EditingConflict', msg:
+            # XXX Can we get an error msg through?  Should we be raising an
+            #     exception, to be handled in the FTP mechanism?  Inquiring
+            #     minds...
+            get_transaction().abort()
+            RESPONSE.setStatus(450)
+            return RESPONSE
 
         RESPONSE.setStatus(204)
         return RESPONSE