diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 1f20560..7061dab 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -98,6 +98,8 @@ class BaseMixin: if not created: from DateTime import DateTime obj.modified = DateTime() + # Unlock the currently saved page on the object + if rq: self.removeLock(rq['page']) obj.reindex() return obj, msg @@ -188,6 +190,53 @@ class BaseMixin: obj = createObject(tool.getPath('/temp_folder'), id, className, appName) return self.goto(obj.getUrl(**urlParams)) + def setLock(self, user, page): + '''A p_user edits a given p_page on this object: we will set a lock, to + prevent other users to edit this page at the same time.''' + if not hasattr(self.aq_base, 'locks'): + # Create the persistent mapping that will store the lock + # ~{s_page: s_userId}~ + from persistent.mapping import PersistentMapping + self.locks = PersistentMapping() + # Raise an error is the page is already locked by someone else. If the + # page is already locked by the same user, we don't mind: he could have + # used back/forward buttons of its browser... + userId = user.getId() + if (page in self.locks) and (userId != self.locks[page]): + from AccessControl import Unauthorized + raise Unauthorized('This page is locked.') + # Set the lock + self.locks[page] = userId + + def isLocked(self, user, page): + '''Is this page locked? If the page is locked by the same user, we don't + mind and consider the page as unlocked. If the page is locked, this + method returns the id of the user that has locked the page.''' + if hasattr(self.aq_base, 'locks') and (page in self.locks): + if (user.getId() != self.locks[page]): return self.locks[page] + + def removeLock(self, page): + '''Removes the lock on the current page. This happens after the page has + been saved: the lock must be released.''' + if page not in self.locks: return + # Raise an error if the user that saves changes is not the one that + # has locked the page. + userId = self.getUser().getId() + if self.locks[page] != userId: + from AccessControl import Unauthorized + raise Unauthorized('This page was locked by someone else.') + # Remove the lock + del self.locks[page] + + def removeMyLock(self, user, page): + '''If p_user has set a lock on p_page, this method removes it. This + method is called when the user that locked a page consults + view.pt for this page. In this case, we consider that the user has + left the edit page in an unexpected way and we remove the lock.''' + if hasattr(self.aq_base, 'locks') and (page in self.locks) and \ + (user.getId() == self.locks[page]): + del self.locks[page] + def onCreateWithoutForm(self): '''This method is called when a user wants to create a object from a reference field, automatically (without displaying a form).''' @@ -263,6 +312,7 @@ class BaseMixin: else: urlBack = self.getUrl() self.say(self.translate('object_canceled')) + self.removeLock(rq['page']) return self.goto(urlBack) # Object for storing validation errors diff --git a/gen/po.py b/gen/po.py index 83b3308..fc21943 100644 --- a/gen/po.py +++ b/gen/po.py @@ -201,6 +201,7 @@ appyLabels = [ ('changes_show', 'Show changes'), ('changes_hide', 'Hide changes'), ('anonymous', 'an anonymous user'), + ('page_locked', 'This page is locked by ${user}.'), ] # Some default values for labels whose ids are not fixed (so they can't be diff --git a/gen/ui/edit.gif b/gen/ui/edit.gif deleted file mode 100644 index 5fd9cfa..0000000 Binary files a/gen/ui/edit.gif and /dev/null differ diff --git a/gen/ui/edit.png b/gen/ui/edit.png new file mode 100644 index 0000000..f31f10d Binary files /dev/null and b/gen/ui/edit.png differ diff --git a/gen/ui/edit.pt b/gen/ui/edit.pt index d4facda..c99ee50 100644 --- a/gen/ui/edit.pt +++ b/gen/ui/edit.pt @@ -8,6 +8,7 @@ phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType); phase phaseInfo/name; page request/page|python:contextObj.getDefaultEditPage(); + dummy python: contextObj.setLock(user, page); confirmMsg request/confirmMsg | nothing; groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);" tal:on-error="structure python: tool.manageError(error)"> diff --git a/gen/ui/locked.png b/gen/ui/locked.png new file mode 100644 index 0000000..8b81e7f Binary files /dev/null and b/gen/ui/locked.png differ diff --git a/gen/ui/portlet.pt b/gen/ui/portlet.pt index 88dbd1e..8e00b8d 100644 --- a/gen/ui/portlet.pt +++ b/gen/ui/portlet.pt @@ -115,7 +115,8 @@ + page python: req.get('page', 'main'); + mayEdit python: contextObj.allows('Modify portal content')"> The box containing phase-related information @@ -132,10 +133,19 @@ - - - + + + + + + + + Next lines: links diff --git a/gen/ui/result.pt b/gen/ui/result.pt index 496fceb..278db68 100644 --- a/gen/ui/result.pt +++ b/gen/ui/result.pt @@ -95,7 +95,7 @@ - - + diff --git a/gen/ui/view.pt b/gen/ui/view.pt index 0e3d108..d0e5695 100644 --- a/gen/ui/view.pt +++ b/gen/ui/view.pt @@ -8,6 +8,7 @@ phase phaseInfo/name; cssJs python: {}; page req/page|python:contextObj.getDefaultViewPage(); + dummy python: contextObj.removeMyLock(user, page); groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);" tal:on-error="structure python: tool.manageError(error)"> diff --git a/gen/ui/widgets/ref.pt b/gen/ui/widgets/ref.pt index 739dd54..e8434e8 100644 --- a/gen/ui/widgets/ref.pt +++ b/gen/ui/widgets/ref.pt @@ -27,7 +27,7 @@
-