[gen] First draft of a system for locking pages when editing it.

This commit is contained in:
Gaetan Delannay 2013-01-14 16:58:30 +01:00
parent 27197f5b9d
commit bdf41adf36
11 changed files with 71 additions and 8 deletions

View file

@ -98,6 +98,8 @@ class BaseMixin:
if not created: if not created:
from DateTime import DateTime from DateTime import DateTime
obj.modified = DateTime() obj.modified = DateTime()
# Unlock the currently saved page on the object
if rq: self.removeLock(rq['page'])
obj.reindex() obj.reindex()
return obj, msg return obj, msg
@ -188,6 +190,53 @@ class BaseMixin:
obj = createObject(tool.getPath('/temp_folder'), id, className, appName) obj = createObject(tool.getPath('/temp_folder'), id, className, appName)
return self.goto(obj.getUrl(**urlParams)) 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): def onCreateWithoutForm(self):
'''This method is called when a user wants to create a object from a '''This method is called when a user wants to create a object from a
reference field, automatically (without displaying a form).''' reference field, automatically (without displaying a form).'''
@ -263,6 +312,7 @@ class BaseMixin:
else: else:
urlBack = self.getUrl() urlBack = self.getUrl()
self.say(self.translate('object_canceled')) self.say(self.translate('object_canceled'))
self.removeLock(rq['page'])
return self.goto(urlBack) return self.goto(urlBack)
# Object for storing validation errors # Object for storing validation errors

View file

@ -201,6 +201,7 @@ appyLabels = [
('changes_show', 'Show changes'), ('changes_show', 'Show changes'),
('changes_hide', 'Hide changes'), ('changes_hide', 'Hide changes'),
('anonymous', 'an anonymous user'), ('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 # Some default values for labels whose ids are not fixed (so they can't be

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

BIN
gen/ui/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

View file

@ -8,6 +8,7 @@
phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType); phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType);
phase phaseInfo/name; phase phaseInfo/name;
page request/page|python:contextObj.getDefaultEditPage(); page request/page|python:contextObj.getDefaultEditPage();
dummy python: contextObj.setLock(user, page);
confirmMsg request/confirmMsg | nothing; confirmMsg request/confirmMsg | nothing;
groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);" groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);"
tal:on-error="structure python: tool.manageError(error)"> tal:on-error="structure python: tool.manageError(error)">

BIN
gen/ui/locked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

View file

@ -115,7 +115,8 @@
</tal:comment> </tal:comment>
<table metal:define-macro="phases" class="portletContent" <table metal:define-macro="phases" class="portletContent"
tal:define="singlePhase python: phases and (len(phases) == 1); tal:define="singlePhase python: phases and (len(phases) == 1);
page python: req.get('page', 'main')"> page python: req.get('page', 'main');
mayEdit python: contextObj.allows('Modify portal content')">
<tal:phase repeat="phase phases"> <tal:phase repeat="phase phases">
<tal:comment replace="nothing">The box containing phase-related information</tal:comment> <tal:comment replace="nothing">The box containing phase-related information</tal:comment>
<tr tal:define="singlePage python: len(phase['pages']) == 1"> <tr tal:define="singlePage python: len(phase['pages']) == 1">
@ -132,10 +133,19 @@
<a tal:attributes="href python: contextObj.getUrl(page=aPage)" <a tal:attributes="href python: contextObj.getUrl(page=aPage)"
tal:content="structure python: _('%s_page_%s' % (contextObj.meta_type, aPage))"> tal:content="structure python: _('%s_page_%s' % (contextObj.meta_type, aPage))">
</a> </a>
<a tal:condition="python: contextObj.allows('Modify portal content') and phase['pagesInfo'][aPage]['showOnEdit']" <tal:icons define="locked python: contextObj.isLocked(user, aPage);
tal:attributes="href python: contextObj.getUrl(mode='edit', page=aPage)"> editable python: mayEdit and phase['pagesInfo'][aPage]['showOnEdit']">
<img title="Edit" tal:attributes="src string: $appUrl/ui/edit.gif"/> <a tal:condition="python: editable and not locked"
</a> tal:attributes="href python: contextObj.getUrl(mode='edit', page=aPage)">
<img tal:attributes="src string: $appUrl/ui/edit.png;
title python: _('object_edit')"/>
</a>
<a tal:condition="python: editable and locked">
<img style="cursor: help"
tal:attributes="src string: $appUrl/ui/locked.png;
title python: _('page_locked', mapping={'user':tool.getUserName(locked)})"/>
</a>
</tal:icons>
</div> </div>
<tal:comment replace="nothing">Next lines: links</tal:comment> <tal:comment replace="nothing">Next lines: links</tal:comment>
<tal:links define="links python: phase['pagesInfo'][aPage].get('links')" tal:condition="links"> <tal:links define="links python: phase['pagesInfo'][aPage].get('links')" tal:condition="links">

View file

@ -95,7 +95,7 @@
<a tal:define="navInfo python:'search.%s.%s.%d.%d' % (className, searchName, repeat['obj'].number()+startNumber, totalNumber);" <a tal:define="navInfo python:'search.%s.%s.%d.%d' % (className, searchName, repeat['obj'].number()+startNumber, totalNumber);"
tal:attributes="href python: obj.getUrl(mode='edit', page=obj.getDefaultEditPage(), nav=navInfo)" tal:attributes="href python: obj.getUrl(mode='edit', page=obj.getDefaultEditPage(), nav=navInfo)"
tal:condition="obj/mayEdit"> tal:condition="obj/mayEdit">
<img tal:attributes="src string: $appUrl/ui/edit.gif; <img tal:attributes="src string: $appUrl/ui/edit.png;
title python: _('object_edit')"/></a><img title python: _('object_edit')"/></a><img
tal:condition="obj/mayDelete" style="cursor:pointer" tal:condition="obj/mayDelete" style="cursor:pointer"
tal:attributes="src string: $appUrl/ui/delete.png; tal:attributes="src string: $appUrl/ui/delete.png;

View file

@ -175,7 +175,7 @@
<span tal:content="python: userInfo[0]"></span> <span tal:content="python: userInfo[0]"></span>
<a tal:condition="python: userInfo[1]" <a tal:condition="python: userInfo[1]"
tal:attributes="href python: userInfo[1]"> tal:attributes="href python: userInfo[1]">
<img tal:attributes="src string: $appUrl/ui/edit.gif"/> <img tal:attributes="src string: $appUrl/ui/edit.png"/>
</a> </a>
</td> </td>
</tr> </tr>

View file

@ -8,6 +8,7 @@
phase phaseInfo/name; phase phaseInfo/name;
cssJs python: {}; cssJs python: {};
page req/page|python:contextObj.getDefaultViewPage(); page req/page|python:contextObj.getDefaultViewPage();
dummy python: contextObj.removeMyLock(user, page);
groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);" groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);"
tal:on-error="structure python: tool.manageError(error)"> tal:on-error="structure python: tool.manageError(error)">

View file

@ -27,7 +27,7 @@
<td tal:condition="python: not appyType['noForm'] and obj.mayEdit() and appyType['delete']"> <td tal:condition="python: not appyType['noForm'] and obj.mayEdit() and appyType['delete']">
<a tal:define="navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['pageName'], repeat['obj'].number()+startNumber, totalNumber);" <a tal:define="navInfo python:'ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, appyType['pageName'], repeat['obj'].number()+startNumber, totalNumber);"
tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)"> tal:attributes="href python: obj.getUrl(mode='edit', page='main', nav=navInfo)">
<img tal:attributes="src string: $appUrl/ui/edit.gif; <img tal:attributes="src string: $appUrl/ui/edit.png;
title python: _('object_edit')"/> title python: _('object_edit')"/>
</a> </a>
</td> </td>