From 9e8e65b9f5e8c87d61aa2773a073b0f748b1f3c4 Mon Sep 17 00:00:00 2001 From: gforcada Date: Sat, 14 Oct 2023 13:04:48 +0200 Subject: [PATCH] [fc] Repository: plone.api Branch: refs/heads/master Date: 2023-10-14T12:57:58+02:00 Author: Gil Forcada Codinachs (gforcada) Commit: https://github.com/plone/plone.api/commit/f36be80af5deacd379a93fa2d64e88517858741a chore(gha): update versions There is a warning that the are about to be deprecated. Files changed: M .github/workflows/black.yml M .github/workflows/docs.yml M .github/workflows/isort.yml M .github/workflows/plone_python.yml Repository: plone.api Branch: refs/heads/master Date: 2023-10-14T12:58:17+02:00 Author: Gil Forcada Codinachs (gforcada) Commit: https://github.com/plone/plone.api/commit/8d34f95512b06d25474f4cb12b0bc369b0b1ac87 fix(gha): adjust Plone/python versions - remove Plone 5.2 - remove Python 3.7 - add Python 3.10 and 3.11 Files changed: M .github/workflows/plone_python.yml Repository: plone.api Branch: refs/heads/master Date: 2023-10-14T12:58:33+02:00 Author: Gil Forcada Codinachs (gforcada) Commit: https://github.com/plone/plone.api/commit/b15af98535422fcfb1c9711597079cc77cc12273 fix: adjust tox.ini as well Files changed: M tox.ini Repository: plone.api Branch: refs/heads/master Date: 2023-10-14T12:58:38+02:00 Author: Gil Forcada Codinachs (gforcada) Commit: https://github.com/plone/plone.api/commit/67c8d25ff3f2b14f4a6796054b9928e43b290c3d Create 1.internal Files changed: A news/1.internal Repository: plone.api Branch: refs/heads/master Date: 2023-10-14T13:04:48+02:00 Author: Gil Forcada Codinachs (gforcada) Commit: https://github.com/plone/plone.api/commit/e6c274fba0fd8b665b8129c1116f30d226a616ff Merge pull request #519 from plone/gforcada-patch-1 Update GHA Files changed: A news/1.internal M .github/workflows/black.yml M .github/workflows/docs.yml M .github/workflows/isort.yml M .github/workflows/plone_python.yml M tox.ini --- last_commit.txt | 97 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 24 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index 35107b7ac8..43a4f38f9f 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,42 +1,91 @@ -Repository: plone.app.content +Repository: plone.api 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 +Date: 2023-10-14T12:57:58+02:00 +Author: Gil Forcada Codinachs (gforcada) +Commit: https://github.com/plone/plone.api/commit/f36be80af5deacd379a93fa2d64e88517858741a -Fix cut / delete for content with lock created by current user +chore(gha): update versions -This refs #266 +There is a warning that the are about to be deprecated. 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 +M .github/workflows/black.yml +M .github/workflows/docs.yml +M .github/workflows/isort.yml +M .github/workflows/plone_python.yml -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' +b'diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml\nindex 420028cc..fc5371c7 100644\n--- a/.github/workflows/black.yml\n+++ b/.github/workflows/black.yml\n@@ -10,11 +10,11 @@ jobs:\n \n steps:\n # git checkout\n- - uses: actions/checkout@v2\n+ - uses: actions/checkout@v4\n \n # python setup\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v1\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n@@ -23,7 +23,7 @@ jobs:\n pip install tox tox-gh-actions\n \n # python cache\n- - uses: actions/cache@v1\n+ - uses: actions/cache@v3\n with:\n path: ~/.cache/pip\n key: ${{ runner.os }}-pip-${{ hashFiles(\'**/requirements.txt\') }}\ndiff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml\nindex 93fc39fc..1cad5df7 100644\n--- a/.github/workflows/docs.yml\n+++ b/.github/workflows/docs.yml\n@@ -10,11 +10,11 @@ jobs:\n \n steps:\n # git checkout\n- - uses: actions/checkout@v2\n+ - uses: actions/checkout@v4\n \n # python setup\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v1\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n@@ -23,7 +23,7 @@ jobs:\n pip install tox tox-gh-actions\n \n # python cache\n- - uses: actions/cache@v1\n+ - uses: actions/cache@v3\n with:\n path: ~/.cache/pip\n key: ${{ runner.os }}-pip-${{ hashFiles(\'**/requirements.txt\') }}\ndiff --git a/.github/workflows/isort.yml b/.github/workflows/isort.yml\nindex c6f499fc..3176e03f 100644\n--- a/.github/workflows/isort.yml\n+++ b/.github/workflows/isort.yml\n@@ -10,11 +10,11 @@ jobs:\n \n steps:\n # git checkout\n- - uses: actions/checkout@v2\n+ - uses: actions/checkout@v4\n \n # python setup\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v1\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n@@ -23,7 +23,7 @@ jobs:\n pip install tox tox-gh-actions\n \n # python cache\n- - uses: actions/cache@v1\n+ - uses: actions/cache@v3\n with:\n path: ~/.cache/pip\n key: ${{ runner.os }}-pip-${{ hashFiles(\'**/requirements.txt\') }}\ndiff --git a/.github/workflows/plone_python.yml b/.github/workflows/plone_python.yml\nindex 86771fec..efa78c69 100644\n--- a/.github/workflows/plone_python.yml\n+++ b/.github/workflows/plone_python.yml\n@@ -20,11 +20,11 @@ jobs:\n plone-version: ["5.2", "6.0"]\n \n steps:\n- - uses: actions/checkout@v3\n+ - uses: actions/checkout@v4\n - name: Install system libraries\n run: sudo apt-get install libxml2-dev libxslt1-dev libjpeg-dev\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v2\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n' -Repository: plone.app.content +Repository: plone.api 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 +Date: 2023-10-14T12:58:17+02:00 +Author: Gil Forcada Codinachs (gforcada) +Commit: https://github.com/plone/plone.api/commit/8d34f95512b06d25474f4cb12b0bc369b0b1ac87 -Merge pull request #267 from plone/laulaz-issue-266-cut-delete-with-lock +fix(gha): adjust Plone/python versions -Fix cut / delete for content with lock created by current user +- remove Plone 5.2 +- remove Python 3.7 +- add Python 3.10 and 3.11 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 +M .github/workflows/plone_python.yml -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' +b'diff --git a/.github/workflows/plone_python.yml b/.github/workflows/plone_python.yml\nindex efa78c69..9b1948b9 100644\n--- a/.github/workflows/plone_python.yml\n+++ b/.github/workflows/plone_python.yml\n@@ -16,8 +16,8 @@ jobs:\n strategy:\n fail-fast: false\n matrix:\n- python-version: ["3.7", "3.8", "3.9"]\n- plone-version: ["5.2", "6.0"]\n+ python-version: ["3.8", "3.9", "3.10", "3.11"]\n+ plone-version: ["6.0"]\n \n steps:\n - uses: actions/checkout@v4\n' + +Repository: plone.api + + +Branch: refs/heads/master +Date: 2023-10-14T12:58:33+02:00 +Author: Gil Forcada Codinachs (gforcada) +Commit: https://github.com/plone/plone.api/commit/b15af98535422fcfb1c9711597079cc77cc12273 + +fix: adjust tox.ini as well + +Files changed: +M tox.ini + +b'diff --git a/tox.ini b/tox.ini\nindex 7f4d12ee..4ec548d9 100644\n--- a/tox.ini\n+++ b/tox.ini\n@@ -1,7 +1,6 @@\n [tox]\n envlist =\n- py{37,38,39}-plone{52}\n- py{38,39}-plone{60}\n+ py{38,39,310,311}-plone{60}\n # towncrier\n # black-enforce\n black-check\n@@ -17,13 +16,13 @@ skip_missing_interpreters = True\n \n [gh-actions]\n python =\n- 3.7: py37\n 3.8: py38\n 3.9: py39\n+ 3.10: py310\n+ 3.11: py311\n \n [gh-actions:env]\n PLONE =\n- 52: plone52\n 60: plone60\n \n \n@@ -40,7 +39,6 @@ commands =\n \n setenv =\n BUILDOUT_FILE=test_plone-60.cfg\n- plone52: BUILDOUT_FILE=test_plone-52.cfg\n \n deps =\n pdbpp\n' + +Repository: plone.api + + +Branch: refs/heads/master +Date: 2023-10-14T12:58:38+02:00 +Author: Gil Forcada Codinachs (gforcada) +Commit: https://github.com/plone/plone.api/commit/67c8d25ff3f2b14f4a6796054b9928e43b290c3d + +Create 1.internal + +Files changed: +A news/1.internal + +b'diff --git a/news/1.internal b/news/1.internal\nnew file mode 100644\nindex 00000000..4af1df1b\n--- /dev/null\n+++ b/news/1.internal\n@@ -0,0 +1,2 @@\n+Update GHA\n+[gforcada]\n' + +Repository: plone.api + + +Branch: refs/heads/master +Date: 2023-10-14T13:04:48+02:00 +Author: Gil Forcada Codinachs (gforcada) +Commit: https://github.com/plone/plone.api/commit/e6c274fba0fd8b665b8129c1116f30d226a616ff + +Merge pull request #519 from plone/gforcada-patch-1 + +Update GHA + +Files changed: +A news/1.internal +M .github/workflows/black.yml +M .github/workflows/docs.yml +M .github/workflows/isort.yml +M .github/workflows/plone_python.yml +M tox.ini + +b'diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml\nindex 420028cc..fc5371c7 100644\n--- a/.github/workflows/black.yml\n+++ b/.github/workflows/black.yml\n@@ -10,11 +10,11 @@ jobs:\n \n steps:\n # git checkout\n- - uses: actions/checkout@v2\n+ - uses: actions/checkout@v4\n \n # python setup\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v1\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n@@ -23,7 +23,7 @@ jobs:\n pip install tox tox-gh-actions\n \n # python cache\n- - uses: actions/cache@v1\n+ - uses: actions/cache@v3\n with:\n path: ~/.cache/pip\n key: ${{ runner.os }}-pip-${{ hashFiles(\'**/requirements.txt\') }}\ndiff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml\nindex 93fc39fc..1cad5df7 100644\n--- a/.github/workflows/docs.yml\n+++ b/.github/workflows/docs.yml\n@@ -10,11 +10,11 @@ jobs:\n \n steps:\n # git checkout\n- - uses: actions/checkout@v2\n+ - uses: actions/checkout@v4\n \n # python setup\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v1\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n@@ -23,7 +23,7 @@ jobs:\n pip install tox tox-gh-actions\n \n # python cache\n- - uses: actions/cache@v1\n+ - uses: actions/cache@v3\n with:\n path: ~/.cache/pip\n key: ${{ runner.os }}-pip-${{ hashFiles(\'**/requirements.txt\') }}\ndiff --git a/.github/workflows/isort.yml b/.github/workflows/isort.yml\nindex c6f499fc..3176e03f 100644\n--- a/.github/workflows/isort.yml\n+++ b/.github/workflows/isort.yml\n@@ -10,11 +10,11 @@ jobs:\n \n steps:\n # git checkout\n- - uses: actions/checkout@v2\n+ - uses: actions/checkout@v4\n \n # python setup\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v1\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\n@@ -23,7 +23,7 @@ jobs:\n pip install tox tox-gh-actions\n \n # python cache\n- - uses: actions/cache@v1\n+ - uses: actions/cache@v3\n with:\n path: ~/.cache/pip\n key: ${{ runner.os }}-pip-${{ hashFiles(\'**/requirements.txt\') }}\ndiff --git a/.github/workflows/plone_python.yml b/.github/workflows/plone_python.yml\nindex 86771fec..9b1948b9 100644\n--- a/.github/workflows/plone_python.yml\n+++ b/.github/workflows/plone_python.yml\n@@ -16,15 +16,15 @@ jobs:\n strategy:\n fail-fast: false\n matrix:\n- python-version: ["3.7", "3.8", "3.9"]\n- plone-version: ["5.2", "6.0"]\n+ python-version: ["3.8", "3.9", "3.10", "3.11"]\n+ plone-version: ["6.0"]\n \n steps:\n- - uses: actions/checkout@v3\n+ - uses: actions/checkout@v4\n - name: Install system libraries\n run: sudo apt-get install libxml2-dev libxslt1-dev libjpeg-dev\n - name: Set up Python ${{ matrix.python-version }}\n- uses: actions/setup-python@v2\n+ uses: actions/setup-python@v4\n with:\n python-version: ${{ matrix.python-version }}\n - name: Install dependencies\ndiff --git a/news/1.internal b/news/1.internal\nnew file mode 100644\nindex 00000000..4af1df1b\n--- /dev/null\n+++ b/news/1.internal\n@@ -0,0 +1,2 @@\n+Update GHA\n+[gforcada]\ndiff --git a/tox.ini b/tox.ini\nindex 7f4d12ee..4ec548d9 100644\n--- a/tox.ini\n+++ b/tox.ini\n@@ -1,7 +1,6 @@\n [tox]\n envlist =\n- py{37,38,39}-plone{52}\n- py{38,39}-plone{60}\n+ py{38,39,310,311}-plone{60}\n # towncrier\n # black-enforce\n black-check\n@@ -17,13 +16,13 @@ skip_missing_interpreters = True\n \n [gh-actions]\n python =\n- 3.7: py37\n 3.8: py38\n 3.9: py39\n+ 3.10: py310\n+ 3.11: py311\n \n [gh-actions:env]\n PLONE =\n- 52: plone52\n 60: plone60\n \n \n@@ -40,7 +39,6 @@ commands =\n \n setenv =\n BUILDOUT_FILE=test_plone-60.cfg\n- plone52: BUILDOUT_FILE=test_plone-52.cfg\n \n deps =\n pdbpp\n'