Repository: wagtail/wagtail

Issue:
Page/snippet cannot proceed in a workflow if it's locked by someone outside of the `GroupApprovalTask` group
<!--
Found a bug? Please fill out the sections below. 👍
-->

### Issue Summary

When a `GroupApprovalTask` is started while the page/snippet is locked by someone outside of the task's group, the object cannot proceed in the workflow as the lock is not properly released.

This is due to a logic error regression in d85db866ccdc8e936f475bb39a533b2f2d671565. The `LockableMixin` check should be done against `workflow_state.content_object`, not `workflow_state`.

<!--
A summary of the issue.
-->

### Steps to Reproduce

1. Spin up bakerydemo
2. Assign lock permission to the Editors group
3. Do not assign unlock permission to the Moderators group
4. As an editor, lock a page and then submit it to the moderators approval workflow
5. Log in as a moderator, and the page will be stuck in the locked state, leaving you unable to approve/reject it until someone unlocks it



Discussion:

Resolving patch:
diff --git a/wagtail/models/__init__.py b/wagtail/models/__init__.py
--- a/wagtail/models/__init__.py
+++ b/wagtail/models/__init__.py
@@ -3854,7 +3854,7 @@ class GroupApprovalTask(Task):
 
     def start(self, workflow_state, user=None):
         if (
-            isinstance(workflow_state, LockableMixin)
+            isinstance(workflow_state.content_object, LockableMixin)
             and workflow_state.content_object.locked_by
         ):
             # If the person who locked the object isn't in one of the groups, unlock the object


Test patch:
diff --git a/wagtail/tests/test_workflow.py b/wagtail/tests/test_workflow.py
--- a/wagtail/tests/test_workflow.py
+++ b/wagtail/tests/test_workflow.py
@@ -7,6 +7,7 @@
 from django.core.exceptions import ValidationError
 from django.db.utils import IntegrityError
 from django.test import TestCase, override_settings
+from django.utils import timezone
 from freezegun import freeze_time
 
 from wagtail.models import (
@@ -21,6 +22,7 @@
     WorkflowTask,
 )
 from wagtail.test.testapp.models import FullFeaturedSnippet, ModeratedModel, SimplePage
+from wagtail.test.utils.wagtail_tests import WagtailTestUtils
 
 
 class TestWorkflowModels(TestCase):
@@ -116,7 +118,7 @@ def test_get_workflow_from_closest_ancestor(self):
         self.assertTrue(workflow_2.all_pages().filter(id=goodbye_page.id).exists())
 
 
-class TestPageWorkflows(TestCase):
+class TestPageWorkflows(WagtailTestUtils, TestCase):
     fixtures = ["test.json"]
 
     @classmethod
@@ -416,6 +418,37 @@ def test_tasks_with_state(self):
             )[0],
         )
 
+    def test_start_workflow_group_approval_task_locked(self):
+        self.object.locked = True
+        self.object.locked_at = timezone.now()
+        self.object.locked_by = self.create_user("user1")
+        self.object.save()
+
+        # Create a workflow with one group approval task for the moderators group
+        moderators = Group.objects.get(name="Moderators")
+        workflow = Workflow.objects.create(name="test_workflow_foo")
+        task_1 = GroupApprovalTask.objects.create(name="test_task_1")
+        task_1.groups.add(moderators)
+        WorkflowTask.objects.create(workflow=workflow, task=task_1, sort_order=1)
+
+        # The object was locked by a non-moderator
+        self.assertFalse(self.object.locked_by.groups.filter(id=moderators.id).exists())
+
+        # Start the workflow as another user
+        self.object.save_revision()
+        workflow_state = workflow.start(self.object, self.create_user("user2"))
+
+        self.assertEqual(workflow_state.workflow, workflow)
+        self.assertEqual(workflow_state.content_object, self.object)
+        self.assertEqual(workflow_state.status, "in_progress")
+
+        self.object.refresh_from_db()
+
+        # The lock should be removed as otherwise the object would be stuck
+        self.assertFalse(self.object.locked)
+        self.assertIsNone(self.object.locked_at)
+        self.assertIsNone(self.object.locked_by)
+
 
 class TestSnippetWorkflows(TestPageWorkflows):
     fixtures = None
@@ -428,3 +461,21 @@ def setUpTestData(cls):
 
 class TestSnippetWorkflowsNotLockable(TestSnippetWorkflows):
     model = ModeratedModel
+
+    def test_start_workflow_group_approval_task_locked(self):
+        # Test normal GroupApprovalTask.start() as the object is not lockable
+
+        # Create a workflow with one group approval task for the moderators group
+        moderators = Group.objects.get(name="Moderators")
+        workflow = Workflow.objects.create(name="test_workflow_foo")
+        task_1 = GroupApprovalTask.objects.create(name="test_task_1")
+        task_1.groups.add(moderators)
+        WorkflowTask.objects.create(workflow=workflow, task=task_1, sort_order=1)
+
+        # Start the workflow
+        self.object.save_revision()
+        workflow_state = workflow.start(self.object, self.create_user("user2"))
+
+        self.assertEqual(workflow_state.workflow, workflow)
+        self.assertEqual(workflow_state.content_object, self.object)
+        self.assertEqual(workflow_state.status, "in_progress")