From 0c168e302a0b0fec0e9a0dbc2a5867c5bdd9797d Mon Sep 17 00:00:00 2001
From: Matt Westcott <matt@west.co.tt>
Date: Tue, 18 Feb 2025 00:01:32 +0000
Subject: [PATCH] Move LockableMixin to locking submodule

---
 wagtail/models/__init__.py | 77 +----------------------------------
 wagtail/models/locking.py  | 82 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+), 75 deletions(-)
 create mode 100644 wagtail/models/locking.py

diff --git a/wagtail/models/__init__.py b/wagtail/models/__init__.py
index 163c0edb0a..7b25578484 100644
--- a/wagtail/models/__init__.py
+++ b/wagtail/models/__init__.py
@@ -67,7 +67,7 @@ from wagtail.coreutils import (
 )
 from wagtail.fields import StreamField
 from wagtail.forms import TaskStateCommentForm
-from wagtail.locks import BasicLock, WorkflowLock
+from wagtail.locks import WorkflowLock
 from wagtail.log_actions import log
 from wagtail.query import PageQuerySet, SpecificQuerySetMixin
 from wagtail.search import index
@@ -106,6 +106,7 @@ from .i18n import (  # noqa: F401
     bootstrap_translatable_model,
     get_translatable_models,
 )
+from .locking import LockableMixin
 from .media import (  # noqa: F401
     BaseCollectionManager,
     Collection,
@@ -291,80 +292,6 @@ class PageBase(models.base.ModelBase):
             PAGE_MODEL_CLASSES.append(cls)
 
 
-class LockableMixin(models.Model):
-    locked = models.BooleanField(
-        verbose_name=_("locked"), default=False, editable=False
-    )
-    locked_at = models.DateTimeField(
-        verbose_name=_("locked at"), null=True, editable=False
-    )
-    locked_by = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        verbose_name=_("locked by"),
-        null=True,
-        blank=True,
-        editable=False,
-        on_delete=models.SET_NULL,
-        related_name="locked_%(class)ss",
-    )
-    locked_by.wagtail_reference_index_ignore = True
-
-    class Meta:
-        abstract = True
-
-    @classmethod
-    def check(cls, **kwargs):
-        return [
-            *super().check(**kwargs),
-            *cls._check_revision_mixin(),
-        ]
-
-    @classmethod
-    def _check_revision_mixin(cls):
-        mro = cls.mro()
-        error = checks.Error(
-            "LockableMixin must be applied before RevisionMixin.",
-            hint="Move LockableMixin in the model's base classes before RevisionMixin.",
-            obj=cls,
-            id="wagtailcore.E005",
-        )
-
-        try:
-            if mro.index(RevisionMixin) < mro.index(LockableMixin):
-                return [error]
-        except ValueError:
-            # LockableMixin can be used without RevisionMixin.
-            return []
-
-        return []
-
-    def with_content_json(self, content):
-        """
-        Similar to :meth:`RevisionMixin.with_content_json`,
-        but with the following fields also preserved:
-
-        * ``locked``
-        * ``locked_at``
-        * ``locked_by``
-        """
-        obj = super().with_content_json(content)
-
-        # Ensure other values that are meaningful for the object as a whole (rather than
-        # to a specific revision) are preserved
-        obj.locked = self.locked
-        obj.locked_at = self.locked_at
-        obj.locked_by = self.locked_by
-
-        return obj
-
-    def get_lock(self):
-        """
-        Returns a sub-class of ``BaseLock`` if the instance is locked, otherwise ``None``.
-        """
-        if self.locked:
-            return BasicLock(self)
-
-
 class WorkflowMixin:
     """A mixin that allows a model to have workflows."""
 
diff --git a/wagtail/models/locking.py b/wagtail/models/locking.py
new file mode 100644
index 0000000000..351f999b16
--- /dev/null
+++ b/wagtail/models/locking.py
@@ -0,0 +1,82 @@
+from django.conf import settings
+from django.core import checks
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from wagtail.locks import BasicLock
+
+from .revisions import RevisionMixin
+
+
+class LockableMixin(models.Model):
+    locked = models.BooleanField(
+        verbose_name=_("locked"), default=False, editable=False
+    )
+    locked_at = models.DateTimeField(
+        verbose_name=_("locked at"), null=True, editable=False
+    )
+    locked_by = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        verbose_name=_("locked by"),
+        null=True,
+        blank=True,
+        editable=False,
+        on_delete=models.SET_NULL,
+        related_name="locked_%(class)ss",
+    )
+    locked_by.wagtail_reference_index_ignore = True
+
+    class Meta:
+        abstract = True
+
+    @classmethod
+    def check(cls, **kwargs):
+        return [
+            *super().check(**kwargs),
+            *cls._check_revision_mixin(),
+        ]
+
+    @classmethod
+    def _check_revision_mixin(cls):
+        mro = cls.mro()
+        error = checks.Error(
+            "LockableMixin must be applied before RevisionMixin.",
+            hint="Move LockableMixin in the model's base classes before RevisionMixin.",
+            obj=cls,
+            id="wagtailcore.E005",
+        )
+
+        try:
+            if mro.index(RevisionMixin) < mro.index(LockableMixin):
+                return [error]
+        except ValueError:
+            # LockableMixin can be used without RevisionMixin.
+            return []
+
+        return []
+
+    def with_content_json(self, content):
+        """
+        Similar to :meth:`RevisionMixin.with_content_json`,
+        but with the following fields also preserved:
+
+        * ``locked``
+        * ``locked_at``
+        * ``locked_by``
+        """
+        obj = super().with_content_json(content)
+
+        # Ensure other values that are meaningful for the object as a whole (rather than
+        # to a specific revision) are preserved
+        obj.locked = self.locked
+        obj.locked_at = self.locked_at
+        obj.locked_by = self.locked_by
+
+        return obj
+
+    def get_lock(self):
+        """
+        Returns a sub-class of ``BaseLock`` if the instance is locked, otherwise ``None``.
+        """
+        if self.locked:
+            return BasicLock(self)