From: jlgijsbers Date: Sat, 14 Feb 2004 02:06:27 +0000 (+0000) Subject: Simple version of collision detection, with tests and a new generic template for... X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=a40019d55c6c05e7e752f6d8b8f1f7f56990e7eb;p=roundup.git Simple version of collision detection, with tests and a new generic template for classic and minimal. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@2082 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/CHANGES.txt b/CHANGES.txt index 9631b28..233b45d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py index de4143d..28c9788 100755 --- a/roundup/cgi/actions.py +++ b/roundup/cgi/actions.py @@ -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 diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index dc15309..a2cc184 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -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 index 0000000..fd2cc2b --- /dev/null +++ b/templates/classic/html/_generic.collision.html @@ -0,0 +1,11 @@ + + + + + There has been a collision. Another user updated this node while you were + editing. Please reload + the node and review your edits. + + \ 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 index 0000000..fd2cc2b --- /dev/null +++ b/templates/minimal/html/_generic.collision.html @@ -0,0 +1,11 @@ + + + + + There has been a collision. Another user updated this node while you were + editing. Please reload + the node and review your edits. + + \ No newline at end of file diff --git a/test/test_actions.py b/test/test_actions.py index 10cd733..2c807c1 100755 --- a/test/test_actions.py +++ b/test/test_actions.py @@ -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 @@ class FakeFilterVarsTestCase(SearchActionTestCase): # 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__':