Skip to content

Commit a5017d0

Browse files
author
Johannes Gijsbers
committed
Simple version of collision detection...
...with tests and a new generic template for classic and minimal.
1 parent b341f4f commit a5017d0

File tree

6 files changed

+90
-6
lines changed

6 files changed

+90
-6
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ are given with the most recent entry first.
33

44
200?-??-?? 0.7.0
55
Feature:
6+
- simple support for collision detection (sf rfe 648763)
67
- support confirming registration by replying to the email (sf bug 763668)
78
- support setgid and running on port < 1024 (sf patch 777528)
89
- using Zope3's test runner now, allowing GC checks, nicer controls and

roundup/cgi/actions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,12 +435,37 @@ def _createnode(self, cn, props):
435435
return cl.create(**props)
436436

437437
class EditItemAction(_EditAction):
438+
def lastUserActivity(self):
439+
if self.form.has_key(':lastactivity'):
440+
return date.Date(self.form[':lastactivity'].value)
441+
elif self.form.has_key('@lastactivity'):
442+
return date.Date(self.form['@lastactivity'].value)
443+
else:
444+
return None
445+
446+
def lastNodeActivity(self):
447+
cl = getattr(self.client.db, self.classname)
448+
return cl.get(self.nodeid, 'activity')
449+
450+
def detectCollision(self, userActivity, nodeActivity):
451+
# Result from lastUserActivity may be None. If it is, assume there's no
452+
# conflict, or at least not one we can detect.
453+
if userActivity:
454+
return userActivity < nodeActivity
455+
456+
def handleCollision(self):
457+
self.client.template = 'collision'
458+
438459
def handle(self):
439460
"""Perform an edit of an item in the database.
440461
441462
See parsePropsFromForm and _editnodes for special variables.
442463
443464
"""
465+
if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
466+
self.handleCollision()
467+
return
468+
444469
props, links = self.client.parsePropsFromForm()
445470

446471
# handle the props

roundup/cgi/templating.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -612,14 +612,17 @@ def __getattr__(self, attr):
612612
raise AttributeError, attr
613613

614614
def designator(self):
615-
''' Return this item's designator (classname + id) '''
615+
"""Return this item's designator (classname + id)."""
616616
return '%s%s'%(self._classname, self._nodeid)
617617

618618
def submit(self, label="Submit Changes"):
619-
''' Generate a submit button (and action hidden element)
620-
'''
621-
return self.input(type="hidden",name="@action",value="edit") + '\n' + \
622-
self.input(type="submit",name="submit",value=label)
619+
"""Generate a submit button.
620+
621+
Also sneak in the lastactivity and action hidden elements.
622+
"""
623+
return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
624+
self.input(type="hidden", name="@action", value="edit") + '\n' + \
625+
self.input(type="submit", name="submit", value=label)
623626

624627
def journal(self, direction='descending'):
625628
''' Return a list of HTMLJournalEntry instances.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<tal:block metal:use-macro="templates/page/macros/icing">
2+
<title metal:fill-slot="head_title"
3+
tal:content="python:context._classname.capitalize()+' Edit Collision'"></title>
4+
<span metal:fill-slot="body_title" tal:omit-tag="python:1"
5+
tal:content="python:context._classname.capitalize()+' Edit Collision'"></span>
6+
<td class="content" metal:fill-slot="content">
7+
There has been a collision. Another user updated this node while you were
8+
editing. Please <a tal:attributes="href context/designator">reload</a>
9+
the node and review your edits.
10+
</td>
11+
</tal:block>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<tal:block metal:use-macro="templates/page/macros/icing">
2+
<title metal:fill-slot="head_title"
3+
tal:content="python:context._classname.capitalize()+' Edit Collision'"></title>
4+
<span metal:fill-slot="body_title" tal:omit-tag="python:1"
5+
tal:content="python:context._classname.capitalize()+' Edit Collision'"></span>
6+
<td class="content" metal:fill-slot="content">
7+
There has been a collision. Another user updated this node while you were
8+
editing. Please <a tal:attributes="href context/designator">reload</a>
9+
the node and review your edits.
10+
</td>
11+
</tal:block>

test/test_actions.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from __future__ import nested_scopes
2+
13
import unittest
24
from cgi import FieldStorage, MiniFieldStorage
35

46
from roundup import hyperdb
7+
from roundup.date import Date, Interval
58
from roundup.cgi.actions import *
69
from roundup.cgi.exceptions import Redirect, Unauthorised
710

@@ -130,13 +133,43 @@ def testTokenizedStringKey(self):
130133

131134
# The single value gets replaced with the tokenized list.
132135
self.assertEqual([x.value for x in self.form['foo']], ['hello', 'world'])
136+
137+
class CollisionDetectionTestCase(ActionTestCase):
138+
def setUp(self):
139+
ActionTestCase.setUp(self)
140+
self.action = EditItemAction(self.client)
141+
self.now = Date('.')
142+
143+
def testLastUserActivity(self):
144+
self.assertEqual(self.action.lastUserActivity(), None)
145+
146+
self.client.form.value.append(MiniFieldStorage('@lastactivity', str(self.now)))
147+
self.assertEqual(self.action.lastUserActivity(), self.now)
148+
149+
def testLastNodeActivity(self):
150+
self.action.classname = 'issue'
151+
self.action.nodeid = '1'
152+
153+
def get(nodeid, propname):
154+
self.assertEqual(nodeid, '1')
155+
self.assertEqual(propname, 'activity')
156+
return self.now
157+
self.client.db.issue.get = get
158+
159+
self.assertEqual(self.action.lastNodeActivity(), self.now)
160+
161+
def testCollision(self):
162+
self.failUnless(self.action.detectCollision(self.now, self.now + Interval("1d")))
163+
self.failIf(self.action.detectCollision(self.now, self.now - Interval("1d")))
164+
self.failIf(self.action.detectCollision(None, self.now))
133165

134166
def test_suite():
135167
suite = unittest.TestSuite()
136168
suite.addTest(unittest.makeSuite(RetireActionTestCase))
137169
suite.addTest(unittest.makeSuite(StandardSearchActionTestCase))
138170
suite.addTest(unittest.makeSuite(FakeFilterVarsTestCase))
139-
suite.addTest(unittest.makeSuite(ShowActionTestCase))
171+
suite.addTest(unittest.makeSuite(ShowActionTestCase))
172+
suite.addTest(unittest.makeSuite(CollisionDetectionTestCase))
140173
return suite
141174

142175
if __name__ == '__main__':

0 commit comments

Comments
 (0)