changeset 2014:366d3bbce982

Simple version of collision detection... ...with tests and a new generic template for classic and minimal.
author Johannes Gijsbers <jlgijsbers@users.sourceforge.net>
date Sat, 14 Feb 2004 02:06:27 +0000
parents d116293863a4
children 95f2c726f664
files CHANGES.txt roundup/cgi/actions.py roundup/cgi/templating.py templates/classic/html/_generic.collision.html templates/minimal/html/_generic.collision.html test/test_actions.py
diffstat 6 files changed, 90 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sat Feb 14 01:55:35 2004 +0000
+++ b/CHANGES.txt	Sat Feb 14 02:06:27 2004 +0000
@@ -3,6 +3,7 @@
 
 200?-??-?? 0.7.0
 Feature:
+- simple support for collision detection (sf rfe 648763)
 - support confirming registration by replying to the email (sf bug 763668)
 - support setgid and running on port < 1024 (sf patch 777528)
 - using Zope3's test runner now, allowing GC checks, nicer controls and
--- a/roundup/cgi/actions.py	Sat Feb 14 01:55:35 2004 +0000
+++ b/roundup/cgi/actions.py	Sat Feb 14 02:06:27 2004 +0000
@@ -435,12 +435,37 @@
         return cl.create(**props)
 
 class EditItemAction(_EditAction):
+    def lastUserActivity(self):
+        if self.form.has_key(':lastactivity'):
+            return date.Date(self.form[':lastactivity'].value)
+        elif self.form.has_key('@lastactivity'):
+            return date.Date(self.form['@lastactivity'].value)
+        else:
+            return None
+
+    def lastNodeActivity(self):
+        cl = getattr(self.client.db, self.classname)
+        return cl.get(self.nodeid, 'activity')
+
+    def detectCollision(self, userActivity, nodeActivity):
+        # Result from lastUserActivity may be None. If it is, assume there's no
+        # conflict, or at least not one we can detect.
+        if userActivity:
+            return userActivity < nodeActivity
+
+    def handleCollision(self):
+        self.client.template = 'collision'
+    
     def handle(self):
         """Perform an edit of an item in the database.
 
         See parsePropsFromForm and _editnodes for special variables.
         
         """
+        if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
+            self.handleCollision()
+            return
+
         props, links = self.client.parsePropsFromForm()
 
         # handle the props
--- a/roundup/cgi/templating.py	Sat Feb 14 01:55:35 2004 +0000
+++ b/roundup/cgi/templating.py	Sat Feb 14 02:06:27 2004 +0000
@@ -612,14 +612,17 @@
             raise AttributeError, attr
 
     def designator(self):
-        ''' Return this item's designator (classname + id) '''
+        """Return this item's designator (classname + id)."""
         return '%s%s'%(self._classname, self._nodeid)
     
     def submit(self, label="Submit Changes"):
-        ''' Generate a submit button (and action hidden element)
-        '''
-        return self.input(type="hidden",name="@action",value="edit") + '\n' + \
-               self.input(type="submit",name="submit",value=label)
+        """Generate a submit button.
+
+        Also sneak in the lastactivity and action hidden elements.
+        """
+        return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
+               self.input(type="hidden", name="@action", value="edit") + '\n' + \
+               self.input(type="submit", name="submit", value=label)
 
     def journal(self, direction='descending'):
         ''' Return a list of HTMLJournalEntry instances.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/classic/html/_generic.collision.html	Sat Feb 14 02:06:27 2004 +0000
@@ -0,0 +1,11 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+  <title metal:fill-slot="head_title"
+         tal:content="python:context._classname.capitalize()+' Edit Collision'"></title>
+  <span metal:fill-slot="body_title" tal:omit-tag="python:1"
+        tal:content="python:context._classname.capitalize()+' Edit Collision'"></span>
+  <td class="content" metal:fill-slot="content">
+    There has been a collision. Another user updated this node while you were
+    editing. Please <a tal:attributes="href context/designator">reload</a>
+    the node and review your edits.
+  </td>
+</tal:block>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/minimal/html/_generic.collision.html	Sat Feb 14 02:06:27 2004 +0000
@@ -0,0 +1,11 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+  <title metal:fill-slot="head_title"
+         tal:content="python:context._classname.capitalize()+' Edit Collision'"></title>
+  <span metal:fill-slot="body_title" tal:omit-tag="python:1"
+        tal:content="python:context._classname.capitalize()+' Edit Collision'"></span>
+  <td class="content" metal:fill-slot="content">
+    There has been a collision. Another user updated this node while you were
+    editing. Please <a tal:attributes="href context/designator">reload</a>
+    the node and review your edits.
+  </td>
+</tal:block>
\ No newline at end of file
--- a/test/test_actions.py	Sat Feb 14 01:55:35 2004 +0000
+++ b/test/test_actions.py	Sat Feb 14 02:06:27 2004 +0000
@@ -1,7 +1,10 @@
+from __future__ import nested_scopes
+
 import unittest
 from cgi import FieldStorage, MiniFieldStorage
 
 from roundup import hyperdb
+from roundup.date import Date, Interval
 from roundup.cgi.actions import *
 from roundup.cgi.exceptions import Redirect, Unauthorised
 
@@ -130,13 +133,43 @@
 
         # The single value gets replaced with the tokenized list.
         self.assertEqual([x.value for x in self.form['foo']], ['hello', 'world'])
+
+class CollisionDetectionTestCase(ActionTestCase):
+    def setUp(self):
+        ActionTestCase.setUp(self)
+        self.action = EditItemAction(self.client)
+        self.now = Date('.')
+        
+    def testLastUserActivity(self):
+        self.assertEqual(self.action.lastUserActivity(), None)
+
+        self.client.form.value.append(MiniFieldStorage('@lastactivity', str(self.now)))        
+        self.assertEqual(self.action.lastUserActivity(), self.now)
+
+    def testLastNodeActivity(self):
+        self.action.classname = 'issue'
+        self.action.nodeid = '1'
+
+        def get(nodeid, propname):
+            self.assertEqual(nodeid, '1')
+            self.assertEqual(propname, 'activity')
+            return self.now
+        self.client.db.issue.get = get
+
+        self.assertEqual(self.action.lastNodeActivity(), self.now)
+
+    def testCollision(self):
+        self.failUnless(self.action.detectCollision(self.now, self.now + Interval("1d")))
+        self.failIf(self.action.detectCollision(self.now, self.now - Interval("1d")))
+        self.failIf(self.action.detectCollision(None, self.now))        
         
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(RetireActionTestCase))
     suite.addTest(unittest.makeSuite(StandardSearchActionTestCase))
     suite.addTest(unittest.makeSuite(FakeFilterVarsTestCase))
-    suite.addTest(unittest.makeSuite(ShowActionTestCase))    
+    suite.addTest(unittest.makeSuite(ShowActionTestCase))
+    suite.addTest(unittest.makeSuite(CollisionDetectionTestCase))
     return suite
 
 if __name__ == '__main__':

Roundup Issue Tracker: http://roundup-tracker.org/