Code

Simple version of collision detection, with tests and a new generic template for...
authorjlgijsbers <jlgijsbers@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sat, 14 Feb 2004 02:06:27 +0000 (02:06 +0000)
committerjlgijsbers <jlgijsbers@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sat, 14 Feb 2004 02:06:27 +0000 (02:06 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@2082 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
roundup/cgi/actions.py
roundup/cgi/templating.py
templates/classic/html/_generic.collision.html [new file with mode: 0644]
templates/minimal/html/_generic.collision.html [new file with mode: 0644]
test/test_actions.py

index 9631b2898e5f1ed74546ee8f01dc53f90ada126a..233b45dfb11d5ecfbf67b9d62fb51bee216eb24e 100644 (file)
@@ -3,6 +3,7 @@ are given with the most recent entry first.
 
 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
index de4143d4fa9b1ff07d39f1a487ac3420768ff480..28c97883165a440f3561e8115bc78f2e82788fd1 100755 (executable)
@@ -435,12 +435,37 @@ class _EditAction(Action):
         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
index dc1530951eb343f35c932b4a84b14cfb5a9682c0..a2cc1843634de3ccc02392268dd0d7583fdab3a7 100644 (file)
@@ -612,14 +612,17 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
             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.
diff --git a/templates/classic/html/_generic.collision.html b/templates/classic/html/_generic.collision.html
new file mode 100644 (file)
index 0000000..fd2cc2b
--- /dev/null
@@ -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
diff --git a/templates/minimal/html/_generic.collision.html b/templates/minimal/html/_generic.collision.html
new file mode 100644 (file)
index 0000000..fd2cc2b
--- /dev/null
@@ -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
index 10cd7334e26e44f3bf484eb55b02e00ddbb042bb..2c807c125a332606d0d5025e6420490d0f991154 100755 (executable)
@@ -1,7 +1,10 @@
+from __future__ import nested_scopes\r
+\r
 import unittest\r
 from cgi import FieldStorage, MiniFieldStorage\r
 \r
 from roundup import hyperdb\r
+from roundup.date import Date, Interval\r
 from roundup.cgi.actions import *\r
 from roundup.cgi.exceptions import Redirect, Unauthorised\r
 \r
@@ -130,13 +133,43 @@ class FakeFilterVarsTestCase(SearchActionTestCase):
 \r
         # The single value gets replaced with the tokenized list.\r
         self.assertEqual([x.value for x in self.form['foo']], ['hello', 'world'])\r
+\r
+class CollisionDetectionTestCase(ActionTestCase):\r
+    def setUp(self):\r
+        ActionTestCase.setUp(self)\r
+        self.action = EditItemAction(self.client)\r
+        self.now = Date('.')\r
+        \r
+    def testLastUserActivity(self):\r
+        self.assertEqual(self.action.lastUserActivity(), None)\r
+\r
+        self.client.form.value.append(MiniFieldStorage('@lastactivity', str(self.now)))        \r
+        self.assertEqual(self.action.lastUserActivity(), self.now)\r
+\r
+    def testLastNodeActivity(self):\r
+        self.action.classname = 'issue'\r
+        self.action.nodeid = '1'\r
+\r
+        def get(nodeid, propname):\r
+            self.assertEqual(nodeid, '1')\r
+            self.assertEqual(propname, 'activity')\r
+            return self.now\r
+        self.client.db.issue.get = get\r
+\r
+        self.assertEqual(self.action.lastNodeActivity(), self.now)\r
+\r
+    def testCollision(self):\r
+        self.failUnless(self.action.detectCollision(self.now, self.now + Interval("1d")))\r
+        self.failIf(self.action.detectCollision(self.now, self.now - Interval("1d")))\r
+        self.failIf(self.action.detectCollision(None, self.now))        \r
         \r
 def test_suite():\r
     suite = unittest.TestSuite()\r
     suite.addTest(unittest.makeSuite(RetireActionTestCase))\r
     suite.addTest(unittest.makeSuite(StandardSearchActionTestCase))\r
     suite.addTest(unittest.makeSuite(FakeFilterVarsTestCase))\r
-    suite.addTest(unittest.makeSuite(ShowActionTestCase))    \r
+    suite.addTest(unittest.makeSuite(ShowActionTestCase))\r
+    suite.addTest(unittest.makeSuite(CollisionDetectionTestCase))\r
     return suite\r
 \r
 if __name__ == '__main__':\r