diff --git a/last_commit.txt b/last_commit.txt index e69de29bb2..35107b7ac8 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -0,0 +1,42 @@ +Repository: plone.app.content + + +Branch: refs/heads/master +Date: 2023-09-22T22:47:48+02:00 +Author: Laurent Lasudry (laulaz) +Commit: https://github.com/plone/plone.app.content/commit/f68091eb8c77e2604e2862197d4538c04a07b569 + +Fix cut / delete for content with lock created by current user + +This refs #266 + +Files changed: +A news/266.bugfix +M plone/app/content/browser/actions.py +M plone/app/content/browser/contents/cut.py +M plone/app/content/browser/contents/delete.py +M plone/app/content/tests/test_folder.py + +b'diff --git a/news/266.bugfix b/news/266.bugfix\nnew file mode 100644\nindex 00000000..571aae7d\n--- /dev/null\n+++ b/news/266.bugfix\n@@ -0,0 +1 @@\n+Fix cut / delete for content with lock created by current user. [laulaz]\ndiff --git a/plone/app/content/browser/actions.py b/plone/app/content/browser/actions.py\nindex e760bf18..b939b341 100644\n--- a/plone/app/content/browser/actions.py\n+++ b/plone/app/content/browser/actions.py\n@@ -5,6 +5,7 @@\n from plone.base import PloneMessageFactory as _\n from plone.base.utils import get_user_friendly_types\n from plone.base.utils import safe_text\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser import BrowserView\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n@@ -76,6 +77,14 @@ def handle_delete(self, action):\n # has the context object been acquired from a place it should not have\n # been?\n if self.context.aq_chain == self.context.aq_inner.aq_chain:\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked() and not lock_info.is_locked_for_current_user():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n parent.manage_delObjects(self.context.getId())\n IStatusMessage(self.request).add(\n _("${title} has been deleted.", mapping={"title": title})\n@@ -219,7 +228,7 @@ def updateActions(self):\n self.actions["Cancel"].addClass("btn-secondary")\n \n \n-class ObjectCutView(LockingBase):\n+class ObjectCutView(BrowserView):\n @property\n def title(self):\n return self.context.Title()\n@@ -251,16 +260,24 @@ def do_redirect(self, url, message=None, message_type="info", raise_exception=No\n raise raise_exception\n \n def do_action(self):\n- if self.is_locked:\n- return self.do_redirect(\n- self.view_url,\n- _(\n- "${title} is locked and cannot be cut.",\n- mapping={\n- "title": self.title,\n- },\n- ),\n- )\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ return self.do_redirect(\n+ self.view_url,\n+ _(\n+ "${title} is locked and cannot be cut.",\n+ mapping={\n+ "title": self.title,\n+ },\n+ ),\n+ )\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n \n try:\n cp = self.parent.manage_cutObjects(self.context.getId())\ndiff --git a/plone/app/content/browser/contents/cut.py b/plone/app/content/browser/contents/cut.py\nindex 1e87b455..e785931f 100644\n--- a/plone/app/content/browser/contents/cut.py\n+++ b/plone/app/content/browser/contents/cut.py\n@@ -4,6 +4,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from zope.i18n import translate\n from zope.interface import implementer\n \n@@ -36,14 +37,23 @@ def action(self, obj):\n def finish(self):\n oblist = []\n for ob in self.oblist:\n- if ob.wl_isLocked():\n- self.errors.append(\n- _(\n- "${title} is being edited and cannot be cut.",\n- mapping={"title": self.objectTitle(ob)},\n+ try:\n+ lock_info = ob.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ self.errors.append(\n+ _(\n+ "${title} is being edited and cannot be cut.",\n+ mapping={"title": self.objectTitle(ob)},\n+ )\n )\n- )\n- continue\n+ continue\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(ob).unlock()\n+\n if not ob.cb_isMoveable():\n self.errors.append(\n _(\ndiff --git a/plone/app/content/browser/contents/delete.py b/plone/app/content/browser/contents/delete.py\nindex 658e665d..5adac14a 100644\n--- a/plone/app/content/browser/contents/delete.py\n+++ b/plone/app/content/browser/contents/delete.py\n@@ -3,6 +3,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n from zope.component import getMultiAdapter\n@@ -70,19 +71,25 @@ def action(self, obj):\n lock_info = obj.restrictedTraverse("@@plone_lock_info")\n except AttributeError:\n lock_info = None\n-\n- if lock_info is not None and lock_info.is_locked():\n- self.errors.append(\n- _("${title} is locked and cannot be deleted.", mapping={"title": title})\n- )\n- return\n- else:\n- try:\n- parent.manage_delObjects(obj.getId())\n- except Unauthorized:\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n self.errors.append(\n _(\n- "You are not authorized to delete ${title}.",\n- mapping={"title": self.objectTitle(self.dest)},\n+ "${title} is locked and cannot be deleted.",\n+ mapping={"title": title},\n )\n )\n+ return\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(obj).unlock()\n+\n+ try:\n+ parent.manage_delObjects(obj.getId())\n+ except Unauthorized:\n+ self.errors.append(\n+ _(\n+ "You are not authorized to delete ${title}.",\n+ mapping={"title": self.objectTitle(self.dest)},\n+ )\n+ )\ndiff --git a/plone/app/content/tests/test_folder.py b/plone/app/content/tests/test_folder.py\nindex c0e7a3fd..01d2764f 100644\n--- a/plone/app/content/tests/test_folder.py\n+++ b/plone/app/content/tests/test_folder.py\n@@ -2,9 +2,11 @@\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_FUNCTIONAL_TESTING\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_INTEGRATION_TESTING\n from plone.app.testing import login\n+from plone.app.testing import logout\n from plone.app.testing import setRoles\n from plone.app.testing import TEST_USER_ID\n from plone.app.testing import TEST_USER_NAME\n+from plone.app.testing import TEST_USER_PASSWORD\n from plone.dexterity.fti import DexterityFTI\n from plone.locking.interfaces import IRefreshableLockable\n from plone.protect.authenticator import createToken\n@@ -224,6 +226,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -237,14 +243,33 @@ def setUp(self):\n }\n self.request.REQUEST_METHOD = "POST"\n \n- def test_cut_object_when_locked(self):\n+ def test_cut_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.cut import CutActionView\n \n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n+ self.assertTrue(plone_lock_info.is_locked())\n+ view = CutActionView(self.portal, self.request)\n+ view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertFalse(plone_lock_info.is_locked())\n+\n+ def test_cut_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.cut import CutActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n view = CutActionView(self.portal, self.request)\n view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n \n class DeleteDXTest(BaseTest):\n@@ -257,6 +282,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -285,14 +314,32 @@ def test_delete_object(self):\n view()\n self.assertTrue(page_id not in self.portal)\n \n- def test_delete_object_when_locked(self):\n+ def test_delete_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.delete import DeleteActionView\n \n+ page_id = self.portal.page.id\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n view = DeleteActionView(self.portal, self.request)\n view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertTrue(page_id not in self.portal)\n+\n+ def test_delete_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.delete import DeleteActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n+ view = DeleteActionView(self.portal, self.request)\n+ view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n def test_delete_wrong_object_by_acquisition(self):\n page_id = self.portal.page.id\n' + +Repository: plone.app.content + + +Branch: refs/heads/master +Date: 2023-10-13T17:07:04-07:00 +Author: David Glick (davisagli) +Commit: https://github.com/plone/plone.app.content/commit/fcd73064346ed0ab0b4e9269a0a27d056009ace1 + +Merge pull request #267 from plone/laulaz-issue-266-cut-delete-with-lock + +Fix cut / delete for content with lock created by current user + +Files changed: +A news/266.bugfix +M plone/app/content/browser/actions.py +M plone/app/content/browser/contents/cut.py +M plone/app/content/browser/contents/delete.py +M plone/app/content/tests/test_folder.py + +b'diff --git a/news/266.bugfix b/news/266.bugfix\nnew file mode 100644\nindex 0000000..571aae7\n--- /dev/null\n+++ b/news/266.bugfix\n@@ -0,0 +1 @@\n+Fix cut / delete for content with lock created by current user. [laulaz]\ndiff --git a/plone/app/content/browser/actions.py b/plone/app/content/browser/actions.py\nindex e760bf1..b939b34 100644\n--- a/plone/app/content/browser/actions.py\n+++ b/plone/app/content/browser/actions.py\n@@ -5,6 +5,7 @@\n from plone.base import PloneMessageFactory as _\n from plone.base.utils import get_user_friendly_types\n from plone.base.utils import safe_text\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser import BrowserView\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n@@ -76,6 +77,14 @@ def handle_delete(self, action):\n # has the context object been acquired from a place it should not have\n # been?\n if self.context.aq_chain == self.context.aq_inner.aq_chain:\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked() and not lock_info.is_locked_for_current_user():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n parent.manage_delObjects(self.context.getId())\n IStatusMessage(self.request).add(\n _("${title} has been deleted.", mapping={"title": title})\n@@ -219,7 +228,7 @@ def updateActions(self):\n self.actions["Cancel"].addClass("btn-secondary")\n \n \n-class ObjectCutView(LockingBase):\n+class ObjectCutView(BrowserView):\n @property\n def title(self):\n return self.context.Title()\n@@ -251,16 +260,24 @@ def do_redirect(self, url, message=None, message_type="info", raise_exception=No\n raise raise_exception\n \n def do_action(self):\n- if self.is_locked:\n- return self.do_redirect(\n- self.view_url,\n- _(\n- "${title} is locked and cannot be cut.",\n- mapping={\n- "title": self.title,\n- },\n- ),\n- )\n+ try:\n+ lock_info = self.context.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ return self.do_redirect(\n+ self.view_url,\n+ _(\n+ "${title} is locked and cannot be cut.",\n+ mapping={\n+ "title": self.title,\n+ },\n+ ),\n+ )\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(self.context).unlock()\n \n try:\n cp = self.parent.manage_cutObjects(self.context.getId())\ndiff --git a/plone/app/content/browser/contents/cut.py b/plone/app/content/browser/contents/cut.py\nindex 1e87b45..e785931 100644\n--- a/plone/app/content/browser/contents/cut.py\n+++ b/plone/app/content/browser/contents/cut.py\n@@ -4,6 +4,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from zope.i18n import translate\n from zope.interface import implementer\n \n@@ -36,14 +37,23 @@ def action(self, obj):\n def finish(self):\n oblist = []\n for ob in self.oblist:\n- if ob.wl_isLocked():\n- self.errors.append(\n- _(\n- "${title} is being edited and cannot be cut.",\n- mapping={"title": self.objectTitle(ob)},\n+ try:\n+ lock_info = ob.restrictedTraverse("@@plone_lock_info")\n+ except AttributeError:\n+ lock_info = None\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n+ self.errors.append(\n+ _(\n+ "${title} is being edited and cannot be cut.",\n+ mapping={"title": self.objectTitle(ob)},\n+ )\n )\n- )\n- continue\n+ continue\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(ob).unlock()\n+\n if not ob.cb_isMoveable():\n self.errors.append(\n _(\ndiff --git a/plone/app/content/browser/contents/delete.py b/plone/app/content/browser/contents/delete.py\nindex 658e665..5adac14 100644\n--- a/plone/app/content/browser/contents/delete.py\n+++ b/plone/app/content/browser/contents/delete.py\n@@ -3,6 +3,7 @@\n from plone.app.content.browser.contents import ContentsBaseAction\n from plone.app.content.interfaces import IStructureAction\n from plone.base import PloneMessageFactory as _\n+from plone.locking.interfaces import ILockable\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n from zope.component import getMultiAdapter\n@@ -70,19 +71,25 @@ def action(self, obj):\n lock_info = obj.restrictedTraverse("@@plone_lock_info")\n except AttributeError:\n lock_info = None\n-\n- if lock_info is not None and lock_info.is_locked():\n- self.errors.append(\n- _("${title} is locked and cannot be deleted.", mapping={"title": title})\n- )\n- return\n- else:\n- try:\n- parent.manage_delObjects(obj.getId())\n- except Unauthorized:\n+ if lock_info is not None:\n+ if lock_info.is_locked_for_current_user():\n self.errors.append(\n _(\n- "You are not authorized to delete ${title}.",\n- mapping={"title": self.objectTitle(self.dest)},\n+ "${title} is locked and cannot be deleted.",\n+ mapping={"title": title},\n )\n )\n+ return\n+ elif lock_info.is_locked():\n+ # unlock object as it is locked by current user\n+ ILockable(obj).unlock()\n+\n+ try:\n+ parent.manage_delObjects(obj.getId())\n+ except Unauthorized:\n+ self.errors.append(\n+ _(\n+ "You are not authorized to delete ${title}.",\n+ mapping={"title": self.objectTitle(self.dest)},\n+ )\n+ )\ndiff --git a/plone/app/content/tests/test_folder.py b/plone/app/content/tests/test_folder.py\nindex c0e7a3f..01d2764 100644\n--- a/plone/app/content/tests/test_folder.py\n+++ b/plone/app/content/tests/test_folder.py\n@@ -2,9 +2,11 @@\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_FUNCTIONAL_TESTING\n from plone.app.content.testing import PLONE_APP_CONTENT_DX_INTEGRATION_TESTING\n from plone.app.testing import login\n+from plone.app.testing import logout\n from plone.app.testing import setRoles\n from plone.app.testing import TEST_USER_ID\n from plone.app.testing import TEST_USER_NAME\n+from plone.app.testing import TEST_USER_PASSWORD\n from plone.dexterity.fti import DexterityFTI\n from plone.locking.interfaces import IRefreshableLockable\n from plone.protect.authenticator import createToken\n@@ -224,6 +226,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -237,14 +243,33 @@ def setUp(self):\n }\n self.request.REQUEST_METHOD = "POST"\n \n- def test_cut_object_when_locked(self):\n+ def test_cut_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.cut import CutActionView\n \n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n+ self.assertTrue(plone_lock_info.is_locked())\n+ view = CutActionView(self.portal, self.request)\n+ view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertFalse(plone_lock_info.is_locked())\n+\n+ def test_cut_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.cut import CutActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n view = CutActionView(self.portal, self.request)\n view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n \n class DeleteDXTest(BaseTest):\n@@ -257,6 +282,10 @@ def setUp(self):\n login(self.portal, TEST_USER_NAME)\n setRoles(self.portal, TEST_USER_ID, ["Manager"])\n \n+ self.portal.acl_users.userFolderAddUser(\n+ "editor", TEST_USER_PASSWORD, ["Editor"], []\n+ )\n+\n self.portal.invokeFactory("Document", id="page", title="page")\n self.portal.page.reindexObject()\n \n@@ -285,14 +314,32 @@ def test_delete_object(self):\n view()\n self.assertTrue(page_id not in self.portal)\n \n- def test_delete_object_when_locked(self):\n+ def test_delete_object_when_locked_by_current_user(self):\n from plone.app.content.browser.contents.delete import DeleteActionView\n \n+ page_id = self.portal.page.id\n lockable = IRefreshableLockable(self.portal.page)\n lockable.lock()\n view = DeleteActionView(self.portal, self.request)\n view()\n+ self.assertEqual(len(view.errors), 0)\n+ self.assertTrue(page_id not in self.portal)\n+\n+ def test_delete_object_when_locked_by_other_user(self):\n+ from plone.app.content.browser.contents.delete import DeleteActionView\n+\n+ plone_lock_info = self.portal.page.restrictedTraverse("@@plone_lock_info")\n+ lockable = IRefreshableLockable(self.portal.page)\n+ lockable.lock()\n+ logout()\n+\n+ login(self.portal, "editor")\n+ self.assertTrue(plone_lock_info.is_locked())\n+ self.request.form["_authenticator"] = createToken()\n+ view = DeleteActionView(self.portal, self.request)\n+ view()\n self.assertEqual(len(view.errors), 1)\n+ self.assertTrue(plone_lock_info.is_locked())\n \n def test_delete_wrong_object_by_acquisition(self):\n page_id = self.portal.page.id\n' +